155 lines
5.4 KiB
JavaScript
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);
|
|
});
|