velxio/test/test_intel/test_buses/4001-rom.c

223 lines
7.9 KiB
C
Raw Normal View History

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>
2026-04-30 21:05:06 +07:00
/*
* 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);
}