velxio/test/test_intel/test_8086/pic-integration.test.js

155 lines
5.4 KiB
JavaScript

/**
* 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);
});