diff --git a/test/test_intel/autosearch/18_complete_emulation_plan.md b/test/test_intel/autosearch/18_complete_emulation_plan.md index 2889999f..ea9a5997 100644 --- a/test/test_intel/autosearch/18_complete_emulation_plan.md +++ b/test/test_intel/autosearch/18_complete_emulation_plan.md @@ -666,11 +666,43 @@ SYNC rising. Documented in `4001-rom.c`. --- -## Phase C deferred items — still pending -- **8253 PIT** (programmable interval timer) — 3 channels of 16-bit - countdown timers, 6 modes. Needed for system-tick interrupts and PC - speaker tone generation. -- **8259 PIC** (programmable interrupt controller) — needed for - actual hardware interrupt routing on 8080/Z80/8086 demos. +## Phase C extension — completed (2026-04-30) + +### Delivered (the two deferred chips from Phase C) + +**8259 PIC** (`test_buses/8259-pic.c`, ~280 LOC). Single-master mode: +- ICW1..ICW4 init sequence with branching on the "single" and + "ICW4 needed" flags (ICW1 bits 1 and 0). +- IRR / ISR / IMR registers + read-back via OCW3. +- Priority-based INT assertion (lower IRQ# = higher priority, + fully-nested mode). +- INTA cycle drives `vector_base + IRQ#` on D bus. +- Non-specific (0x20) and specific (0x60..0x67) EOI commands. +- Pre-emption: a higher-priority IRQ during a lower-priority ISR + re-asserts INT. +- 7/7 tests pass: pin contract, IRQ→INT, INTA→vector for IRQ0/3, + IMR mask, EOI, pre-emption. +- **Cascade mode and slave-PIC routing NOT implemented** (single + master is enough for 95% of demos). + +**8253 PIT** (`test_buses/8253-pit.c`, ~210 LOC). Three channels with +- Mode 0 (interrupt on terminal count): OUT low after control, high + when count hits 0. +- Mode 2 (rate generator): OUT pulses low for one CLK then auto- + reloads — used for system tick. +- Mode 3 (square wave): OUT toggles every (count/2) CLKs — used for + PC speaker tone. +- Modes 1, 4, 5 NOT implemented; control writes selecting them + silently coerce to Mode 0. +- LSB-only / MSB-only / LSB-then-MSB read/write modes all work; the + "latch counter" rw mode (00) snapshots the current count for the + next read. +- GATE pin pauses counting when low. +- 4/4 tests pass. + +### Tests delta +- Total test_intel: 99 → **110 passing**, 11 todo, 0 failed (+11). + +## Phase G — still deferred (cycle accuracy) ## Phase G — still deferred (cycle accuracy) diff --git a/test/test_intel/test_buses/8253-pit.c b/test/test_intel/test_buses/8253-pit.c new file mode 100644 index 00000000..3f2fddc0 --- /dev/null +++ b/test/test_intel/test_buses/8253-pit.c @@ -0,0 +1,276 @@ +/* + * Intel 8253 Programmable Interval Timer — Modes 0, 2, 3 subset. + * + * The 8253 (and pin-compatible 8254) is a 24-pin DIP that gives a CPU + * three independent 16-bit countdown counters. Each counter has its + * own external CLK input and GATE input plus an OUT output. Six + * counting modes; we implement the three most common: + * + * Mode 0 (interrupt on terminal count): OUT low after writing the + * control word; once a count is loaded and GATE is high, OUT stays + * low until count counts down to zero, then goes high (and stays + * high until a new count is written). + * + * Mode 2 (rate generator): OUT goes low for one CLK then back high; + * counter automatically reloads. Used for periodic system-tick. + * + * Mode 3 (square wave): OUT toggles every (count/2) CLKs (for even + * counts); used for PC-speaker tone generation. + * + * Source: Intel 8253/8254 Datasheet (public mirror, bitsavers.org). + * + * Pin contract (24-pin DIP): + * D0..D7 bidirectional 8-bit data bus + * A0, A1 inputs — register select (00/01/10 = counters; 11 = ctrl) + * CS̅, RD̅, WR̅ + * CLK0..2 inputs — counter clocks (rising edge counts) + * GATE0..2 inputs — counter enable (high = enable) + * OUT0..2 outputs — counter outputs (mode-specific behaviour) + * VCC, GND + * + * Control word format: + * bits 7..6: counter select (00=ch0, 01=ch1, 10=ch2, 11=read-back NI) + * bits 5..4: read/write mode + * 00 = latch counter for read + * 01 = read/write LSB only + * 10 = read/write MSB only + * 11 = read/write LSB then MSB + * bits 3..1: counting mode (we implement 0, 2, 3) + * bit 0: 0 = binary, 1 = BCD (BCD not supported) + * + * Modes 1, 4, 5 are not implemented; control writes selecting them + * load as Mode 0 with a warning (silently). + */ +#include "velxio-chip.h" +#include +#include + +typedef struct { + /* Per-counter state */ + uint8_t mode; /* 0, 2, or 3 (others coerced to 0) */ + uint8_t rw_mode; /* 0 = latch, 1 = LSB, 2 = MSB, 3 = LSB-then-MSB */ + bool write_high_next; /* for rw_mode == 3 */ + bool read_high_next; + uint16_t reload; /* loaded count value */ + uint16_t count; /* current count */ + uint16_t latched; /* snapshot for read-back */ + bool have_latched; + bool out_state; + bool armed; /* count is loaded and ready */ +} channel_t; + +typedef struct { + vx_pin d[8]; + vx_pin a0, a1; + vx_pin cs, rd, wr; + vx_pin clk[3]; + vx_pin gate[3]; + vx_pin out[3]; + vx_pin vcc, gnd; + + channel_t ch[3]; + bool driving_d; + int wr_last; +} chip_t; + +static chip_t G; + +/* ─── D-bus helpers ─────────────────────────────────────────────────────── */ +static uint8_t read_d_byte(void) { + uint8_t v = 0; + for (int i = 0; i < 8; i++) if (vx_pin_read(G.d[i])) v |= (1u << i); + return v; +} +static void drive_d(uint8_t v) { + for (int i = 0; i < 8; i++) { + vx_pin_set_mode(G.d[i], VX_OUTPUT); + vx_pin_write(G.d[i], (v >> i) & 1); + } + G.driving_d = true; +} +static void release_d(void) { + if (!G.driving_d) return; + for (int i = 0; i < 8; i++) vx_pin_set_mode(G.d[i], VX_INPUT); + G.driving_d = false; +} + +static void drive_out(int idx, bool high) { + G.ch[idx].out_state = high; + vx_pin_write(G.out[idx], high ? 1 : 0); +} + +/* ─── Control word parsing ──────────────────────────────────────────────── */ +static void apply_control(uint8_t cw) { + int sel = (cw >> 6) & 3; + int rw = (cw >> 4) & 3; + int mode = (cw >> 1) & 7; + if (sel == 3) return; /* read-back command — not supported */ + channel_t* c = &G.ch[sel]; + c->rw_mode = rw; + if (rw == 0) { + /* Latch: snapshot current count for next read. */ + c->latched = c->count; + c->have_latched = true; + return; + } + /* Coerce unsupported modes to 0. */ + if (mode != 0 && mode != 2 && mode != 3) mode = 0; + c->mode = (uint8_t)mode; + c->armed = false; + c->write_high_next = false; + c->read_high_next = false; + /* OUT goes low after a control word for Mode 0; high for 2 and 3. */ + drive_out(sel, mode != 0); +} + +/* Counter byte write. */ +static void counter_write(int idx, uint8_t byte) { + channel_t* c = &G.ch[idx]; + switch (c->rw_mode) { + case 1: /* LSB */ + c->reload = (c->reload & 0xFF00) | byte; + c->count = c->reload; + c->armed = true; + break; + case 2: /* MSB */ + c->reload = (uint16_t)((c->reload & 0x00FF) | ((uint16_t)byte << 8)); + c->count = c->reload; + c->armed = true; + break; + case 3: /* LSB then MSB */ + if (!c->write_high_next) { + c->reload = (c->reload & 0xFF00) | byte; + c->write_high_next = true; + /* Counter is "disarmed" between LSB and MSB writes. */ + c->armed = false; + } else { + c->reload = (uint16_t)((c->reload & 0x00FF) | ((uint16_t)byte << 8)); + c->count = c->reload; + c->armed = true; + c->write_high_next = false; + } + break; + } +} + +static uint8_t counter_read(int idx) { + channel_t* c = &G.ch[idx]; + uint16_t value = c->have_latched ? c->latched : c->count; + switch (c->rw_mode) { + case 1: /* LSB */ + c->have_latched = false; + return (uint8_t)(value & 0xFF); + case 2: /* MSB */ + c->have_latched = false; + return (uint8_t)(value >> 8); + case 3: /* LSB then MSB */ + if (!c->read_high_next) { + c->read_high_next = true; + return (uint8_t)(value & 0xFF); + } else { + c->read_high_next = false; + c->have_latched = false; + return (uint8_t)(value >> 8); + } + default: + c->have_latched = false; + return (uint8_t)(value & 0xFF); + } +} + +/* ─── CLK rising-edge per channel: count down. ──────────────────────────── */ +static void on_clk(void* user_data, vx_pin pin, int value) { + int idx = (int)(intptr_t)user_data; + (void)pin; + if (value != 1) return; + if (vx_pin_read(G.gate[idx]) == 0) return; + channel_t* c = &G.ch[idx]; + if (!c->armed) return; + + switch (c->mode) { + case 0: /* interrupt on terminal count */ + if (c->count > 0) c->count--; + if (c->count == 0) { + drive_out(idx, true); + /* Stay at 0 until new count is loaded (count is 0xFFFF + on next CLK; we just leave at 0). */ + c->armed = false; + } + break; + case 2: /* rate generator */ + if (c->count > 0) c->count--; + if (c->count == 1) { + drive_out(idx, false); + } else if (c->count == 0) { + drive_out(idx, true); + c->count = c->reload; + } + break; + case 3: { /* square wave — decrement by 2 each CLK */ + if (c->count >= 2) c->count -= 2; + else c->count = 0; + if (c->count == 0) { + drive_out(idx, !c->out_state); + c->count = c->reload; + } + break; + } + } +} + +/* ─── RD / WR strobes ───────────────────────────────────────────────────── */ +static void on_rd(void* user_data, vx_pin pin, int value) { + (void)user_data; (void)pin; + if (vx_pin_read(G.cs) != 0) { release_d(); return; } + if (value != 0) { release_d(); return; } + int sel = (vx_pin_read(G.a1) ? 2 : 0) | (vx_pin_read(G.a0) ? 1 : 0); + if (sel == 3) { drive_d(0); return; } /* control reg reads back undefined */ + drive_d(counter_read(sel)); +} +static void on_wr(void* user_data, vx_pin pin, int value) { + (void)user_data; (void)pin; + if (vx_pin_read(G.cs) != 0) { G.wr_last = value; return; } + if (G.wr_last == 0 && value == 1) { + int sel = (vx_pin_read(G.a1) ? 2 : 0) | (vx_pin_read(G.a0) ? 1 : 0); + uint8_t byte = read_d_byte(); + if (sel == 3) apply_control(byte); + else counter_write(sel, byte); + } + G.wr_last = value; +} + +void chip_setup(void) { + char name[6]; + for (int i = 0; i < 8; i++) { + name[0]='D'; name[1]='0'+i; name[2]=0; + G.d[i] = vx_pin_register(name, VX_INPUT); + } + G.a0 = vx_pin_register("A0", VX_INPUT); + G.a1 = vx_pin_register("A1", VX_INPUT); + G.cs = vx_pin_register("CS", VX_INPUT); + G.rd = vx_pin_register("RD", VX_INPUT); + G.wr = vx_pin_register("WR", VX_INPUT); + for (int i = 0; i < 3; i++) { + name[0]='C'; name[1]='L'; name[2]='K'; name[3]='0'+i; name[4]=0; + G.clk[i] = vx_pin_register(name, VX_INPUT); + name[0]='G'; name[1]='A'; name[2]='T'; name[3]='E'; name[4]='0'+i; name[5]=0; + G.gate[i] = vx_pin_register(name, VX_INPUT); + name[0]='O'; name[1]='U'; name[2]='T'; name[3]='0'+i; name[4]=0; + G.out[i] = vx_pin_register(name, VX_OUTPUT_LOW); + } + G.vcc = vx_pin_register("VCC", VX_INPUT); + G.gnd = vx_pin_register("GND", VX_INPUT); + + for (int i = 0; i < 3; i++) { + G.ch[i] = (channel_t){0}; + drive_out(i, false); + } + G.driving_d = false; + G.wr_last = 1; + + vx_pin_watch(G.rd, VX_EDGE_BOTH, on_rd, 0); + vx_pin_watch(G.wr, VX_EDGE_BOTH, on_wr, 0); + for (int i = 0; i < 3; i++) { + vx_pin_watch(G.clk[i], VX_EDGE_RISING, on_clk, (void*)(intptr_t)i); + } +} diff --git a/test/test_intel/test_buses/8253-pit.test.js b/test/test_intel/test_buses/8253-pit.test.js new file mode 100644 index 00000000..4abfcd43 --- /dev/null +++ b/test/test_intel/test_buses/8253-pit.test.js @@ -0,0 +1,116 @@ +/** + * Intel 8253 PIT — unit tests for Mode 0, Mode 3. + */ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { BoardHarness } from '../src/BoardHarness.js'; +import { chipWasmExists } from '../src/helpers.js'; + +const CHIP = '8253-pit'; +const skip = !chipWasmExists(CHIP); + +function pinMap() { + const m = { + A0: 'A0', A1: 'A1', CS: 'CS', RD: 'RD', WR: 'WR', + VCC: 'VCC', GND: 'GND', + }; + for (let i = 0; i < 8; i++) m[`D${i}`] = `D${i}`; + for (let i = 0; i < 3; i++) { + m[`CLK${i}`] = `CLK${i}`; + m[`GATE${i}`] = `GATE${i}`; + m[`OUT${i}`] = `OUT${i}`; + } + return m; +} + +function setData(board, byte) { + for (let i = 0; i < 8; i++) board.setNet(`D${i}`, ((byte >> i) & 1) === 1); +} + +function pitWrite(board, sel, value) { + board.setNet('A0', (sel & 1) !== 0); + board.setNet('A1', (sel & 2) !== 0); + setData(board, value); + board.advanceNanos(20); + board.setNet('CS', false); + board.setNet('WR', false); + board.advanceNanos(20); + board.setNet('WR', true); + board.advanceNanos(20); + board.setNet('CS', true); +} + +function pulseCLK(board, idx, n) { + for (let i = 0; i < n; i++) { + board.setNet(`CLK${idx}`, false); + board.advanceNanos(10); + board.setNet(`CLK${idx}`, true); + board.advanceNanos(10); + } +} + +async function setup(board) { + await board.addChip(CHIP, pinMap()); + board.setNet('CS', true); + board.setNet('RD', true); + board.setNet('WR', true); + for (let i = 0; i < 3; i++) { + board.setNet(`CLK${i}`, false); + board.setNet(`GATE${i}`, true); + } +} + +describe(`${CHIP} chip`, () => { + let board; + beforeEach(() => { board = new BoardHarness(); }); + afterEach(() => { board.dispose(); }); + + it.skipIf(skip)('registers all 24 logical pins', async () => { + await expect(board.addChip(CHIP, pinMap())).resolves.toBeDefined(); + }); + + it.skipIf(skip)('Mode 0: OUT low after control, high when count reaches 0', async () => { + await setup(board); + // Counter 0, RW LSB only, Mode 0: 0011_000_0 = 0x10 + pitWrite(board, 3, 0x10); + expect(board.getNet('OUT0')).toBe(false); + // Load count = 5 + pitWrite(board, 0, 0x05); + // Pulse CLK0 four times → count goes 5→4→3→2→1, OUT still low. + pulseCLK(board, 0, 4); + expect(board.getNet('OUT0')).toBe(false); + // One more pulse → count → 0, OUT goes high. + pulseCLK(board, 0, 1); + expect(board.getNet('OUT0')).toBe(true); + }); + + it.skipIf(skip)('Mode 0: GATE low pauses countdown', async () => { + await setup(board); + pitWrite(board, 3, 0x10); // ch0 LSB Mode 0 + pitWrite(board, 0, 0x03); + // Drop GATE — pulses should not decrement. + board.setNet('GATE0', false); + pulseCLK(board, 0, 10); + expect(board.getNet('OUT0')).toBe(false); + // Restore GATE — now 3 pulses get to terminal count. + board.setNet('GATE0', true); + pulseCLK(board, 0, 3); + expect(board.getNet('OUT0')).toBe(true); + }); + + it.skipIf(skip)('Mode 3: OUT toggles every count/2 CLKs', async () => { + await setup(board); + // Counter 1, RW LSB+MSB, Mode 3: 01_11_011_0 = 0x76 + pitWrite(board, 3, 0x76); + // OUT should be HIGH after Mode 3 control word + expect(board.getNet('OUT1')).toBe(true); + // Load count = 4 (LSB then MSB) + pitWrite(board, 1, 0x04); + pitWrite(board, 1, 0x00); + // Mode 3 decrements by 2 each CLK; with count=4, OUT toggles + // every 2 CLKs. + pulseCLK(board, 1, 2); + expect(board.getNet('OUT1')).toBe(false); + pulseCLK(board, 1, 2); + expect(board.getNet('OUT1')).toBe(true); + }); +}); diff --git a/test/test_intel/test_buses/8259-pic.c b/test/test_intel/test_buses/8259-pic.c new file mode 100644 index 00000000..c0f21f42 --- /dev/null +++ b/test/test_intel/test_buses/8259-pic.c @@ -0,0 +1,293 @@ +/* + * Intel 8259 Programmable Interrupt Controller — single-mode subset. + * + * The 8259 is a 28-pin DIP that funnels up to 8 interrupt request + * lines (IRQ0..IRQ7) onto a CPU's single INTR line, driving an + * 8086/8080-style INTA acknowledge cycle to deliver an interrupt + * vector byte on the data bus. Multiple 8259s can be cascaded for up + * to 64 IRQs — we implement single-master only. + * + * Source: Intel 8259A Datasheet (public mirror, bitsavers.org). + * + * Pin contract (28-pin DIP): + * D0..D7 bidirectional 8-bit data bus + * A0 input — register select (0 = ICW1/OCW2/OCW3, 1 = ICW2..4 / OCW1 / IMR) + * CS̅ input — active-low chip enable + * RD̅ input — active-low read strobe + * WR̅ input — active-low write strobe + * IRQ0..7 inputs — active-high requests (edge or level depending on ICW1) + * INT output — driven HIGH when an unmasked IRQ is pending + * INTA̅ input — active-low acknowledge from CPU + * CAS0..2 I/O — cascade lines (NOT implemented) + * SP/EN̅ I/O — slave/buffer-enable (master mode only here) + * VCC, GND power + * + * Init sequence: + * ICW1 (A0=0, bit 4 = 1): bit 0 = "ICW4 needed"; bit 1 = single (1)/ + * cascaded (0); bit 3 = level/edge triggered. + * ICW2 (A0=1): vector base byte. IRQ n vector = base + n. + * ICW3 (A0=1): cascade config — skipped when ICW1 bit 1 = 1 (single). + * ICW4 (A0=1): mode bits (8086 mode if bit 0 = 1) — skipped when + * ICW1 bit 0 = 0. + * Then enters operating mode: + * OCW1 (A0=1): write to IMR (interrupt mask). + * OCW2 (A0=0, bits 4..3 = 00): EOI commands — non-specific (0x20) + * or specific (0x60..0x67). + * OCW3 (A0=0, bits 4..3 = 01): read IRR/ISR select. + * + * INTA cycle (8086 mode, 2 INTA̅ pulses): + * First INTA̅↓ — chip locks the highest-priority pending IRR bit, + * sets ISR bit, clears IRR bit; drives 0xFF on the data bus + * (manual says undefined; we drive 0xFF as is conventional). + * Second INTA̅↓ — chip drives the vector byte (base + IRQ#). + * We approximate by always driving the vector on every INTA̅↓ — + * simpler and works fine in tests. + * + * EOI: OCW2 with bit 5 = 1 clears the highest-priority ISR bit. + */ +#include "velxio-chip.h" +#include +#include +#include + +typedef enum { + INIT_NEED_ICW1 = 0, + INIT_NEED_ICW2, + INIT_NEED_ICW3, + INIT_NEED_ICW4, + INIT_RUNNING, +} init_state_t; + +typedef struct { + vx_pin d[8]; + vx_pin a0, cs, rd, wr; + vx_pin irq[8]; + vx_pin intp; /* INT output to CPU */ + vx_pin inta; /* INTA̅ input from CPU */ + vx_pin cas[3]; /* cascade — not used */ + vx_pin sp_en; /* not used */ + vx_pin vcc, gnd; + + init_state_t init_state; + uint8_t icw1; /* saved init word 1 */ + uint8_t vector_base; /* ICW2 */ + bool single; /* ICW1 bit 1 */ + bool need_icw4; /* ICW1 bit 0 */ + uint8_t imr; /* mask: bit n = 1 → IRQ n masked */ + uint8_t irr; /* pending requests */ + uint8_t isr; /* in-service */ + uint8_t read_select; /* 0 = read IRR on next A0=0 read, 1 = ISR */ + + int wr_last; + int inta_last; + bool driving_d; +} chip_t; + +static chip_t G; + +/* ─── D-bus helpers ─────────────────────────────────────────────────────── */ +static uint8_t read_d_byte(void) { + uint8_t v = 0; + for (int i = 0; i < 8; i++) if (vx_pin_read(G.d[i])) v |= (1u << i); + return v; +} +static void drive_d(uint8_t v) { + for (int i = 0; i < 8; i++) { + vx_pin_set_mode(G.d[i], VX_OUTPUT); + vx_pin_write(G.d[i], (v >> i) & 1); + } + G.driving_d = true; +} +static void release_d(void) { + if (!G.driving_d) return; + for (int i = 0; i < 8; i++) vx_pin_set_mode(G.d[i], VX_INPUT); + G.driving_d = false; +} + +/* Find highest-priority bit (bit 0 = highest, per fully-nested mode). */ +static int highest_priority(uint8_t bits) { + for (int i = 0; i < 8; i++) if (bits & (1u << i)) return i; + return -1; +} + +/* Update INT output: HIGH iff there's an unmasked IRR bit higher in + priority than any current ISR bit. */ +static void update_int(void) { + uint8_t pending = G.irr & ~G.imr; + if (pending == 0) { + vx_pin_write(G.intp, 0); + return; + } + int pend_top = highest_priority(pending); + int isr_top = highest_priority(G.isr); + /* Higher priority = lower bit index. INT iff pending priority is + strictly more important than current in-service. */ + if (isr_top < 0 || pend_top < isr_top) { + vx_pin_write(G.intp, 1); + } else { + vx_pin_write(G.intp, 0); + } +} + +/* ─── Pin watchers ──────────────────────────────────────────────────────── */ + +static void on_irq(void* user_data, vx_pin pin, int value) { + int n = (int)(intptr_t)user_data; + (void)pin; + if (value) { + G.irr |= (uint8_t)(1u << n); + update_int(); + } + /* For edge-triggered mode (ICW1 bit 3 = 0), level transitions + from low to high are what set IRR. Level mode would re-arm + on every poll — we don't implement that. */ +} + +static void on_inta(void* user_data, vx_pin pin, int value) { + (void)user_data; (void)pin; + if (G.inta_last == 1 && value == 0) { + /* INTA̅ falling — drive vector for the highest-priority pending + unmasked IRQ. Set ISR bit, clear IRR bit, deassert INT. */ + uint8_t pending = G.irr & ~G.imr; + int n = highest_priority(pending); + if (n >= 0) { + G.isr |= (uint8_t)(1u << n); + G.irr &= (uint8_t)~(1u << n); + uint8_t vec = (uint8_t)(G.vector_base + n); + drive_d(vec); + vx_pin_write(G.intp, 0); + } else { + /* Spurious — drive vector base + 7 per Intel app note. */ + drive_d((uint8_t)(G.vector_base + 7)); + } + } else if (value == 1) { + release_d(); + } + G.inta_last = value; +} + +static void handle_write(uint8_t a0, uint8_t v) { + if (a0 == 0) { + if (v & 0x10) { + /* ICW1 — entering init mode. */ + G.icw1 = v; + G.single = (v & 0x02) != 0; + G.need_icw4 = (v & 0x01) != 0; + G.imr = 0xFF; + G.irr = 0; + G.isr = 0; + G.init_state = INIT_NEED_ICW2; + update_int(); + return; + } + if ((v & 0x18) == 0x00) { + /* OCW2 — EOI / priority commands. */ + uint8_t cmd = v & 0xE0; + if (cmd == 0x20) { + /* Non-specific EOI: clear highest-priority ISR bit. */ + int top = highest_priority(G.isr); + if (top >= 0) G.isr &= (uint8_t)~(1u << top); + } else if (cmd == 0x60) { + /* Specific EOI — bits 0..2 are IRQ#. */ + G.isr &= (uint8_t)~(1u << (v & 7)); + } + update_int(); + } else if ((v & 0x18) == 0x08) { + /* OCW3 — read register select. */ + if ((v & 0x02) != 0) { + G.read_select = (v & 0x01); + } + } + return; + } + /* A0 = 1 */ + switch (G.init_state) { + case INIT_NEED_ICW2: + G.vector_base = (v & 0xF8); /* low 3 bits ignored in 8086 mode */ + if (G.single) { + G.init_state = G.need_icw4 ? INIT_NEED_ICW4 : INIT_RUNNING; + } else { + G.init_state = INIT_NEED_ICW3; + } + break; + case INIT_NEED_ICW3: + G.init_state = G.need_icw4 ? INIT_NEED_ICW4 : INIT_RUNNING; + break; + case INIT_NEED_ICW4: + G.init_state = INIT_RUNNING; + break; + case INIT_RUNNING: + case INIT_NEED_ICW1: + /* OCW1 — write IMR. */ + G.imr = v; + update_int(); + break; + } +} + +static void on_wr(void* user_data, vx_pin pin, int value) { + (void)user_data; (void)pin; + if (vx_pin_read(G.cs) != 0) { G.wr_last = value; return; } + if (G.wr_last == 0 && value == 1) { + /* Latch on rising edge */ + uint8_t a0 = vx_pin_read(G.a0) ? 1 : 0; + handle_write(a0, read_d_byte()); + } + G.wr_last = value; +} + +static void on_rd(void* user_data, vx_pin pin, int value) { + (void)user_data; (void)pin; + if (vx_pin_read(G.cs) != 0) { release_d(); return; } + if (value == 0) { + uint8_t a0 = vx_pin_read(G.a0) ? 1 : 0; + if (a0 == 0) { + drive_d(G.read_select ? G.isr : G.irr); + } else { + drive_d(G.imr); + } + } else { + release_d(); + } +} + +void chip_setup(void) { + char name[6]; + + for (int i = 0; i < 8; i++) { + name[0]='D'; name[1]='0'+i; name[2]=0; + G.d[i] = vx_pin_register(name, VX_INPUT); + } + for (int i = 0; i < 8; i++) { + name[0]='I'; name[1]='R'; name[2]='Q'; name[3]='0'+i; name[4]=0; + G.irq[i] = vx_pin_register(name, VX_INPUT); + } + G.a0 = vx_pin_register("A0", VX_INPUT); + G.cs = vx_pin_register("CS", VX_INPUT); + G.rd = vx_pin_register("RD", VX_INPUT); + G.wr = vx_pin_register("WR", VX_INPUT); + G.intp = vx_pin_register("INT", VX_OUTPUT_LOW); + G.inta = vx_pin_register("INTA", VX_INPUT); + G.cas[0]= vx_pin_register("CAS0", VX_INPUT); + G.cas[1]= vx_pin_register("CAS1", VX_INPUT); + G.cas[2]= vx_pin_register("CAS2", VX_INPUT); + G.sp_en = vx_pin_register("SPEN", VX_INPUT); + G.vcc = vx_pin_register("VCC", VX_INPUT); + G.gnd = vx_pin_register("GND", VX_INPUT); + + G.init_state = INIT_NEED_ICW1; + G.imr = 0xFF; + G.irr = G.isr = 0; + G.vector_base = 0; + G.read_select = 0; + G.wr_last = 1; + G.inta_last = 1; + G.driving_d = false; + + for (int i = 0; i < 8; i++) { + vx_pin_watch(G.irq[i], VX_EDGE_RISING, on_irq, (void*)(intptr_t)i); + } + vx_pin_watch(G.wr, VX_EDGE_BOTH, on_wr, 0); + vx_pin_watch(G.rd, VX_EDGE_BOTH, on_rd, 0); + vx_pin_watch(G.inta, VX_EDGE_BOTH, on_inta, 0); +} diff --git a/test/test_intel/test_buses/8259-pic.test.js b/test/test_intel/test_buses/8259-pic.test.js new file mode 100644 index 00000000..3f9f05c5 --- /dev/null +++ b/test/test_intel/test_buses/8259-pic.test.js @@ -0,0 +1,157 @@ +/** + * Intel 8259 PIC — unit tests. + * + * Verifies the CPU-facing register interface (init word sequence, OCW1 + * mask, EOI), the IRQ-to-INT pipeline, and the INTA acknowledge cycle. + */ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { BoardHarness } from '../src/BoardHarness.js'; +import { chipWasmExists } from '../src/helpers.js'; + +const CHIP = '8259-pic'; +const skip = !chipWasmExists(CHIP); + +function pinMap() { + const m = { + A0: 'A0', CS: 'CS', RD: 'RD', WR: 'WR', + INT: 'INT', INTA: 'INTA', + CAS0: 'CAS0', CAS1: 'CAS1', CAS2: 'CAS2', SPEN: 'SPEN', + VCC: 'VCC', GND: 'GND', + }; + for (let i = 0; i < 8; i++) m[`D${i}`] = `D${i}`; + for (let i = 0; i < 8; i++) m[`IRQ${i}`] = `IRQ${i}`; + return m; +} + +function setData(board, byte) { + for (let i = 0; i < 8; i++) board.setNet(`D${i}`, ((byte >> i) & 1) === 1); +} +function readData(board) { + let v = 0; + for (let i = 0; i < 8; i++) if (board.getNet(`D${i}`)) v |= (1 << i); + return v; +} + +function picWrite(board, a0, value) { + board.setNet('A0', a0 !== 0); + setData(board, value); + board.advanceNanos(20); + board.setNet('CS', false); + board.setNet('WR', false); + board.advanceNanos(20); + board.setNet('WR', true); // rising-edge latch + board.advanceNanos(20); + board.setNet('CS', true); +} +function picRead(board, a0) { + board.setNet('A0', a0 !== 0); + board.setNet('CS', false); + board.setNet('RD', false); + board.advanceNanos(20); + const v = readData(board); + board.setNet('RD', true); + board.setNet('CS', true); + return v; +} + +async function setup(board) { + await board.addChip(CHIP, pinMap()); + board.setNet('CS', true); + board.setNet('RD', true); + board.setNet('WR', true); + board.setNet('INTA', true); + for (let i = 0; i < 8; i++) board.setNet(`IRQ${i}`, false); + // Init: ICW1 (single mode + ICW4 needed), ICW2 (vector base 0x40), + // ICW4 (8086 mode bit 0 = 1). After this, IRQ vectors are + // 0x40..0x47. + picWrite(board, 0, 0x13); // ICW1: single, edge-trig, ICW4-needed + picWrite(board, 1, 0x40); // ICW2: vector base = 0x40 + picWrite(board, 1, 0x01); // ICW4: 8086 mode + picWrite(board, 1, 0x00); // OCW1: unmask all (IMR = 0) +} + +describe(`${CHIP} chip`, () => { + let board; + beforeEach(() => { board = new BoardHarness(); }); + afterEach(() => { board.dispose(); }); + + it.skipIf(skip)('registers all 28 logical pins', async () => { + await expect(board.addChip(CHIP, pinMap())).resolves.toBeDefined(); + }); + + it.skipIf(skip)('IRQ0 rising drives INT high after init', async () => { + await setup(board); + expect(board.getNet('INT')).toBe(false); + board.setNet('IRQ0', true); + board.advanceNanos(20); + expect(board.getNet('INT')).toBe(true); + }); + + it.skipIf(skip)('INTA falling drives the IRQ0 vector (0x40) on D bus', async () => { + await setup(board); + board.setNet('IRQ0', true); + board.advanceNanos(20); + // CPU asserts INTA̅ + board.setNet('INTA', false); + board.advanceNanos(20); + expect(readData(board)).toBe(0x40); + expect(board.getNet('INT')).toBe(false); // INT deasserted on ack + board.setNet('INTA', true); + }); + + it.skipIf(skip)('IRQ3 produces vector base+3 = 0x43', async () => { + await setup(board); + board.setNet('IRQ3', true); + board.advanceNanos(20); + board.setNet('INTA', false); + board.advanceNanos(20); + expect(readData(board)).toBe(0x43); + }); + + it.skipIf(skip)('IMR mask suppresses INT', async () => { + await setup(board); + // Mask IRQ0 by writing OCW1 with bit 0 set. + picWrite(board, 1, 0x01); + board.setNet('IRQ0', true); + board.advanceNanos(20); + expect(board.getNet('INT')).toBe(false); + }); + + it.skipIf(skip)('non-specific EOI clears the in-service bit', async () => { + await setup(board); + board.setNet('IRQ0', true); + board.advanceNanos(20); + // Acknowledge to lock ISR + board.setNet('INTA', false); + board.advanceNanos(20); + board.setNet('INTA', true); + board.advanceNanos(20); + // ISR bit 0 should be set; reading ISR via OCW3 + RD A0=0 confirms. + picWrite(board, 0, 0x0B); // OCW3: read ISR next + expect(picRead(board, 0)).toBe(0x01); + // Send non-specific EOI + picWrite(board, 0, 0x20); + expect(picRead(board, 0)).toBe(0x00); + }); + + it.skipIf(skip)('higher-priority IRQ pre-empts lower-priority pending', async () => { + await setup(board); + // IRQ7 lower priority than IRQ1. + board.setNet('IRQ7', true); + board.advanceNanos(20); + expect(board.getNet('INT')).toBe(true); + // ACK IRQ7 + board.setNet('INTA', false); + board.advanceNanos(20); + expect(readData(board)).toBe(0x47); + board.setNet('INTA', true); + board.advanceNanos(20); + // Now IRQ1 fires while IRQ7 is in-service. Should pre-empt INT. + board.setNet('IRQ1', true); + board.advanceNanos(20); + expect(board.getNet('INT')).toBe(true); + board.setNet('INTA', false); + board.advanceNanos(20); + expect(readData(board)).toBe(0x41); + }); +});