diff --git a/README.md b/README.md index f18dd23..a4ba36c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ A minimalist and educational 6502 CPU emulator written in JavaScript. This project is designed for learning and experimentation, focusing on the core functionality of the 6502 processor. -Features +#### Features • Basic Instruction Set: Implements a subset of the 6502 instruction set, allowing for the execution of simple programs. • 16-bit Addressing: Supports 16-bit memory addressing with 8-bit data registers. @@ -11,7 +11,7 @@ Features • Customizable: Designed with simplicity in mind, making it easy to extend and modify. • Testing: Covered with tests 11/151 OPCODES, which are official and documented guidelines. (in progress) -Installation +#### Installation Clone the repository and navigate to the project directory: @@ -19,7 +19,8 @@ Clone the repository and navigate to the project directory: git clone https://github.com/kostDev/6502-emulator-UA.git cd 6502-emulator-UA ``` -Usage + +#### Usage To start using the emulator, simply include the js folder files in your project and create an instance of the CPU class, as example: @@ -36,14 +37,14 @@ const cpu = new CPU(new Memory(0x10000), params); // 64kb -> 0x10000 cpu.run(); ``` -Roadmap +#### Roadmap - • Implement full 6502 instruction set. Current 11/151 (256) + • Implement full 6502 instruction set. Current 22/151 (256) • Add debugging tools. • Create example programs for testing and learning. • Develop a simple UI for interactive use. -License +#### License This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/actions/testing.yml b/actions/testing.yml deleted file mode 100644 index a069e28..0000000 --- a/actions/testing.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Unit tests - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: '22' - - - name: Install dependencies - run: npm install - - - name: Run tests - run: npm test \ No newline at end of file diff --git a/js/cpu.js b/js/cpu.js index b5679b4..054b3f8 100644 --- a/js/cpu.js +++ b/js/cpu.js @@ -6,7 +6,7 @@ class CPU { this.running = running; this.memory = memory; - // this.cycles = 0; + this.cycles = 0; this._registers = { PC: 0x0600, //(16bit) Program Counter, 0x0600 start address SP: 0xFF, // (8bit) Stack Pointer @@ -165,7 +165,7 @@ class CPU { decode(opcode) { const instruction = OPCODES[opcode]; if (instruction) { - // this.cycles += instruction.t; // for future game console emulation processes + this.cycles += instruction.t; // for future console emulation processes instruction.run && instruction.run(this); } else throw new Error(`Unknown opcode: 0x${opcode.toString(16).toUpperCase()}`); } diff --git a/js/memory.js b/js/memory.js index 93e16fa..75aa032 100644 --- a/js/memory.js +++ b/js/memory.js @@ -14,7 +14,7 @@ class Memory { // write in memory 16bit value (2byte) writeWord(address, value) { - // devide 16bit value on 2parts in two memory 8bit cells as result store 16bit value in two 8bit memory addresses + // divide 16bit value on 2parts in two memory 8bit cells as result store 16bit value in two 8bit memory addresses this.memory[address] = value & 0xFF; // low byte this.memory[address+1] = (value >> 8) & 0xFF; // high byte } diff --git a/js/opcodes.js b/js/opcodes.js index 22313b0..6ab4d05 100644 --- a/js/opcodes.js +++ b/js/opcodes.js @@ -24,7 +24,16 @@ const OPCODES = { 0x06: "", // asl("d") 0x07: "", // slo("d") // illegal 0x08: "", // php_implied() - 0x09: "", // ora("#i") + 0x09: { + name: "ORA #immediate", // AND Memory with Accumulator + t: 2, + code: 0x09, + run: (cpu) => { + cpu.ACC |= cpu.fetch(); + cpu.negativeFlag = cpu.ACC; + cpu.zeroFlag = cpu.ACC; + }, + }, 0x0A: "", // asl_implied() 0x0B: "", // anc("#i") // illegal 0x0C: "", // nop("a") // illegal @@ -56,7 +65,16 @@ const OPCODES = { 0x26: "", // rol("d") 0x27: "", // rla("d") // illegal 0x28: "", // plp_implied() - 0x29: "", // and("#i") + 0x29: { + name: "AND #immediate", // AND Memory with Accumulator + t: 2, + code: 0x29, + run: (cpu) => { + cpu.ACC &= cpu.fetch(); + cpu.negativeFlag = cpu.ACC; + cpu.zeroFlag = cpu.ACC; + }, + }, 0x2A: "", // rol_implied() 0x2B: "", // anc("#i") // illegal 0x2C: "", // bit("a") @@ -88,7 +106,16 @@ const OPCODES = { 0x46: "", // lsr("d") 0x47: "", // sre("d") // illegal 0x48: "", // pha_implied() - 0x49: "", // eor("#i") + 0x49: { + name: "EOR #immediate", // AND Memory with Accumulator + t: 2, + code: 0x49, + run: (cpu) => { + cpu.ACC ^= cpu.fetch(); + cpu.negativeFlag = cpu.ACC; + cpu.zeroFlag = cpu.ACC; + }, + }, 0x4A: "", // lsr_implied() 0x4B: "", // alr("#i") // illegal 0x4C: "", // jmp("a") @@ -167,11 +194,44 @@ const OPCODES = { 0x81: "", // sta("(d,x)") 0x82: "", // nop("#i") // illegal 0x83: "", // sax("(d,x)") // illegal - 0x84: "", // sty("d") - 0x85: "", // sta("d") - 0x86: "", // stx("d") + 0x84: { + name: "STY zeropage", // Sore Index Y in Memory + t: 3, + code: 0x84, + run: (cpu) => { + const address = cpu.fetch(); + cpu.memory.writeByte(address, cpu.Y); + } + }, + 0x85: { + name: "STA zeropage", // Store Accumulator in Memory + t: 3, + code: 0x85, + run: (cpu) => { + const address = cpu.fetch(); + cpu.memory.writeByte(address, cpu.ACC); + } + }, + 0x86: { + name: "STX zeropage", // Store Index X in Memory + t: 3, + code: 0x86, + run: (cpu) => { + const address = cpu.fetch(); + cpu.memory.writeByte(address, cpu.X); + } + }, 0x87: "", // sax("d") // illegal - 0x88: "", // dey_implied() + 0x88: { + name: "DEX", // Decrement Index X by One + t: 2, + code: 0x88, + run: (cpu) => { + cpu.Y -= 1; + cpu.negativeFlag = cpu.Y; + cpu.zeroFlag = cpu.Y; + } + }, 0x89: "", // nop("#i") // illegal 0x8A: { name: "TXA", // Transfer Index X to Accumulator @@ -188,7 +248,27 @@ const OPCODES = { 0x8D: "", // sta("a") 0x8E: "", // stx("a") 0x8F: "", // sax("a") // illegal - 0x90: "", // bcc("*+d") + 0x90: { + name: "BCC", // Branch if Carry Clear + t: 2, + code: 0x90, + run: (cpu) => { + const offset = cpu.fetch(); // Fetch the offset from memory + // Check if the Carry flag is clear + if (!cpu.carryFlag) { + // Calculate the new program counter (PC) considering the offset + const newPC = cpu.PC + (offset >= 0x80 ? offset - 0x100 : offset); + // Add 1 cycle if the branch is taken + cpu.cycles++; + // Add 1 more cycle if the branch crosses a page boundary + if ((cpu.PC & 0xFF00) !== (newPC & 0xFF00)) { + cpu.cycles++; + } + // Update the program counter (PC) to the new address + cpu.PC = newPC; + } + } + }, 0x91: "", // sta("(d),y") 0x92: "", // stp_implied() // illegal 0x93: "", // ahx("(d),y") // illegal @@ -298,9 +378,27 @@ const OPCODES = { 0xC5: "", // cmp("d") 0xC6: "", // dec("d") 0xC7: "", // dcp("d") // illegal - 0xC8: "", // iny_implied() + 0xC8: { + name: "INY", // Increment Index Y by One + t: 2, + code: 0xC8, + run: (cpu) => { + cpu.Y += 1; + cpu.negativeFlag = cpu.Y; + cpu.zeroFlag = cpu.Y; + } + }, 0xC9: "", // cmp("#i") - 0xCA: "", // dex_implied() + 0xCA: { + name: "DEX", // Decrement Index X by One + t: 2, + code: 0xCA, + run: (cpu) => { + cpu.X -= 1; + cpu.negativeFlag = cpu.X; + cpu.zeroFlag = cpu.X; + } + }, 0xCB: "", // axs("#i") // illegal 0xCC: "", 0xCD: "", @@ -330,7 +428,16 @@ const OPCODES = { 0xE5: "", 0xE6: "", 0xE7: "", // isc("d") // illegal - 0xE8: "", + 0xE8: { + name: "INX", // Increment Index X by One + t: 2, + code: 0xE8, + run: (cpu) => { + cpu.X += 1; + cpu.negativeFlag = cpu.X; + cpu.zeroFlag = cpu.X; + } + }, 0xE9: { name: "SBC #immediate", // Subtract Memory from Accumulator with Borrow t: 2, diff --git a/tests/cpu.test.js b/tests/cpu.test.js index a6a93a6..83b94f1 100644 --- a/tests/cpu.test.js +++ b/tests/cpu.test.js @@ -10,14 +10,12 @@ describe("memory:", () => { test("memory size 64kb", () => { expect(memory.memory.length).toBe(0x10000); }); - test("memory clear", () => { const clearStatus = memory.memory.filter(Boolean).every((val) => val == 0); expect(clearStatus).toBe(true); }); }); - describe("cpu:", () => { let memory, cpu, params; @@ -156,4 +154,90 @@ describe("cpu:", () => { }); }); }); + + describe("flags check:", () => { + describe("(0x80) negativeFlag", () => { + test("set 42", () => { + cpu.negativeFlag = 42; + expect(cpu.negativeFlag).toBe(false); + }) + test("set -42", () => { + cpu.negativeFlag = -42; + expect(cpu.negativeFlag).toBe(true); + }) + }); + describe("(0x40) overflowFlag", () => { + test("Subtract check(SBC) v1", () => { + const ACC = 0x14; + const carry = 1; + const value = 250; + const res = ACC - value - carry; + cpu.overflowFlag = ((ACC ^ res) & (~ACC ^ value)) & 0x80; + expect(cpu.overflowFlag).toBe(false); + }); + test("Subtract check(SBC) v2", () => { + const ACC = 0xFF; + const carry = 0; + const value = 128; + const res = ACC - value - carry; + cpu.overflowFlag = ((ACC ^ res) & (~ACC ^ value)) & 0x80; + expect(cpu.overflowFlag).toBe(true); + }); + }); + describe("(0x10) breakFlag", () => { + test("true", () => { + cpu.breakFlag = true; + expect(cpu.breakFlag).toBe(true); + }); + test("false", () => { + cpu.breakFlag = false; + expect(cpu.breakFlag).toBe(false); + }); + }); + describe("(0x08) decimalFlag", () => { + test("true", () => { + cpu.decimalFlag = true; + expect(cpu.decimalFlag).toBe(true); + }); + test("false", () => { + cpu.breakFlag = false; + expect(cpu.decimalFlag).toBe(false); + }); + }); + describe("(0x04) interruptDisableFlag", () => { + test("true", () => { + cpu.interruptDisableFlag = true; + expect(cpu.interruptDisableFlag).toBe(true); + }); + test("false", () => { + cpu.interruptDisableFlag = false; + expect(cpu.interruptDisableFlag).toBe(false); + }); + }); + describe("(0x02) zeroFlag", () => { + test("true", () => { + cpu.zeroFlag = 0; + expect(cpu.zeroFlag).toBe(true); + }); + test("false", () => { + cpu.zeroFlag = 0x2A; + expect(cpu.zeroFlag).toBe(false); + cpu.zeroFlag = 0x01; + expect(cpu.zeroFlag).toBe(false); + cpu.zeroFlag = 0xFF; + expect(cpu.zeroFlag).toBe(false); + }); + }); + describe("(0x01) carryFlag", () => { + test("true", () => { + cpu.carryFlag = true; + expect(cpu.carryFlag).toBe(true); + }); + test("false", () => { + cpu.zeroFlag = false; + expect(cpu.carryFlag).toBe(false); + }); + }); + + }); }); \ No newline at end of file diff --git a/tests/opcodes.test.js b/tests/opcodes.test.js index 7c09aba..5bdb872 100644 --- a/tests/opcodes.test.js +++ b/tests/opcodes.test.js @@ -24,7 +24,46 @@ describe("OPCODES:", () => { expect(cpu.running).toBe(false); }); }); - + + describe("0x09 -> ORA #immediate", () => { + test("decode", () => { + const instruction = OPCODES[0x09]; + cpu.ACC = 0x10; + cpu.decode(instruction.code); + + expect(instruction.code).toBe(0x09); + expect(cpu.ACC).toBe(0x10); // test only on empty memory + expect(cpu.negativeFlag).toBe(false); + expect(cpu.zeroFlag).toBe(false); + }); + }); + + describe("0x29 -> AND #immediate", () => { + test("decode", () => { + const instruction = OPCODES[0x29]; + cpu.ACC = 0x05; + cpu.decode(instruction.code); + + expect(instruction.code).toBe(0x29); + expect(cpu.ACC).toBe(0x00); // test only on empty memory + expect(cpu.negativeFlag).toBe(false); + expect(cpu.zeroFlag).toBe(true); + }); + }); + + describe("0x49 -> EOR #immediate", () => { + test("decode", () => { + const instruction = OPCODES[0x49]; + cpu.ACC = 0xAA; + cpu.decode(instruction.code); + + expect(instruction.code).toBe(0x49); + expect(cpu.ACC).toBe(0xAA); // XOR but test only on empty memory + expect(cpu.negativeFlag).toBe(true); // 0xAA = 10101010 + expect(cpu.zeroFlag).toBe(false); + }); + }); + describe("0x69 -> TYA", () => { test("decode", () => { const instruction = OPCODES[0x69]; @@ -39,6 +78,81 @@ describe("OPCODES:", () => { expect(cpu.carryFlag).toBe(false); }); }); + + describe("0x84 -> STY zeropage", () => { + test("decode", () => { + const instruction = OPCODES[0x84]; + cpu.Y = 0x05; + cpu.decode(instruction.code); + + const memoryAddress = cpu.memory.readByte(cpu.PC-1); + const memoryValue = cpu.memory.readByte(memoryAddress); + + expect(instruction.code).toBe(0x84); + expect(memoryValue).toBe(cpu.Y); // check store Y value in memory + expect(cpu.negativeFlag).toBe(false); + expect(cpu.zeroFlag).toBe(false); + }); + }); + + describe("0x85 -> STA zeropage", () => { + test("decode", () => { + const instruction = OPCODES[0x85]; + cpu.ACC = 0x85; + cpu.decode(instruction.code); + + const memoryAddress = cpu.memory.readByte(cpu.PC-1); + const memoryValue = cpu.memory.readByte(memoryAddress); + + expect(instruction.code).toBe(0x85); + expect(memoryValue).toBe(cpu.ACC); // check store value in memory + expect(cpu.negativeFlag).toBe(false); + expect(cpu.zeroFlag).toBe(false); + }); + }); + + describe("0x86 -> STX zeropage", () => { + test("decode", () => { + const instruction = OPCODES[0x86]; + cpu.X = 0xA1; + cpu.decode(instruction.code); + + const memoryAddress = cpu.memory.readByte(cpu.PC-1); + const memoryValue = cpu.memory.readByte(memoryAddress); + + expect(instruction.code).toBe(0x86); + expect(memoryValue).toBe(cpu.X); // check store X value in memory + expect(cpu.negativeFlag).toBe(false); + expect(cpu.zeroFlag).toBe(false); + }); + }); + + describe("0x88 -> DEY", () => { + test("decode", () => { + const instruction = OPCODES[0x88]; + cpu.Y = 0x0F; // 15 + cpu.decode(instruction.code); + + expect(instruction.code).toBe(0x88); + expect(cpu.Y).toBe(0x0E); // 14 + expect(cpu.negativeFlag).toBe(false); + expect(cpu.zeroFlag).toBe(false); + + cpu.Y = 0; + cpu.decode(instruction.code); + + expect(cpu.Y).toBe(0xFF); // 8bit value max + expect(cpu.negativeFlag).toBe(true); + expect(cpu.zeroFlag).toBe(false) + + cpu.Y = 0x01; + cpu.decode(instruction.code); + + expect(cpu.Y).toBe(0x00); + expect(cpu.negativeFlag).toBe(false); + expect(cpu.zeroFlag).toBe(true); + }); + }); describe("0x8A -> TXA", () => { test("decode", () => { @@ -52,6 +166,52 @@ describe("OPCODES:", () => { expect(cpu.zeroFlag).toBe(false); }); }); + + describe("0x90 -> BCC", () => { + beforeEach(() => { + cpu.memory.writeByte = jest.fn(); // Mock writeByte if needed + }); + + test("decode and execute with Carry Clear (Branch Taken)", () => { + const instruction = OPCODES[0x90]; + + cpu.carryFlag = false; // Ensure Carry flag is clear (C = 0) + cpu.PC = 0x1000; // Set an initial value for the program counter + cpu.memory.readByte = jest.fn(() => 0x02); // Mock fetch to return a positive offset + + cpu.decode(instruction.code); + + expect(instruction.code).toBe(0x90); + expect(cpu.PC).toBe(0x1002 + 0x01); // Expect the PC to be updated correctly + expect(cpu.cycles).toBe(3); // Base cycles (2) + 1 additional cycle for taken branch + }); + test("decode and execute with Carry Set (No Branch)", () => { + const instruction = OPCODES[0x90]; + + cpu.carryFlag = true; // Set Carry flag (C = 1) + cpu.PC = 0x1000; // Set an initial value for the program counter + cpu.memory.readByte = jest.fn(() => 0x02); // Mock fetch to return a positive offset + + cpu.decode(instruction.code); + + expect(instruction.code).toBe(0x90); + expect(cpu.PC).toBe(0x1001); // Expect the PC to increment by 1 (no branch taken) + expect(cpu.cycles).toBe(2); // Only base cycles (2) are used + }); + test("decode and execute with Carry Clear (Branch Crosses Page)", () => { + const instruction = OPCODES[0x90]; + + cpu.carryFlag = false; // Ensure Carry flag is clear (C = 0) + cpu.PC = 0x10FF; // Set the program counter near a page boundary + cpu.memory.readByte = jest.fn(() => 0x01); // Mock fetch to return an offset that crosses the page + + cpu.decode(instruction.code); + + expect(instruction.code).toBe(0x90); + expect(cpu.PC).toBe(0x1101); // Expect the PC to cross the page boundary + expect(cpu.cycles).toBe(3); // Base cycles (2) + 1 for taken branch + 1 for page cross + }); + }); describe("0x98 -> TYA", () => { test("decode", () => { @@ -127,6 +287,57 @@ describe("OPCODES:", () => { expect(cpu.zeroFlag).toBe(false); }); }); + + describe("0xC8 -> INY", () => { + test("decode", () => { + const instruction = OPCODES[0xC8]; + cpu.decode(instruction.code); + + expect(instruction.code).toBe(0xC8); + expect(cpu.Y).toBe(0x01); + expect(cpu.negativeFlag).toBe(false); + expect(cpu.zeroFlag).toBe(false); + }); + }); + + describe("0xCA -> DEX", () => { + test("decode", () => { + const instruction = OPCODES[0xCA]; + cpu.X = 0x10; // 16 + cpu.decode(instruction.code); + + expect(instruction.code).toBe(0xCA); + expect(cpu.X).toBe(0x0F); // 15 + expect(cpu.negativeFlag).toBe(false); + expect(cpu.zeroFlag).toBe(false); + + cpu.X = 0; + cpu.decode(instruction.code); + + expect(cpu.X).toBe(0xFF); // 8bit value max + expect(cpu.negativeFlag).toBe(true); + expect(cpu.zeroFlag).toBe(false) + + cpu.X = 0x01; + cpu.decode(instruction.code); + + expect(cpu.X).toBe(0x00); + expect(cpu.negativeFlag).toBe(false); + expect(cpu.zeroFlag).toBe(true); + }); + }); + + describe("0xE8 -> INX", () => { + test("decode", () => { + const instruction = OPCODES[0xE8]; + cpu.decode(instruction.code); + + expect(instruction.code).toBe(0xE8); + expect(cpu.X).toBe(0x01); + expect(cpu.negativeFlag).toBe(false); + expect(cpu.zeroFlag).toBe(false); + }); + }); describe("0xE9 -> SBC #immediate", () => { test("decode", () => {