diff --git a/test/test_intel/test_8086/8086.c b/test/test_intel/test_8086/8086.c index f8b5d144..a4fddd41 100644 --- a/test/test_intel/test_8086/8086.c +++ b/test/test_intel/test_8086/8086.c @@ -983,13 +983,16 @@ static void step(void) { return; } if (G.intr_line && (G.flags & F_IF)) { - /* Approximate: read the vector byte from the data bus during - an INTA cycle. The actual external 8259 PIC would jam the - vector. Here we synthesise INT 0 if no fixture drives the - bus (reset state); a test fixture can override by driving - the data bus when our chip asserts INTA̅ low. */ + /* Hardware interrupt acknowledge cycle. Real 8086 in min mode + runs two INTA̅ pulses; the second has the data bus driven by + the external 8259 PIC with the vector byte. We collapse to + one pulse here. Critically, we must NOT drive AD ourselves + during this cycle — the PIC owns the bus. */ + release_ad(); vx_pin_write(G.inta, 0); - uint8_t vec = bus_read_byte(0, false); /* dummy read for cycle */ + /* PIC's INTA̅-falling-edge watcher fires synchronously and + drives AD0..AD7 with the vector. Sample. */ + uint8_t vec = (uint8_t)(read_ad() & 0xFF); vx_pin_write(G.inta, 1); do_int(vec); G.halted = false; @@ -1430,13 +1433,15 @@ static void on_intr(void* user_data, vx_pin pin, int value) { static void on_clock(void* user_data) { (void)user_data; if (G.reset_active) return; - if (G.halted) return; if (vx_pin_read(G.ready) == 0) return; /* wait state */ if (vx_pin_read(G.hold) == 1) { /* bus hold */ vx_pin_write(G.hlda, 1); return; } vx_pin_write(G.hlda, 0); + /* Do NOT early-return on halted — step() handles that and also + serves an interrupt that wakes us up. Real 8086 HLT is + interruptible. */ step(); } diff --git a/test/test_intel/test_8086/pic-integration.test.js b/test/test_intel/test_8086/pic-integration.test.js new file mode 100644 index 00000000..def7ac9a --- /dev/null +++ b/test/test_intel/test_8086/pic-integration.test.js @@ -0,0 +1,154 @@ +/** + * 8086 + 8259 PIC integration test. + * + * Wires both chips on one board, configures the PIC, fires an + * IRQ, and verifies the 8086 takes the interrupt and runs an ISR + * that writes a sentinel byte to memory. + * + * This is the first end-to-end test of hardware-interrupt routing + * from an external chip (the PIC) into the CPU's interrupt + * pipeline — proving the INTA bus cycle works between two real + * WASM chips. + */ +import { describe, it, expect } from 'vitest'; +import { BoardHarness } from '../src/BoardHarness.js'; +import { chipWasmExists } from '../src/helpers.js'; + +const CPU = '8086'; +const PIC = '8259-pic'; +const skip = !chipWasmExists(CPU) || !chipWasmExists(PIC); + +const CLOCK_NS = 200; + +function cpuPinMap() { + const m = { + ALE: 'ALE', RD: 'RD', WR: 'WR', MIO: 'MIO', DTR: 'DTR', DEN: 'DEN', + HOLD: 'HOLD', HLDA: 'HLDA', + INTR: 'INTR', NMI: 'NMI', INTA: 'INTA', + RESET: 'RESET', READY: 'READY', TEST: 'TEST', CLK: 'CLK', + MNMX: 'MNMX', BHE: 'BHE', + VCC: 'VCC', GND: 'GND', + }; + for (let i = 0; i < 16; i++) m[`AD${i}`] = `AD${i}`; + for (let i = 16; i < 20; i++) m[`A${i}`] = `A${i}`; + return m; +} + +function picPinMap() { + // PIC's D bus is the low byte of the 8086's AD bus. PIC's INT pin + // wires to CPU's INTR; PIC's INTA pin wires to CPU's INTA̅. PIC has + // its own A0/CS̅/RD̅/WR̅ — we'd normally wire CS̅ to a chip-select + // decode line, but for this test we just leave it tied to the test + // fixture (we toggle it manually). + const m = { + A0: 'PIC_A0', CS: 'PIC_CS', RD: 'PIC_RD', WR: 'PIC_WR', + INT: 'INTR', // ← shared net with CPU's INTR + INTA: 'INTA', // ← shared net with CPU's INTA̅ + CAS0: 'PIC_CAS0', CAS1: 'PIC_CAS1', CAS2: 'PIC_CAS2', SPEN: 'PIC_SPEN', + VCC: 'VCC', GND: 'GND', + }; + // PIC's D0..D7 share with CPU's AD0..AD7 + for (let i = 0; i < 8; i++) m[`D${i}`] = `AD${i}`; + for (let i = 0; i < 8; i++) m[`IRQ${i}`] = `IRQ${i}`; + return m; +} + +describe('8086 + 8259 PIC integration', () => { + it.skipIf(skip)('IRQ0 fires the ISR which writes a sentinel byte', async () => { + const board = new BoardHarness(); + + // PIC must be added BEFORE the CPU so its INTA-falling watcher + // fires first per advanceNanos and drives D bus with the vector + // before the CPU samples AD. + await board.addChip(PIC, picPinMap()); + await board.addChip(CPU, cpuPinMap()); + + // RAM covering the full 1 MB. ISR vector at 0x40 → table entry + // at physical (0x40 << 2) = 0x100..0x103: { offset_lo, offset_hi, + // segment_lo, segment_hi }. We make the ISR live at CS=0xF000, + // IP=0x0200, so vector entry is { 0x00, 0x02, 0x00, 0xF0 }. + const ram = board.installFake8086Bus({}); + + // ISR at physical 0xF0200: write 0xAA to [0x9000], then IRET. + const isr = [ + 0xC6, 0x06, 0x00, 0x90, 0xAA, // MOV byte [0x9000], 0xAA + 0xCF, // IRET + ]; + for (let i = 0; i < isr.length; i++) ram.poke(0xF0200 + i, isr[i]); + + // IVT entry for vector 0x40 + ram.poke(0x100, 0x00); + ram.poke(0x101, 0x02); + ram.poke(0x102, 0x00); + ram.poke(0x103, 0xF0); + + // Boot stub: JMP FAR 0xF000:0x0100 at the reset vector. + ram.poke(0xFFFF0, 0xEA); + ram.poke(0xFFFF1, 0x00); + ram.poke(0xFFFF2, 0x01); + ram.poke(0xFFFF3, 0x00); + ram.poke(0xFFFF4, 0xF0); + + // Main program at 0xF0100: STI ; HLT (we'll get interrupted out + // of the HLT). Actually 8086 HLT continues on interrupt — perfect. + const main = [ + 0xFB, // STI + 0xF4, // HLT + ]; + for (let i = 0; i < main.length; i++) ram.poke(0xF0100 + i, main[i]); + + // Quiet inputs. + board.setNet('MNMX', true); + board.setNet('READY', true); + board.setNet('TEST', true); + board.setNet('NMI', false); + board.setNet('HOLD', false); + board.setNet('PIC_CS', true); + board.setNet('PIC_RD', true); + board.setNet('PIC_WR', true); + for (let i = 0; i < 8; i++) board.setNet(`IRQ${i}`, false); + + // Reset CPU + board.setNet('RESET', true); + board.advanceNanos(CLOCK_NS * 8); + board.setNet('RESET', false); + + // Helper to write to PIC. We need to NOT collide with the CPU's + // bus, but during this test the CPU is still mid-reset / running + // the boot JMP. We'll wait until the CPU is in HLT state (after + // ~2000 cycles) before driving the PIC, to avoid contention. + function picWrite(a0, value) { + board.setNet('PIC_A0', a0 !== 0); + // We use the AD bus for PIC data writes too (since PIC's D maps + // to AD0..AD7). The CPU is halted so AD is idle. + for (let i = 0; i < 8; i++) { + board.setNet(`AD${i}`, ((value >> i) & 1) === 1); + } + board.advanceNanos(20); + board.setNet('PIC_CS', false); + board.setNet('PIC_WR', false); + board.advanceNanos(20); + board.setNet('PIC_WR', true); + board.advanceNanos(20); + board.setNet('PIC_CS', true); + } + + // Run a few cycles to get past the JMP-FAR + STI + HLT. + for (let i = 0; i < 2000; i++) board.advanceNanos(CLOCK_NS); + + // Configure PIC: ICW1 (single, ICW4-needed) + ICW2 (vector base 0x40) + // + ICW4 (8086 mode) + OCW1 (mask = 0). + picWrite(0, 0x13); + picWrite(1, 0x40); + picWrite(1, 0x01); + picWrite(1, 0x00); + + // Fire IRQ0 — should produce INT, INTA cycle drives 0x40 on bus, + // CPU executes do_int(0x40), runs the ISR, RETs back. + board.setNet('IRQ0', true); + for (let i = 0; i < 5000; i++) board.advanceNanos(CLOCK_NS); + + expect(ram.peek(0x9000)).toBe(0xAA); + board.dispose(); + }, 30_000); +});