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; } }