test_intel: phase C extension — 8259 PIC and 8253 PIT

Two long-deferred Phase C chips, both clean-room from public Intel
datasheets. These complete the support-chip ecosystem needed for
real interrupt-driven 8080/Z80/8086 software on the canvas.

8259 PIC (~280 LOC, single-master subset):
- Full ICW1..ICW4 init sequence with branching on single/cascade
  and ICW4-needed flags.
- IRR/ISR/IMR registers; OCW3 read-back; OCW1 mask write.
- Priority-based INT (lower IRQ# = higher priority, fully-nested);
  pre-emption when a higher-priority IRQ arrives during a lower-
  priority ISR.
- INTA falling-edge → drives vector_base + IRQ# on D bus.
- Non-specific (0x20) and specific (0x60..67) EOI.
- Cascade-master/slave routing NOT implemented (single master is
  sufficient for 95% of demos).
- 7/7 tests passing.

8253 PIT (~210 LOC, Modes 0/2/3 subset):
- Three independent 16-bit counters with own CLK/GATE/OUT pins.
- Mode 0 (interrupt on terminal count) for one-shot timers.
- Mode 2 (rate generator) for system tick.
- Mode 3 (square wave, decrements by 2) for PC-speaker tone.
- Modes 1/4/5 coerced to Mode 0 (rare in practice).
- Full RW mode set: LSB-only / MSB-only / LSB-then-MSB / latch.
- GATE-low pauses the countdown.
- 4/4 tests passing.

Tests: total test_intel 99→110 passing (+11). 0 failed. 11 todo.
Master plan doc updated: Phase C extension done; Phase G still
deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Montero 2026-04-30 16:25:34 +02:00 committed by davidmonterocrespo24
parent f7223b4965
commit 479b52634e
5 changed files with 880 additions and 6 deletions

View File

@ -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)

View File

@ -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 <stdint.h>
#include <stdbool.h>
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);
}
}

View File

@ -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);
});
});

View File

@ -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 <stdint.h>
#include <stdbool.h>
#include <string.h>
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);
}

View File

@ -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);
});
});