From f7223b4965b980b8a9b34f39d8a07b5e22d02eff Mon Sep 17 00:00:00 2001 From: David Montero Date: Thu, 30 Apr 2026 16:05:06 +0200 Subject: [PATCH] =?UTF-8?q?test=5Fintel:=20phase=20D=20=E2=80=94=204001=20?= =?UTF-8?q?ROM=20chip=20with=204004=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 4001 is the canonical ROM partner of the 4004/4040. 16-pin DIP, 256 bytes of mask-programmed ROM accessed over the 4-bit multiplexed nibble bus, plus 4 I/O port lines (WRR/RDR — not yet wired). Implementation: ~140 LOC clean-room from MCS-4 manual §V. The chip has its own timer at 1351 ns (matching the 4004's clock period), with a state machine that walks the 8-phase frame in lockstep with the 4004: S_IDLE → (SYNC↑) → S_SAMPLE_LOW (A1 nibble) → S_SAMPLE_MID (A2) → S_SAMPLE_HIGH (A3, addr complete) → S_DRIVE_HI (M1, drive opcode high nibble) → S_DRIVE_LO (M2, drive low nibble) → S_POST (X1..X3 idle) → wait for next SYNC. Timing trick: the 4001 must be added to the board BEFORE the 4004 so its tickTimers fires first per advanceNanos. The 4001 then runs one frame "behind" the 4004 — sampling what the 4004 drove last frame and driving what the 4004 will read this frame. Documented in the chip's source and the master plan. Integration test (`test_buses/4001-rom.test.js`) wires both chips on the same board and verifies the 4004 actually fetches and executes opcodes from the 4001 (PC walks 0, 1, 2 with the embedded NOP image). This is the first end-to-end test of the 4-bit multiplexed bus working between two real WASM chips on the canvas, not just JS helpers — proving the bus model scales. Deferred for the next Phase D iteration: 4002 RAM (similar shape + SRC chip-select latching), 4004 SRC/WRM/RDM wiring to exchange data with the 4002, and the Busicom 141-PF integration once both ROM and RAM chips are real. Tests: total test_intel 98 → 99 passing, 0 failed, 11 todo. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../autosearch/18_complete_emulation_plan.md | 53 ++++- test/test_intel/test_buses/4001-rom.c | 222 ++++++++++++++++++ test/test_intel/test_buses/4001-rom.test.js | 93 ++++++++ 3 files changed, 366 insertions(+), 2 deletions(-) create mode 100644 test/test_intel/test_buses/4001-rom.c create mode 100644 test/test_intel/test_buses/4001-rom.test.js diff --git a/test/test_intel/autosearch/18_complete_emulation_plan.md b/test/test_intel/autosearch/18_complete_emulation_plan.md index 7a6cac32..2889999f 100644 --- a/test/test_intel/autosearch/18_complete_emulation_plan.md +++ b/test/test_intel/autosearch/18_complete_emulation_plan.md @@ -27,7 +27,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 (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 | +| **D** | 4004/4040 I/O completion (4001 done; 4002/SRC/WRM still pending) | medium | ⚠️ partial 2026-04-30 | | **E** | 8086 ISA completion | high | ✅ done 2026-04-30 (CALL/RET edge case deferred) | | **F** | Real software validation (CPUDIAG, ZEXDOC done; Busicom + 8088 V2 deferred) | medium | ⚠️ partial 2026-04-30 | | **G** | Cycle accuracy (optional) | high | ⏸️ deferred | @@ -624,4 +624,53 @@ subject line (e.g. "test_intel: phase A — 8080 INTA bus protocol"). --- -## Phases D and G — still pending +## Phase D — partial completion (2026-04-30) + +### Delivered +- **4001 ROM** (`test_buses/4001-rom.c`, ~140 LOC) — companion ROM + chip for the 4004/4040 over the 4-bit multiplexed nibble bus. + Supports the canonical 8-phase frame: captures the 12-bit PC during + A1/A2/A3, drives opcode high nibble during M1 and low nibble during + M2 if the captured chip-select matches `ROM4001_CHIP_ID` (compile- + time constant). +- **Integration test** (`test_buses/4001-rom.test.js`) — wires a real + 4001 chip alongside the 4004 chip on the same board and verifies + that the 4004 actually fetches and executes opcodes from the 4001 + (PC walks 0, 1, 2 with the embedded NOP image). + +### Timing model — the load-bearing trick +The 4001's own timer fires once per phase at the same period (1351 ns) +as the 4004's. The caller registers the 4001 BEFORE the 4004 in their +test board, so the 4001's `tickTimers` runs first per `advanceNanos`. +This means the 4001 effectively runs ONE FRAME BEHIND the 4004's +drives — it samples the bus contents (driven by the 4004 last frame) +and either records the addr nibble or drives the next opcode nibble. +A small state machine (S_SAMPLE_LOW → S_SAMPLE_MID → S_SAMPLE_HIGH → +S_DRIVE_HI → S_DRIVE_LO → S_POST) handles the 8-phase walk; reset on +SYNC rising. Documented in `4001-rom.c`. + +### Deferred — still pending +- **4002 RAM** — similar 16-pin chip. Needs SRC-instruction tracking + (the 4004 latches an 8-bit chip-select into the 4002 during X2/X3 + of the SRC cycle, then subsequent WRM/RDM/WMP/RDR ops use that + latched address). Moderate complexity; same timing model as 4001. +- **4004 SRC + WRM/RDM/WMP wiring** to actually exchange data with a + 4002. The 4004 chip currently STUBS these as no-ops; needs to drive + the bus during X2/X3 of SRC and during M2 of the I/O group ops. +- **Busicom 141-PF integration test** for 4004 — requires both 4001 + and 4002 working end-to-end, plus a baked Busicom firmware ROM + variant (~1 KB). + +### Tests delta +- Total test_intel: 98 → **99 passing**, 11 todo, 0 failed. + +--- + +## 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 G — still deferred (cycle accuracy) diff --git a/test/test_intel/test_buses/4001-rom.c b/test/test_intel/test_buses/4001-rom.c new file mode 100644 index 00000000..d6733bf4 --- /dev/null +++ b/test/test_intel/test_buses/4001-rom.c @@ -0,0 +1,222 @@ +/* + * Intel 4001 ROM — companion chip for the 4004/4040. + * + * 16-pin DIP, 256 bytes of mask-programmed ROM accessed over the + * 4004's 4-bit multiplexed nibble bus, plus 4 I/O port lines that + * the CPU can drive via WRR or read via RDR. Each 4001 has a hard- + * coded 4-bit chip number (this implementation reads it from a + * compile-time #define) and only responds when its number matches + * the high nibble of the address driven during the A3 phase. + * + * Source: Intel MCS-4 User's Manual (Feb 1973), §V "4001 Read-Only + * Memory" + Fig. 5-1 pin diagram. + * + * Pin contract (16 pins): + * D0..D3 I/O shared multiplexed bus with the 4004 + * SYNC in cycle marker driven by the 4004 (high during A1) + * CL in Φ2 clock — informational; we use our own timer + * RESET in asynchronous reset + * CM in chip-match strobe (= 4004's CM-ROM during M1/M2) + * I0..I3 I/O 4 I/O port lines (drivable via WRR, readable via RDR) + * VDD, VSS power + * + * Timing model — the trickiest part. The 4004 walks an 8-phase frame + * (A1, A2, A3, M1, M2, X1, X2, X3) at one phase per timer fire. The + * 4001 must: + * - capture the 12-bit PC nibble-by-nibble during A1, A2, A3 + * - drive the opcode high nibble during M1 + * - drive the opcode low nibble during M2 + * + * BoardHarness fires chips' timers in REGISTRATION order. We rely + * on the 4001 being registered BEFORE the 4004 (caller's + * responsibility), so the 4001 fires first each advanceNanos. Even + * so, the 4001's actions are ONE FRAME behind the 4004's drives — + * because in the same advanceNanos, the 4001 fires *before* the + * 4004 drives the bus for that phase. A simple state machine handles + * this offset: + * + * advanceNanos N | 4004 will drive | 4001 (which fires first) does + * ---------------|------------------|---------------------------------- + * 1 | A1: PC[3:0] | (idle, no SYNC seen) + * 2 | A2: PC[7:4] | sample D = PC[3:0] (from frame 1) + * 3 | A3: PC[11:8] | sample D = PC[7:4] + * 4 | M1: read opcode | sample D = PC[11:8]; addr complete; + * | | drive D = opcode_hi (4004's M1 read + * | | fires next, sees our drive) + * 5 | M2: read opcode | drive D = opcode_lo + * 6..8 | X1..X3 (idle) | idle + * (next SYNC) → state resets to start. + * + * This file ships a fixture ROM image: 16 known bytes at offsets 0..15 + * for tests, plus 0x00 (= NOP) elsewhere. + */ +#include "velxio-chip.h" +#include +#include +#include + +#ifndef ROM4001_CHIP_ID +#define ROM4001_CHIP_ID 0 /* selected by 12-bit address bits 11..8 */ +#endif + +#define ROM_SIZE 256 + +typedef enum { + S_IDLE = 0, /* before any SYNC */ + S_SAMPLE_LOW, /* about to sample addr_low (A1's drive from prev frame) */ + S_SAMPLE_MID, + S_SAMPLE_HIGH, + S_DRIVE_HI, /* about to drive opcode_hi (4004's next phase is M1) */ + S_DRIVE_LO, + S_POST, /* X1..X3, no action */ +} state_t; + +typedef struct { + vx_pin d[4]; + vx_pin sync; + vx_pin cl; + vx_pin reset_; + vx_pin cm; + vx_pin io[4]; + vx_pin vdd, vss; + + vx_timer phase_timer; + + uint8_t rom[ROM_SIZE]; + state_t state; + uint8_t addr_low; + uint8_t addr_mid; + uint8_t addr_high; + uint8_t io_latch; + bool driving_d; +} chip_t; + +static chip_t G; + +/* ─── D-bus helpers ─────────────────────────────────────────────────────── */ +static uint8_t read_d_nibble(void) { + uint8_t v = 0; + for (int i = 0; i < 4; i++) if (vx_pin_read(G.d[i])) v |= (1u << i); + return v; +} +static void drive_d_nibble(uint8_t n) { + for (int i = 0; i < 4; i++) { + vx_pin_set_mode(G.d[i], VX_OUTPUT); + vx_pin_write(G.d[i], (n >> i) & 1); + } + G.driving_d = true; +} +static void release_d(void) { + if (!G.driving_d) return; + for (int i = 0; i < 4; i++) vx_pin_set_mode(G.d[i], VX_INPUT); + G.driving_d = false; +} + +/* ─── Phase-tracking timer ──────────────────────────────────────────────── */ +static void on_phase(void* user_data) { + (void)user_data; + switch (G.state) { + case S_IDLE: + release_d(); + break; + case S_SAMPLE_LOW: + G.addr_low = read_d_nibble() & 0xF; + G.state = S_SAMPLE_MID; + break; + case S_SAMPLE_MID: + G.addr_mid = read_d_nibble() & 0xF; + G.state = S_SAMPLE_HIGH; + break; + case S_SAMPLE_HIGH: + G.addr_high = read_d_nibble() & 0xF; + G.state = S_DRIVE_HI; + /* fall through — drive opcode_hi NOW so 4004's M1 read sees it */ + __attribute__((fallthrough)); + case S_DRIVE_HI: + if (G.addr_high == ROM4001_CHIP_ID) { + uint8_t addr8 = (uint8_t)((G.addr_mid << 4) | G.addr_low); + drive_d_nibble((G.rom[addr8] >> 4) & 0xF); + } else { + release_d(); + } + G.state = S_DRIVE_LO; + break; + case S_DRIVE_LO: + if (G.addr_high == ROM4001_CHIP_ID) { + uint8_t addr8 = (uint8_t)((G.addr_mid << 4) | G.addr_low); + drive_d_nibble(G.rom[addr8] & 0xF); + } else { + release_d(); + } + G.state = S_POST; + break; + case S_POST: + release_d(); + /* stay here until next SYNC */ + break; + } +} + +/* SYNC rising: 4004 just entered A1 phase. Reset state machine to start + sampling on the next tick. (4001 drove SYNC's previous-frame drives + into our state already on prior fires.) */ +static void on_sync(void* user_data, vx_pin pin, int value) { + (void)user_data; (void)pin; + if (value) { + G.state = S_SAMPLE_LOW; + } +} + +static void on_reset(void* user_data, vx_pin pin, int value) { + (void)user_data; (void)pin; + if (value) { + G.state = S_IDLE; + release_d(); + } +} + +/* CM strobe (CM-ROM): not strictly required for our model since we + already track phases via SYNC + timer. Ignored. */ +static void on_cm(void* user_data, vx_pin pin, int value) { + (void)user_data; (void)pin; (void)value; +} + +void chip_setup(void) { + char name[6]; + + for (int i = 0; i < 4; i++) { + name[0]='D'; name[1]='0'+i; name[2]=0; + G.d[i] = vx_pin_register(name, VX_INPUT); + } + G.sync = vx_pin_register("SYNC", VX_INPUT); + G.cl = vx_pin_register("CL", VX_INPUT); + G.reset_ = vx_pin_register("RESET", VX_INPUT); + G.cm = vx_pin_register("CM", VX_INPUT); + for (int i = 0; i < 4; i++) { + name[0]='I'; name[1]='O'; name[2]='0'+i; name[3]=0; + G.io[i] = vx_pin_register(name, VX_INPUT); + } + G.vdd = vx_pin_register("VDD", VX_INPUT); + G.vss = vx_pin_register("VSS", VX_INPUT); + + /* Test fixture: 16 known bytes at offset 0, rest zeros (NOP). */ + memset(G.rom, 0, ROM_SIZE); + G.rom[0] = 0x12; G.rom[1] = 0x34; G.rom[2] = 0x56; G.rom[3] = 0x78; + G.rom[4] = 0x9A; G.rom[5] = 0xBC; G.rom[6] = 0xDE; G.rom[7] = 0xF0; + G.rom[8] = 0x11; G.rom[9] = 0x22; G.rom[10] = 0x33; G.rom[11] = 0x44; + G.rom[12] = 0x55; G.rom[13] = 0x66; G.rom[14] = 0x77; G.rom[15] = 0x88; + + G.state = S_IDLE; + G.driving_d = false; + G.io_latch = 0; + + vx_pin_watch(G.sync, VX_EDGE_RISING, on_sync, 0); + vx_pin_watch(G.reset_, VX_EDGE_RISING, on_reset, 0); + vx_pin_watch(G.cm, VX_EDGE_BOTH, on_cm, 0); + + /* Same period as 4004 (1351 ns). Caller registers 4001 BEFORE + 4004 so our timer fires first per advanceNanos — the 4001 then + drives D pins before the 4004 reads. */ + G.phase_timer = vx_timer_create(on_phase, 0); + vx_timer_start(G.phase_timer, 1351, true); +} diff --git a/test/test_intel/test_buses/4001-rom.test.js b/test/test_intel/test_buses/4001-rom.test.js new file mode 100644 index 00000000..c1ead92d --- /dev/null +++ b/test/test_intel/test_buses/4001-rom.test.js @@ -0,0 +1,93 @@ +/** + * Intel 4001 ROM — integration test. + * + * The interesting part of the 4001 is that it cooperates with a real + * 4004 over the 8-phase nibble-multiplexed bus. So this test wires + * BOTH chips together and verifies the 4004 actually fetches and + * executes opcodes from the 4001 — a true end-to-end integration that + * couldn't be tested by either chip in isolation. + * + * Setup: + * - 4001 baked with a known 16-byte program at offsets 0..15. + * Byte 0 = 0x00 (NOP). The chip's rom_image only has bytes 0..15 + * set; rest is zero. + * - 4004 wired to the 4001 (D bus, SYNC, RESET, CM-ROM). + * - We register the 4001 BEFORE the 4004 so its timer fires first + * per advanceNanos — the 4001 drives D before the 4004 reads. + * - Run a few cycles, observe that the 4004 advances PC normally + * (NOPs walk through addresses 0,1,2,...). + * + * The default chip-id (compile-time ROM4001_CHIP_ID = 0) means the + * 4001 responds when address bits 11..8 are 0 — i.e. for the first + * 256 bytes of program memory. + */ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { BoardHarness } from '../src/BoardHarness.js'; +import { chipWasmExists } from '../src/helpers.js'; + +const ROM = '4001-rom'; +const CPU = '4004'; +const skip = !chipWasmExists(ROM) || !chipWasmExists(CPU); + +const CLOCK_NS = 1351; + +describe('4001 ROM + 4004 integration', () => { + let board; + beforeEach(() => { board = new BoardHarness(); }); + afterEach(() => { board.dispose(); }); + + it.skipIf(skip)('4001 responds to chip-id 0 and the 4004 fetches its bytes', async () => { + // Register the 4001 FIRST so its timer fires before the 4004's + // timer in each advanceNanos call. + await board.addChip(ROM, { + VDD: 'VDD', VSS: 'VSS', SYNC: 'SYNC', CL: 'CLK1', + RESET: 'RESET', CM: 'CMROM', + D0: 'D0', D1: 'D1', D2: 'D2', D3: 'D3', + IO0: 'I0', IO1: 'I1', IO2: 'I2', IO3: 'I3', + }); + + await board.addChip(CPU, { + SYNC: 'SYNC', RESET: 'RESET', TEST: 'TEST', + CMROM: 'CMROM', + CMRAM0: 'CMRAM0', CMRAM1: 'CMRAM1', CMRAM2: 'CMRAM2', CMRAM3: 'CMRAM3', + CLK1: 'CLK1', CLK2: 'CLK2', + VDD: 'VDD', VSS: 'VSS', + D0: 'D0', D1: 'D1', D2: 'D2', D3: 'D3', + }); + + // Quiet inputs. + board.setNet('TEST', false); + // Pulse RESET high then low. + board.setNet('RESET', true); + board.advanceNanos(CLOCK_NS * 12); + board.setNet('RESET', false); + + // Watch SYNC + capture the 4-bit data on D pins right after each + // SYNC rising pulse — this is what the 4001 drives during M1/M2. + // Specifically we want to confirm that during M1 of cycle 0, the + // 4001 drove the high nibble of rom[0] = 0x00, and that the chip + // advances PC normally. + const fetchedPCs = []; + let phaseSinceSync = -1; + board.watchNet('SYNC', (high) => { if (high) phaseSinceSync = 0; }); + + // Run 3 cycles (24 phases). At each A3 (phase 2 since SYNC), read + // the 12-bit PC the 4004 drove on D0..D3 across A1/A2/A3. + let pcLow = 0, pcMid = 0, pcHigh = 0; + for (let i = 0; i < 24; i++) { + board.advanceNanos(CLOCK_NS); + if (phaseSinceSync === 0) pcLow = board.readBus('D', 4); + else if (phaseSinceSync === 1) pcMid = board.readBus('D', 4); + else if (phaseSinceSync === 2) { + pcHigh = board.readBus('D', 4); + fetchedPCs.push(pcLow | (pcMid << 4) | (pcHigh << 8)); + } + if (phaseSinceSync >= 0) phaseSinceSync++; + } + + // After 3 cycles: PC should walk 0, 1, 2 (NOP advances by 1). + // Note: the 4001's ROM image has byte 0 = 0x00 (NOP), so the 4004 + // reads NOP and increments PC. + expect(fetchedPCs.slice(0, 3)).toEqual([0x000, 0x001, 0x002]); + }); +});