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.
This commit is contained in:
Will Scullin 2021-10-13 09:15:29 -07:00 committed by GitHub
parent 8ab5faee8e
commit badc2fdb74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 826 additions and 331 deletions

View File

@ -18,12 +18,19 @@ export interface CpuOptions {
} }
export interface CpuState { export interface CpuState {
/** Accumulator */
a: byte, a: byte,
/** X index */
x: byte, x: byte,
/** Y index */
y: byte, y: byte,
/** Status register */
s: byte, s: byte,
/** Program counter */
pc: word, pc: word,
/** Stack pointer */
sp: byte, sp: byte,
/** Elapsed cycles */
cycles: number cycles: number
} }
@ -74,13 +81,23 @@ export const sizes: Modes = {
/** Status register flag numbers. */ /** Status register flag numbers. */
export type flag = 0x80 | 0x40 | 0x20 | 0x10 | 0x08 | 0x04 | 0x02 | 0x01; export type flag = 0x80 | 0x40 | 0x20 | 0x10 | 0x08 | 0x04 | 0x02 | 0x01;
/**
*
*/
export type DebugInfo = { export type DebugInfo = {
/** Program counter */
pc: word, pc: word,
/** Accumulator */
ar: byte, ar: byte,
/** X index */
xr: byte, xr: byte,
/** Y index */
yr: byte, yr: byte,
/** Status register */
sr: byte, sr: byte,
/** Stack pointer */
sp: byte, sp: byte,
/** Current command */
cmd: byte[], cmd: byte[],
}; };
@ -88,6 +105,7 @@ export type DebugInfo = {
export const flags: { [key: string]: flag } = { export const flags: { [key: string]: flag } = {
N: 0x80, // Negative N: 0x80, // Negative
V: 0x40, // oVerflow V: 0x40, // oVerflow
X: 0x20, // Unused, always 1
B: 0x10, // Break B: 0x10, // Break
D: 0x08, // Decimal D: 0x08, // Decimal
I: 0x04, // Interrupt I: 0x04, // Interrupt
@ -117,7 +135,7 @@ const BLANK_PAGE: Memory = {
}; };
interface Opts { interface Opts {
rwm?: boolean; inc?: boolean;
} }
type ReadFn = () => byte; type ReadFn = () => byte;
@ -143,24 +161,44 @@ type StrictInstruction =
type Instructions = Record<byte, StrictInstruction> type Instructions = Record<byte, StrictInstruction>
type callback = (cpu: CPU6502) => void; type callback = (cpu: CPU6502) => boolean | void;
export default class CPU6502 { export default class CPU6502 {
/** 65C02 emulation mode flag */
private readonly is65C02: boolean; private readonly is65C02: boolean;
/* Registers */ /**
private pc: word = 0; // Program Counter * Registers
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
/** 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); private memPages: Memory[] = new Array(0x100);
/** Callbacks invoked on reset signal */
private resetHandlers: ResettablePageHandler[] = []; private resetHandlers: ResettablePageHandler[] = [];
/** Elapsed cycles */
private cycles = 0; private cycles = 0;
/** Command being fetched signal */
private sync = false; private sync = false;
/** Filled array of CPU operations */
private readonly opary: Instruction[]; private readonly opary: Instruction[];
constructor(options: CpuOptions = {}) { constructor(options: CpuOptions = {}) {
@ -212,40 +250,75 @@ export default class CPU6502 {
* Returns `a + b`, unless `sub` is true, in which case it performs * Returns `a + b`, unless `sub` is true, in which case it performs
* `a - b`. The status register is updated according to the result. * `a - b`. The status register is updated according to the result.
*/ */
private add(a: byte, b: byte, sub: boolean) { private add(a: byte, b: byte, sub: boolean): byte {
// KEGS const a7 = a >> 7;
let c, v; 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) { if ((this.sr & flags.D) !== 0) {
// BCD // BCD
c = (a & 0x0f) + (b & 0x0f) + (this.sr & flags.C);
if (sub) { if (sub) {
if (c < 0x10) if (c < 0x10) {
c = (c - 0x06) & 0x0f; c = (c - 0x06) & 0x0f;
}
c += (a & 0xf0) + (b & 0xf0); c += (a & 0xf0) + (b & 0xf0);
v = (c >> 1) ^ c; updateFlags(c);
if (c < 0x100) if (c < 0x100) {
c = (c + 0xa0) & 0xff; c += 0xa0;
}
} else { } else {
if (c > 0x09) if (c > 0x9) {
c = (c - 0x0a) | 0x10; // carry to MSN c = 0x10 + ((c + 0x6) & 0xf);
}
c += (a & 0xf0) + (b & 0xf0); c += (a & 0xf0) + (b & 0xf0);
v = (c >> 1) ^ c; updateFlags(c);
if (c > 0x99) if (c >= 0xa0) {
c += 0x60; c += 0x60;
}
} }
updateBCDFlags(c);
} else { } else {
c = a + b + (this.sr & flags.C); c += (a & 0xf0) + (b & 0xf0);
v = (c ^ a) & 0x80; updateFlags(c);
} }
c = c & 0xff;
if (((a ^ b) & 0x80) !== 0) { this.setFlag(flags.N, !!n);
v = 0;
}
this.setFlag(flags.C, c > 0xff);
this.setFlag(flags.V, !!v); 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. */ /** Increments `a` and returns the value, setting the status register. */
@ -258,20 +331,15 @@ export default class CPU6502 {
} }
private readBytePC(): byte { private readBytePC(): byte {
const addr = this.pc, const result = this.readByte(this.pc);
page = addr >> 8,
off = addr & 0xff;
const result = this.memPages[page].read(page, off);
this.pc = (this.pc + 1) & 0xffff; this.pc = (this.pc + 1) & 0xffff;
this.cycles++;
return result; return result;
} }
private readByte(addr: word): byte { private readByte(addr: word): byte {
this.addr = addr;
const page = addr >> 8, const page = addr >> 8,
off = addr & 0xff; off = addr & 0xff;
@ -282,14 +350,8 @@ export default class CPU6502 {
return result; 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) { private writeByte(addr: word, val: byte) {
this.addr = addr;
const page = addr >> 8, const page = addr >> 8,
off = addr & 0xff; off = addr & 0xff;
@ -335,11 +397,46 @@ export default class CPU6502 {
return (msb << 8) | lsb; 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 function
*/ */
implied() { implied = () => {
this.readByte(this.pc);
} }
/* /*
@ -363,28 +460,20 @@ export default class CPU6502 {
// $0000,X // $0000,X
readAbsoluteX = (): byte => { readAbsoluteX = (): byte => {
let addr = this.readWordPC(); const addr = this.readWordPC();
const oldPage = addr >> 8; const pc = this.addr;
addr = (addr + this.xr) & 0xffff; const addrIdx = (addr + this.xr) & 0xffff;
const newPage = addr >> 8; this.workCycleIndexedRead(pc, addr, addrIdx);
if (newPage != oldPage) { return this.readByte(addrIdx);
const off = addr & 0xff;
this.readByte(oldPage << 8 | off);
}
return this.readByte(addr);
} }
// $0000,Y // $0000,Y
readAbsoluteY = (): byte => { readAbsoluteY = (): byte => {
let addr = this.readWordPC(); const addr = this.readWordPC();
const oldPage = addr >> 8; const pc = this.addr;
addr = (addr + this.yr) & 0xffff; const addrIdx = (addr + this.yr) & 0xffff;
const newPage = addr >> 8; this.workCycleIndexedRead(pc, addr, addrIdx);
if (newPage != oldPage) { return this.readByte(addrIdx);
const off = addr & 0xff;
this.readByte(oldPage << 8 | off);
}
return this.readByte(addr);
} }
// $00,X // $00,X
@ -411,15 +500,12 @@ export default class CPU6502 {
// ($00),Y // ($00),Y
readZeroPageIndirectY = (): byte => { readZeroPageIndirectY = (): byte => {
let addr = this.readZPWord(this.readBytePC()); const zpAddr = this.readBytePC();
const oldPage = addr >> 8; const pc = this.addr;
addr = (addr + this.yr) & 0xffff; const addr = this.readZPWord(zpAddr);
const newPage = addr >> 8; const addrIdx = (addr + this.yr) & 0xffff;
if (newPage != oldPage) { this.workCycleIndexedRead(pc, addr, addrIdx);
const off = addr & 0xff; return this.readByte(addrIdx);
this.readByte(oldPage << 8 | off);
}
return this.readByte(addr);
} }
// ($00) (65C02) // ($00) (65C02)
@ -443,22 +529,20 @@ export default class CPU6502 {
// $0000,X // $0000,X
writeAbsoluteX = (val: byte) => { writeAbsoluteX = (val: byte) => {
let addr = this.readWordPC(); const addr = this.readWordPC();
const oldPage = addr >> 8; const pc = this.addr;
addr = (addr + this.xr) & 0xffff; const addrIdx = (addr + this.xr) & 0xffff;
const off = addr & 0xff; this.workCycleIndexedWrite(pc, addr, addrIdx);
this.readByte(oldPage << 8 | off); this.writeByte(addrIdx, val);
this.writeByte(addr, val);
} }
// $0000,Y // $0000,Y
writeAbsoluteY = (val: byte) => { writeAbsoluteY = (val: byte) => {
let addr = this.readWordPC(); const addr = this.readWordPC();
const oldPage = addr >> 8; const pc = this.addr;
addr = (addr + this.yr) & 0xffff; const addrIdx = (addr + this.yr) & 0xffff;
const off = addr & 0xff; this.workCycleIndexedWrite(pc, addr, addrIdx);
this.readByte(oldPage << 8 | off); this.writeByte(addrIdx, val);
this.writeByte(addr, val);
} }
// $00,X // $00,X
@ -485,12 +569,12 @@ export default class CPU6502 {
// ($00),Y // ($00),Y
writeZeroPageIndirectY = (val: byte) => { writeZeroPageIndirectY = (val: byte) => {
let addr = this.readZPWord(this.readBytePC()); const zpAddr = this.readBytePC();
const oldPage = addr >> 8; const pc = this.addr;
addr = (addr + this.yr) & 0xffff; const addr = this.readZPWord(zpAddr);
const off = addr & 0xff; const addrIdx = (addr + this.yr) & 0xffff;
this.readByte(oldPage << 8 | off); this.workCycleIndexedWrite(pc, addr, addrIdx);
this.writeByte(addr, val); this.writeByte(addrIdx, val);
} }
// ($00) (65C02) // ($00) (65C02)
@ -527,28 +611,46 @@ export default class CPU6502 {
// ($0000) (65C02) // ($0000) (65C02)
readAddrAbsoluteIndirect = (): word => { readAddrAbsoluteIndirect = (): word => {
const lsb = this.readBytePC(); const addr = this.readWord(this.readWordPC());
const msb = this.readBytePC(); this.readByte(this.addr);
this.readByte(this.pc); return addr;
return this.readWord(msb << 8 | lsb);
} }
// $0000,X // $0000,X
readAddrAbsoluteX = (opts: Opts = {}): word => { readAddrAbsoluteX = (opts?: Opts): word => {
const addr = this.readWordPC(); let addr = this.readWordPC();
if (!this.is65C02 || opts.rwm) { const page = addr & 0xff00;
this.readByte(addr); 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 { } else {
this.readByte(this.pc); const off = addr & 0x00ff;
this.readByte(page | off);
} }
return (addr + this.xr) & 0xffff; return addr;
} }
// $(0000,X) (65C02) // $(0000,X) (65C02)
readAddrAbsoluteXIndirect = (): word => { readAddrAbsoluteXIndirect = (): word => {
const address = this.readWordPC(); const lsb = this.readBytePC();
this.readByte(this.pc); const pc = this.addr;
return this.readWord((address + this.xr) & 0xffff); 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 */ /* Break */
@ -615,9 +717,9 @@ export default class CPU6502 {
} }
inc = (readAddrFn: ReadAddrFn) => { inc = (readAddrFn: ReadAddrFn) => {
const addr = readAddrFn({ rwm: true }); const addr = readAddrFn({ inc: true });
const oldVal = this.readByte(addr); const oldVal = this.readByte(addr);
this.writeByte(addr, oldVal); this.workCycle(addr, oldVal);
const val = this.increment(oldVal); const val = this.increment(oldVal);
this.writeByte(addr, val); this.writeByte(addr, val);
} }
@ -641,9 +743,9 @@ export default class CPU6502 {
} }
dec = (readAddrFn: ReadAddrFn) => { dec = (readAddrFn: ReadAddrFn) => {
const addr = readAddrFn({ rwm: true }); const addr = readAddrFn({ inc: true});
const oldVal = this.readByte(addr); const oldVal = this.readByte(addr);
this.writeByte(addr, oldVal); this.workCycle(addr, oldVal);
const val = this.decrement(oldVal); const val = this.decrement(oldVal);
this.writeByte(addr, val); this.writeByte(addr, val);
} }
@ -672,9 +774,9 @@ export default class CPU6502 {
} }
asl = (readAddrFn: ReadAddrFn) => { asl = (readAddrFn: ReadAddrFn) => {
const addr = readAddrFn({ rwm: true }); const addr = readAddrFn();
const oldVal = this.readByte(addr); const oldVal = this.readByte(addr);
this.writeByte(addr, oldVal); this.workCycle(addr, oldVal);
const val = this.shiftLeft(oldVal); const val = this.shiftLeft(oldVal);
this.writeByte(addr, val); this.writeByte(addr, val);
} }
@ -691,9 +793,9 @@ export default class CPU6502 {
} }
lsr = (readAddrFn: ReadAddrFn) => { lsr = (readAddrFn: ReadAddrFn) => {
const addr = readAddrFn({ rwm: true }); const addr = readAddrFn();
const oldVal = this.readByte(addr); const oldVal = this.readByte(addr);
this.writeByte(addr, oldVal); this.workCycle(addr, oldVal);
const val = this.shiftRight(oldVal); const val = this.shiftRight(oldVal);
this.writeByte(addr, val); this.writeByte(addr, val);
} }
@ -711,9 +813,9 @@ export default class CPU6502 {
} }
rol = (readAddrFn: ReadAddrFn) => { rol = (readAddrFn: ReadAddrFn) => {
const addr = readAddrFn({ rwm: true }); const addr = readAddrFn();
const oldVal = this.readByte(addr); const oldVal = this.readByte(addr);
this.writeByte(addr, oldVal); this.workCycle(addr, oldVal);
const val = this.rotateLeft(oldVal); const val = this.rotateLeft(oldVal);
this.writeByte(addr, val); this.writeByte(addr, val);
} }
@ -731,9 +833,9 @@ export default class CPU6502 {
} }
ror = (readAddrFn: ReadAddrFn) => { ror = (readAddrFn: ReadAddrFn) => {
const addr = readAddrFn({ rwm: true }); const addr = readAddrFn();
const oldVal = this.readByte(addr); const oldVal = this.readByte(addr);
this.writeByte(addr, oldVal); this.workCycle(addr, oldVal);
const val = this.rotateRight(oldVal); const val = this.rotateRight(oldVal);
this.writeByte(addr, val); this.writeByte(addr, val);
} }
@ -831,12 +933,12 @@ export default class CPU6502 {
const off = this.readBytePC(); // changes pc const off = this.readBytePC(); // changes pc
if ((f & this.sr) !== 0) { if ((f & this.sr) !== 0) {
this.readByte(this.pc); this.readByte(this.pc);
const oldPage = this.pc >> 8; const oldPage = this.pc & 0xff00;
this.pc += off > 127 ? off - 256 : off; this.pc += off > 127 ? off - 256 : off;
this.pc &= 0xffff; this.pc &= 0xffff;
const newPage = this.pc >> 8; const newPage = this.pc & 0xff00;
const newOff = this.pc & 0xff; 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 const off = this.readBytePC(); // changes pc
if ((f & this.sr) === 0) { if ((f & this.sr) === 0) {
this.readByte(this.pc); this.readByte(this.pc);
const oldPage = this.pc >> 8; const oldPage = this.pc & 0xff00;
this.pc += off > 127 ? off - 256 : off; this.pc += off > 127 ? off - 256 : off;
this.pc &= 0xffff; this.pc &= 0xffff;
const newPage = this.pc >> 8; const newPage = this.pc & 0xff00;
const newOff = this.pc & 0xff; 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) => { bbr = (b: byte) => {
const zpAddr = this.readBytePC(); const zpAddr = this.readBytePC();
const val = this.readByte(zpAddr); const val = this.readByte(zpAddr);
this.readByte(zpAddr); this.writeByte(zpAddr, val);
const off = this.readBytePC(); // changes pc 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) { if (((1 << b) & val) === 0) {
const oldPc = this.pc; this.pc = newPC;
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);
}
} }
} }
bbs = (b: byte) => { bbs = (b: byte) => {
const zpAddr = this.readBytePC(); const zpAddr = this.readBytePC();
const val = this.readByte(zpAddr); const val = this.readByte(zpAddr);
this.readByte(zpAddr); this.writeByte(zpAddr, val);
const off = this.readBytePC(); // changes pc 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) { if (((1 << b) & val) !== 0) {
const oldPc = this.pc; this.pc = newPC;
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);
}
} }
} }
@ -920,7 +1018,7 @@ export default class CPU6502 {
php = () => { this.readByte(this.pc); this.pushByte(this.sr | flags.B); } 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 */ /* Jump */
jmp = (readAddrFn: ReadAddrFn) => { jmp = (readAddrFn: ReadAddrFn) => {
@ -949,7 +1047,7 @@ export default class CPU6502 {
rti = () => { rti = () => {
this.readByte(this.pc); this.readByte(this.pc);
this.readByte(0x0100 | this.sp); this.readByte(0x0100 | this.sp);
this.sr = this.pullByte() & ~flags.B; this.sr = (this.pullByte() & ~flags.B) | flags.X;
this.pc = this.pullWordRaw(); this.pc = this.pullWordRaw();
} }
@ -965,9 +1063,8 @@ export default class CPU6502 {
} }
/* No-Op */ /* No-Op */
nop = (impliedFn: ImpliedFn) => { nop = (readFn: ImpliedFn | ReadFn) => {
this.readByte(this.pc); readFn();
impliedFn();
} }
private unknown(b: byte) { private unknown(b: byte) {
@ -998,9 +1095,9 @@ export default class CPU6502 {
public step(cb?: callback) { public step(cb?: callback) {
this.sync = true; this.sync = true;
const op = this.opary[this.readBytePC()]; this.op = this.opary[this.readBytePC()];
this.sync = false; this.sync = false;
op.op(op.modeFn); this.op.op(this.op.modeFn);
cb?.(this); cb?.(this);
} }
@ -1008,9 +1105,9 @@ export default class CPU6502 {
public stepN(n: number, cb?: callback) { public stepN(n: number, cb?: callback) {
for (let idx = 0; idx < n; idx++) { for (let idx = 0; idx < n; idx++) {
this.sync = true; this.sync = true;
const op = this.opary[this.readBytePC()]; this.op = this.opary[this.readBytePC()];
this.sync = false; this.sync = false;
op.op(op.modeFn); this.op.op(this.op.modeFn);
if (cb?.(this)) { if (cb?.(this)) {
return; return;
@ -1023,9 +1120,9 @@ export default class CPU6502 {
while (this.cycles < end) { while (this.cycles < end) {
this.sync = true; this.sync = true;
const op = this.opary[this.readBytePC()]; this.op = this.opary[this.readBytePC()];
this.sync = false; 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) { while (this.cycles < end) {
this.sync = true; this.sync = true;
const op = this.opary[this.readBytePC()]; this.op = this.opary[this.readBytePC()];
this.sync = false; this.sync = false;
op.op(op.modeFn); this.op.op(this.op.modeFn);
if (cb?.(this)) { if (cb?.(this)) {
return; return;
@ -1054,7 +1151,7 @@ export default class CPU6502 {
public reset() { public reset() {
// cycles = 0; // cycles = 0;
this.sr = 0x20; this.sr = flags.X;
this.sp = 0xff; this.sp = 0xff;
this.ar = 0; this.ar = 0;
this.yr = 0; this.yr = 0;
@ -1099,13 +1196,13 @@ export default class CPU6502 {
} }
public getDebugInfo(): DebugInfo { public getDebugInfo(): DebugInfo {
const b = this.readByteDebug(this.pc); const b = this.read(this.pc);
const op = this.opary[b]; const op = this.opary[b];
const size = sizes[op.mode]; const size = sizes[op.mode];
const cmd = new Array(size); const cmd = new Array(size);
cmd[0] = b; cmd[0] = b;
for (let idx = 1; idx < size; idx++) { for (let idx = 1; idx < size; idx++) {
cmd[idx] = this.readByteDebug(this.pc + idx); cmd[idx] = this.read(this.pc + idx);
} }
return { return {
@ -1509,17 +1606,17 @@ export default class CPU6502 {
0x02: { name: 'NOP', op: this.nop, modeFn: this.readImmediate, mode: 'immediate' }, 0x02: { name: 'NOP', op: this.nop, modeFn: this.readImmediate, mode: 'immediate' },
0x22: { 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' }, 0x42: { name: 'NOP', op: this.nop, modeFn: this.readImmediate, mode: 'immediate' },
0x44: { 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.readImmediate, mode: 'immediate' }, 0x54: { name: 'NOP', op: this.nop, modeFn: this.readZeroPageX, mode: 'immediate' },
0x62: { name: 'NOP', op: this.nop, modeFn: this.readImmediate, mode: 'immediate' }, 0x62: { name: 'NOP', op: this.nop, modeFn: this.readImmediate, mode: 'immediate' },
0x82: { 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' }, 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' }, 0xE2: { name: 'NOP', op: this.nop, modeFn: this.readImmediate, mode: 'immediate' },
0xF4: { name: 'NOP', op: this.nop, modeFn: this.readImmediate, mode: 'immediate' }, 0xF4: { name: 'NOP', op: this.nop, modeFn: this.readZeroPageX, mode: 'immediate' },
0x5C: { name: 'NOP', op: this.nop, modeFn: this.readAbsolute, mode: 'absolute' }, 0x5C: { name: 'NOP', op: this.nop, modeFn: this.readNop, mode: 'absolute' },
0xDC: { name: 'NOP', op: this.nop, modeFn: this.readAbsolute, mode: 'absolute' }, 0xDC: { name: 'NOP', op: this.nop, modeFn: this.readNop, mode: 'absolute' },
0xFC: { name: 'NOP', op: this.nop, modeFn: this.readAbsolute, mode: 'absolute' }, 0xFC: { name: 'NOP', op: this.nop, modeFn: this.readNop, mode: 'absolute' },
// PHX // PHX
0xDA: { name: 'PHX', op: this.phx, modeFn: this.implied, mode: 'implied' }, 0xDA: { name: 'PHX', op: this.phx, modeFn: this.implied, mode: 'implied' },

View File

@ -14,6 +14,18 @@ type breakpointFn = (info: DebugInfo) => boolean
const alwaysBreak = (_info: DebugInfo) => { return true; }; 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 { export default class Debugger {
private cpu: CPU6502; private cpu: CPU6502;
private verbose = false; private verbose = false;
@ -138,14 +150,7 @@ export default class Debugger {
' P=' + toHex(sr), ' P=' + toHex(sr),
' S=' + toHex(sp), ' S=' + toHex(sp),
' ', ' ',
((sr & flags.N) ? 'N' : '-'), dumpStatusRegister(sr),
((sr & flags.V) ? 'V' : '-'),
'-',
((sr & flags.B) ? 'B' : '-'),
((sr & flags.D) ? 'D' : '-'),
((sr & flags.I) ? 'I' : '-'),
((sr & flags.Z) ? 'Z' : '-'),
((sr & flags.C) ? 'C' : '-')
].join(''); ].join('');
} }

235
test/cpu-tom-harte.spec.ts Normal file
View File

@ -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(); });
}

View File

@ -1,5 +1,5 @@
import CPU6502 from '../js/cpu6502'; import CPU6502 from '../js/cpu6502';
// From https://github.com/Klaus2m5/6502_65C02_functional_tests
import Test6502 from './roms/6502test'; import Test6502 from './roms/6502test';
import Test65C02 from './roms/65C02test'; import Test65C02 from './roms/65C02test';

File diff suppressed because it is too large Load Diff

View File

@ -52,7 +52,7 @@ describe('Debugger', () => {
it('should dump registers', () => { it('should dump registers', () => {
const regs = theDebugger.dumpRegisters(); const regs = theDebugger.dumpRegisters();
expect(regs).toEqual( expect(regs).toEqual(
'A=00 X=00 Y=00 P=20 S=FF --------' 'A=00 X=00 Y=00 P=20 S=FF --X-----'
); );
}); });
}); });

22
test/util/cpu.ts Normal file
View File

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

View File

@ -1,8 +1,11 @@
import { MemoryPages, byte } from '../../js/types'; import { MemoryPages, byte, word } from 'js/types';
import { assertByte } from './asserts'; import { assertByte } from './asserts';
export type Log = [address: word, value: byte, types: 'read'|'write']
export class TestMemory implements MemoryPages { export class TestMemory implements MemoryPages {
private data: Buffer; private data: Buffer;
private logging: boolean = false;
private log: Log[] = [];
constructor(private size: number) { constructor(private size: number) {
this.data = Buffer.alloc(size << 8); this.data = Buffer.alloc(size << 8);
@ -20,7 +23,11 @@ export class TestMemory implements MemoryPages {
assertByte(page); assertByte(page);
assertByte(off); 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) { write(page: byte, off: byte, val: byte) {
@ -28,10 +35,27 @@ export class TestMemory implements MemoryPages {
assertByte(off); assertByte(off);
assertByte(val); assertByte(val);
if (this.logging) {
this.log.push([page << 8 | off, val, 'write']);
}
this.data[(page << 8) | off] = val; this.data[(page << 8) | off] = val;
} }
reset() { reset() {
this.log = [];
}
logStart() {
this.log = [];
this.logging = true;
}
logStop() {
this.logging = false;
}
getLog() {
return this.log;
} }
} }