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:
parent
f7223b4965
commit
479b52634e
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue