test_intel: 8086 + 8259 PIC end-to-end interrupt integration
First test wiring the 8086 CPU to a real 8259 PIC chip on the same board and proving hardware-interrupt routing works end-to-end: IRQ0 input → PIC asserts INT → CPU's INTR pin → CPU runs INTA cycle → PIC drives vector 0x40 on AD bus → CPU does do_int(0x40) → fetches CS:IP from IVT entry at 0x100 → ISR runs → IRET → main resumes from HLT. Two related chip fixes required to make this work: 1. 8086 INTA cycle no longer drives AD itself. Real 8086 INTA bus cycle has the PIC drive the data lines, not the CPU. My earlier code did `bus_read_byte(0, false)` which first drove AD with addr=0, overwriting whatever the PIC had driven. Fix: release_ad → INTA̅ low → sample AD (PIC's INTA watcher fires synchronously and drives) → INTA̅ high. 2. 8086 HLT now interruptible. on_clock previously early-returned on G.halted, so step() never ran and the INTR check never executed. Real 8086 HLT wakes on INTR/NMI. Fix: remove the early return; step()'s own halted check (later in the function) only no-ops if no pending interrupt. Tests: total test_intel 110 → 111 passing (+1, the integration test). 0 failed. 11 todo. test_8086 now 11→12 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
479b52634e
commit
555a4315be
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
Loading…
Reference in New Issue