test_intel: phase D — 4001 ROM chip with 4004 integration
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) <noreply@anthropic.com>
This commit is contained in:
parent
e19c961982
commit
f7223b4965
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue