From badc2fdb74e7fce2ddb08118dd673bc4c9bb60de Mon Sep 17 00:00:00 2001 From: Will Scullin Date: Wed, 13 Oct 2021 09:15:29 -0700 Subject: [PATCH] Support Tom Harte test suite (#88) A test data set was published at https://github.com/TomHarte/ProcessorTests which contain cycle traces of instructions for various versions of the 6502. This adds a test harness that reads those data files, and adjusts the CPU6502 behavior to match the behavior of the vanilla and WDC 65C02 test data. Also converts the existing CPU tests to Typescript, and fixes any inconsistencies that came up from the new behaviors. --- js/cpu6502.ts | 423 ++++++++++++--------- js/debugger.ts | 21 +- test/cpu-tom-harte.spec.ts | 235 ++++++++++++ test/cpu.spec.ts | 2 +- test/{cpu6502.spec.js => cpu6502.spec.ts} | 424 ++++++++++++++-------- test/js/debugger.spec.ts | 2 +- test/util/cpu.ts | 22 ++ test/util/memory.ts | 28 +- 8 files changed, 826 insertions(+), 331 deletions(-) create mode 100644 test/cpu-tom-harte.spec.ts rename test/{cpu6502.spec.js => cpu6502.spec.ts} (83%) create mode 100644 test/util/cpu.ts diff --git a/js/cpu6502.ts b/js/cpu6502.ts index cc492e8..8cfa08d 100644 --- a/js/cpu6502.ts +++ b/js/cpu6502.ts @@ -18,12 +18,19 @@ export interface CpuOptions { } export interface CpuState { + /** Accumulator */ a: byte, + /** X index */ x: byte, + /** Y index */ y: byte, + /** Status register */ s: byte, + /** Program counter */ pc: word, + /** Stack pointer */ sp: byte, + /** Elapsed cycles */ cycles: number } @@ -74,13 +81,23 @@ export const sizes: Modes = { /** Status register flag numbers. */ export type flag = 0x80 | 0x40 | 0x20 | 0x10 | 0x08 | 0x04 | 0x02 | 0x01; +/** + * + */ export type DebugInfo = { + /** Program counter */ pc: word, + /** Accumulator */ ar: byte, + /** X index */ xr: byte, + /** Y index */ yr: byte, + /** Status register */ sr: byte, + /** Stack pointer */ sp: byte, + /** Current command */ cmd: byte[], }; @@ -88,6 +105,7 @@ export type DebugInfo = { export const flags: { [key: string]: flag } = { N: 0x80, // Negative V: 0x40, // oVerflow + X: 0x20, // Unused, always 1 B: 0x10, // Break D: 0x08, // Decimal I: 0x04, // Interrupt @@ -117,7 +135,7 @@ const BLANK_PAGE: Memory = { }; interface Opts { - rwm?: boolean; + inc?: boolean; } type ReadFn = () => byte; @@ -143,24 +161,44 @@ type StrictInstruction = type Instructions = Record -type callback = (cpu: CPU6502) => void; +type callback = (cpu: CPU6502) => boolean | void; export default class CPU6502 { + /** 65C02 emulation mode flag */ private readonly is65C02: boolean; - /* Registers */ - private pc: word = 0; // Program Counter - private sr: byte = 0x20; // Process Status Register - private ar: byte = 0; // Accumulator - private xr: byte = 0; // X Register - private yr: byte = 0; // Y Register - private sp: byte = 0xff; // Stack Pointer + /** + * Registers + */ + /** Program counter */ + private pc: word = 0; + /** Status register */ + private sr: byte = flags.X + /** Accumulator */ + private ar: byte = 0; + /** X index */ + private xr: byte = 0; + /** Y index */ + private yr: byte = 0; + /** Stack pointer */ + private sp: byte = 0xff; + + /** Current instruction */ + private op: Instruction + /** Last accessed memory address */ + private addr: word = 0; + + /** Filled array of memory handlers by address page */ private memPages: Memory[] = new Array(0x100); + /** Callbacks invoked on reset signal */ private resetHandlers: ResettablePageHandler[] = []; + /** Elapsed cycles */ private cycles = 0; + /** Command being fetched signal */ private sync = false; + /** Filled array of CPU operations */ private readonly opary: Instruction[]; constructor(options: CpuOptions = {}) { @@ -212,40 +250,75 @@ export default class CPU6502 { * Returns `a + b`, unless `sub` is true, in which case it performs * `a - b`. The status register is updated according to the result. */ - private add(a: byte, b: byte, sub: boolean) { - // KEGS - let c, v; + private add(a: byte, b: byte, sub: boolean): byte { + const a7 = a >> 7; + const b7 = b >> 7; + const ci = this.sr & flags.C; + let c; + let co; + let v; + let n; + let z; + + const updateFlags = (c: byte) => { + const bin = c & 0xff; + n = bin >> 7; + co = c >> 8; + z = !((a + b + ci) & 0xff); + v = a7 ^ b7 ^ n ^ co; + }; + + const updateBCDFlags = (c: byte) => { + if (this.is65C02) { + const bin = c & 0xff; + n = bin >> 7; + z = !bin; + if (this.op.mode === 'immediate') { + this.readByte(sub ? 0xB8 : 0x7F); + } else { + this.readByte(this.addr); + } + } + if (!sub) { + co = c >> 8; + } + }; + + c = (a & 0x0f) + (b & 0x0f) + ci; if ((this.sr & flags.D) !== 0) { // BCD - c = (a & 0x0f) + (b & 0x0f) + (this.sr & flags.C); if (sub) { - if (c < 0x10) + if (c < 0x10) { c = (c - 0x06) & 0x0f; + } c += (a & 0xf0) + (b & 0xf0); - v = (c >> 1) ^ c; - if (c < 0x100) - c = (c + 0xa0) & 0xff; + updateFlags(c); + if (c < 0x100) { + c += 0xa0; + } } else { - if (c > 0x09) - c = (c - 0x0a) | 0x10; // carry to MSN + if (c > 0x9) { + c = 0x10 + ((c + 0x6) & 0xf); + } c += (a & 0xf0) + (b & 0xf0); - v = (c >> 1) ^ c; - if (c > 0x99) + updateFlags(c); + if (c >= 0xa0) { c += 0x60; + } } + updateBCDFlags(c); } else { - c = a + b + (this.sr & flags.C); - v = (c ^ a) & 0x80; + c += (a & 0xf0) + (b & 0xf0); + updateFlags(c); } + c = c & 0xff; - if (((a ^ b) & 0x80) !== 0) { - v = 0; - } - - this.setFlag(flags.C, c > 0xff); + this.setFlag(flags.N, !!n); this.setFlag(flags.V, !!v); + this.setFlag(flags.Z, !!z); + this.setFlag(flags.C, !!co); - return this.testNZ(c & 0xff); + return c; } /** Increments `a` and returns the value, setting the status register. */ @@ -258,20 +331,15 @@ export default class CPU6502 { } private readBytePC(): byte { - const addr = this.pc, - page = addr >> 8, - off = addr & 0xff; - - const result = this.memPages[page].read(page, off); + const result = this.readByte(this.pc); this.pc = (this.pc + 1) & 0xffff; - this.cycles++; - return result; } private readByte(addr: word): byte { + this.addr = addr; const page = addr >> 8, off = addr & 0xff; @@ -282,14 +350,8 @@ export default class CPU6502 { return result; } - private readByteDebug(addr: word) { - const page = addr >> 8, - off = addr & 0xff; - - return this.memPages[page].read(page, off); - } - private writeByte(addr: word, val: byte) { + this.addr = addr; const page = addr >> 8, off = addr & 0xff; @@ -335,11 +397,46 @@ export default class CPU6502 { return (msb << 8) | lsb; } + // Helpers that replicate false reads and writes during work cycles that + // vary between CPU versions + + private workCycle(addr: word, val: byte) { + if (this.is65C02) { + this.readByte(addr); + } else { + this.writeByte(addr, val); + } + } + + private workCycleIndexedWrite(pc: word, addr: word, addrIdx: word): void { + const oldPage = addr & 0xff00; + if (this.is65C02) { + this.readByte(pc); + } else { + const off = addrIdx & 0xff; + this.readByte(oldPage | off); + } + } + + private workCycleIndexedRead(pc: word, addr: word, addrIdx: word): void { + const oldPage = addr & 0xff00; + const newPage = addrIdx & 0xff00; + if (newPage !== oldPage) { + if (this.is65C02) { + this.readByte(pc); + } else { + const off = addrIdx & 0xff; + this.readByte(oldPage | off); + } + } + } + /* * Implied function */ - implied() { + implied = () => { + this.readByte(this.pc); } /* @@ -363,28 +460,20 @@ export default class CPU6502 { // $0000,X readAbsoluteX = (): byte => { - let addr = this.readWordPC(); - const oldPage = addr >> 8; - addr = (addr + this.xr) & 0xffff; - const newPage = addr >> 8; - if (newPage != oldPage) { - const off = addr & 0xff; - this.readByte(oldPage << 8 | off); - } - return this.readByte(addr); + const addr = this.readWordPC(); + const pc = this.addr; + const addrIdx = (addr + this.xr) & 0xffff; + this.workCycleIndexedRead(pc, addr, addrIdx); + return this.readByte(addrIdx); } // $0000,Y readAbsoluteY = (): byte => { - let addr = this.readWordPC(); - const oldPage = addr >> 8; - addr = (addr + this.yr) & 0xffff; - const newPage = addr >> 8; - if (newPage != oldPage) { - const off = addr & 0xff; - this.readByte(oldPage << 8 | off); - } - return this.readByte(addr); + const addr = this.readWordPC(); + const pc = this.addr; + const addrIdx = (addr + this.yr) & 0xffff; + this.workCycleIndexedRead(pc, addr, addrIdx); + return this.readByte(addrIdx); } // $00,X @@ -411,15 +500,12 @@ export default class CPU6502 { // ($00),Y readZeroPageIndirectY = (): byte => { - let addr = this.readZPWord(this.readBytePC()); - const oldPage = addr >> 8; - addr = (addr + this.yr) & 0xffff; - const newPage = addr >> 8; - if (newPage != oldPage) { - const off = addr & 0xff; - this.readByte(oldPage << 8 | off); - } - return this.readByte(addr); + const zpAddr = this.readBytePC(); + const pc = this.addr; + const addr = this.readZPWord(zpAddr); + const addrIdx = (addr + this.yr) & 0xffff; + this.workCycleIndexedRead(pc, addr, addrIdx); + return this.readByte(addrIdx); } // ($00) (65C02) @@ -443,22 +529,20 @@ export default class CPU6502 { // $0000,X writeAbsoluteX = (val: byte) => { - let addr = this.readWordPC(); - const oldPage = addr >> 8; - addr = (addr + this.xr) & 0xffff; - const off = addr & 0xff; - this.readByte(oldPage << 8 | off); - this.writeByte(addr, val); + const addr = this.readWordPC(); + const pc = this.addr; + const addrIdx = (addr + this.xr) & 0xffff; + this.workCycleIndexedWrite(pc, addr, addrIdx); + this.writeByte(addrIdx, val); } // $0000,Y writeAbsoluteY = (val: byte) => { - let addr = this.readWordPC(); - const oldPage = addr >> 8; - addr = (addr + this.yr) & 0xffff; - const off = addr & 0xff; - this.readByte(oldPage << 8 | off); - this.writeByte(addr, val); + const addr = this.readWordPC(); + const pc = this.addr; + const addrIdx = (addr + this.yr) & 0xffff; + this.workCycleIndexedWrite(pc, addr, addrIdx); + this.writeByte(addrIdx, val); } // $00,X @@ -485,12 +569,12 @@ export default class CPU6502 { // ($00),Y writeZeroPageIndirectY = (val: byte) => { - let addr = this.readZPWord(this.readBytePC()); - const oldPage = addr >> 8; - addr = (addr + this.yr) & 0xffff; - const off = addr & 0xff; - this.readByte(oldPage << 8 | off); - this.writeByte(addr, val); + const zpAddr = this.readBytePC(); + const pc = this.addr; + const addr = this.readZPWord(zpAddr); + const addrIdx = (addr + this.yr) & 0xffff; + this.workCycleIndexedWrite(pc, addr, addrIdx); + this.writeByte(addrIdx, val); } // ($00) (65C02) @@ -527,28 +611,46 @@ export default class CPU6502 { // ($0000) (65C02) readAddrAbsoluteIndirect = (): word => { - const lsb = this.readBytePC(); - const msb = this.readBytePC(); - this.readByte(this.pc); - return this.readWord(msb << 8 | lsb); + const addr = this.readWord(this.readWordPC()); + this.readByte(this.addr); + return addr; } // $0000,X - readAddrAbsoluteX = (opts: Opts = {}): word => { - const addr = this.readWordPC(); - if (!this.is65C02 || opts.rwm) { - this.readByte(addr); + readAddrAbsoluteX = (opts?: Opts): word => { + let addr = this.readWordPC(); + const page = addr & 0xff00; + addr = (addr + this.xr) & 0xffff; + if (this.is65C02) { + if (opts?.inc) { + this.readByte(this.addr); + } else { + const newPage = addr & 0xff00; + if (page !== newPage) { + this.readByte(this.addr); + } + } } else { - this.readByte(this.pc); + const off = addr & 0x00ff; + this.readByte(page | off); } - return (addr + this.xr) & 0xffff; + return addr; } // $(0000,X) (65C02) readAddrAbsoluteXIndirect = (): word => { - const address = this.readWordPC(); - this.readByte(this.pc); - return this.readWord((address + this.xr) & 0xffff); + const lsb = this.readBytePC(); + const pc = this.addr; + const msb = this.readBytePC(); + const addr = (((msb << 8) | lsb) + this.xr) & 0xffff; + this.readByte(pc); + return this.readWord(addr); + } + + // 5C, DC, FC NOP + readNop = (): void => { + this.readWordPC(); + this.readByte(this.addr); } /* Break */ @@ -615,9 +717,9 @@ export default class CPU6502 { } inc = (readAddrFn: ReadAddrFn) => { - const addr = readAddrFn({ rwm: true }); + const addr = readAddrFn({ inc: true }); const oldVal = this.readByte(addr); - this.writeByte(addr, oldVal); + this.workCycle(addr, oldVal); const val = this.increment(oldVal); this.writeByte(addr, val); } @@ -641,9 +743,9 @@ export default class CPU6502 { } dec = (readAddrFn: ReadAddrFn) => { - const addr = readAddrFn({ rwm: true }); + const addr = readAddrFn({ inc: true}); const oldVal = this.readByte(addr); - this.writeByte(addr, oldVal); + this.workCycle(addr, oldVal); const val = this.decrement(oldVal); this.writeByte(addr, val); } @@ -672,9 +774,9 @@ export default class CPU6502 { } asl = (readAddrFn: ReadAddrFn) => { - const addr = readAddrFn({ rwm: true }); + const addr = readAddrFn(); const oldVal = this.readByte(addr); - this.writeByte(addr, oldVal); + this.workCycle(addr, oldVal); const val = this.shiftLeft(oldVal); this.writeByte(addr, val); } @@ -691,9 +793,9 @@ export default class CPU6502 { } lsr = (readAddrFn: ReadAddrFn) => { - const addr = readAddrFn({ rwm: true }); + const addr = readAddrFn(); const oldVal = this.readByte(addr); - this.writeByte(addr, oldVal); + this.workCycle(addr, oldVal); const val = this.shiftRight(oldVal); this.writeByte(addr, val); } @@ -711,9 +813,9 @@ export default class CPU6502 { } rol = (readAddrFn: ReadAddrFn) => { - const addr = readAddrFn({ rwm: true }); + const addr = readAddrFn(); const oldVal = this.readByte(addr); - this.writeByte(addr, oldVal); + this.workCycle(addr, oldVal); const val = this.rotateLeft(oldVal); this.writeByte(addr, val); } @@ -731,9 +833,9 @@ export default class CPU6502 { } ror = (readAddrFn: ReadAddrFn) => { - const addr = readAddrFn({ rwm: true }); + const addr = readAddrFn(); const oldVal = this.readByte(addr); - this.writeByte(addr, oldVal); + this.workCycle(addr, oldVal); const val = this.rotateRight(oldVal); this.writeByte(addr, val); } @@ -831,12 +933,12 @@ export default class CPU6502 { const off = this.readBytePC(); // changes pc if ((f & this.sr) !== 0) { this.readByte(this.pc); - const oldPage = this.pc >> 8; + const oldPage = this.pc & 0xff00; this.pc += off > 127 ? off - 256 : off; this.pc &= 0xffff; - const newPage = this.pc >> 8; + const newPage = this.pc & 0xff00; const newOff = this.pc & 0xff; - if (newPage != oldPage) this.readByte(oldPage << 8 | newOff); + if (newPage !== oldPage) this.readByte(oldPage | newOff); } } @@ -844,12 +946,12 @@ export default class CPU6502 { const off = this.readBytePC(); // changes pc if ((f & this.sr) === 0) { this.readByte(this.pc); - const oldPage = this.pc >> 8; + const oldPage = this.pc & 0xff00; this.pc += off > 127 ? off - 256 : off; this.pc &= 0xffff; - const newPage = this.pc >> 8; + const newPage = this.pc & 0xff00; const newOff = this.pc & 0xff; - if (newPage != oldPage) this.readByte(oldPage << 8 | newOff); + if (newPage !== oldPage) this.readByte(oldPage | newOff); } } @@ -858,38 +960,34 @@ export default class CPU6502 { bbr = (b: byte) => { const zpAddr = this.readBytePC(); const val = this.readByte(zpAddr); - this.readByte(zpAddr); + this.writeByte(zpAddr, val); const off = this.readBytePC(); // changes pc + const oldPc = this.pc; + const oldPage = oldPc & 0xff00; + let newPC = this.pc + (off > 127 ? off - 256 : off); + newPC &= 0xffff; + const newOff = newPC & 0xff; + this.readByte(oldPage | newOff); if (((1 << b) & val) === 0) { - const oldPc = this.pc; - const oldPage = oldPc >> 8; - this.readByte(oldPc); - this.pc += off > 127 ? off - 256 : off; - this.pc &= 0xffff; - const newPage = this.pc >> 8; - if (oldPage != newPage) { - this.readByte(oldPc); - } + this.pc = newPC; } } bbs = (b: byte) => { const zpAddr = this.readBytePC(); const val = this.readByte(zpAddr); - this.readByte(zpAddr); + this.writeByte(zpAddr, val); const off = this.readBytePC(); // changes pc + const oldPc = this.pc; + const oldPage = oldPc & 0xff00; + let newPC = this.pc + (off > 127 ? off - 256 : off); + newPC &= 0xffff; + const newOff = newPC & 0xff; + this.readByte(oldPage | newOff); if (((1 << b) & val) !== 0) { - const oldPc = this.pc; - const oldPage = oldPc >> 8; - this.readByte(oldPc); - this.pc += off > 127 ? off - 256 : off; - this.pc &= 0xffff; - const newPage = this.pc >> 8; - if (oldPage != newPage) { - this.readByte(oldPc); - } + this.pc = newPC; } } @@ -920,7 +1018,7 @@ export default class CPU6502 { php = () => { this.readByte(this.pc); this.pushByte(this.sr | flags.B); } - plp = () => { this.readByte(this.pc); this.readByte(0x0100 | this.sp); this.sr = (this.pullByte() & ~flags.B) | 0x20; } + plp = () => { this.readByte(this.pc); this.readByte(0x0100 | this.sp); this.sr = (this.pullByte() & ~flags.B) | flags.X; } /* Jump */ jmp = (readAddrFn: ReadAddrFn) => { @@ -949,7 +1047,7 @@ export default class CPU6502 { rti = () => { this.readByte(this.pc); this.readByte(0x0100 | this.sp); - this.sr = this.pullByte() & ~flags.B; + this.sr = (this.pullByte() & ~flags.B) | flags.X; this.pc = this.pullWordRaw(); } @@ -965,9 +1063,8 @@ export default class CPU6502 { } /* No-Op */ - nop = (impliedFn: ImpliedFn) => { - this.readByte(this.pc); - impliedFn(); + nop = (readFn: ImpliedFn | ReadFn) => { + readFn(); } private unknown(b: byte) { @@ -998,9 +1095,9 @@ export default class CPU6502 { public step(cb?: callback) { this.sync = true; - const op = this.opary[this.readBytePC()]; + this.op = this.opary[this.readBytePC()]; this.sync = false; - op.op(op.modeFn); + this.op.op(this.op.modeFn); cb?.(this); } @@ -1008,9 +1105,9 @@ export default class CPU6502 { public stepN(n: number, cb?: callback) { for (let idx = 0; idx < n; idx++) { this.sync = true; - const op = this.opary[this.readBytePC()]; + this.op = this.opary[this.readBytePC()]; this.sync = false; - op.op(op.modeFn); + this.op.op(this.op.modeFn); if (cb?.(this)) { return; @@ -1023,9 +1120,9 @@ export default class CPU6502 { while (this.cycles < end) { this.sync = true; - const op = this.opary[this.readBytePC()]; + this.op = this.opary[this.readBytePC()]; this.sync = false; - op.op(op.modeFn); + this.op.op(this.op.modeFn); } } @@ -1034,9 +1131,9 @@ export default class CPU6502 { while (this.cycles < end) { this.sync = true; - const op = this.opary[this.readBytePC()]; + this.op = this.opary[this.readBytePC()]; this.sync = false; - op.op(op.modeFn); + this.op.op(this.op.modeFn); if (cb?.(this)) { return; @@ -1054,7 +1151,7 @@ export default class CPU6502 { public reset() { // cycles = 0; - this.sr = 0x20; + this.sr = flags.X; this.sp = 0xff; this.ar = 0; this.yr = 0; @@ -1099,13 +1196,13 @@ export default class CPU6502 { } public getDebugInfo(): DebugInfo { - const b = this.readByteDebug(this.pc); + const b = this.read(this.pc); const op = this.opary[b]; const size = sizes[op.mode]; const cmd = new Array(size); cmd[0] = b; for (let idx = 1; idx < size; idx++) { - cmd[idx] = this.readByteDebug(this.pc + idx); + cmd[idx] = this.read(this.pc + idx); } return { @@ -1509,17 +1606,17 @@ export default class CPU6502 { 0x02: { name: 'NOP', op: this.nop, modeFn: this.readImmediate, mode: 'immediate' }, 0x22: { name: 'NOP', op: this.nop, modeFn: this.readImmediate, mode: 'immediate' }, 0x42: { name: 'NOP', op: this.nop, modeFn: this.readImmediate, mode: 'immediate' }, - 0x44: { name: 'NOP', op: this.nop, modeFn: this.readImmediate, mode: 'immediate' }, - 0x54: { name: 'NOP', op: this.nop, modeFn: this.readImmediate, mode: 'immediate' }, + 0x44: { name: 'NOP', op: this.nop, modeFn: this.readZeroPage, mode: 'immediate' }, + 0x54: { name: 'NOP', op: this.nop, modeFn: this.readZeroPageX, mode: 'immediate' }, 0x62: { name: 'NOP', op: this.nop, modeFn: this.readImmediate, mode: 'immediate' }, 0x82: { name: 'NOP', op: this.nop, modeFn: this.readImmediate, mode: 'immediate' }, 0xC2: { name: 'NOP', op: this.nop, modeFn: this.readImmediate, mode: 'immediate' }, - 0xD4: { name: 'NOP', op: this.nop, modeFn: this.readImmediate, mode: 'immediate' }, + 0xD4: { name: 'NOP', op: this.nop, modeFn: this.readZeroPageX, mode: 'immediate' }, 0xE2: { name: 'NOP', op: this.nop, modeFn: this.readImmediate, mode: 'immediate' }, - 0xF4: { name: 'NOP', op: this.nop, modeFn: this.readImmediate, mode: 'immediate' }, - 0x5C: { name: 'NOP', op: this.nop, modeFn: this.readAbsolute, mode: 'absolute' }, - 0xDC: { name: 'NOP', op: this.nop, modeFn: this.readAbsolute, mode: 'absolute' }, - 0xFC: { name: 'NOP', op: this.nop, modeFn: this.readAbsolute, mode: 'absolute' }, + 0xF4: { name: 'NOP', op: this.nop, modeFn: this.readZeroPageX, mode: 'immediate' }, + 0x5C: { name: 'NOP', op: this.nop, modeFn: this.readNop, mode: 'absolute' }, + 0xDC: { name: 'NOP', op: this.nop, modeFn: this.readNop, mode: 'absolute' }, + 0xFC: { name: 'NOP', op: this.nop, modeFn: this.readNop, mode: 'absolute' }, // PHX 0xDA: { name: 'PHX', op: this.phx, modeFn: this.implied, mode: 'implied' }, diff --git a/js/debugger.ts b/js/debugger.ts index f4d5ad8..2b88f8f 100644 --- a/js/debugger.ts +++ b/js/debugger.ts @@ -14,6 +14,18 @@ type breakpointFn = (info: DebugInfo) => boolean const alwaysBreak = (_info: DebugInfo) => { return true; }; +export const dumpStatusRegister = (sr: byte) => + [ + (sr & flags.N) ? 'N' : '-', + (sr & flags.V) ? 'V' : '-', + (sr & flags.X) ? 'X' : '-', + (sr & flags.B) ? 'B' : '-', + (sr & flags.D) ? 'D' : '-', + (sr & flags.I) ? 'I' : '-', + (sr & flags.Z) ? 'Z' : '-', + (sr & flags.C) ? 'C' : '-', + ].join(''); + export default class Debugger { private cpu: CPU6502; private verbose = false; @@ -138,14 +150,7 @@ export default class Debugger { ' P=' + toHex(sr), ' S=' + toHex(sp), ' ', - ((sr & flags.N) ? 'N' : '-'), - ((sr & flags.V) ? 'V' : '-'), - '-', - ((sr & flags.B) ? 'B' : '-'), - ((sr & flags.D) ? 'D' : '-'), - ((sr & flags.I) ? 'I' : '-'), - ((sr & flags.Z) ? 'Z' : '-'), - ((sr & flags.C) ? 'C' : '-') + dumpStatusRegister(sr), ].join(''); } diff --git a/test/cpu-tom-harte.spec.ts b/test/cpu-tom-harte.spec.ts new file mode 100644 index 0000000..2bbb4ca --- /dev/null +++ b/test/cpu-tom-harte.spec.ts @@ -0,0 +1,235 @@ +/** + * Tom Harte test data based test suite + * + * Uses test files from https://github.com/TomHarte/ProcessorTests + * To use, set TOM_HARTE_TEST_PATH to local copy of that repository + */ + +import fs from 'fs'; + +import CPU6502 from 'js/cpu6502'; +import { toHex } from 'js/util'; +import type { byte, word } from 'js/types'; + +import { toReadableState } from './util/cpu'; +import { TestMemory } from './util/memory'; + +// JEST_DETAIL=true converts decimal values to hex before testing +// expectations, for better readability at the expense of speed. +const detail = !!process.env.JEST_DETAIL; + +/** + * Types for JSON tests + */ + +/** + * Memory address and value + */ +type MemoryValue = [address: word, value: byte] + +/** + * Represents initial and final CPU and memory states + */ + interface TestState { + /** Program counter */ + pc: word + /** Stack register */ + s: byte + /** Accumulator */ + a: byte + /** X index */ + x: byte + /** Y index */ + y: byte + /** Processor status register */ + p: byte + /** M */ + ram: MemoryValue[] +} + +/** + * CPU cycle memory operation + */ +type Cycle = [address: word, value: byte, type: 'read'|'write'] + +/** + * One test record + */ +interface Test { + /** Test name */ + name: string + /** Initial CPU register and memory state */ + initial: TestState + /** Final CPU register and memory state */ + final: TestState + /** Detailed CPU cycles */ + cycles: Cycle[] +} + +/** + * Initialize cpu and memory before test + * + * @param cpu Target cpu instance + * @param state Initial test state + */ +function initState(cpu: CPU6502, state: TestState) { + const { pc, s, a, x, y, p, ram } = state; + cpu.setState({ cycles: 0, pc, sp: s, a, x, y, s: p }); + + for (let idx = 0; idx < ram.length; idx++) { + const [address, mem] = ram[idx]; + cpu.write(address, mem); + } +} + +/** + * Pretty print 'address: val' if detail is turned on, + * or passes through raw test data if not. + * + * @returns string or raw test data + */ +function toAddrValHex([address, val]: MemoryValue) { + if (detail) { + return `${toHex(address, 4)}: ${toHex(val)}`; + } else { + return [address, val]; + } +} + +/** + * Pretty print 'address: val (read|write)' if detail is turned on, + * or passes through raw test data if not. + * + * @returns string or raw test data + */ +function toAddrValHexType([address, val, type]: Cycle) { + if (detail) { + return `${toHex(address, 4)}: ${toHex(val)} ${type}`; + } else { + return [address, val, type]; + } +} + +/** + * Compare end state and read write behavior of test run + * + * @param cpu Test CPU + * @param memory Test memory + * @param test Test data to compare against + */ +function expectState(cpu: CPU6502, memory: TestMemory, test: Test) { + const { pc, s, a, x, y, p, ram } = test.final; + expect( + toReadableState(cpu.getState()) + ).toEqual( + toReadableState({cycles: test.cycles.length, pc, sp: s, a, x, y, s: p }) + ); + + // Retrieve relevant memory locations and values + const result = []; + for (let idx = 0; idx < ram.length; idx++) { + const [address] = ram[idx]; + result.push([address, cpu.read(address)]); + } + expect( + result.map(toAddrValHex) + ).toEqual( + ram.map(toAddrValHex) + ); + + expect( + memory.getLog().map(toAddrValHexType) + ).toEqual( + test.cycles.map(toAddrValHexType) + ); +} + +interface OpTest { + op: string + name: string + mode: string +} + +const testPath = process.env.TOM_HARTE_TEST_PATH; + +// There are 10,0000 tests per test file, which would take several hours +// in jest. 16 is a manageable quantity that still gets good coverage. +const maxTests = 16; + +if (testPath) { + const testPath6502 = `${testPath}/6502/v1/`; + const testPath65C02 = `${testPath}/wdc65c02/v1/`; + + const opAry6502: OpTest[] = []; + const opAry65C02: OpTest[] = []; + + const buildOpArrays = () => { + const cpu = new CPU6502(); + + // Grab the implemented op codes + // TODO: Decide which undocumented opcodes are worthwhile. + for (const op in cpu.OPS_6502) { + const { name, mode } = cpu.OPS_6502[op]; + const test = { op: toHex(+op), name, mode }; + opAry6502.push(test); + opAry65C02.push(test); + } + + for (const op in cpu.OPS_65C02) { + const { name, mode } = cpu.OPS_65C02[op]; + const test = { op: toHex(+op), name, mode }; + opAry65C02.push(test); + } + }; + + buildOpArrays(); + + describe('Tom Harte', function() { + let cpu: CPU6502; + let memory: TestMemory; + + describe('6502', function() { + beforeAll(function() { + cpu = new CPU6502(); + memory = new TestMemory(256); + cpu.addPageHandler(memory); + }); + + describe.each(opAry6502)('Test op $op $name $mode', ({op}) => { + const data = fs.readFileSync(`${testPath6502}${op}.json`, 'utf-8'); + const tests = JSON.parse(data) as Test[]; + + it.each(tests.slice(0, maxTests))('Test $name', (test) => { + initState(cpu, test.initial); + memory.logStart(); + cpu.step(); + memory.logStop(); + expectState(cpu, memory, test); + }); + }); + }); + + describe('WDC 65C02', function() { + beforeAll(function() { + cpu = new CPU6502({ '65C02': true }); + memory = new TestMemory(256); + cpu.addPageHandler(memory); + }); + + describe.each(opAry65C02)('Test op $op $name $mode', ({op}) => { + const data = fs.readFileSync(`${testPath65C02}${op}.json`, 'utf-8'); + const tests = JSON.parse(data) as Test[]; + + it.each(tests.slice(0, maxTests))('Test $name', (test) => { + initState(cpu, test.initial); + memory.logStart(); + cpu.step(); + memory.logStop(); + expectState(cpu, memory, test); + }); + }); + }); + }); +} else { + test('Skipping Tom Harte tests', () => { expect(testPath).toBeFalsy(); }); +} diff --git a/test/cpu.spec.ts b/test/cpu.spec.ts index 74e21a3..c04df8b 100644 --- a/test/cpu.spec.ts +++ b/test/cpu.spec.ts @@ -1,5 +1,5 @@ - import CPU6502 from '../js/cpu6502'; +// From https://github.com/Klaus2m5/6502_65C02_functional_tests import Test6502 from './roms/6502test'; import Test65C02 from './roms/65C02test'; diff --git a/test/cpu6502.spec.js b/test/cpu6502.spec.ts similarity index 83% rename from test/cpu6502.spec.js rename to test/cpu6502.spec.ts index 8bd7a0e..a98ce02 100644 --- a/test/cpu6502.spec.js +++ b/test/cpu6502.spec.ts @@ -1,21 +1,11 @@ -import CPU6502 from '../js/cpu6502'; +import CPU6502, { CpuState, flags } from '../js/cpu6502'; import { TestMemory } from './util/memory'; import { bios, Program } from './util/bios'; +import { toReadableState } from './util/cpu'; -var FLAGS = { - N: 0x80, // Negative - V: 0x40, // oVerflow - DEFAULT: 0x20, // Default - B: 0x10, // Break - D: 0x08, // Decimal - I: 0x04, // Interrupt - Z: 0x02, // Zero - C: 0x01 // Carry -}; - -var DEFAULT_STATE = { +const DEFAULT_STATE: CpuState = { cycles: 0, - s: FLAGS.DEFAULT, + s: flags.X, sp: 0xff, a: 0x00, x: 0x00, @@ -23,27 +13,27 @@ var DEFAULT_STATE = { pc: 0x0400 }; -var memory; -var cpu; -var program; +let memory; +let cpu: CPU6502; +let program; -function initState(initialState) { - var state = Object.assign({}, DEFAULT_STATE, initialState); +function initState(initialState: Partial) { + const state = {...DEFAULT_STATE, ...initialState}; cpu.setState(state); } -function expectState(initialState, expectedState) { - var state = Object.assign({}, initialState, expectedState); - expect(cpu.getState()).toEqual(state); +function expectState(initialState: CpuState, expectedState: Partial) { + const state = {...initialState, ...expectedState}; + expect(toReadableState(cpu.getState())).toEqual(toReadableState(state)); } -function initMemory(memAry) { - for (var idx = 0; idx < memAry.length; idx++) { - var mem = memAry[idx]; - var page = mem[0]; - var off = mem[1]; - var data = mem[2]; - for (var jdx = 0; jdx < data.length; jdx++) { +function initMemory(memAry: [page: number, off: number, data: number[]][]) { + for (let idx = 0; idx < memAry.length; idx++) { + const mem = memAry[idx]; + let page = mem[0]; + let off = mem[1]; + const data = mem[2]; + for (let jdx = 0; jdx < data.length; jdx++) { cpu.write(page, off++, data[jdx]); if (off > 0xff) { page++; @@ -53,15 +43,15 @@ function initMemory(memAry) { } } -function expectMemory(expectAry) { - var memAry = []; - for (var idx = 0; idx < expectAry.length; idx++) { - var mem = expectAry[idx]; - var page = mem[0]; - var off = mem[1]; - var expectData = mem[2]; - var data = []; - for (var jdx = 0; jdx < expectData.length; jdx++) { +function expectMemory(expectAry: [page: number, off: number, data: number[]][]) { + const memAry = []; + for (let idx = 0; idx < expectAry.length; idx++) { + const mem = expectAry[idx]; + let page = mem[0]; + let off = mem[1]; + const expectData = mem[2]; + const data = []; + for (let jdx = 0; jdx < expectData.length; jdx++) { data.push(cpu.read(page, off++)); if (off > 0xff) { page++; @@ -73,16 +63,14 @@ function expectMemory(expectAry) { expect(memAry).toEqual(expectAry); } -function expectStack(expectAry) { - var state = cpu.getState(); +function expectStack(expectAry: number[]) { + const state = cpu.getState(); expectMemory([[0x01, state.sp + 1, expectAry]]); } -function testCode(code, steps, setupState, expectedState) { - var initialState = Object.assign({}, DEFAULT_STATE, setupState); - var finalState = Object.assign({ - pc: initialState.pc + code.length - }, expectedState); +function testCode(code: number[], steps: number, setupState: Partial, expectedState: Partial) { + const initialState = {...DEFAULT_STATE, ...setupState}; + const finalState = { pc: initialState.pc + code.length, ...expectedState }; program = new Program(0x04, code); cpu.addPageHandler(program); @@ -101,6 +89,122 @@ describe('CPU6502', function() { cpu.addPageHandler(bios); }); + describe('#step functions', function() { + const code = [0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA]; + const initialState = {...DEFAULT_STATE}; + + it('step', function() { + cpu.setState(initialState); + program = new Program(0x04, code); + cpu.addPageHandler(program); + cpu.step(); + expect(cpu.getState()).toEqual( + { ...DEFAULT_STATE, pc: 0x401, cycles: 2 } + ); + expect(cpu.getCycles()).toEqual(2); + }); + + it('step with callback', function() { + const callback = jest.fn(); + cpu.setState(initialState); + program = new Program(0x04, code); + cpu.addPageHandler(program); + cpu.step(callback); + expect(cpu.getState()).toEqual({ + ...DEFAULT_STATE, pc: 0x401, cycles: 2, + }); + expect(cpu.getCycles()).toEqual(2); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('stepN', function() { + cpu.setState(initialState); + program = new Program(0x04, code); + cpu.addPageHandler(program); + cpu.stepN(4); + expect(cpu.getState()).toEqual({ + ...DEFAULT_STATE, pc: 0x404, cycles: 8, + }); + expect(cpu.getCycles()).toEqual(8); + }); + + it('stepN with callback', function() { + const callback = jest.fn(); + cpu.setState(initialState); + program = new Program(0x04, code); + cpu.addPageHandler(program); + cpu.stepN(4, callback); + expect(cpu.getState()).toEqual({ + ...DEFAULT_STATE, pc: 0x404, cycles: 8, + }); + expect(cpu.getCycles()).toEqual(8); + expect(callback).toHaveBeenCalledTimes(4); + }); + + it('stepN with breakpoint', function() { + const callback = jest.fn().mockReturnValue(true); + cpu.setState(initialState); + program = new Program(0x04, code); + cpu.addPageHandler(program); + cpu.stepN(4, callback); + expect(cpu.getState()).toEqual({ + ...DEFAULT_STATE, pc: 0x401, cycles: 2, + }); + expect(cpu.getCycles()).toEqual(2); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('stepCycles', function() { + cpu.setState(initialState); + program = new Program(0x04, code); + cpu.addPageHandler(program); + cpu.stepCycles(4); + expect(cpu.getState()).toEqual({ + ...DEFAULT_STATE, pc: 0x402, cycles: 4, + }); + expect(cpu.getCycles()).toEqual(4); + }); + + it('stepCyclesDebug', function() { + cpu.setState(initialState); + program = new Program(0x04, code); + cpu.addPageHandler(program); + cpu.stepCyclesDebug(4); + expect(cpu.getState()).toEqual({ + ...DEFAULT_STATE, pc: 0x402, cycles: 4, + }); + expect(cpu.getCycles()).toEqual(4); + }); + + it('stepCyclesDebug with callback', function() { + const callback = jest.fn(); + + cpu.setState(initialState); + program = new Program(0x04, code); + cpu.addPageHandler(program); + cpu.stepCyclesDebug(4, callback); + expect(cpu.getState()).toEqual({ + ...DEFAULT_STATE, pc: 0x402, cycles: 4, + }); + expect(cpu.getCycles()).toEqual(4); + expect(callback).toHaveBeenCalledTimes(2); + }); + + it('stepCyclesDebug with breakpoint', function() { + const callback = jest.fn().mockReturnValue(true); + + cpu.setState(initialState); + program = new Program(0x04, code); + cpu.addPageHandler(program); + cpu.stepCyclesDebug(4, callback); + expect(cpu.getState()).toEqual({ + ...DEFAULT_STATE, pc: 0x401, cycles: 2, + }); + expect(cpu.getCycles()).toEqual(2); + expect(callback).toHaveBeenCalledTimes(1); + }); + }); + describe('#signals', function () { it('should reset', function () { cpu.reset(); @@ -115,7 +219,7 @@ describe('CPU6502', function() { expectState(DEFAULT_STATE, { cycles: 5, - s: FLAGS.DEFAULT | FLAGS.I, + s: flags.X | flags.I, sp: 0xfc, pc: 0xff00 }); @@ -123,13 +227,13 @@ describe('CPU6502', function() { it('should not irq if I set', function () { initState({ - s: FLAGS.DEFAULT | FLAGS.I + s: flags.X | flags.I }); cpu.irq(); expectState(DEFAULT_STATE, { - s: FLAGS.DEFAULT | FLAGS.I, + s: flags.X | flags.I, pc: 0x400 }); }); @@ -139,7 +243,7 @@ describe('CPU6502', function() { expectState(DEFAULT_STATE, { cycles: 5, - s: FLAGS.DEFAULT | FLAGS.I, + s: flags.X | flags.I, sp: 0xfc, pc: 0xff00 }); @@ -156,7 +260,7 @@ describe('CPU6502', function() { it('should BRK', function () { testCode([0x00, 0x00], 1, {}, { cycles: 7, - s: FLAGS.DEFAULT | FLAGS.I, + s: flags.X | flags.I, sp: 0xfc, pc: 0xff00 }); @@ -168,11 +272,19 @@ describe('CPU6502', function() { sp: 0xFC }, { cycles: 6, - s: FLAGS.DEFAULT | FLAGS.N, + s: flags.X | flags.N, sp: 0xFF, pc: 0x1234 }); }); + + it('should log unimplemented opcodes', () => { + jest.spyOn(console, 'log').mockImplementation(); + testCode([0xFF], 1, {}, { + cycles: 1 + }); + expect(console.log).toHaveBeenLastCalledWith('Unknown OpCode: FF at 0400'); + }); }); describe('#registers', function() { @@ -274,57 +386,57 @@ describe('CPU6502', function() { it('should SEC', function () { testCode([0x38], 1, {}, { cycles: 2, - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }); }); it('should CLC', function () { testCode([0x18], 1, { - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }, { cycles: 2, - s: FLAGS.DEFAULT + s: flags.X }); }); it('should SEI', function () { testCode([0x78], 1, {}, { cycles: 2, - s: FLAGS.DEFAULT | FLAGS.I + s: flags.X | flags.I }); }); it('should CLI', function () { testCode([0x58], 1, { - s: FLAGS.DEFAULT | FLAGS.I + s: flags.X | flags.I }, { cycles: 2, - s: FLAGS.DEFAULT + s: flags.X }); }); it('should CLV', function () { testCode([0xB8], 1, { - s: FLAGS.DEFAULT | FLAGS.V + s: flags.X | flags.V }, { cycles: 2, - s: FLAGS.DEFAULT + s: flags.X }); }); it('should SED', function () { testCode([0xF8], 1, {}, { cycles: 2, - s: FLAGS.DEFAULT | FLAGS.D + s: flags.X | flags.D }); }); it('should CLD', function () { testCode([0xD8], 1, { - s: FLAGS.DEFAULT | FLAGS.D + s: flags.X | flags.D }, { cycles: 2, - s: FLAGS.DEFAULT + s: flags.X }); }); }); @@ -371,21 +483,21 @@ describe('CPU6502', function() { it('should PHP', function() { testCode([0x08], 1, { - s: FLAGS.DEFAULT | FLAGS.N | FLAGS.C + s: flags.X | flags.N | flags.C }, { cycles: 3, sp: 0xfe }); - expectStack([FLAGS.DEFAULT | FLAGS.B | FLAGS.N | FLAGS.C]); + expectStack([flags.X | flags.B | flags.N | flags.C]); }); it('should PLP', function() { - initMemory([[0x01, 0xff, [FLAGS.N | FLAGS.C]]]); + initMemory([[0x01, 0xff, [flags.N | flags.C]]]); testCode([0x28], 1, { sp: 0xfe }, { cycles: 4, - s: FLAGS.DEFAULT | FLAGS.N | FLAGS.C, + s: flags.X | flags.N | flags.C, sp: 0xff }); }); @@ -407,7 +519,7 @@ describe('CPU6502', function() { }); }); - it('should JMP (abs) across page boundries with bugs', function () { + it('should JMP (abs) across page boundaries with bugs', function () { initMemory([[0x02, 0xFF, [0x34, 0x12]], [0x02, 0x00, [0xff]]]); testCode([0x6C, 0xFF, 0x02], 1, {}, { @@ -441,7 +553,7 @@ describe('CPU6502', function() { // ********** bcs it('should BCS forward', function () { testCode([0xB0, 0x7F], 1, { - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }, { cycles: 3, pc: 0x0481 @@ -450,7 +562,7 @@ describe('CPU6502', function() { it('should BCS backward', function () { testCode([0xB0, 0xff], 1, { - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }, { cycles: 3, pc: 0x0401 @@ -459,7 +571,7 @@ describe('CPU6502', function() { it('should BCS across pages with an extra cycle', function () { testCode([0xB0, 0xfd], 1, { - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }, { cycles: 4, pc: 0x03FF @@ -496,7 +608,7 @@ describe('CPU6502', function() { it('should not BCC if carry set', function () { testCode([0x90, 0xfd], 1, { - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }, { cycles: 2, pc: 0x0402 @@ -857,7 +969,7 @@ describe('CPU6502', function() { }, { cycles: 2, a: 0xAA, - s: FLAGS.DEFAULT | FLAGS.N + s: flags.X | flags.N }); }); @@ -867,7 +979,7 @@ describe('CPU6502', function() { }, { cycles: 2, a: 0x54, - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }); }); @@ -876,7 +988,7 @@ describe('CPU6502', function() { testCode([0x0E, 0x33, 0x03], 1, { }, { cycles: 6, - s: FLAGS.DEFAULT | FLAGS.N + s: flags.X | flags.N }); expectMemory([[0x03, 0x33, [0xAA]]]); }); @@ -886,7 +998,7 @@ describe('CPU6502', function() { testCode([0x0E, 0x33, 0x03], 1, { }, { cycles: 6, - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }); expectMemory([[0x03, 0x33, [0x54]]]); }); @@ -898,7 +1010,7 @@ describe('CPU6502', function() { }, { cycles: 2, a: 0xAA, - s: FLAGS.DEFAULT | FLAGS.N + s: flags.X | flags.N }); }); @@ -908,18 +1020,18 @@ describe('CPU6502', function() { }, { cycles: 2, a: 0x54, - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }); }); it('should ROL A with carry in', function () { testCode([0x2A], 1, { - s: FLAGS.DEFAULT | FLAGS.C, + s: flags.X | flags.C, a: 0xAA }, { cycles: 2, a: 0x55, - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }); }); @@ -928,7 +1040,7 @@ describe('CPU6502', function() { testCode([0x2E, 0x33, 0x03], 1, { }, { cycles: 6, - s: FLAGS.DEFAULT | FLAGS.N + s: flags.X | flags.N }); expectMemory([[0x03, 0x33, [0xAA]]]); }); @@ -938,7 +1050,7 @@ describe('CPU6502', function() { testCode([0x2E, 0x33, 0x03], 1, { }, { cycles: 6, - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }); expectMemory([[0x03, 0x33, [0x54]]]); }); @@ -946,10 +1058,10 @@ describe('CPU6502', function() { it('should ROL abs with carry in', function () { initMemory([[0x03, 0x33, [0xAA]]]); testCode([0x2E, 0x33, 0x03], 1, { - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }, { cycles: 6, - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }); expectMemory([[0x03, 0x33, [0x55]]]); }); @@ -970,7 +1082,7 @@ describe('CPU6502', function() { }, { cycles: 2, a: 0x2A, - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }); }); @@ -988,7 +1100,7 @@ describe('CPU6502', function() { testCode([0x4E, 0x33, 0x03], 1, { }, { cycles: 6, - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }); expectMemory([[0x03, 0x33, [0x2A]]]); }); @@ -1008,18 +1120,18 @@ describe('CPU6502', function() { a: 0x55 }, { cycles: 2, - s: FLAGS.DEFAULT | FLAGS.C, + s: flags.X | flags.C, a: 0x2A }); }); it('should ROR A with carry in', function () { testCode([0x6A], 1, { - s: FLAGS.DEFAULT | FLAGS.C, + s: flags.X | flags.C, a: 0x55 }, { cycles: 2, - s: FLAGS.DEFAULT | FLAGS.C | FLAGS.N, + s: flags.X | flags.C | flags.N, a: 0xAA }); }); @@ -1038,7 +1150,7 @@ describe('CPU6502', function() { testCode([0x6E, 0x33, 0x03], 1, { }, { cycles: 6, - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }); expectMemory([[0x03, 0x33, [0x2A]]]); }); @@ -1046,10 +1158,10 @@ describe('CPU6502', function() { it('should ROR abs with carry in', function () { initMemory([[0x03, 0x33, [0x55]]]); testCode([0x6E, 0x33, 0x03], 1, { - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }, { cycles: 6, - s: FLAGS.DEFAULT | FLAGS.C | FLAGS.N + s: flags.X | flags.C | flags.N }); expectMemory([[0x03, 0x33, [0xAA]]]); }); @@ -1070,7 +1182,7 @@ describe('CPU6502', function() { a: 0xA0 }, { cycles: 4, - s: FLAGS.DEFAULT | FLAGS.N, + s: flags.X | flags.N, a: 0xF5 }); }); @@ -1081,7 +1193,7 @@ describe('CPU6502', function() { a: 0xA5 }, { cycles: 4, - s: FLAGS.DEFAULT | FLAGS.N, + s: flags.X | flags.N, a: 0xF0 }); }); @@ -1092,7 +1204,7 @@ describe('CPU6502', function() { a: 0x55 }, { cycles: 3, - s: FLAGS.DEFAULT | FLAGS.V + s: flags.X | flags.V }); }); @@ -1101,7 +1213,7 @@ describe('CPU6502', function() { testCode([0x2C, 0x33, 0x03], 1, { }, { cycles: 4, - s: FLAGS.DEFAULT | FLAGS.N | FLAGS.Z + s: flags.X | flags.N | flags.Z }); }); }); @@ -1114,18 +1226,18 @@ describe('CPU6502', function() { }, { cycles: 2, a: 0x78, - s: FLAGS.DEFAULT + s: flags.X }); }); it('should ADC with carry in', function () { testCode([0x69, 0x55], 1, { a: 0x23, - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }, { cycles: 2, a: 0x79, - s: FLAGS.DEFAULT + s: flags.X }); }); @@ -1135,7 +1247,7 @@ describe('CPU6502', function() { }, { cycles: 2, a: 0x80, - s: FLAGS.DEFAULT | FLAGS.N | FLAGS.V + s: flags.X | flags.N | flags.V }); }); @@ -1145,120 +1257,120 @@ describe('CPU6502', function() { }, { cycles: 2, a: 0x10, - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }); }); // ********** ADC BCD it('should ADC BCD', function () { testCode([0x69, 0x16], 1, { - s: FLAGS.DEFAULT | FLAGS.D, + s: flags.X | flags.D, a: 0x25 }, { cycles: 2, - s: FLAGS.DEFAULT | FLAGS.D | FLAGS.V, + s: flags.X | flags.D, a: 0x41 }); }); it('should ADC BCD with carry in', function () { testCode([0x69, 0x55], 1, { - s: FLAGS.DEFAULT | FLAGS.D | FLAGS.C, + s: flags.X | flags.D | flags.C, a: 0x23 }, { cycles: 2, - s: FLAGS.DEFAULT| FLAGS.D | FLAGS.V, + s: flags.X | flags.D, a: 0x79 }); }); it('should ADC BCD with carry out', function () { testCode([0x69, 0x10], 1, { - s: FLAGS.DEFAULT | FLAGS.D, + s: flags.X | flags.D, a: 0x91 }, { cycles: 2, a: 0x01, - s: FLAGS.DEFAULT | FLAGS.D | FLAGS.C + s: flags.X | flags.N | flags.D | flags.C }); }); // ********** SBC it('should SBC', function () { testCode([0xE9, 0x23], 1, { - s: FLAGS.DEFAULT | FLAGS.C, + s: flags.X | flags.C, a: 0x55 }, { cycles: 2, a: 0x32, - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }); }); it('should SBC with borrow in', function () { testCode([0xE9, 0x23], 1, { - s: FLAGS.DEFAULT, + s: flags.X, a: 0x55 }, { cycles: 2, a: 0x31, - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }); }); it('should SBC with borrow out', function () { testCode([0xE9, 0x55], 1, { - s: FLAGS.DEFAULT | FLAGS.C, + s: flags.X | flags.C, a: 0x23 }, { cycles: 2, a: 0xCE, - s: FLAGS.DEFAULT | FLAGS.N + s: flags.X | flags.N }); }); it('should SBC with overflow out', function () { testCode([0xE9, 0x7F], 1, { - s: FLAGS.DEFAULT | FLAGS.C, + s: flags.X | flags.C, a: 0xAF }, { cycles: 2, a: 0x30, - s: FLAGS.DEFAULT | FLAGS.V | FLAGS.C + s: flags.X | flags.V | flags.C }); }); // ********** SBC BCD it('should SBC BCD', function () { testCode([0xE9, 0x23], 1, { - s: FLAGS.DEFAULT | FLAGS.D | FLAGS.C, + s: flags.X | flags.D | flags.C, a: 0x55 }, { cycles: 2, a: 0x32, - s: FLAGS.DEFAULT | FLAGS.D | FLAGS.C + s: flags.X | flags.D | flags.C }); }); it('should SBC BCD with borrow in', function () { testCode([0xE9, 0x23], 1, { - s: FLAGS.DEFAULT | FLAGS.D, + s: flags.X | flags.D, a: 0x55 }, { cycles: 2, a: 0x31, - s: FLAGS.DEFAULT | FLAGS.D | FLAGS.C + s: flags.X | flags.D | flags.C }); }); it('should SBC BCD with borrow out', function () { testCode([0xE9, 0x55], 1, { - s: FLAGS.DEFAULT | FLAGS.D | FLAGS.C, + s: flags.X | flags.D | flags.C, a: 0x23 }, { cycles: 2, a: 0x68, - s: FLAGS.DEFAULT | FLAGS.D + s: flags.X | flags.N | flags.D }); }); @@ -1348,7 +1460,7 @@ describe('CPU6502', function() { a: 0x33 }, { cycles: 2, - s: FLAGS.DEFAULT | FLAGS.N + s: flags.X | flags.N }); }); @@ -1357,7 +1469,7 @@ describe('CPU6502', function() { a: 0x44 }, { cycles: 2, - s: FLAGS.DEFAULT | FLAGS.Z | FLAGS.C + s: flags.X | flags.Z | flags.C }); }); @@ -1366,7 +1478,7 @@ describe('CPU6502', function() { a: 0x55 }, { cycles: 2, - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }); }); @@ -1376,7 +1488,7 @@ describe('CPU6502', function() { x: 0x33 }, { cycles: 2, - s: FLAGS.DEFAULT | FLAGS.N + s: flags.X | flags.N }); }); @@ -1385,7 +1497,7 @@ describe('CPU6502', function() { x: 0x44 }, { cycles: 2, - s: FLAGS.DEFAULT | FLAGS.Z | FLAGS.C + s: flags.X | flags.Z | flags.C }); }); @@ -1394,7 +1506,7 @@ describe('CPU6502', function() { x: 0x55 }, { cycles: 2, - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }); }); @@ -1404,7 +1516,7 @@ describe('CPU6502', function() { y: 0x33 }, { cycles: 2, - s: FLAGS.DEFAULT | FLAGS.N + s: flags.X | flags.N }); }); @@ -1413,7 +1525,7 @@ describe('CPU6502', function() { y: 0x44 }, { cycles: 2, - s: FLAGS.DEFAULT | FLAGS.Z | FLAGS.C + s: flags.X | flags.Z | flags.C }); }); @@ -1422,7 +1534,7 @@ describe('CPU6502', function() { y: 0x55 }, { cycles: 2, - s: FLAGS.DEFAULT | FLAGS.C + s: flags.X | flags.C }); }); }); @@ -1440,14 +1552,14 @@ describe('65c02', function() { describe('#signals', function() { it('should clear D on IRQ', function() { initState({ - s: FLAGS.DEFAULT | FLAGS.D + s: flags.X | flags.D }); cpu.irq(); expectState(DEFAULT_STATE, { cycles: 5, - s: FLAGS.DEFAULT | FLAGS.I, + s: flags.X | flags.I, sp: 0xfc, pc: 0xff00 }); @@ -1455,14 +1567,14 @@ describe('65c02', function() { it('should clear D on NMI', function() { initState({ - s: FLAGS.DEFAULT | FLAGS.D + s: flags.X | flags.D }); cpu.nmi(); expectState(DEFAULT_STATE, { cycles: 5, - s: FLAGS.DEFAULT | FLAGS.I, + s: flags.X | flags.I, sp: 0xfc, pc: 0xff00 }); @@ -1470,10 +1582,10 @@ describe('65c02', function() { it('should clear D on BRK', function () { testCode([0x00, 0x00], 1, { - s: FLAGS.DEFAULT | FLAGS.D + s: flags.X | flags.D }, { cycles: 7, - s: FLAGS.DEFAULT | FLAGS.I, + s: flags.X | flags.I, sp: 0xfc, pc: 0xff00 }); @@ -1620,11 +1732,11 @@ describe('65c02', function() { describe('#logical operators', function() { it('should BIT imm and effect other flags', function() { testCode([0x89, 0x33], 1, { - s: FLAGS.DEFAULT | FLAGS.N, + s: flags.X | flags.N, a: 0x44 }, { cycles: 2, - s: FLAGS.DEFAULT | FLAGS.Z | FLAGS.N + s: flags.X | flags.Z | flags.N }); }); @@ -1633,7 +1745,7 @@ describe('65c02', function() { a: 0x03 }, { cycles: 2, - s: FLAGS.DEFAULT + s: flags.X }); }); @@ -1654,7 +1766,7 @@ describe('65c02', function() { a: 0xAA }, { cycles: 6, - s: FLAGS.DEFAULT | FLAGS.Z + s: flags.X | flags.Z }); expectMemory([[0x00, 0x33, [0x00]]]); }); @@ -1676,7 +1788,7 @@ describe('65c02', function() { a: 0xAA }, { cycles: 6, - s: FLAGS.DEFAULT | FLAGS.Z + s: flags.X | flags.Z }); expectMemory([[0x03, 0x33, [0xFF]]]); }); @@ -1759,7 +1871,7 @@ describe('65c02', function() { it('BBR0 should not branch if bit 0 set', function() { initMemory([[0x00, 0x33, [0x01]]]); testCode([0x0F, 0x33, 0x7F], 1, {}, { - cycles: 5, + cycles: 6, pc: 0x0403 }); }); @@ -1767,7 +1879,7 @@ describe('65c02', function() { it('BBR1 should not branch if bit 1 set', function() { initMemory([[0x00, 0x33, [0x02]]]); testCode([0x1F, 0x33, 0x7F], 1, {}, { - cycles: 5, + cycles: 6, pc: 0x0403 }); }); @@ -1775,7 +1887,7 @@ describe('65c02', function() { it('BBR2 should not branch if bit 2 set', function() { initMemory([[0x00, 0x33, [0x04]]]); testCode([0x2F, 0x33, 0x7F], 1, {}, { - cycles: 5, + cycles: 6, pc: 0x0403 }); }); @@ -1783,7 +1895,7 @@ describe('65c02', function() { it('BBR3 should not branch if bit 3 set', function() { initMemory([[0x00, 0x33, [0x08]]]); testCode([0x3F, 0x33, 0x7F], 1, {}, { - cycles: 5, + cycles: 6, pc: 0x0403 }); }); @@ -1791,7 +1903,7 @@ describe('65c02', function() { it('BBR4 should not branch if bit 4 set', function() { initMemory([[0x00, 0x33, [0x10]]]); testCode([0x4F, 0x33, 0x7F], 1, {}, { - cycles: 5, + cycles: 6, pc: 0x0403 }); }); @@ -1799,7 +1911,7 @@ describe('65c02', function() { it('BBR5 should not branch if bit 5 set', function() { initMemory([[0x00, 0x33, [0x20]]]); testCode([0x5F, 0x33, 0x7F], 1, {}, { - cycles: 5, + cycles: 6, pc: 0x0403 }); }); @@ -1807,7 +1919,7 @@ describe('65c02', function() { it('BBR6 should not branch if bit 6 set', function() { initMemory([[0x00, 0x33, [0x40]]]); testCode([0x6F, 0x33, 0x7F], 1, {}, { - cycles: 5, + cycles: 6, pc: 0x0403 }); }); @@ -1815,7 +1927,7 @@ describe('65c02', function() { it('BBR7 should not branch if bit 7 set', function() { initMemory([[0x00, 0x33, [0x80]]]); testCode([0x7F, 0x33, 0x7F], 1, {}, { - cycles: 5, + cycles: 6, pc: 0x0403 }); }); @@ -1896,7 +2008,7 @@ describe('65c02', function() { it('BBS0 should not branch if bit 0 clear', function() { initMemory([[0x00, 0x33, [0xFE]]]); testCode([0x8F, 0x33, 0x7F], 1, {}, { - cycles: 5, + cycles: 6, pc: 0x0403 }); }); @@ -1904,7 +2016,7 @@ describe('65c02', function() { it('BBS1 should not branch if bit 1 clear', function() { initMemory([[0x00, 0x33, [0xFD]]]); testCode([0x9F, 0x33, 0x7F], 1, {}, { - cycles: 5, + cycles: 6, pc: 0x0403 }); }); @@ -1912,7 +2024,7 @@ describe('65c02', function() { it('BBS2 should not branch if bit 2 clear', function() { initMemory([[0x00, 0x33, [0xFB]]]); testCode([0xAF, 0x33, 0x7F], 1, {}, { - cycles: 5, + cycles: 6, pc: 0x0403 }); }); @@ -1920,7 +2032,7 @@ describe('65c02', function() { it('BBS3 should not branch if bit 3 clear', function() { initMemory([[0x00, 0x33, [0xF7]]]); testCode([0xBF, 0x33, 0x7F], 1, {}, { - cycles: 5, + cycles: 6, pc: 0x0403 }); }); @@ -1928,7 +2040,7 @@ describe('65c02', function() { it('BBS4 should not branch if bit 4 clear', function() { initMemory([[0x00, 0x33, [0xEF]]]); testCode([0xCF, 0x33, 0x7F], 1, {}, { - cycles: 5, + cycles: 6, pc: 0x0403 }); }); @@ -1936,7 +2048,7 @@ describe('65c02', function() { it('BBS5 should not branch if bit 5 clear', function() { initMemory([[0x00, 0x33, [0xDF]]]); testCode([0xDF, 0x33, 0x7F], 1, {}, { - cycles: 5, + cycles: 6, pc: 0x0403 }); }); @@ -1944,7 +2056,7 @@ describe('65c02', function() { it('BBS6 should not branch if bit 6 clear', function() { initMemory([[0x00, 0x33, [0xBF]]]); testCode([0xEF, 0x33, 0x7F], 1, {}, { - cycles: 5, + cycles: 6, pc: 0x0403 }); }); @@ -1952,7 +2064,7 @@ describe('65c02', function() { it('BBS7 should not branch if bit 7 clear', function() { initMemory([[0x00, 0x33, [0x7B]]]); testCode([0xFF, 0x33, 0x7F], 1, {}, { - cycles: 5, + cycles: 6, pc: 0x0403 }); }); diff --git a/test/js/debugger.spec.ts b/test/js/debugger.spec.ts index f38549e..6e7b01b 100644 --- a/test/js/debugger.spec.ts +++ b/test/js/debugger.spec.ts @@ -52,7 +52,7 @@ describe('Debugger', () => { it('should dump registers', () => { const regs = theDebugger.dumpRegisters(); expect(regs).toEqual( - 'A=00 X=00 Y=00 P=20 S=FF --------' + 'A=00 X=00 Y=00 P=20 S=FF --X-----' ); }); }); diff --git a/test/util/cpu.ts b/test/util/cpu.ts new file mode 100644 index 0000000..1760584 --- /dev/null +++ b/test/util/cpu.ts @@ -0,0 +1,22 @@ +import type { CpuState } from 'js/cpu6502'; +import { toHex } from 'js/util'; +import { dumpStatusRegister } from 'js/debugger'; + +const detail = !!process.env.JEST_DETAIL; + +export function toReadableState(state: CpuState) { + if (detail) { + const { pc, sp, a, x, y, s } = state; + + return { + pc: toHex(pc, 4), + sp: toHex(sp), + a: toHex(a), + x: toHex(x), + y: toHex(y), + s: dumpStatusRegister(s) + }; + } else { + return state; + } +} diff --git a/test/util/memory.ts b/test/util/memory.ts index 3c67921..f46abd3 100644 --- a/test/util/memory.ts +++ b/test/util/memory.ts @@ -1,8 +1,11 @@ -import { MemoryPages, byte } from '../../js/types'; +import { MemoryPages, byte, word } from 'js/types'; import { assertByte } from './asserts'; +export type Log = [address: word, value: byte, types: 'read'|'write'] export class TestMemory implements MemoryPages { private data: Buffer; + private logging: boolean = false; + private log: Log[] = []; constructor(private size: number) { this.data = Buffer.alloc(size << 8); @@ -20,7 +23,11 @@ export class TestMemory implements MemoryPages { assertByte(page); assertByte(off); - return this.data[(page << 8) | off]; + const val = this.data[(page << 8) | off]; + if (this.logging) { + this.log.push([page << 8 | off, val, 'read']); + } + return val; } write(page: byte, off: byte, val: byte) { @@ -28,10 +35,27 @@ export class TestMemory implements MemoryPages { assertByte(off); assertByte(val); + if (this.logging) { + this.log.push([page << 8 | off, val, 'write']); + } this.data[(page << 8) | off] = val; } reset() { + this.log = []; + } + + logStart() { + this.log = []; + this.logging = true; + } + + logStop() { + this.logging = false; + } + + getLog() { + return this.log; } }