test_intel: phase C — support chips (partial: rom-1m, 8255, 8251)

Three new bus-device chips, all clean-room from public Intel datasheets:
- rom-1m: 64 KB ROM mapped at 0xF0000..0xFFFFF for 8086 boot. 20-bit
  address bus watch with out-of-range tristate. 16-byte known signature
  pre-loaded at the reset vector 0xFFFF0.
- 8255 PPI: Mode-0 (basic I/O) only; three 8-bit ports with split
  upper/lower port C. Control word parsing for direction setup. Bit
  set/reset and Modes 1/2 deferred.
- 8251 USART: async-mode UART using vx_uart_attach for bit-timing;
  mode word + command word + status interface; TxRDY/RxRDY/TxEMPTY
  status pins; DTR/RTS pass-through. Internal-reset honoured.

Deferred to a follow-up Phase C+:
- 4001 ROM and 4002 RAM (multi-phase 4-bit bus timing requires either
  an external clock-gen chip or Bus4004-equivalent host coordination
  that's only available in the JS test harness today).
- 8253 PIT (6 modes, countdown logic).
- 8259 PIC (ICW init state machine + cascade + INTA cycle).
These are flagged in autosearch/18_complete_emulation_plan.md as
deferred — Phase D (4004/4040 I/O) and the 8259 work depend on them
landing first.

Tests: test_buses 17 → 30 passing (+13). Total test_intel 73 → 86
passing, 0 failed, 17 todo. Master plan doc updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Montero 2026-04-30 04:53:31 +02:00
parent 8249c9ebeb
commit 8a1d75a96c
7 changed files with 990 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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