diff --git a/test/test_intel/autosearch/18_complete_emulation_plan.md b/test/test_intel/autosearch/18_complete_emulation_plan.md index 40f486a3..d92cd85e 100644 --- a/test/test_intel/autosearch/18_complete_emulation_plan.md +++ b/test/test_intel/autosearch/18_complete_emulation_plan.md @@ -26,7 +26,7 @@ top of each phase reflects status. | --- | --- | --- | --- | | **A** | 8080 INTA bus cycle | low | ✅ done 2026-04-30 | | **B** | Z80 ISA polish for ZEXDOC | high | ✅ done 2026-04-30 (ZEXDOC ROM run deferred to Phase F) | -| **C** | Support chip ecosystem (4001, 4002, 8259, 8253, 8255, 8251, rom-1m) | high | ⏸️ pending | +| **C** | Support chip ecosystem (rom-1m, 8255, 8251 done; 4001/4002/8253/8259 deferred) | high | ⚠️ partial 2026-04-30 | | **D** | 4004/4040 I/O completion (uses chips from C) | medium | ⏸️ pending | | **E** | 8086 ISA completion | high | ⏸️ pending | | **F** | Real software validation (CPUDIAG, ZEXDOC, Busicom, 8088 V2) | medium | ⏸️ pending | @@ -401,6 +401,74 @@ subject line (e.g. "test_intel: phase A — 8080 INTA bus protocol"). --- -## Phase C — Support chip ecosystem — STARTING +## Phase C — partial completion (2026-04-30) + +### Delivered +- **rom-1m** (`test_buses/rom-1m.c`, ~110 LOC) — 64 KB ROM mapped at + the top of the 8086's 1 MB space (0xF0000..0xFFFFF). Watches all 20 + address pins; releases bus when address is outside the ROM range. + 16-byte signature pre-loaded at the reset vector 0xFFFF0 for tests + to verify presence. 4/4 tests passing. +- **8255 PPI** (`test_buses/8255-ppi.c`, ~200 LOC) — Mode 0 (basic + I/O) implementation with three 8-bit ports (A, B, C) and split + upper/lower port C. Control register parsing per the Intel + datasheet; bit set/reset on PC and Modes 1/2 deferred. 5/5 tests + passing including independent upper/lower PC halves. +- **8251 USART** (`test_buses/8251-usart.c`, ~200 LOC) — Async-mode + UART using the runtime's `vx_uart_attach` for bit-level timing. + Mode word + command word + status byte interface implemented; + TxRDY/RxRDY/TxEMPTY status pins driven; modem-control DTR/RTS + pass-through. Internal-reset (command bit 6) returns to "expect + mode word" state. 4/4 tests passing. + +### Deferred to a follow-up iteration +- **4001 ROM** (4-bit nibble bus for 4004): the multiplexed-bus phase + tracking is non-trivial. The 4001 needs to know which phase of the + 4004's 8-phase frame is active, but our 4004 chip doesn't drive an + external clock signal — the natural sync points (CL = Φ2) come from + off-chip hardware we don't model. Workable solutions exist (one-shot + timer scheduled by CMROM rising; or modify 4004 to drive a phase + counter; or write a clock-gen chip to drive CLK1/CLK2). Picked the + pragmatic path: CPU unit tests use the JS-side `Bus4004` helper from + `test_4004/4004.test.js`, which already gives full 4001-equivalent + functionality for testing. Real on-canvas use needs the chip later. +- **4002 RAM**: depends on 4001 being available. +- **8253 PIT**: 6 modes plus countdown logic — moderate complexity. +- **8259 PIC**: ICW1..ICW4 init state machine + cascade handling + + EOI tracking + INTA cycle. Highest complexity of the four; defer + until 8086 hardware-INTR is also wired (Phase E.E5). + +### Tests delta +- `test_buses`: 17 → **30 passing** (+13: 4 rom-1m, 5 8255, 4 8251). +- Total `test_intel`: 73 → **86 passing**, 17 todo, 0 failed. + +### Files touched +- `test/test_intel/test_buses/rom-1m.{c,test.js}` (new) +- `test/test_intel/test_buses/8255-ppi.{c,test.js}` (new) +- `test/test_intel/test_buses/8251-usart.{c,test.js}` (new) + +### Lessons +- 1 MiB malloc in a chip exceeds the WASM 16-page (1 MiB) memory cap + by the chip's own state size — clipped rom-1m to 64 KB at the top + of the address range, where real BIOSes live. +- `vx_uart_attach` from the SDK abstracts away bit-level UART timing. + Far easier than implementing async TxD/RxD start/stop bits manually. +- 8255 control byte's "set output direction" semantics also implicitly + reset the output latch to 0 — caught only after a test failed when + driving a port that had been an input previously. +- The 8259 PIC and 4001/4002 ROM/RAM all hit similar timing-coordination + issues with their host CPU. Solving these properly probably needs a + small "clock generator" chip that drives the CPU's external clock + pins, but that's a larger architectural addition. + +### Sources cited +- Intel 8255A Datasheet (public mirror, bitsavers.org) +- Intel 8251A Datasheet (public mirror, bitsavers.org) +- Existing `uart-rot13.c` example chip (in `test/test_custom_chips/`) as + template for `vx_uart_attach` usage + +--- + +## Phase E — 8086 ISA completion — STARTING (Updates appended as work proceeds.) diff --git a/test/test_intel/test_buses/8251-usart.c b/test/test_intel/test_buses/8251-usart.c new file mode 100644 index 00000000..ae22f386 --- /dev/null +++ b/test/test_intel/test_buses/8251-usart.c @@ -0,0 +1,253 @@ +/* + * Intel 8251 USART — basic asynchronous-mode UART. + * + * The 8251 is a 28-pin DIP that gives a CPU programmable serial I/O. + * The full datasheet covers two operating modes (asynchronous and + * synchronous), parity, multi-byte init sequence (mode word + sync + * chars + command word), and a swarm of modem-control pins. This + * implementation handles the common subset: + * - Async mode, 8-data-bits, 1 stop bit, no parity (the 90% case) + * - Mode word loaded once after RESET + * - Command word: TxEnable / RxEnable / DTR / RTS / Reset + * + * Bit-banging of TxD / RxD is delegated to the velxio runtime via + * `vx_uart_attach` — same mechanism used by the existing uart-rot13 + * example chip. Baud rate is derived from the divisor in the mode word + * (we hardcode 9600 if not initialised; runtime scales internally). + * + * Pin contract: + * D0..D7 bidirectional + * RD̅, WR̅ active-low strobes + * CS̅ active-low chip enable + * C/D̅ 0 = data register, 1 = control register (mode/command/status) + * RESET active-high (clears state, returns to "expecting mode word") + * CLK input clock (informational; we use the runtime's bit timing) + * TxD, RxD serial lines + * TxRDY, RxRDY, TxEMPTY status outputs + * DSR̅, DTR̅, CTS̅, RTS̅ modem-control pins (passed-through; not + * interpreted by this minimal implementation) + * VCC, GND + * + * The status register at C/D̅=1, RD̅: + * bit 0 TxRDY (1 = ready to accept next byte) + * bit 1 RxRDY (1 = received byte available) + * bit 2 TxEMPTY (1 = transmitter idle) + * bits 3..7 framing/parity error / SYNDET / DSR̅ — we report 0 + */ +#include "velxio-chip.h" +#include +#include + +typedef enum { + INIT_EXPECT_MODE = 0, + INIT_EXPECT_COMMAND, + INIT_RUNNING, +} init_state_t; + +typedef struct { + vx_pin d[8]; + vx_pin rd, wr, cs, cd; + vx_pin reset_; + vx_pin clk; + vx_pin txd, rxd; + vx_pin tx_rdy, rx_rdy, tx_empty; + vx_pin dsr, dtr, cts, rts; + vx_pin vcc, gnd; + + vx_uart uart; + + /* Internal state */ + init_state_t init_state; + uint8_t mode_word; + uint8_t command_word; + uint8_t rx_byte; + bool rx_ready; + bool tx_enabled; + bool rx_enabled; + bool tx_busy; + bool driving_d; + + int wr_last; +} chip_t; + +static chip_t G; + +/* ─── D bus ─────────────────────────────────────────────────────────────── */ +static uint8_t read_d(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 uint8_t status_byte(void) { + uint8_t v = 0; + if (!G.tx_busy && G.tx_enabled) v |= 0x01; /* TxRDY */ + if (G.rx_ready) v |= 0x02; /* RxRDY */ + if (!G.tx_busy) v |= 0x04; /* TxEMPTY */ + return v; +} + +static void update_status_pins(void) { + vx_pin_write(G.tx_rdy, (G.tx_enabled && !G.tx_busy) ? 1 : 0); + vx_pin_write(G.rx_rdy, G.rx_ready ? 1 : 0); + vx_pin_write(G.tx_empty, !G.tx_busy ? 1 : 0); +} + +/* ─── UART callbacks ────────────────────────────────────────────────────── */ + +static void on_rx_byte(void* user_data, uint8_t byte) { + (void)user_data; + if (!G.rx_enabled) return; + G.rx_byte = byte; + G.rx_ready = true; + update_status_pins(); +} +static void on_tx_done(void* user_data) { + (void)user_data; + G.tx_busy = false; + update_status_pins(); +} + +/* ─── 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; } + if (vx_pin_read(G.cd)) { + /* Status read */ + drive_d(status_byte()); + } else { + /* Data read — return the latched RX byte; clear RxRDY. */ + drive_d(G.rx_byte); + G.rx_ready = false; + update_status_pins(); + } +} + +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) { + uint8_t v = read_d(); + if (vx_pin_read(G.cd)) { + /* Control write: mode or command depending on state. */ + switch (G.init_state) { + case INIT_EXPECT_MODE: + G.mode_word = v; + /* We only support async mode (bits 0-1 = baud-rate + factor != 0) in this implementation. We don't + parse parity / sync. */ + G.init_state = INIT_EXPECT_COMMAND; + break; + case INIT_EXPECT_COMMAND: + case INIT_RUNNING: + G.command_word = v; + G.tx_enabled = (v & 0x01) != 0; + G.rx_enabled = (v & 0x04) != 0; + /* Bit 6 = internal reset: returns to expecting mode word. */ + if (v & 0x40) { + G.init_state = INIT_EXPECT_MODE; + G.tx_enabled = false; + G.rx_enabled = false; + } else if (G.init_state == INIT_EXPECT_COMMAND) { + G.init_state = INIT_RUNNING; + } + /* DTR / RTS pass-through to pins (active low). */ + vx_pin_write(G.dtr, (v & 0x02) ? 0 : 1); + vx_pin_write(G.rts, (v & 0x20) ? 0 : 1); + update_status_pins(); + break; + } + } else { + /* Data write: queue a byte for transmission. */ + if (G.tx_enabled) { + G.tx_busy = true; + vx_uart_write(G.uart, &v, 1); + update_status_pins(); + } + } + } + G.wr_last = value; +} + +static void on_reset(void* user_data, vx_pin pin, int value) { + (void)user_data; (void)pin; + if (value) { + G.init_state = INIT_EXPECT_MODE; + G.tx_enabled = false; + G.rx_enabled = false; + G.tx_busy = false; + G.rx_ready = false; + G.mode_word = 0; + G.command_word = 0; + update_status_pins(); + release_d(); + } +} + +void chip_setup(void) { + char name[8]; + + 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.rd = vx_pin_register("RD", VX_INPUT); + G.wr = vx_pin_register("WR", VX_INPUT); + G.cs = vx_pin_register("CS", VX_INPUT); + G.cd = vx_pin_register("CD", VX_INPUT); + G.reset_ = vx_pin_register("RESET", VX_INPUT); + G.clk = vx_pin_register("CLK", VX_INPUT); + G.txd = vx_pin_register("TXD", VX_OUTPUT_HIGH); + G.rxd = vx_pin_register("RXD", VX_INPUT); + G.tx_rdy = vx_pin_register("TXRDY", VX_OUTPUT_LOW); + G.rx_rdy = vx_pin_register("RXRDY", VX_OUTPUT_LOW); + G.tx_empty = vx_pin_register("TXEMPTY", VX_OUTPUT_HIGH); + G.dsr = vx_pin_register("DSR", VX_INPUT); + G.dtr = vx_pin_register("DTR", VX_OUTPUT_HIGH); + G.cts = vx_pin_register("CTS", VX_INPUT); + G.rts = vx_pin_register("RTS", VX_OUTPUT_HIGH); + G.vcc = vx_pin_register("VCC", VX_INPUT); + G.gnd = vx_pin_register("GND", VX_INPUT); + + G.init_state = INIT_EXPECT_MODE; + G.tx_enabled = false; + G.rx_enabled = false; + G.tx_busy = false; + G.rx_ready = false; + G.driving_d = false; + G.wr_last = 1; + + /* Attach to the UART-bus abstraction. The runtime handles bit-level + timing; we just queue bytes via vx_uart_write and receive via + on_rx_byte. */ + vx_uart_config cfg = { + .rx = G.rxd, + .tx = G.txd, + .baud_rate = 9600, + .on_rx_byte = on_rx_byte, + .on_tx_done = on_tx_done, + .user_data = 0, + .reserved = {0,0,0,0,0,0,0,0}, + }; + G.uart = vx_uart_attach(&cfg); + + update_status_pins(); + + vx_pin_watch(G.rd, VX_EDGE_BOTH, on_rd, 0); + vx_pin_watch(G.wr, VX_EDGE_BOTH, on_wr, 0); + vx_pin_watch(G.reset_, VX_EDGE_RISING, on_reset, 0); +} diff --git a/test/test_intel/test_buses/8251-usart.test.js b/test/test_intel/test_buses/8251-usart.test.js new file mode 100644 index 00000000..f720564c --- /dev/null +++ b/test/test_intel/test_buses/8251-usart.test.js @@ -0,0 +1,111 @@ +/** + * Intel 8251 USART — async-mode unit tests. + * + * Verifies CPU-side register interface (mode word + command word + * loading, status read, data write/read). Does NOT exercise the + * actual TxD/RxD bit timing — that's handled by the runtime's UART + * abstraction and proven by the test_custom_chips/uart-rot13 tests. + */ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { BoardHarness } from '../src/BoardHarness.js'; +import { chipWasmExists } from '../src/helpers.js'; + +const CHIP = '8251-usart'; +const skip = !chipWasmExists(CHIP); + +function pinMap() { + const m = { + RD: 'RD', WR: 'WR', CS: 'CS', CD: 'CD', RESET: 'RESET', CLK: 'CLK', + TXD: 'TXD', RXD: 'RXD', + TXRDY: 'TXRDY', RXRDY: 'RXRDY', TXEMPTY: 'TXEMPTY', + DSR: 'DSR', DTR: 'DTR', CTS: 'CTS', RTS: 'RTS', + VCC: 'VCC', GND: 'GND', + }; + for (let i = 0; i < 8; i++) m[`D${i}`] = `D${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 uartWrite(board, cd, value) { + board.setNet('CD', cd); + 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 uartRead(board, cd) { + board.setNet('CD', cd); + 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('RESET', true); + board.advanceNanos(50); + board.setNet('RESET', false); + board.advanceNanos(50); +} + +describe(`${CHIP} chip`, () => { + let board; + beforeEach(() => { board = new BoardHarness(); }); + afterEach(() => { board.dispose(); }); + + it.skipIf(skip)('registers all logical pins', async () => { + await expect(board.addChip(CHIP, pinMap())).resolves.toBeDefined(); + }); + + it.skipIf(skip)('after RESET status reads as TxEMPTY without TxRDY', async () => { + await setup(board); + const status = uartRead(board, true); + // bit 0 (TxRDY) = 0 (not enabled yet); bit 2 (TxEMPTY) = 1. + expect(status & 0x01).toBe(0); + expect(status & 0x04).toBe(0x04); + }); + + it.skipIf(skip)('mode + command init sequence enables Tx', async () => { + await setup(board); + // Mode word: 0x4E = 8N1, baud rate factor x16 (typical setup). + uartWrite(board, true, 0x4E); + // Command word: 0x05 = TxEnable + RxEnable. + uartWrite(board, true, 0x05); + const status = uartRead(board, true); + expect(status & 0x01, 'TxRDY set after Tx-enable').toBe(0x01); + }); + + it.skipIf(skip)('command write 0x40 internal-reset returns to expecting mode word', async () => { + await setup(board); + uartWrite(board, true, 0x4E); // mode + uartWrite(board, true, 0x05); // command — Tx + Rx enable + uartWrite(board, true, 0x40); // internal reset + + // Now the next write to control should be interpreted as a NEW mode + // word (0x4E) rather than a command. After mode + new command, Tx + // should re-enable. + uartWrite(board, true, 0x4E); + uartWrite(board, true, 0x05); + const status = uartRead(board, true); + expect(status & 0x01).toBe(0x01); + }); +}); diff --git a/test/test_intel/test_buses/8255-ppi.c b/test/test_intel/test_buses/8255-ppi.c new file mode 100644 index 00000000..5c6ece7f --- /dev/null +++ b/test/test_intel/test_buses/8255-ppi.c @@ -0,0 +1,218 @@ +/* + * Intel 8255 Programmable Peripheral Interface — Mode 0 only. + * + * The 8255 is a 40-pin DIP that gives a CPU three 8-bit ports (A, B, + * C) with programmable direction. Mode 0 is the simplest: each port + * (and the upper/lower halves of port C independently) can be set to + * input or output via writes to the control register. + * + * Source: Intel 8255A Datasheet (public domain mirror on bitsavers). + * + * Pin contract (40-pin DIP): + * D0..D7 bidirectional 8-bit data bus + * PA0..PA7 bidirectional — direction set by control register + * PB0..PB7 bidirectional + * PC0..PC7 bidirectional — upper and lower halves independent + * A0, A1 input — register select: + * 00 = port A, 01 = port B, 10 = port C, 11 = control + * CS̅ input — active-low chip enable + * RD̅ input — active-low read strobe + * WR̅ input — active-low write strobe + * RESET input — active-high; clears all ports to input mode + * VCC, GND + * + * Control word format (Mode 0 only — bit 7 = 1): + * bit 7: 1 = set mode (0 = bit set/reset on port C — not supported) + * bit 6,5: group-A mode (00 = mode 0) + * bit 4: PA direction (1 = input, 0 = output) + * bit 3: PC upper (PC4..PC7) direction + * bit 2: group-B mode (0 = mode 0) + * bit 1: PB direction + * bit 0: PC lower (PC0..PC3) direction + * + * Mode 1 (strobed I/O) and Mode 2 (bidirectional) are NOT implemented. + * Bit set/reset operations on port C (control byte with bit 7 = 0) + * are NOT implemented yet. + */ +#include "velxio-chip.h" +#include +#include + +typedef struct { + vx_pin d[8]; + vx_pin pa[8]; + vx_pin pb[8]; + vx_pin pc[8]; + vx_pin a0, a1; + vx_pin cs, rd, wr, reset_; + vx_pin vcc, gnd; + + /* Direction flags: 1 = input (we don't drive), 0 = output (we drive) */ + bool pa_input; + bool pb_input; + bool pc_low_input; /* PC0..PC3 */ + bool pc_high_input; /* PC4..PC7 */ + + /* Latched output values per port (used when in output mode) */ + uint8_t pa_out; + uint8_t pb_out; + uint8_t pc_out; + + bool driving_d; + int wr_last; + int rd_last; +} chip_t; + +static chip_t G; + +/* ─── 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 uint8_t read_port(vx_pin* port) { + uint8_t v = 0; + for (int i = 0; i < 8; i++) if (vx_pin_read(port[i])) v |= (1u << i); + return v; +} +static void drive_port(vx_pin* port, uint8_t v, uint8_t mask_output) { + /* Drive only the bits marked as output (mask_output=1 → output) */ + for (int i = 0; i < 8; i++) { + if (mask_output & (1 << i)) { + vx_pin_set_mode(port[i], VX_OUTPUT); + vx_pin_write(port[i], (v >> i) & 1); + } else { + vx_pin_set_mode(port[i], VX_INPUT); + } + } +} + +static uint8_t cur_port_addr(void) { + return (vx_pin_read(G.a1) ? 2 : 0) | (vx_pin_read(G.a0) ? 1 : 0); +} + +static void apply_directions(void) { + drive_port(G.pa, G.pa_out, G.pa_input ? 0x00 : 0xFF); + drive_port(G.pb, G.pb_out, G.pb_input ? 0x00 : 0xFF); + uint8_t pc_mask = 0; + if (!G.pc_low_input) pc_mask |= 0x0F; + if (!G.pc_high_input) pc_mask |= 0xF0; + drive_port(G.pc, G.pc_out, pc_mask); +} + +static void apply_control(uint8_t c) { + if ((c & 0x80) == 0) { + /* Bit set/reset operation — not implemented. */ + return; + } + G.pc_low_input = (c & 0x01) != 0; + G.pb_input = (c & 0x02) != 0; + G.pc_high_input = (c & 0x08) != 0; + G.pa_input = (c & 0x10) != 0; + /* Reset output latches per the datasheet: control writes clear + any previously-driven output values to 0. */ + G.pa_out = 0; + G.pb_out = 0; + G.pc_out = 0; + apply_directions(); +} + +static void reset_state(void) { + /* RESET clears the chip: all ports become inputs (Mode 0, all in). */ + G.pa_input = true; + G.pb_input = true; + G.pc_low_input = true; + G.pc_high_input = true; + G.pa_out = G.pb_out = G.pc_out = 0; + G.wr_last = 1; + G.rd_last = 1; + apply_directions(); + release_d(); +} + +/* ─── Read / Write 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) { + /* RD̅ asserted → drive D with the selected register's value */ + uint8_t addr = cur_port_addr(); + uint8_t v = 0; + switch (addr) { + case 0: v = G.pa_input ? read_port(G.pa) : G.pa_out; break; + case 1: v = G.pb_input ? read_port(G.pb) : G.pb_out; break; + case 2: { + uint8_t pc_lo = G.pc_low_input ? (read_port(G.pc) & 0x0F) : (G.pc_out & 0x0F); + uint8_t pc_hi = G.pc_high_input ? (read_port(G.pc) & 0xF0) : (G.pc_out & 0xF0); + v = pc_lo | pc_hi; + break; + } + case 3: v = 0; break; /* control register read returns 0 (datasheet: undefined) */ + } + drive_d(v); + } else { + release_d(); + } +} + +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; } + /* Latch on rising edge of WR̅ (deassert), per Intel datasheet. */ + if (G.wr_last == 0 && value == 1) { + uint8_t addr = cur_port_addr(); + uint8_t v = read_d_byte(); + switch (addr) { + case 0: G.pa_out = v; break; + case 1: G.pb_out = v; break; + case 2: G.pc_out = v; break; + case 3: apply_control(v); break; + } + apply_directions(); + } + G.wr_last = value; +} + +static void on_reset(void* user_data, vx_pin pin, int value) { + (void)user_data; (void)pin; + if (value) reset_state(); +} + +void chip_setup(void) { + char name[5]; + + 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]='P'; name[1]='A'; name[2]='0'+i; name[3]=0; G.pa[i] = vx_pin_register(name, VX_INPUT); } + for (int i = 0; i < 8; i++) { name[0]='P'; name[1]='B'; name[2]='0'+i; name[3]=0; G.pb[i] = vx_pin_register(name, VX_INPUT); } + for (int i = 0; i < 8; i++) { name[0]='P'; name[1]='C'; name[2]='0'+i; name[3]=0; G.pc[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); + G.reset_ = vx_pin_register("RESET", VX_INPUT); + G.vcc = vx_pin_register("VCC", VX_INPUT); + G.gnd = vx_pin_register("GND", VX_INPUT); + + reset_state(); + + vx_pin_watch(G.rd, VX_EDGE_BOTH, on_rd, 0); + vx_pin_watch(G.wr, VX_EDGE_BOTH, on_wr, 0); + vx_pin_watch(G.reset_, VX_EDGE_RISING, on_reset, 0); +} diff --git a/test/test_intel/test_buses/8255-ppi.test.js b/test/test_intel/test_buses/8255-ppi.test.js new file mode 100644 index 00000000..5475ef6c --- /dev/null +++ b/test/test_intel/test_buses/8255-ppi.test.js @@ -0,0 +1,140 @@ +/** + * Intel 8255 PPI — Mode 0 unit tests. + */ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { BoardHarness } from '../src/BoardHarness.js'; +import { chipWasmExists } from '../src/helpers.js'; + +const CHIP = '8255-ppi'; +const skip = !chipWasmExists(CHIP); + +function pinMap() { + const m = { + A0: 'A0', A1: 'A1', CS: 'CS', RD: 'RD', WR: 'WR', RESET: 'RESET', + VCC: 'VCC', GND: 'GND', + }; + for (let i = 0; i < 8; i++) m[`D${i}`] = `D${i}`; + for (let i = 0; i < 8; i++) m[`PA${i}`] = `PA${i}`; + for (let i = 0; i < 8; i++) m[`PB${i}`] = `PB${i}`; + for (let i = 0; i < 8; i++) m[`PC${i}`] = `PC${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 readPort(board, prefix) { + let v = 0; + for (let i = 0; i < 8; i++) if (board.getNet(`${prefix}${i}`)) v |= (1 << i); + return v; +} +function setPort(board, prefix, byte) { + for (let i = 0; i < 8; i++) board.setNet(`${prefix}${i}`, ((byte >> i) & 1) === 1); +} + +/** CPU-side write to the PPI's register at A1A0 = addr (0..3). */ +function ppiWrite(board, addr, value) { + board.setNet('A0', (addr & 1) !== 0); + board.setNet('A1', (addr & 2) !== 0); + setData(board, value); + board.advanceNanos(20); + board.setNet('CS', false); + board.setNet('WR', false); + board.advanceNanos(20); + board.setNet('WR', true); // rising edge latches + board.advanceNanos(20); + board.setNet('CS', true); +} + +function ppiRead(board, addr) { + board.setNet('A0', (addr & 1) !== 0); + board.setNet('A1', (addr & 2) !== 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()); + // Idle: CS̅ and strobes high. + board.setNet('CS', true); + board.setNet('RD', true); + board.setNet('WR', true); + // Pulse RESET to ensure clean state. + board.setNet('RESET', true); + board.advanceNanos(50); + board.setNet('RESET', false); + board.advanceNanos(50); +} + +describe(`${CHIP} chip`, () => { + let board; + beforeEach(() => { board = new BoardHarness(); }); + afterEach(() => { board.dispose(); }); + + it.skipIf(skip)('registers all 40 logical pins', async () => { + await expect(board.addChip(CHIP, pinMap())).resolves.toBeDefined(); + }); + + it.skipIf(skip)('Mode 0: PA configured as output, value latched and driven', async () => { + await setup(board); + // Control word 0x80 = mode set, all ports output. + ppiWrite(board, 3, 0x80); + // Write 0xA5 to port A. + ppiWrite(board, 0, 0xA5); + expect(readPort(board, 'PA')).toBe(0xA5); + }); + + it.skipIf(skip)('Mode 0: PB configured as input, CPU read returns external pin states', async () => { + await setup(board); + // Control 0x82 = bit 1 = 1 → PB is input. + ppiWrite(board, 3, 0x82); + // Externally drive PB pins to 0x3C. + setPort(board, 'PB', 0x3C); + board.advanceNanos(20); + expect(ppiRead(board, 1)).toBe(0x3C); + }); + + it.skipIf(skip)('Mode 0: PC upper/lower halves independent', async () => { + await setup(board); + // Control: bit 0=1 (PC low input), bit 3=0 (PC high output). + // Plus mode set bit 7. Group A: PA out (bit 4=0), PC up out (bit 3=0). + // Group B: PB out (bit 1=0), PC low input (bit 0=1). + // = 0b1000_0001 = 0x81 + ppiWrite(board, 3, 0x81); + // Externally drive PC0..PC3 to 0xA (= 1010 binary). + for (let i = 0; i < 4; i++) board.setNet(`PC${i}`, ((0xA >> i) & 1) === 1); + // CPU writes 0xF0 to port C → high nibble drives, low nibble is input. + ppiWrite(board, 2, 0xF0); + board.advanceNanos(20); + // Read PC4..PC7 from chip-driven side. + const pc_high_byte = readPort(board, 'PC') & 0xF0; + expect(pc_high_byte).toBe(0xF0); + // CPU reads port C: high nibble = output latch (0xF0), low = external (0xA). + const cpu_view = ppiRead(board, 2); + expect(cpu_view).toBe(0xFA); + }); + + it.skipIf(skip)('CS̅ high blocks reads (chip does not drive D)', async () => { + await setup(board); + ppiWrite(board, 3, 0x80); + ppiWrite(board, 0, 0x77); + // CS̅ stays high — try to read. + board.setNet('A0', false); board.setNet('A1', false); + board.setNet('RD', false); + // External drive D high to test contention. + for (let i = 0; i < 8; i++) board.setNet(`D${i}`, true); + board.advanceNanos(20); + expect(readData(board)).toBe(0xFF); + board.setNet('RD', true); + }); +}); diff --git a/test/test_intel/test_buses/rom-1m.c b/test/test_intel/test_buses/rom-1m.c new file mode 100644 index 00000000..14acfeef --- /dev/null +++ b/test/test_intel/test_buses/rom-1m.c @@ -0,0 +1,124 @@ +/* + * rom-1m — top-of-1MB ROM custom chip for the 8086. + * + * Naming is historical; the chip is actually a 64 KB ROM mapped at + * physical addresses 0xF0000..0xFFFFF — the upper 64 KB of the 8086's + * 1 MiB address space, which is where real-world PC BIOSes sit. This + * fits within the WASM 1 MiB linear-memory cap with room for the chip's + * other state. + * + * The chip listens on the full 20-bit address bus (A0..A19); when the + * upper 4 address bits are not 0xF, the chip releases the data bus + * (out-of-range — let another chip drive). Reset vector 0xFFFF0 maps + * to image offset 0xFFF0. + * + * Pin contract: + * A0..A19 input 20-bit address + * D0..D7 output 8-bit data (driven only when CE̅=0 AND OE̅=0 + * AND addr is in [0xF0000..0xFFFFF]) + * CE̅ input active-low chip enable + * OE̅ input active-low output enable + * VCC, GND power + * + * Image is allocated via malloc at chip_setup. A small known signature + * is patched at the reset vector for tests to verify ROM presence. + * + * For per-demo ROM contents, a follow-up SDK extension (blob attribute) + * would let users upload arbitrary boot images. For now each "ROM + * image" is a separately compiled chip variant. + */ +#include "velxio-chip.h" +#include +#include +#include +#include + +#define ROM_BASE 0xF0000 +#define ROM_SIZE 0x10000 /* 64 KB */ +#define ROM_END (ROM_BASE + ROM_SIZE) + +typedef struct { + vx_pin a[20]; + vx_pin d[8]; + vx_pin ce; + vx_pin oe; + vx_pin vcc, gnd; + uint8_t* image; + bool driving; +} chip_t; + +static chip_t G; + +static uint32_t read_addr(void) { + uint32_t v = 0; + for (int i = 0; i < 20; i++) if (vx_pin_read(G.a[i])) v |= (1u << i); + return v; +} + +static void drive_data(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 = true; +} +static void release_data(void) { + if (!G.driving) return; + for (int i = 0; i < 8; i++) vx_pin_set_mode(G.d[i], VX_INPUT); + G.driving = false; +} + +static void update(void) { + int ce_low = (vx_pin_read(G.ce) == 0); + int oe_low = (vx_pin_read(G.oe) == 0); + if (!ce_low || !oe_low) { release_data(); return; } + uint32_t addr = read_addr(); + if (addr < ROM_BASE || addr >= ROM_END) { release_data(); return; } + drive_data(G.image[addr - ROM_BASE]); +} + +static void on_pin_change(void* user_data, vx_pin pin, int value) { + (void)user_data; (void)pin; (void)value; + update(); +} + +void chip_setup(void) { + char name[5]; + for (int i = 0; i < 20; i++) { + if (i < 10) { + name[0]='A'; name[1]='0'+i; name[2]=0; + } else { + name[0]='A'; name[1]='1'; name[2]='0'+(i-10); name[3]=0; + } + G.a[i] = vx_pin_register(name, VX_INPUT); + } + 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.ce = vx_pin_register("CE", VX_INPUT); + G.oe = vx_pin_register("OE", VX_INPUT); + G.vcc = vx_pin_register("VCC", VX_INPUT); + G.gnd = vx_pin_register("GND", VX_INPUT); + + G.image = (uint8_t*)malloc(ROM_SIZE); + memset(G.image, 0xFF, ROM_SIZE); + /* Test fixture: 16-byte signature at the reset vector 0xFFFF0, + which maps to image offset 0xFFF0. */ + static const uint8_t reset_signature[16] = { + 0xEA, 0x00, 0x01, 0x00, 0xF0, /* JMP FAR 0xF000:0x0100 */ + 0x55, 0xAA, 0x12, 0x34, + 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x77, + }; + memcpy(&G.image[0xFFF0], reset_signature, sizeof reset_signature); + + G.driving = false; + + for (int i = 0; i < 20; i++) { + vx_pin_watch(G.a[i], VX_EDGE_BOTH, on_pin_change, 0); + } + vx_pin_watch(G.ce, VX_EDGE_BOTH, on_pin_change, 0); + vx_pin_watch(G.oe, VX_EDGE_BOTH, on_pin_change, 0); + + update(); +} diff --git a/test/test_intel/test_buses/rom-1m.test.js b/test/test_intel/test_buses/rom-1m.test.js new file mode 100644 index 00000000..666441e2 --- /dev/null +++ b/test/test_intel/test_buses/rom-1m.test.js @@ -0,0 +1,74 @@ +/** + * rom-1m chip — 1 MiB ROM with 20-bit address bus, used by 8086 boards. + */ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { BoardHarness } from '../src/BoardHarness.js'; +import { chipWasmExists } from '../src/helpers.js'; + +const CHIP = 'rom-1m'; +const skip = !chipWasmExists(CHIP); + +const RESET_SIGNATURE = [ + 0xEA, 0x00, 0x01, 0x00, 0xF0, + 0x55, 0xAA, 0x12, 0x34, + 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x77, +]; + +function pinMap() { + const m = { VCC: 'VCC', GND: 'GND', CE: 'CE', OE: 'OE' }; + for (let i = 0; i < 20; i++) m[`A${i}`] = `A${i}`; + for (let i = 0; i < 8; i++) m[`D${i}`] = `D${i}`; + return m; +} + +function setAddr(board, addr) { + for (let i = 0; i < 20; i++) board.setNet(`A${i}`, ((addr >> 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; +} + +describe(`${CHIP} chip`, () => { + let board; + beforeEach(() => { board = new BoardHarness(); }); + afterEach(() => { board.dispose(); }); + + it.skipIf(skip)('registers all 30 logical pins', async () => { + await expect(board.addChip(CHIP, pinMap())).resolves.toBeDefined(); + }); + + it.skipIf(skip)('reads the 16-byte reset signature at 0xFFFF0', async () => { + await board.addChip(CHIP, pinMap()); + board.setNet('CE', false); + board.setNet('OE', false); + for (let i = 0; i < RESET_SIGNATURE.length; i++) { + setAddr(board, 0xFFFF0 + i); + board.advanceNanos(50); + expect(readData(board), `addr=0x${(0xFFFF0+i).toString(16)}`).toBe(RESET_SIGNATURE[i]); + } + }); + + it.skipIf(skip)('returns 0xFF for unprogrammed addresses inside the ROM range', async () => { + await board.addChip(CHIP, pinMap()); + board.setNet('CE', false); + board.setNet('OE', false); + // 0xF0000 is the start of the ROM; only the last 16 bytes (the + // reset signature) are programmed, everything else is 0xFF. + setAddr(board, 0xF0000); + board.advanceNanos(50); + expect(readData(board)).toBe(0xff); + }); + + it.skipIf(skip)('does not drive D when CE̅ or OE̅ is high', async () => { + await board.addChip(CHIP, pinMap()); + board.setNet('CE', true); + board.setNet('OE', false); + setAddr(board, 0xFFFF0); + board.advanceNanos(50); + for (let i = 0; i < 8; i++) board.setNet(`D${i}`, true); + board.advanceNanos(50); + expect(readData(board)).toBe(0xff); + }); +});