From c24c01539d20d57428ed7230929d4cf5f407b9d4 Mon Sep 17 00:00:00 2001 From: Will Scullin Date: Wed, 22 Dec 2021 10:37:05 -0800 Subject: [PATCH] No Opcode left behind (#92) * Better 65C02 support * NMOS illegal opcodes --- js/apple2.ts | 10 +- js/cpu6502.ts | 509 +++++++++++++++++++++++++++++++++++-- test/cpu-tom-harte.spec.ts | 73 +++++- test/cpu.spec.ts | 4 +- test/cpu6502.spec.ts | 16 +- 5 files changed, 561 insertions(+), 51 deletions(-) diff --git a/js/apple2.ts b/js/apple2.ts index da103fa..d72aced 100644 --- a/js/apple2.ts +++ b/js/apple2.ts @@ -17,7 +17,11 @@ import { } from './gl'; import ROM from './roms/rom'; import { Apple2IOState } from './apple2io'; -import CPU6502, { CpuState } from './cpu6502'; +import CPU6502, { + CpuState, + FLAVOR_6502, + FLAVOR_ROCKWELL_65C02, +} from './cpu6502'; import MMU, { MMUState } from './mmu'; import RAM, { RAMState } from './ram'; @@ -92,7 +96,9 @@ export class Apple2 implements Restorable, DebuggerContainer { const HiresPage = options.gl ? HiresPageGL : HiresPage2D; const VideoModes = options.gl ? VideoModesGL : VideoModes2D; - this.cpu = new CPU6502({ '65C02': options.enhanced }); + this.cpu = new CPU6502({ + flavor: options.enhanced ? FLAVOR_ROCKWELL_65C02 : FLAVOR_6502 + }); this.vm = new VideoModes(options.canvas, options.e); const [{ default: Apple2ROM }, { default: characterRom }] = await Promise.all([ diff --git a/js/cpu6502.ts b/js/cpu6502.ts index b01be6e..781fa51 100644 --- a/js/cpu6502.ts +++ b/js/cpu6502.ts @@ -1,8 +1,24 @@ -import { Memory, MemoryPages, byte, word } from './types'; -import { debug, toHex } from './util'; +import { Memory, MemberOf, MemoryPages, byte, word } from './types'; +import { toHex } from './util'; + +export const FLAVOR_6502 = '6502'; +export const FLAVOR_ROCKWELL_65C02 = 'rockwell65c02'; +export const FLAVOR_WDC_65C02 = 'wdc65c02'; + +export const FLAVORS_65C02 = [ + FLAVOR_ROCKWELL_65C02, + FLAVOR_WDC_65C02 +]; + +export const FLAVORS = [ + FLAVOR_6502, + ...FLAVORS_65C02 +]; + +export type Flavor = MemberOf; export interface CpuOptions { - '65C02'?: boolean; + flavor?: Flavor } export interface CpuState { @@ -152,6 +168,8 @@ type Instructions = Record type callback = (cpu: CPU6502) => boolean | void; export default class CPU6502 { + /** flavor */ + private readonly flavor: Flavor; /** 65C02 emulation mode flag */ private readonly is65C02: boolean; @@ -186,11 +204,17 @@ export default class CPU6502 { /** Command being fetched signal */ private sync = false; + /** Processor is in WAI mode */ + private wait = false; + /** Processor is in STP mode */ + private stop = false; + /** Filled array of CPU operations */ private readonly opary: Instruction[]; - constructor(options: CpuOptions = {}) { - this.is65C02 = options['65C02'] ? true : false; + constructor({ flavor }: CpuOptions = {}) { + this.flavor = flavor ?? FLAVOR_6502; + this.is65C02 = !!flavor && FLAVORS_65C02.includes(flavor); this.memPages.fill(BLANK_PAGE); this.memPages.fill(BLANK_PAGE); @@ -198,17 +222,35 @@ export default class CPU6502 { // Create this CPU's instruction table let ops = { ...this.OPS_6502 }; - if (this.is65C02) { - ops = { ...ops, ...this.OPS_65C02 }; + + switch (this.flavor) { + case FLAVOR_WDC_65C02: + ops = { + ...ops, + ...this.OPS_65C02, + ...this.OPS_WDC_65C02, + }; + break; + case FLAVOR_ROCKWELL_65C02: + ops = { + ...ops, + ...this.OPS_65C02, + ...this.OPS_ROCKWELL_65C02, + }; + break; + default: + ops = { + ...ops, + ...this.OPS_NMOS_6502, + }; } // Certain browsers benefit from using arrays over maps - const opary: Instruction[] = new Array(0x100); + this.opary = new Array(0x100); for (let idx = 0; idx < 0x100; idx++) { - opary[idx] = ops[idx] || this.unknown(idx); + this.opary[idx] = ops[idx] || this.unknown(idx); } - this.opary = opary; } /** @@ -262,7 +304,11 @@ export default class CPU6502 { n = bin >> 7; z = !bin; if (this.op.mode === 'immediate') { - this.readByte(sub ? 0xB8 : 0x7F); + if (this.flavor === FLAVOR_WDC_65C02) { + this.readByte(sub ? 0xB8 : 0x7F); + } else { // rockwell65c02 + this.readByte(sub ? 0xB1 : 0x59); + } } else { this.readByte(this.addr); } @@ -625,6 +671,34 @@ export default class CPU6502 { return addr; }; + // $0000,Y (NMOS 6502) + readAddrAbsoluteY = (): word => { + let addr = this.readWordPC(); + const page = addr & 0xff00; + addr = (addr + this.yr) & 0xffff; + const off = addr & 0x00ff; + this.readByte(page | off); + return addr; + }; + + // ($00,X) (NMOS 6502) + readAddrZeroPageXIndirect = () => { + const zpAddr = this.readBytePC(); + this.readByte(zpAddr); + return this.readZPWord((zpAddr + this.xr) & 0xff); + }; + + // ($00),Y (NMOS 6502) + readAddrZeroPageIndirectY = () => { + const zpAddr = this.readBytePC(); + const addr = this.readZPWord(zpAddr); + const addrIdx = (addr + this.yr) & 0xffff; + const oldPage = addr & 0xff00; + const off = addrIdx & 0xff; + this.readByte(oldPage | off); + return addrIdx; + }; + // $(0000,X) (65C02) readAddrAbsoluteXIndirect = (): word => { const lsb = this.readBytePC(); @@ -635,12 +709,17 @@ export default class CPU6502 { return this.readWord(addr); }; - // 5C, DC, FC NOP + // 5C, DC, FC NOP (65C02) readNop = (): void => { this.readWordPC(); this.readByte(this.addr); }; + // NOP (65C02) + readNopImplied = (): void => { + // Op is 1 cycle + }; + /* Break */ brk = (readFn: ReadFn) => { readFn(); @@ -653,6 +732,20 @@ export default class CPU6502 { this.pc = this.readWord(loc.BRK); }; + /* Stop (65C02) */ + stp = () => { + this.stop = true; + this.readByte(this.pc); + this.readByte(this.pc); + }; + + /* Wait (65C02) */ + wai = () => { + this.wait = true; + this.readByte(this.pc); + this.readByte(this.pc); + }; + /* Load Accumulator */ lda = (readFn: ReadFn) => { this.ar = this.testNZ(readFn()); @@ -1055,32 +1148,219 @@ export default class CPU6502 { readFn(); }; + /* NMOS 6502 Illegal opcodes */ + + /* ASO = ASL + ORA */ + aso = (readAddrFn: ReadAddrFn) => { + const addr = readAddrFn(); + const oldVal = this.readByte(addr); + this.workCycle(addr, oldVal); + const val = this.shiftLeft(oldVal); + this.writeByte(addr, val); + this.ar |= val; + this.testNZ(this.ar); + }; + + /* RLA = ROL + AND */ + rla = (readAddrFn: ReadAddrFn) => { + const addr = readAddrFn(); + const oldVal = this.readByte(addr); + this.workCycle(addr, oldVal); + const val = this.rotateLeft(oldVal); + this.writeByte(addr, val); + this.ar &= val; + this.testNZ(this.ar); + }; + + /* LSE = LSR + EOR */ + lse = (readAddrFn: ReadAddrFn) => { + const addr = readAddrFn(); + const oldVal = this.readByte(addr); + this.workCycle(addr, oldVal); + const val = this.shiftRight(oldVal); + this.writeByte(addr, val); + this.ar ^= val; + this.testNZ(this.ar); + }; + + /* RRA = ROR + ADC */ + rra = (readAddrFn: ReadAddrFn) => { + const addr = readAddrFn(); + const oldVal = this.readByte(addr); + this.workCycle(addr, oldVal); + const val = this.rotateRight(oldVal); + this.writeByte(addr, val); + this.ar = this.add(this.ar, val, false); + }; + + /* AXS = Store A & X */ + axs = (writeFn: WriteFn) => { + writeFn(this.ar & this.xr); + }; + + /* LAX = Load A & X */ + lax = (readFn: ReadFn) => { + const val = readFn(); + this.ar = val; + this.xr = val; + this.testNZ(val); + }; + + /* DCM = DEC + CMP */ + dcm = (readAddrFn: ReadAddrFn) => { + const addr = readAddrFn({ inc: true}); + const oldVal = this.readByte(addr); + this.workCycle(addr, oldVal); + const val = this.decrement(oldVal); + this.writeByte(addr, val); + this.compare(this.ar, val); + }; + + /* INS = INC + SBC */ + ins = (readAddrFn: ReadAddrFn) => { + const addr = readAddrFn({ inc: true}); + const oldVal = this.readByte(addr); + this.workCycle(addr, oldVal); + const val = this.increment(oldVal); + this.writeByte(addr, val); + this.ar = this.add(this.ar, val ^ 0xff, true); + }; + + /* ALR = AND + LSR */ + alr = (readFn: ReadFn) => { + const val = readFn() & this.ar; + this.ar = this.shiftRight(val); + }; + + /* ARR = AND + ROR */ + arr = (readFn: ReadFn) => { + const val = readFn() & this.ar; + const ah = val >> 4; + const al = val & 0xf; + const b7 = val >> 7; + const b6 = (val >> 6) & 0x1; + this.ar = this.rotateRight(val); + let c = !!b7; + const v = !!(b7 ^ b6); + if (this.sr & flags.D) { + if (al + (al & 0x1) > 0x5) { + this.ar = (this.ar & 0xf0) | ((this.ar + 0x6) & 0xf); + } + if (ah + (ah & 0x1) > 5) { + c = true; + this.ar =((this.ar + 0x60) & 0xff); + } + } + this.setFlag(flags.V, v); + this.setFlag(flags.C, c); + }; + + /* XAA = TAX + AND */ + xaa = (readFn: ReadFn) => { + const val = readFn(); + this.ar = (this.xr & 0xEE) | (this.xr & this.ar & 0x11); + this.ar = this.testNZ(this.ar & val); + }; + + /** OAL = ORA + AND */ + oal = (readFn: ReadFn) => { + this.ar |= 0xEE; + const val = this.testNZ(this.ar & readFn()); + this.ar = val; + this.xr = val; + }; + + /* SAX = A & X + SBC */ + sax = (readFn: ReadFn) => { + const a = this.xr & this.ar; + let b = readFn(); + b = (b ^ 0xff); + const c = a + b + 1; + this.setFlag(flags.C, c > 0xff); + this.xr = this.testNZ(c & 0xff); + }; + + /* TAS = X & Y -> S */ + tas = (readAddrFn: ReadAddrFn) => { + const addr = readAddrFn(); + let val = this.xr & this.ar; + this.sp = val; + const msb = addr >> 8; + val = val & ((msb + 1) & 0xff); + this.writeByte(addr, val); + }; + + /* SAY = Y & AH + 1 */ + say = (readAddrFn: ReadAddrFn) => { + const addr = readAddrFn(); + const msb = addr >> 8; + const val = this.yr & ((msb + 1) & 0xff); + this.writeByte(addr, val); + }; + + /* XAS = X & AH + 1 */ + xas = (readAddrFn: ReadAddrFn) => { + const addr = readAddrFn(); + const msb = addr >> 8; + const val = this.xr & ((msb + 1) & 0xff); + this.writeByte(addr, val); + }; + + /* AXA = X & AH + 1 */ + axa = (readAddrFn: ReadAddrFn) => { + const addr = readAddrFn(); + let val = this.xr & this.ar; + const msb = addr >> 8; + val = val & ((msb + 1) & 0xff); + this.writeByte(addr, val); + }; + + /* ANC = AND with carry */ + anc = (readFn: ReadFn) => { + this.ar = this.testNZ(this.ar & readFn()); + const c = !!(this.ar & 0x80); + this.setFlag(flags.C, c); + }; + + /* LAS = RD & SP -> A, X, S */ + las = (readFn: ReadFn) => { + const val = this.sp & readFn(); + this.sp = val; + this.xr = val; + this.ar = this.testNZ(val); + }; + + /* SKB/SKW */ + skp = (readFn: ReadFn) => { + readFn(); + }; + + /* HLT */ + hlt = (_impliedFn: ImpliedFn) => { + this.readByte(this.pc); + this.readByte(this.pc); + // PC shouldn't have advanced + this.pc = (--this.pc) & 0xffff; + this.stop = true; + }; + private unknown(b: byte) { let unk: StrictInstruction; - if (this.is65C02) { + // Default behavior is a 1 cycle NOP unk = { name: 'NOP', op: this.nop, - modeFn: this.implied, + modeFn: this.readNopImplied, mode: 'implied', }; } else { - const cpu = this; - unk = { - name: '???', - op: function (_impliedFn: ImpliedFn) { - debug('Unknown OpCode: ' + toHex(b) + - ' at ' + toHex(cpu.pc - 1, 4)); - }, - modeFn: this.implied, - mode: 'implied' - }; + // All 6502 Instructions should be defined + throw new Error(`Missing ${toHex(b)}`); } return unk; } - public step(cb?: callback) { this.sync = true; this.op = this.opary[this.readBytePC()]; @@ -1145,6 +1425,8 @@ export default class CPU6502 { this.yr = 0; this.xr = 0; this.pc = this.readWord(loc.RESET); + this.wait = false; + this.stop = false; for (let idx = 0; idx < this.resetHandlers.length; idx++) { this.resetHandlers[idx].reset(); @@ -1161,6 +1443,7 @@ export default class CPU6502 { } this.setFlag(flags.I, true); this.pc = this.readWord(loc.BRK); + this.wait = false; } } @@ -1173,6 +1456,7 @@ export default class CPU6502 { } this.setFlag(flags.I, true); this.pc = this.readWord(loc.NMI); + this.wait = false; } public getPC() { @@ -1207,6 +1491,14 @@ export default class CPU6502 { return this.sync; } + public getStop() { + return this.stop; + } + + public getWait() { + return this.wait; + } + public getCycles() { return this.cycles; } @@ -1652,4 +1944,171 @@ export default class CPU6502 { 0x04: { name: 'TSB', op: this.tsb, modeFn: this.readAddrZeroPage, mode: 'zeroPage' }, 0x0C: { name: 'TSB', op: this.tsb, modeFn: this.readAddrAbsolute, mode: 'absolute' } }; + + OPS_NMOS_6502: Instructions = { + // ASO + 0x0F: { name: 'ASO', op: this.aso, modeFn: this.readAddrAbsolute, mode: 'absolute' }, + 0x1F: { name: 'ASO', op: this.aso, modeFn: this.readAddrAbsoluteX, mode: 'absoluteX' }, + 0x1B: { name: 'ASO', op: this.aso, modeFn: this.readAddrAbsoluteY, mode: 'absoluteY' }, + 0x07: { name: 'ASO', op: this.aso, modeFn: this.readAddrZeroPage, mode: 'zeroPage' }, + 0x17: { name: 'ASO', op: this.aso, modeFn: this.readAddrZeroPageX, mode: 'zeroPageX' }, + 0x03: { name: 'ASO', op: this.aso, modeFn: this.readAddrZeroPageXIndirect, mode: 'zeroPageXIndirect' }, + 0x13: { name: 'ASO', op: this.aso, modeFn: this.readAddrZeroPageIndirectY, mode: 'zeroPageIndirectY' }, + + // RLA + 0x2F: { name: 'RLA', op: this.rla, modeFn: this.readAddrAbsolute, mode: 'absolute' }, + 0x3F: { name: 'RLA', op: this.rla, modeFn: this.readAddrAbsoluteX, mode: 'absoluteX' }, + 0x3B: { name: 'RLA', op: this.rla, modeFn: this.readAddrAbsoluteY, mode: 'absoluteY' }, + 0x27: { name: 'RLA', op: this.rla, modeFn: this.readAddrZeroPage, mode: 'zeroPage' }, + 0x37: { name: 'RLA', op: this.rla, modeFn: this.readAddrZeroPageX, mode: 'zeroPageX' }, + 0x23: { name: 'RLA', op: this.rla, modeFn: this.readAddrZeroPageXIndirect, mode: 'zeroPageXIndirect' }, + 0x33: { name: 'RLA', op: this.rla, modeFn: this.readAddrZeroPageIndirectY, mode: 'zeroPageIndirectY' }, + + // LSE + 0x4F: { name: 'LSE', op: this.lse, modeFn: this.readAddrAbsolute, mode: 'absolute' }, + 0x5F: { name: 'LSE', op: this.lse, modeFn: this.readAddrAbsoluteX, mode: 'absoluteX' }, + 0x5B: { name: 'LSE', op: this.lse, modeFn: this.readAddrAbsoluteY, mode: 'absoluteY' }, + 0x47: { name: 'LSE', op: this.lse, modeFn: this.readAddrZeroPage, mode: 'zeroPage' }, + 0x57: { name: 'LSE', op: this.lse, modeFn: this.readAddrZeroPageX, mode: 'zeroPageX' }, + 0x43: { name: 'LSE', op: this.lse, modeFn: this.readAddrZeroPageXIndirect, mode: 'zeroPageXIndirect' }, + 0x53: { name: 'LSE', op: this.lse, modeFn: this.readAddrZeroPageIndirectY, mode: 'zeroPageIndirectY' }, + + // RRA + 0x6F: { name: 'RRA', op: this.rra, modeFn: this.readAddrAbsolute, mode: 'absolute' }, + 0x7F: { name: 'RRA', op: this.rra, modeFn: this.readAddrAbsoluteX, mode: 'absoluteX' }, + 0x7B: { name: 'RRA', op: this.rra, modeFn: this.readAddrAbsoluteY, mode: 'absoluteY' }, + 0x67: { name: 'RRA', op: this.rra, modeFn: this.readAddrZeroPage, mode: 'zeroPage' }, + 0x77: { name: 'RRA', op: this.rra, modeFn: this.readAddrZeroPageX, mode: 'zeroPageX' }, + 0x63: { name: 'RRA', op: this.rra, modeFn: this.readAddrZeroPageXIndirect, mode: 'zeroPageXIndirect' }, + 0x73: { name: 'RRA', op: this.rra, modeFn: this.readAddrZeroPageIndirectY, mode: 'zeroPageIndirectY' }, + + // AXS + 0x8F: { name: 'AXS', op: this.axs, modeFn: this.writeAbsolute, mode: 'absolute'}, + 0x87: { name: 'AXS', op: this.axs, modeFn: this.writeZeroPage, mode: 'zeroPage'}, + 0x97: { name: 'AXS', op: this.axs, modeFn: this.writeZeroPageY, mode: 'zeroPageY'}, + 0x83: { name: 'AXS', op: this.axs, modeFn: this.writeZeroPageXIndirect, mode: 'zeroPageXIndirect'}, + + // LAX + 0xAF: { name: 'LAX', op: this.lax, modeFn: this.readAbsolute, mode: 'absolute'}, + 0xBF: { name: 'LAX', op: this.lax, modeFn: this.readAbsoluteY, mode: 'absoluteY'}, + 0xA7: { name: 'LAX', op: this.lax, modeFn: this.readZeroPage, mode: 'zeroPage'}, + 0xB7: { name: 'LAX', op: this.lax, modeFn: this.readZeroPageY, mode: 'zeroPageY'}, + 0xA3: { name: 'LAX', op: this.lax, modeFn: this.readZeroPageXIndirect, mode: 'zeroPageXIndirect'}, + 0xB3: { name: 'LAX', op: this.lax, modeFn: this.readZeroPageIndirectY, mode: 'zeroPageIndirectY'}, + + // DCM + 0xCF: { name: 'DCM', op: this.dcm, modeFn: this.readAddrAbsolute, mode: 'absolute' }, + 0xDF: { name: 'DCM', op: this.dcm, modeFn: this.readAddrAbsoluteX, mode: 'absoluteX' }, + 0xDB: { name: 'DCM', op: this.dcm, modeFn: this.readAddrAbsoluteY, mode: 'absoluteY' }, + 0xC7: { name: 'DCM', op: this.dcm, modeFn: this.readAddrZeroPage, mode: 'zeroPage' }, + 0xD7: { name: 'DCM', op: this.dcm, modeFn: this.readAddrZeroPageX, mode: 'zeroPageX' }, + 0xC3: { name: 'DCM', op: this.dcm, modeFn: this.readAddrZeroPageXIndirect, mode: 'zeroPageXIndirect' }, + 0xD3: { name: 'DCM', op: this.dcm, modeFn: this.readAddrZeroPageIndirectY, mode: 'zeroPageIndirectY' }, + + // INS + 0xEF: { name: 'INS', op: this.ins, modeFn: this.readAddrAbsolute, mode: 'absolute' }, + 0xFF: { name: 'INS', op: this.ins, modeFn: this.readAddrAbsoluteX, mode: 'absoluteX' }, + 0xFB: { name: 'INS', op: this.ins, modeFn: this.readAddrAbsoluteY, mode: 'absoluteY' }, + 0xE7: { name: 'INS', op: this.ins, modeFn: this.readAddrZeroPage, mode: 'zeroPage' }, + 0xF7: { name: 'INS', op: this.ins, modeFn: this.readAddrZeroPageX, mode: 'zeroPageX' }, + 0xE3: { name: 'INS', op: this.ins, modeFn: this.readAddrZeroPageXIndirect, mode: 'zeroPageXIndirect' }, + 0xF3: { name: 'INS', op: this.ins, modeFn: this.readAddrZeroPageIndirectY, mode: 'zeroPageIndirectY' }, + + // ALR + 0x4B: { name: 'ALR', op: this.alr, modeFn: this.readImmediate, mode: 'immediate' }, + + // ARR + 0x6B: { name: 'ARR', op: this.arr, modeFn: this.readImmediate, mode: 'immediate' }, + + // XAA + 0x8B: { name: 'XAA', op: this.xaa, modeFn: this.readImmediate, mode: 'immediate' }, + + // OAL + 0xAB: { name: 'OAL', op: this.oal, modeFn: this.readImmediate, mode: 'immediate' }, + + // SAX + 0xCB: { name: 'SAX', op: this.sax, modeFn: this.readImmediate, mode: 'immediate' }, + + // NOP + 0x1a: { name: 'NOP', op: this.nop, modeFn: this.implied, mode: 'implied' }, + 0x3a: { name: 'NOP', op: this.nop, modeFn: this.implied, mode: 'implied' }, + 0x5a: { name: 'NOP', op: this.nop, modeFn: this.implied, mode: 'implied' }, + 0x7a: { name: 'NOP', op: this.nop, modeFn: this.implied, mode: 'implied' }, + 0xda: { name: 'NOP', op: this.nop, modeFn: this.implied, mode: 'implied' }, + 0xfa: { name: 'NOP', op: this.nop, modeFn: this.implied, mode: 'implied' }, + + // SKB + 0x80: { name: 'SKB', op: this.skp, modeFn: this.readImmediate, mode: 'immediate' }, + 0x82: { name: 'SKB', op: this.skp, modeFn: this.readImmediate, mode: 'immediate' }, + 0x89: { name: 'SKB', op: this.skp, modeFn: this.readImmediate, mode: 'immediate' }, + 0xC2: { name: 'SKB', op: this.skp, modeFn: this.readImmediate, mode: 'immediate' }, + 0xE2: { name: 'SKB', op: this.skp, modeFn: this.readImmediate, mode: 'immediate' }, + 0x04: { name: 'SKB', op: this.skp, modeFn: this.readZeroPage, mode: 'zeroPage' }, + 0x14: { name: 'SKB', op: this.skp, modeFn: this.readZeroPageX, mode: 'zeroPageX' }, + 0x34: { name: 'SKB', op: this.skp, modeFn: this.readZeroPageX, mode: 'zeroPageX' }, + 0x44: { name: 'SKB', op: this.skp, modeFn: this.readZeroPage, mode: 'zeroPage' }, + 0x54: { name: 'SKB', op: this.skp, modeFn: this.readZeroPageX, mode: 'zeroPageX' }, + 0x64: { name: 'SKB', op: this.skp, modeFn: this.readZeroPage, mode: 'zeroPage' }, + 0x74: { name: 'SKB', op: this.skp, modeFn: this.readZeroPageX, mode: 'zeroPageX' }, + 0xD4: { name: 'SKB', op: this.skp, modeFn: this.readZeroPageX, mode: 'zeroPageX' }, + 0xF4: { name: 'SKB', op: this.skp, modeFn: this.readZeroPageX, mode: 'zeroPageX' }, + + // SKW + 0x0C: { name: 'SKW', op: this.skp, modeFn: this.readAddrAbsolute, mode: 'absolute' }, + 0x1C: { name: 'SKW', op: this.skp, modeFn: this.readAddrAbsoluteX, mode: 'absoluteX' }, + 0x3C: { name: 'SKW', op: this.skp, modeFn: this.readAddrAbsoluteX, mode: 'absoluteX' }, + 0x5C: { name: 'SKW', op: this.skp, modeFn: this.readAddrAbsoluteX, mode: 'absoluteX' }, + 0x7C: { name: 'SKW', op: this.skp, modeFn: this.readAddrAbsoluteX, mode: 'absoluteX' }, + 0xDC: { name: 'SKW', op: this.skp, modeFn: this.readAddrAbsoluteX, mode: 'absoluteX' }, + 0xFC: { name: 'SKW', op: this.skp, modeFn: this.readAddrAbsoluteX, mode: 'absoluteX' }, + + // HLT + 0x02: { name: 'HLT', op: this.hlt, modeFn: this.readNopImplied, mode: 'implied' }, + 0x12: { name: 'HLT', op: this.hlt, modeFn: this.readNopImplied, mode: 'implied' }, + 0x22: { name: 'HLT', op: this.hlt, modeFn: this.readNopImplied, mode: 'implied' }, + 0x32: { name: 'HLT', op: this.hlt, modeFn: this.readNopImplied, mode: 'implied' }, + 0x42: { name: 'HLT', op: this.hlt, modeFn: this.readNopImplied, mode: 'implied' }, + 0x52: { name: 'HLT', op: this.hlt, modeFn: this.readNopImplied, mode: 'implied' }, + 0x62: { name: 'HLT', op: this.hlt, modeFn: this.readNopImplied, mode: 'implied' }, + 0x72: { name: 'HLT', op: this.hlt, modeFn: this.readNopImplied, mode: 'implied' }, + 0x92: { name: 'HLT', op: this.hlt, modeFn: this.readNopImplied, mode: 'implied' }, + 0xB2: { name: 'HLT', op: this.hlt, modeFn: this.readNopImplied, mode: 'implied' }, + 0xD2: { name: 'HLT', op: this.hlt, modeFn: this.readNopImplied, mode: 'implied' }, + 0xF2: { name: 'HLT', op: this.hlt, modeFn: this.readNopImplied, mode: 'implied' }, + + // TAS + 0x9B: { name: 'TAS', op: this.tas, modeFn: this.readAddrAbsoluteY, mode: 'absoluteY'}, + + // SAY + 0x9C: { name: 'SAY', op: this.say, modeFn: this.readAddrAbsoluteX, mode: 'absoluteX'}, + + // XAS + 0x9E: { name: 'XAS', op: this.xas, modeFn: this.readAddrAbsoluteY, mode: 'absoluteY'}, + + // AXA + 0x9F: { name: 'AXA', op: this.axa, modeFn: this.readAddrAbsoluteY, mode: 'absoluteY'}, + 0x93: { name: 'AXA', op: this.axa, modeFn: this.readAddrZeroPageIndirectY, mode: 'zeroPageIndirectY'}, + + // ANC + 0x2b: { name: 'ANC', op: this.anc, modeFn: this.readImmediate, mode: 'immediate' }, + 0x0b: { name: 'ANC', op: this.anc, modeFn: this.readImmediate, mode: 'immediate' }, + + // LAS + 0xBB: { name: 'LAS', op: this.las, modeFn: this.readAbsoluteY, mode: 'absoluteY'}, + + // SBC + 0xEB: { name: 'SBC', op: this.sbc, modeFn: this.readImmediate, mode: 'immediate'} + }; + + OPS_ROCKWELL_65C02: Instructions = { + 0xCB: { name: 'NOP', op: this.nop, modeFn: this.implied, mode: 'implied' }, + 0xDB: { name: 'NOP', op: this.nop, modeFn: this.readZeroPageX, mode: 'immediate' }, + }; + + /* WDC 65C02 Instructions */ + + OPS_WDC_65C02: Instructions = { + 0xCB: { name: 'WAI', op: this.wai, modeFn: this.implied, mode: 'implied' }, + 0xDB: { name: 'STP', op: this.stp, modeFn: this.implied, mode: 'implied' } + }; } diff --git a/test/cpu-tom-harte.spec.ts b/test/cpu-tom-harte.spec.ts index 2bbb4ca..980d557 100644 --- a/test/cpu-tom-harte.spec.ts +++ b/test/cpu-tom-harte.spec.ts @@ -7,7 +7,7 @@ import fs from 'fs'; -import CPU6502 from 'js/cpu6502'; +import CPU6502, { FLAVOR_ROCKWELL_65C02, FLAVOR_WDC_65C02 } from 'js/cpu6502'; import { toHex } from 'js/util'; import type { byte, word } from 'js/types'; @@ -158,28 +158,56 @@ const maxTests = 16; if (testPath) { const testPath6502 = `${testPath}/6502/v1/`; - const testPath65C02 = `${testPath}/wdc65c02/v1/`; + const testPathWDC65C02 = `${testPath}/wdc65c02/v1/`; + const testPathRW65C02 = `${testPath}/rockwell65c02/v1/`; const opAry6502: OpTest[] = []; - const opAry65C02: OpTest[] = []; + const opAryRW65C02: OpTest[] = []; + const opAryWDC65C02: 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); + opAryRW65C02.push(test); + opAryWDC65C02.push(test); + } + + for (const op in cpu.OPS_NMOS_6502) { + const { name, mode } = cpu.OPS_NMOS_6502[op]; + const test = { op: toHex(+op), name, mode }; + opAry6502.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); + opAryRW65C02.push(test); + opAryWDC65C02.push(test); } + + // WDC 65C02 NOPs + [ + '03', '0b', '13', '1b', '23', '2b', '33', '3b', + '43', '4b', '53', '5b', '63', '6b', '73', '7b', + '83', '8b', '93', '9b', 'a3', 'ab', 'b3', 'bb', + 'c3', 'd3', 'e3', 'eb', 'f3', 'fb' + ].forEach((op) => + opAryWDC65C02.push({ op, name: 'nop', mode: 'implied'}) + ); + + // Rockwell 65C02 NOPs + [ + '03', '0b', '13', '1b', '23', '2b', '33', '3b', + '43', '4b', '53', '5b', '63', '6b', '73', '7b', + '83', '8b', '93', '9b', 'a3', 'ab', 'b3', 'bb', + 'c3', 'cb', 'd3', 'db', 'e3', 'eb', 'f3', 'fb' + ].forEach((op) => + opAryRW65C02.push({ op, name: 'nop', mode: 'implied'}) + ); }; buildOpArrays(); @@ -188,7 +216,7 @@ if (testPath) { let cpu: CPU6502; let memory: TestMemory; - describe('6502', function() { + describe('NMOS 6502', function() { beforeAll(function() { cpu = new CPU6502(); memory = new TestMemory(256); @@ -209,15 +237,36 @@ if (testPath) { }); }); - describe('WDC 65C02', function() { + describe('Rockwell 65C02', function() { beforeAll(function() { - cpu = new CPU6502({ '65C02': true }); + cpu = new CPU6502({ flavor: FLAVOR_ROCKWELL_65C02 }); 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'); + describe.each(opAryRW65C02)('Test op $op $name $mode', ({op}) => { + const data = fs.readFileSync(`${testPathRW65C02}${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({ flavor: FLAVOR_WDC_65C02 }); + memory = new TestMemory(256); + cpu.addPageHandler(memory); + }); + + describe.each(opAryWDC65C02)('Test op $op $name $mode', ({op}) => { + const data = fs.readFileSync(`${testPathWDC65C02}${op}.json`, 'utf-8'); const tests = JSON.parse(data) as Test[]; it.each(tests.slice(0, maxTests))('Test $name', (test) => { diff --git a/test/cpu.spec.ts b/test/cpu.spec.ts index c04df8b..3c52555 100644 --- a/test/cpu.spec.ts +++ b/test/cpu.spec.ts @@ -1,4 +1,4 @@ -import CPU6502 from '../js/cpu6502'; +import CPU6502, { FLAVOR_ROCKWELL_65C02 } from '../js/cpu6502'; // From https://github.com/Klaus2m5/6502_65C02_functional_tests import Test6502 from './roms/6502test'; import Test65C02 from './roms/65C02test'; @@ -33,7 +33,7 @@ describe('CPU', function () { describe('65C02', function () { it('completes the test ROM', function () { - cpu = new CPU6502({'65C02': true}); + cpu = new CPU6502({ flavor: FLAVOR_ROCKWELL_65C02 }); const test = new Test65C02(); cpu.addPageHandler(test); cpu.setPC(0x400); diff --git a/test/cpu6502.spec.ts b/test/cpu6502.spec.ts index a98ce02..d274265 100644 --- a/test/cpu6502.spec.ts +++ b/test/cpu6502.spec.ts @@ -1,4 +1,8 @@ -import CPU6502, { CpuState, flags } from '../js/cpu6502'; +import CPU6502, { + CpuState, + FLAVOR_ROCKWELL_65C02, + flags +} from '../js/cpu6502'; import { TestMemory } from './util/memory'; import { bios, Program } from './util/bios'; import { toReadableState } from './util/cpu'; @@ -277,14 +281,6 @@ describe('CPU6502', function() { 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() { @@ -1542,7 +1538,7 @@ describe('CPU6502', function() { describe('65c02', function() { beforeEach(function() { - cpu = new CPU6502({'65C02': true}); + cpu = new CPU6502({ flavor: FLAVOR_ROCKWELL_65C02 }); memory = new TestMemory(4); cpu.addPageHandler(memory);