test_intel: Z80 INT handling + promoted todo tests
Z80 chip enhancements: - Add maskable INT̅ handling. Pin is level-triggered, active-low. The on_int watcher tracks line state; step() services at instruction boundaries when IFF1=1. - IM 0/1 vector to 0x0038; IM 2 vectors via I:00 indirection (no interrupt-controller hardware on the bus, so we approximate the data byte as 0x00 — user code must pre-load the vector table). - INTA cycle clears IFF1 and IFF2 per Zilog UM008003 p. 24. - Power-on reset state: chip starts with reset_active=true so the RESET̅ rising edge releases the chip (the watcher only fires on edges; without an initial-true assumption, setting RESET=false was a no-op and the chip executed instructions during the test's pre-reset cycles). Test infrastructure: - bootZ80 no longer advances time after RESET deassert. Same lesson as bootCpu in the 8080 tests — caller may need to poke RAM contents BEFORE the chip executes. 5 it.todo tests promoted to passing: - LDIR copies a memory block from HL to DE - LD A, (IX+d) reads via IX with signed displacement - EXX swaps the main register set with the shadow set - NMI̅ falling edge pushes PC and vectors to 0x0066 - IM 1 + INT̅ vectors to 0x0038 Total test_intel: 60 passing (was 55), 0 failed, 17 todo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
148c56d6dc
commit
4aa13897c6
|
|
@ -67,6 +67,7 @@ typedef struct {
|
|||
bool halted;
|
||||
bool reset_active;
|
||||
bool nmi_pending;
|
||||
bool int_line_low; /* tracked from on_int watcher (INT̅ active low) */
|
||||
} cpu_t;
|
||||
|
||||
static cpu_t G;
|
||||
|
|
@ -409,6 +410,41 @@ static void step(void) {
|
|||
return;
|
||||
}
|
||||
|
||||
/* Maskable interrupt: INT̅ is level-triggered (active low). Service
|
||||
at instruction boundary if IFF1 is enabled and we're not halted
|
||||
on a non-interruptible state. Per [U] p. 24: INTA cycle clears
|
||||
both IFF1 and IFF2. */
|
||||
if (G.int_line_low && G.iff1) {
|
||||
G.iff1 = G.iff2 = false;
|
||||
G.halted = false;
|
||||
G.r = (G.r & 0x80) | ((G.r + 1) & 0x7F);
|
||||
push16(G.pc);
|
||||
switch (G.im) {
|
||||
case 0:
|
||||
/* IM 0 reads an instruction byte from the data bus
|
||||
during INTA — usually a RST. Without an interrupt
|
||||
controller wired, default to RST 38h. */
|
||||
G.pc = 0x0038;
|
||||
break;
|
||||
case 1:
|
||||
G.pc = 0x0038;
|
||||
break;
|
||||
case 2:
|
||||
/* IM 2: vector = (I << 8) | data_byte. Without a real
|
||||
interrupt controller we approximate using 0x00 as
|
||||
the data byte; user code must pre-load the vector
|
||||
table at I:00. */
|
||||
{
|
||||
uint16_t va = ((uint16_t)G.i << 8) | 0x00;
|
||||
uint8_t lo = mem_read(va);
|
||||
uint8_t hi = mem_read((uint16_t)(va + 1));
|
||||
G.pc = lo | ((uint16_t)hi << 8);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (G.halted) {
|
||||
/* Re-emit a no-op M1 fetch so RFSH̅ keeps cycling (matches real
|
||||
silicon, which fetches the byte at PC repeatedly while halted). */
|
||||
|
|
@ -666,11 +702,16 @@ static void on_reset(void* user_data, vx_pin pin, int value) {
|
|||
|
||||
static void on_nmi(void* user_data, vx_pin pin, int value) {
|
||||
(void)user_data; (void)pin; (void)value;
|
||||
/* NMI̅ falling edge — pin watch was registered for VX_EDGE_FALLING
|
||||
so this fires only on 1→0. */
|
||||
G.nmi_pending = true;
|
||||
}
|
||||
|
||||
static void on_int(void* user_data, vx_pin pin, int value) {
|
||||
(void)user_data; (void)pin;
|
||||
/* INT̅ is level-triggered, active low. Track its level and let
|
||||
step() decide when to service it. */
|
||||
G.int_line_low = (value == 0);
|
||||
}
|
||||
|
||||
static void on_clock(void* user_data) {
|
||||
(void)user_data;
|
||||
if (G.reset_active) return;
|
||||
|
|
@ -716,8 +757,15 @@ void chip_setup(void) {
|
|||
G.gnd = vx_pin_register("GND", VX_INPUT);
|
||||
|
||||
reset_state();
|
||||
/* Power-on default: hold the chip in reset until something drives
|
||||
RESET̅ HIGH. Real silicon is the same — RESET̅ must be held low
|
||||
for ≥3 clocks at power-on, but in our digital model the watcher
|
||||
only fires on edges, so we start in reset and let the rising
|
||||
edge release us. */
|
||||
G.reset_active = true;
|
||||
vx_pin_watch(G.reset_, VX_EDGE_BOTH, on_reset, 0);
|
||||
vx_pin_watch(G.nmi, VX_EDGE_FALLING, on_nmi, 0);
|
||||
vx_pin_watch(G.intn, VX_EDGE_BOTH, on_int, 0);
|
||||
|
||||
G.cycle_timer = vx_timer_create(on_clock, 0);
|
||||
vx_timer_start(G.cycle_timer, 250, true); /* 4 MHz pseudo-clock */
|
||||
|
|
|
|||
|
|
@ -60,7 +60,9 @@ async function bootZ80(program) {
|
|||
board.setNet('RESET', false);
|
||||
board.advanceNanos(CLOCK_NS * 4);
|
||||
board.setNet('RESET', true);
|
||||
board.advanceNanos(CLOCK_NS * 2);
|
||||
// Do NOT advance after RESET deassert — the caller has its own
|
||||
// advanceNanos loop, and may want to poke RAM contents first
|
||||
// (same lesson as bootCpu in the 8080 tests).
|
||||
return { board, ram };
|
||||
}
|
||||
|
||||
|
|
@ -197,14 +199,120 @@ describe('Zilog Z80 chip', () => {
|
|||
board.dispose();
|
||||
});
|
||||
|
||||
it.todo('LDIR copies a memory block from HL to DE');
|
||||
it.todo('LD A, (IX+d) reads via IX with signed displacement');
|
||||
it.todo('EXX swaps the main register set with the shadow set');
|
||||
it.skipIf(skip)('LDIR copies a memory block from HL to DE', async () => {
|
||||
// Pre-load source: 4 bytes at 0xC000..0xC003. Then LDIR HL=0xC000,
|
||||
// DE=0x9000, BC=4. After: 4 bytes copied to 0x9000..0x9003.
|
||||
const program = new Uint8Array([
|
||||
LD_HL_nn, 0x00, 0xC0, // LD HL, 0xC000
|
||||
LD_DE_nn, 0x00, 0x90, // LD DE, 0x9000
|
||||
LD_BC_nn, 0x04, 0x00, // LD BC, 0x0004
|
||||
LDIR, _LDIR, // ED B0
|
||||
HALT,
|
||||
]);
|
||||
const { board, ram } = await bootZ80(program);
|
||||
ram.poke(0xC000, 0x11);
|
||||
ram.poke(0xC001, 0x22);
|
||||
ram.poke(0xC002, 0x33);
|
||||
ram.poke(0xC003, 0x44);
|
||||
for (let i = 0; i < 500; i++) board.advanceNanos(CLOCK_NS);
|
||||
expect(ram.peek(0x9000)).toBe(0x11);
|
||||
expect(ram.peek(0x9001)).toBe(0x22);
|
||||
expect(ram.peek(0x9002)).toBe(0x33);
|
||||
expect(ram.peek(0x9003)).toBe(0x44);
|
||||
board.dispose();
|
||||
});
|
||||
|
||||
it.skipIf(skip)('LD A, (IX+d) reads via IX with signed displacement', async () => {
|
||||
// Pre-load 0xCD at 0xA005. Set IX = 0xA000. LD A, (IX+5) → A=0xCD.
|
||||
// Then LD (0x9000), A so we can verify.
|
||||
const program = new Uint8Array([
|
||||
LD_IX_nn, _IX_LD_nn, 0x00, 0xA0, // DD 21 00 A0 — LD IX, 0xA000
|
||||
0xDD, 0x7E, 0x05, // DD 7E 05 — LD A, (IX+5)
|
||||
LD_addr_A, 0x00, 0x90, // LD (0x9000), A
|
||||
HALT,
|
||||
]);
|
||||
const { board, ram } = await bootZ80(program);
|
||||
ram.poke(0xA005, 0xCD);
|
||||
for (let i = 0; i < 400; i++) board.advanceNanos(CLOCK_NS);
|
||||
expect(ram.peek(0x9000)).toBe(0xCD);
|
||||
board.dispose();
|
||||
});
|
||||
|
||||
it.skipIf(skip)('EXX swaps the main register set with the shadow set', async () => {
|
||||
// LD HL, 0x1111
|
||||
// EXX ; swap → HL = shadow (0x0000 after reset shadow init)
|
||||
// LD HL, 0x9000 ; main HL now 0x9000 (was the shadow)
|
||||
// EXX ; swap back → original HL = 0x1111 in main set
|
||||
// LD (HL), 0x77 ; writes to 0x1111... wait, main HL is 0x1111
|
||||
// ; that's not in our RAM range (0x8000+).
|
||||
// Restructure: use two HL values both in RAM range.
|
||||
// LD HL, 0x9100 ; EXX ; LD HL, 0x9200 ; EXX ; LD (HL), 0x77 ; HALT
|
||||
// After: write to 0x9100 (the original main HL).
|
||||
const program = new Uint8Array([
|
||||
LD_HL_nn, 0x00, 0x91, // LD HL, 0x9100 (main)
|
||||
EXX, // → main set goes to shadow
|
||||
LD_HL_nn, 0x00, 0x92, // LD HL, 0x9200 (this is now the new "main")
|
||||
EXX, // → swap back; main HL = 0x9100
|
||||
LD_aHL_n, 0x77, // LD (HL), 0x77 → write 0x77 to 0x9100
|
||||
HALT,
|
||||
]);
|
||||
const { board, ram } = await bootZ80(program);
|
||||
for (let i = 0; i < 300; i++) board.advanceNanos(CLOCK_NS);
|
||||
expect(ram.peek(0x9100)).toBe(0x77);
|
||||
// Verify the OTHER write didn't happen (shadow set's HL=0x9200
|
||||
// was never written via LD (HL), 0x77 in the shadow context).
|
||||
expect(ram.peek(0x9200)).toBe(0x00);
|
||||
board.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe('interrupts', () => {
|
||||
it.todo('NMI̅ falling edge pushes PC and vectors to 0x0066');
|
||||
it.todo('IM 1 + INT̅ vectors to 0x0038');
|
||||
it.skipIf(skip)('NMI̅ falling edge pushes PC and vectors to 0x0066', async () => {
|
||||
// EI ; loop: NOP ; JR -1
|
||||
// ISR at 0x0066: LD A, 0xAB ; LD (0x9000), A ; HALT
|
||||
const program = new Uint8Array(0x80);
|
||||
program.fill(0x00);
|
||||
program[0x00] = 0xFB; // EI
|
||||
program[0x01] = 0x00; // NOP
|
||||
program[0x02] = 0x18; program[0x03] = 0xFD; // JR -3 → loop
|
||||
program[0x66] = 0x3E; program[0x67] = 0xAB; // LD A, 0xAB
|
||||
program[0x68] = 0x32; program[0x69] = 0x00; program[0x6A] = 0x90; // LD (0x9000), A
|
||||
program[0x6B] = 0x76; // HALT
|
||||
const { board, ram } = await bootZ80(program);
|
||||
// Run a few cycles to enter the loop.
|
||||
for (let i = 0; i < 50; i++) board.advanceNanos(CLOCK_NS);
|
||||
// Pulse NMI̅ low (active low) → falling edge triggers interrupt.
|
||||
board.setNet('NMI', false);
|
||||
board.advanceNanos(CLOCK_NS * 4);
|
||||
board.setNet('NMI', true);
|
||||
for (let i = 0; i < 200; i++) board.advanceNanos(CLOCK_NS);
|
||||
expect(ram.peek(0x9000)).toBe(0xAB);
|
||||
board.dispose();
|
||||
});
|
||||
|
||||
it.skipIf(skip)('IM 1 + INT̅ vectors to 0x0038', async () => {
|
||||
// EI ; IM 1 ; loop: NOP ; JR -1
|
||||
// ISR at 0x0038: LD A, 0x39 ; LD (0x9000), A ; HALT
|
||||
const program = new Uint8Array(0x80);
|
||||
program.fill(0x00);
|
||||
program[0x00] = 0xFB; // EI
|
||||
program[0x01] = 0xED; program[0x02] = 0x56; // IM 1
|
||||
program[0x03] = 0x00; // NOP loop
|
||||
program[0x04] = 0x18; program[0x05] = 0xFD; // JR -3
|
||||
program[0x38] = 0x3E; program[0x39] = 0x39; // LD A, 0x39
|
||||
program[0x3A] = 0x32; program[0x3B] = 0x00; program[0x3C] = 0x90;
|
||||
program[0x3D] = 0x76; // HALT
|
||||
const { board, ram } = await bootZ80(program);
|
||||
for (let i = 0; i < 50; i++) board.advanceNanos(CLOCK_NS);
|
||||
// INT̅ active-low: drive low to request interrupt.
|
||||
board.setNet('INT', false);
|
||||
for (let i = 0; i < 200; i++) board.advanceNanos(CLOCK_NS);
|
||||
board.setNet('INT', true);
|
||||
for (let i = 0; i < 200; i++) board.advanceNanos(CLOCK_NS);
|
||||
expect(ram.peek(0x9000)).toBe(0x39);
|
||||
board.dispose();
|
||||
});
|
||||
|
||||
it.todo('IM 2 + INT̅ uses I:byte to vector through a table');
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue