diff --git a/src/common/cpu/ZilogZ80.ts b/src/common/cpu/ZilogZ80.ts index 6ef4f5d3..0e4b9365 100644 --- a/src/common/cpu/ZilogZ80.ts +++ b/src/common/cpu/ZilogZ80.ts @@ -3383,12 +3383,17 @@ export class Z80 implements CPU, InstructionBased, IOBusConnected, SavesState this.memBus.read(a), + mem_write: (a, v) => this.memBus.write(a, v), + io_read: (a) => this.ioBus.read(a), + io_write: (a, v) => this.ioBus.write(a, v), }); } } diff --git a/src/machine/pacman.ts b/src/machine/pacman.ts index 00f33b0e..64d432ee 100644 --- a/src/machine/pacman.ts +++ b/src/machine/pacman.ts @@ -238,11 +238,13 @@ export class PacmanMachine extends BasicScanlineMachine { constructor() { super(); - this.cpu.connectIOBus({ + // Go through the base connect* helpers so the probe wrapper is spliced + // in when probing is toggled (and removed when it is not). + this.connectCPUIOBus({ read: (_p) => 0xff, write: (port, val) => { if ((port & 0xff) === 0) this.interruptVector = val & 0xff; } }); - this.cpu.connectMemoryBus({ read: this.readByte, write: this.writeByte }); + this.connectCPUMemoryBus({ read: this.readByte, write: this.writeByte }); this.cpu.retryInterrupts = true; this.rom = new Uint8Array(this.defaultROMSize); this.gfx = new PacmanVideo(this.rom, this.vram, this.cram, this.ram); @@ -319,8 +321,6 @@ export class PacmanMachine extends BasicScanlineMachine { this.waveforms = hasCustom ? custom : PacmanMachine.WAVEFORMS; } - advanceCPU() { return this.cpu.advanceInsn(); } - /** Rebuild voice frequency from low-nibble registers. Voice 0 is 20-bit; 1/2 are 16-bit. */ private voiceFreq(v: number): number { var r = this.soundRegs; diff --git a/test/unit/testz80probe.ts b/test/unit/testz80probe.ts new file mode 100644 index 00000000..68c5e567 --- /dev/null +++ b/test/unit/testz80probe.ts @@ -0,0 +1,122 @@ +import assert from "assert"; +import { describe, it } from "mocha"; +import { Z80 } from "../../src/common/cpu/ZilogZ80"; +import { BasicHeadlessMachine } from "../../src/common/devices"; +import { ProbeRecorder } from "../../src/common/probe"; +import { PacmanMachine } from "../../src/machine/pacman"; + +function makeBus(mem: Uint8Array) { + return { + read: (a: number) => mem[a & 0xffff], + write: (a: number, v: number) => { mem[a & 0xffff] = v & 0xff; }, + }; +} + +function makeIO() { + return { read: (_a: number) => 0xff, write: (_a: number, _v: number) => { } }; +} + +describe('Z80 bus reconnection', function () { + it('should preserve CPU state when the buses are reconnected', function () { + const mem = new Uint8Array(0x10000); + // LD A,$42 ; LD B,$13 + mem.set([0x3e, 0x42, 0x06, 0x13], 0); + const cpu = new Z80(); + cpu.connectMemoryBus(makeBus(mem)); + cpu.connectIOBus(makeIO()); + cpu.advanceInsn(); + cpu.advanceInsn(); + const before = cpu.saveState(); + assert.strictEqual(before.PC, 4); + assert.strictEqual((before.AF >> 8) & 0xff, 0x42); + + // This is what BasicHeadlessMachine.rewireCPUBuses() does when a probe is + // attached or detached. + cpu.connectMemoryBus(makeBus(mem)); + cpu.connectIOBus(makeIO()); + + const after = cpu.saveState(); + assert.strictEqual(after.PC, before.PC); + assert.strictEqual(after.AF, before.AF); + assert.strictEqual(after.BC, before.BC); + }); + + it('should route accesses through the newly connected memory bus', function () { + const memA = new Uint8Array(0x10000); + const memB = new Uint8Array(0x10000); + // LD HL,$0010 ; LD A,(HL) + const prog = [0x21, 0x10, 0x00, 0x7e]; + memA.set(prog, 0); + memB.set(prog, 0); + memA[0x10] = 0xaa; + memB[0x10] = 0xbb; + + const cpu = new Z80(); + cpu.connectMemoryBus(makeBus(memA)); + cpu.connectIOBus(makeIO()); + cpu.advanceInsn(); // LD HL,$0010 + cpu.connectMemoryBus(makeBus(memB)); // swap in a different bus + cpu.advanceInsn(); // LD A,(HL) must read memB + assert.strictEqual((cpu.saveState().AF >> 8) & 0xff, 0xbb); + }); +}); + +class TestZ80Machine extends BasicHeadlessMachine { + cpuFrequency = 1000000; + defaultROMSize = 0x10000; + cpu = new Z80(); + ram = new Uint8Array(0x10000); + + constructor() { + super(); + this.connectCPUMemoryBus(this); + this.connectCPUIOBus(makeIO()); + } + read(a: number): number { return this.ram[a & 0xffff]; } + write(a: number, v: number): void { this.ram[a & 0xffff] = v & 0xff; } +} + +describe('Probing does not reset Z80 machines', function () { + it('should keep CPU state across connectProbe() on a generic Z80 machine', function () { + const m = new TestZ80Machine(); + m.ram.set([0x3e, 0x42, 0x06, 0x13, 0x7e], 0); + m.advanceCPU(); + m.advanceCPU(); + const before = m.saveState().c; + assert.strictEqual(before.PC, 4); + + const rec = new ProbeRecorder(m); + m.connectProbe(rec); + assert.strictEqual(m.saveState().c.PC, before.PC); + assert.strictEqual(m.saveState().c.AF, before.AF); + + m.advanceCPU(); + assert.ok(rec.idx > 0, 'probe should record data while connected'); + + m.connectProbe(null); + assert.strictEqual(m.saveState().c.PC, 5); + }); +}); + +describe('Pacman probing', function () { + it('should not reset and should record data when the probe is toggled', function () { + const m = new PacmanMachine(); + m.rom.fill(0); + // LD A,$42 ; NOP ; LD B,$13 + m.rom.set([0x3e, 0x42, 0x00, 0x06, 0x13], 0); + m.advanceCPU(); + m.advanceCPU(); + const before = m.saveState().c; + assert.strictEqual(before.PC, 3); + + const rec = new ProbeRecorder(m); + m.connectProbe(rec); + assert.strictEqual(m.saveState().c.PC, before.PC, 'probe toggle must not reset Pacman'); + + m.advanceCPU(); + assert.ok(rec.idx > 0, 'Pacman probe should record execute/clocks'); + + m.connectProbe(null); + assert.strictEqual(m.saveState().c.PC, 5); + }); +});