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 {
/** 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<byte, StrictInstruction>
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' },

View File

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

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';
// From https://github.com/Klaus2m5/6502_65C02_functional_tests
import Test6502 from './roms/6502test';
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', () => {
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-----'
);
});
});

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