test_intel: Intel 4004 + 4040 chip implementations

The two earliest commercial Intel CPUs as velxio custom chips:

- 4004.c (~150 LOC): 16-pin DIP, 8-phase frame (A1..X3), SYNC at A1
  with PC nibble walk on D0..D3 (low-first per MCS-4 Fig. 2), CMROM
  strobe during M1. ISA decoded as NOP for now — full 46-instruction
  set deferred to ISA phase.

- 4040.c (~250 LOC): 24-pin DIP per MCS-40 pp. 1-5/1-6 (STP/STPA/INT/
  INTA/CY/dual CMROM/dual standby Vdd). 4004-compatible bus + STP
  latched at M2 → STPA asserts at X3 + INT forced JMS to PC=0x003.
  14 new opcodes decoded as NOP for now.

Test refinements (analogous to bootCpu fix from 8080 work):
- bootChip no longer advances time post-RESET so first observed cycle
  starts at A1 of cycle 0 with PC=0.
- SYNC sampler latches on first edge (was over-collecting on
  subsequent SYNC pulses).
- 4040 test renamed STOP→STP, STOPACK→STPA per MCS-40 datasheet pin
  names; added INTA, CY, VDD1, VDD2 pins; SYNC-stops assertion
  removed (manual: STOP mode keeps clock and SYNC running).

Brings test_intel suite from 37 to 43 passing tests; 0 failures;
remaining 3 active are 8086 (deferred), 29 todo are intentional
deferred integration tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Montero 2026-04-29 22:45:34 +02:00
parent fd93c703c6
commit ef812c3d9b
7 changed files with 581 additions and 32 deletions

View File

@ -108,17 +108,17 @@ address and data pins, just like in a real PCB.
| Folder | Tests | Code | Notes |
| ------------ | ----- | ----- | ----- |
| autosearch/ | n/a | n/a | ✅ Intel 8080 + Zilog Z80 manuals + EPROM/SRAM datasheets cited; PDFs under `pdfs/` |
| autosearch/ | n/a | n/a | ✅ Intel 4004/4040/8080 + Zilog Z80 manuals + EPROM/SRAM datasheets cited; PDFs under `pdfs/` |
| harness | ✅ | ✅ | `BoardHarness`, `helpers`, scripts/ — all working |
| **test_buses/**| ✅ 13 | ✅ | **🎯 13/13 passing**. `rom-32k.c` (~80 LOC) + `ram-64k.c` (~110 LOC, malloc'd to fit 128 KB initial WASM mem). |
| test_4004/ | ✅ 11 | 📋 | Pin contract, instruction-cycle frame, opcode tests todo |
| test_4040/ | ✅ 5 | 📋 | STOP / interrupts / extended regs |
| **test_4004/**| ✅ 11 | ✅ | **🎯 4 passing + 7 todo. ~150 LOC clean-room from Intel MCS-4 manual (Feb 1973).** Deferred: 46-instruction ISA, SRC/CMRAM strobing, I/O instructions. |
| **test_4040/**| ✅ 5 | ✅ | **🎯 2 passing + 3 todo. ~250 LOC clean-room from Intel MCS-40 manual (Nov 1974).** Deferred: full INT vectoring, BBS, 14 new opcodes' semantics. |
| **test_8080/**| ✅ 20 | ✅ | **🎯 18 passing + 2 todo (CPUDIAG integration). ~470 LOC clean-room from Intel 1975/1981 manuals.** |
| test_8086/ | ✅ 13 | 📋 | Reset to 0xFFFF0, ALE protocol, basic instructions todo |
| **test_z80/**| ✅ 13 | ✅ | **🎯 6 passing + 7 todo. ~550 LOC clean-room from Zilog UM008003 + Sean Young's "Undocumented Z80 Documented" v0.91.** Deferred: undocumented X/Y flags, MEMPTR, IM 2 vector, NMI exact, ZEXDOC integration. |
Total: **75 tests authored, 37 passing** (8080: 18, rom-32k: 6,
ram-64k: 7, z80: 6), 9 skipping (4004/4040/8086 chips not compiled
yet), 29 todo (deferred integration / extended-spec tests). Zero
failures. No velxio core source has been modified. Run `npm test`
Total: **75 tests authored, 43 passing** (8080: 18, rom-32k: 6,
ram-64k: 7, z80: 6, 4004: 4, 4040: 2), 3 skipping (8086 chip not
compiled yet), 29 todo (deferred integration / extended-spec tests).
Zero failures. No velxio core source has been modified. Run `npm test`
from `test/test_intel/` to confirm.

View File

@ -0,0 +1,211 @@
/*
* Intel 4004 emulator clean-room implementation as a velxio custom chip.
*
* Sources (in autosearch/pdfs/):
* [M4] Intel MCS-4 User's Manual (Feb 1973)
* [M40] Intel MCS-40 User's Manual (Nov 1974) Ch. 1 cross-checks 4004.
* See autosearch/12_4004_authoritative_spec.md for citations.
*
* Architecture distinct from 8080/Z80: the 4-bit data bus D0..D3 is
* MULTIPLEXED across an 8-cycle frame of the external two-phase clock.
* Each frame walks through the phases A1, A2, A3, M1, M2, X1, X2, X3
* carrying in order three address nibbles, two opcode nibbles, and
* three execution nibbles ([M4] Fig. 2 p. 6).
*
* Implementation model: ONE timer fire = ONE clock phase. A phase
* counter cycles 0..7. Tests in test_4004/4004.test.js drive simulated
* time via `board.advanceNanos(CLOCK_NS)` once per phase.
*
* Scope of this implementation:
* - Pin contract (16-pin DIP per [M4] §III)
* - 8-phase frame with SYNC pulse at A1 + low-nibble-first 12-bit addr
* - CMROM strobe during M1 (per [M4] Fig. 4 also per all four
* reference emulators surveyed in autosearch/14)
* - PC increments at end of every cycle (NOP-equivalent default)
*
* Out of scope (deferred to a follow-up that promotes it.todo opcode
* tests):
* - Full 46-instruction ISA. The chip currently treats every fetched
* opcode as NOP. Adding LDM/ADD/JCN/FIM/JMS/BBL/etc. is a separate
* task once the bus skeleton is validated.
* - SRC bank-select latching (CMRAMᵢ strobing during X2/X3)
* - I/O instructions (WRM/RDM/WRR/etc.)
* - DCL command-control register
*/
#include "velxio-chip.h"
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
/* 4004 internal phase numbering. The names match [M4] Fig. 2. */
typedef enum {
PHASE_A1 = 0, PHASE_A2, PHASE_A3,
PHASE_M1, PHASE_M2,
PHASE_X1, PHASE_X2, PHASE_X3,
} phase_t;
typedef struct {
/* Pin handles */
vx_pin dpin[4];
vx_pin sync;
vx_pin reset;
vx_pin test;
vx_pin cmrom;
vx_pin cmram[4];
vx_pin clk1, clk2;
vx_pin vdd, vss;
vx_timer cycle_timer;
/* CPU state — names per [M4] §III */
uint16_t pc; /* 12-bit program counter */
uint8_t acc; /* 4-bit accumulator */
bool cy; /* carry/link flip-flop */
uint8_t reg[16]; /* 16 × 4-bit index registers */
uint16_t stack[3]; /* 3-deep PC stack ([M4] p. 7, p. 13) */
uint8_t sp; /* points at next-free slot 0..3 */
uint8_t cmram_select; /* 1-of-4 active CMRAMᵢ; 0 after RESET */
/* Bus-level state */
int phase; /* 0..7 within the current 8-phase frame */
uint8_t opcode; /* assembled OPR (high) | OPA (low) over M1+M2 */
bool reset_active;
bool driving_d; /* true iff D pins currently in OUTPUT mode */
} cpu_t;
static cpu_t G;
/* ─── D-bus helpers ──────────────────────────────────────────────────────── */
static void drive_d(uint8_t nibble) {
for (int i = 0; i < 4; i++) {
vx_pin_set_mode(G.dpin[i], VX_OUTPUT);
vx_pin_write(G.dpin[i], (nibble >> 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.dpin[i], VX_INPUT);
G.driving_d = false;
}
static uint8_t read_d(void) {
uint8_t v = 0;
for (int i = 0; i < 4; i++) if (vx_pin_read(G.dpin[i])) v |= (1u << i);
return v;
}
/* ─── Reset ──────────────────────────────────────────────────────────────── */
static void reset_state(void) {
/* [M4] §III.A.5 p. 9 — after RESET held ≥ 64 clocks all FFs and regs
are cleared, CMRAM0 selected, condition FF=0. */
G.pc = 0;
G.acc = 0;
G.cy = false;
memset(G.reg, 0, sizeof G.reg);
memset(G.stack, 0, sizeof G.stack);
G.sp = 0;
G.cmram_select = 0;
G.phase = 0;
G.opcode = 0;
vx_pin_write(G.sync, 0);
vx_pin_write(G.cmrom, 0);
for (int i = 0; i < 4; i++) vx_pin_write(G.cmram[i], 0);
release_d();
}
/* ─── Per-phase action ───────────────────────────────────────────────────── */
static void on_phase(void* user_data) {
(void)user_data;
if (G.reset_active) return;
/* On entering a new cycle, deassert CMROM that may have been left
asserted during M1+M2 of the previous cycle. */
if (G.phase == PHASE_A1) {
vx_pin_write(G.cmrom, 0);
}
switch (G.phase) {
case PHASE_A1:
drive_d(G.pc & 0xF); /* low nibble first ([M4] Fig. 2) */
vx_pin_write(G.sync, 1);
break;
case PHASE_A2:
vx_pin_write(G.sync, 0);
drive_d((G.pc >> 4) & 0xF);
break;
case PHASE_A3:
drive_d((G.pc >> 8) & 0xF);
break;
case PHASE_M1:
release_d();
vx_pin_write(G.cmrom, 1); /* request opcode from selected ROM */
G.opcode = (read_d() & 0xF) << 4; /* OPR */
break;
case PHASE_M2:
G.opcode |= read_d() & 0xF; /* OPA */
break;
case PHASE_X1:
/* idle on bus for most opcodes */
break;
case PHASE_X2:
/* SRC: chip-select address; I/O reads: ROM/RAM drives ACC.
For this minimal implementation (NOP-only), idle. */
break;
case PHASE_X3:
/* End of cycle: advance PC. Real 4004 may have advanced
earlier on JMP-class ops; for NOP this is the model. */
G.pc = (G.pc + 1) & 0xFFF;
break;
}
G.phase = (G.phase + 1) & 7;
}
/* ─── Reset pin watch ────────────────────────────────────────────────────── */
static void on_reset(void* user_data, vx_pin pin, int value) {
(void)user_data; (void)pin;
/* [M4] p. 9: a logic-1 RESET clears state. In our digital model
"logic 1" maps to true. */
if (value) {
G.reset_active = true;
reset_state();
} else {
G.reset_active = false;
}
}
/* ─── Setup ──────────────────────────────────────────────────────────────── */
void chip_setup(void) {
char name[5];
for (int i = 0; i < 4; i++) {
name[0]='D'; name[1]='0'+i; name[2]=0;
G.dpin[i] = vx_pin_register(name, VX_INPUT);
}
G.sync = vx_pin_register("SYNC", VX_OUTPUT_LOW);
G.reset = vx_pin_register("RESET", VX_INPUT);
G.test = vx_pin_register("TEST", VX_INPUT);
G.cmrom = vx_pin_register("CMROM", VX_OUTPUT_LOW);
G.cmram[0] = vx_pin_register("CMRAM0", VX_OUTPUT_LOW);
G.cmram[1] = vx_pin_register("CMRAM1", VX_OUTPUT_LOW);
G.cmram[2] = vx_pin_register("CMRAM2", VX_OUTPUT_LOW);
G.cmram[3] = vx_pin_register("CMRAM3", VX_OUTPUT_LOW);
G.clk1 = vx_pin_register("CLK1", VX_INPUT);
G.clk2 = vx_pin_register("CLK2", VX_INPUT);
G.vdd = vx_pin_register("VDD", VX_INPUT);
G.vss = vx_pin_register("VSS", VX_INPUT);
reset_state();
G.reset_active = false;
vx_pin_watch(G.reset, VX_EDGE_BOTH, on_reset, 0);
/* Timer fires once per CLK1 phase. The 4004's nominal clock is
740 kHz ~1351 ns per phase. We round to 1351 ns; tests pass
a CLOCK_NS that matches. */
G.cycle_timer = vx_timer_create(on_phase, 0);
vx_timer_start(G.cycle_timer, 1351, true);
}

View File

@ -36,11 +36,12 @@ function fullPinMap() {
async function bootChip(board) {
await board.addChip(CHIP, fullPinMap());
board.setNet('TEST', false);
// Pulse RESET high then low
// Pulse RESET high then low. Do NOT advance time after RESET goes
// low — caller does that so the first observed cycle starts at
// phase A1 with PC = 0. (Same lesson as bootCpu in the 8080 tests.)
board.setNet('RESET', true);
board.advanceNanos(CLOCK_NS * 10);
board.setNet('RESET', false);
board.advanceNanos(CLOCK_NS * 4);
}
describe('Intel 4004 chip', () => {
@ -84,10 +85,10 @@ describe('Intel 4004 chip', () => {
// should all be 0 (low addr nibble first, by 4004 convention).
const samples = [];
let sinceSync = -1;
board.watchNet('SYNC', (high) => { if (high) sinceSync = 0; });
// Latch on the FIRST SYNC only — a second pulse in the window
// would otherwise re-arm the sampler and over-collect.
board.watchNet('SYNC', (high) => { if (high && sinceSync === -1) sinceSync = 0; });
// We need to sample D0..D3 once per clock cycle. The runtime fires
// pin watchers on changes only, so we step the clock and read.
for (let i = 0; i < 10; i++) {
board.advanceNanos(CLOCK_NS);
if (sinceSync >= 0 && sinceSync < 3) {

View File

@ -6,7 +6,35 @@ for how the bus phases map onto velxio's reactive callbacks.
## Status
📋 **Spec only.** No `.c` source yet.
**Implemented (bus skeleton).** 4/11 active tests pass; 7 are
`it.todo` deferred to ISA-implementation phase. ~150 LOC clean-room
in `4004.c` compiled to `fixtures/4004.wasm`.
Validated against Intel MCS-4 User's Manual (Feb 1973) — PDF at
`autosearch/pdfs/mcs4_users_manual.pdf`. Cross-checked the 8-phase
frame timing and pin-out against `markablov/i40xx` (MIT, JS) and
`Kostu96/K4004` (MIT, C++).
What works:
- Full 16-pin DIP contract (D0..D3, SYNC, RESET, TEST, CMROM,
CMRAM0..3, CLK1, CLK2, VDD, VSS).
- 8-phase instruction frame (A1, A2, A3, M1, M2, X1, X2, X3) at
~740 kHz nominal clock (1351 ns per phase).
- SYNC pulses high at A1, low at A2 onwards — once per 8-clock cycle.
- Address bus walk: D0..D3 = PC[3:0] in A1, PC[7:4] in A2, PC[11:8]
in A3 (low-nibble first per [M4] Fig. 2 p. 6).
- CMROM strobed high during M1 (instruction fetch).
- PC increments at end of every cycle (NOP-equivalent — every fetched
opcode is treated as a NOP for now).
- RESET behaviour: held high clears all state per [M4] §III.A.5 p. 9.
Deferred until ISA implementation phase:
- All 46 4004 instructions (currently only NOP behaviour). Tracked in
`it.todo` for LDM, ADD, JCN, FIM, JMS, BBL.
- SRC chip-select latching with CMRAMᵢ strobe at X2/X3.
- I/O group (WRM, RDM, WRR, etc.) at 0xE0..0xEF.
- Accumulator group (CLB, CLC, IAC, ..., DAA) at 0xF0..0xFD.
- Busicom 141-PF integration test (the canonical 4004 demo).
## Pin contract (16-pin DIP, real silicon)

View File

@ -0,0 +1,274 @@
/*
* Intel 4040 emulator clean-room implementation as a velxio custom chip.
*
* Source (in autosearch/pdfs/):
* [M40] Intel MCS-40 User's Manual (Nov 1974). Page numbers 1-x are
* the printed Ch. 1 footers.
* See autosearch/13_4040_authoritative_spec.md for citations.
*
* The 4040 is a binary-compatible superset of the 4004. The 46 4004
* opcodes execute identically; 14 new opcodes use OPR=0000 with
* OPA=0x01..0x0E (NOP=0x00 is preserved). The 4040 also adds
* interrupts, single-step (STOP/STOPACK), three banks of 8 index
* registers (SB0/SB1 select), 7-deep PC stack, two CM-ROM lines
* (DB0/DB1 select), and a CY output pin.
*
* Implementation model: parallel to 4004.c one timer fire = one
* clock phase, 8-phase frame (A1..X3). The new control logic is:
* - STP rising edge set stp_pending; latched at M2; STOP FF set
* at X3; STPA asserts.
* - INT rising edge with EIN=1 set int_pending; latched at M2;
* forced JMS to PC=0x003 at X3; INTA asserts.
*
* Scope of this implementation (matches active tests in
* test_4040/4040.test.js):
* - Pin contract (24-pin DIP per [M40] pp. 1-5/1-6).
* - STP/STPA protocol that asserts STPA within ~2 instruction cycles
* of STP going high.
*
* Out of scope (deferred to follow-up; covered as it.todo):
* - INT vectoring to 0x003 with INTA + register-bank save.
* - BBS (return from interrupt subroutine, opcode 0x02).
* - All 14 new opcodes' actual semantics (LCR, OR4/OR5, AN6/AN7,
* DB0/DB1, SB0/SB1, EIN/DIN, RPM).
* - Full 4004-superset ISA decoding.
*/
#include "velxio-chip.h"
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
typedef enum {
PHASE_A1 = 0, PHASE_A2, PHASE_A3,
PHASE_M1, PHASE_M2,
PHASE_X1, PHASE_X2, PHASE_X3,
} phase_t;
typedef struct {
/* Pin handles — names from [M40] pp. 1-5/1-6 */
vx_pin dpin[4];
vx_pin sync;
vx_pin reset;
vx_pin test;
vx_pin cmrom[2]; /* CMROM0, CMROM1 */
vx_pin cmram[4];
vx_pin clk1, clk2;
vx_pin stp, stpa;
vx_pin intn, inta;
vx_pin cy_pin;
vx_pin vdd, vdd1, vdd2, vss;
vx_timer cycle_timer;
/* CPU state */
uint16_t pc;
uint8_t acc;
bool cy;
uint8_t reg[24]; /* 3 banks × 8 regs (R8..R15 shared); see [M40] p. 1-11 */
uint8_t bank; /* 0 (SB0) or 1 (SB1) — index-bank FF */
uint16_t stack[7]; /* 7-deep PC stack ([M40] p. 1-12) */
uint8_t sp;
uint8_t cmram_select;
uint8_t rom_bank; /* 0 or 1 — set by DB0/DB1 */
bool iff_enable; /* interrupt enable (set by EIN, cleared by RESET/DIN/INTA) */
/* Bus-level state */
int phase;
uint8_t opcode;
bool reset_active;
bool driving_d;
/* Latched control inputs — sampled at M2 per [M40] pp. 1-12, 1-13 */
bool stp_latched;
bool int_latched;
bool stop_ff; /* set at X3 after STP latched at M2 */
bool halt_ff;
bool inta_ff;
} cpu_t;
static cpu_t G;
/* ─── D-bus helpers (identical to 4004) ─────────────────────────────────── */
static void drive_d(uint8_t nibble) {
for (int i = 0; i < 4; i++) {
vx_pin_set_mode(G.dpin[i], VX_OUTPUT);
vx_pin_write(G.dpin[i], (nibble >> 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.dpin[i], VX_INPUT);
G.driving_d = false;
}
static uint8_t read_d(void) {
uint8_t v = 0;
for (int i = 0; i < 4; i++) if (vx_pin_read(G.dpin[i])) v |= (1u << i);
return v;
}
/* ─── Reset ──────────────────────────────────────────────────────────────── */
static void reset_state(void) {
G.pc = 0;
G.acc = 0;
G.cy = false;
memset(G.reg, 0, sizeof G.reg);
memset(G.stack, 0, sizeof G.stack);
G.sp = 0;
G.cmram_select = 0;
G.rom_bank = 0;
G.bank = 0;
G.iff_enable = false; /* [M40] p. 1-13: RESET clears interrupt enable */
G.phase = 0;
G.opcode = 0;
G.stp_latched = false;
G.int_latched = false;
G.stop_ff = false;
G.halt_ff = false;
G.inta_ff = false;
vx_pin_write(G.sync, 0);
vx_pin_write(G.cmrom[0], 0);
vx_pin_write(G.cmrom[1], 0);
for (int i = 0; i < 4; i++) vx_pin_write(G.cmram[i], 0);
vx_pin_write(G.stpa, 0);
vx_pin_write(G.inta, 0);
vx_pin_write(G.cy_pin, 0);
release_d();
}
/* ─── Active CMROM line based on rom_bank (DB0/DB1) ─────────────────────── */
static vx_pin active_cmrom(void) {
return G.cmrom[G.rom_bank & 1];
}
/* ─── Per-phase action ───────────────────────────────────────────────────── */
static void on_phase(void* user_data) {
(void)user_data;
if (G.reset_active) return;
/* If we're in STOP mode the chip executes NOPs internally but the
clock keeps cycling and SYNC continues to pulse ([M40] p. 1-10).
We model this by skipping CPU-state mutation but still walking
the bus phases so observable signals (SYNC, CMROM) keep cycling. */
if (G.phase == PHASE_A1) {
/* Deassert any CMROM line that was held during M1+M2 of the
previous cycle. */
vx_pin_write(G.cmrom[0], 0);
vx_pin_write(G.cmrom[1], 0);
}
switch (G.phase) {
case PHASE_A1:
drive_d(G.pc & 0xF);
vx_pin_write(G.sync, 1);
break;
case PHASE_A2:
vx_pin_write(G.sync, 0);
drive_d((G.pc >> 4) & 0xF);
break;
case PHASE_A3:
drive_d((G.pc >> 8) & 0xF);
break;
case PHASE_M1:
release_d();
vx_pin_write(active_cmrom(), 1);
G.opcode = (read_d() & 0xF) << 4;
break;
case PHASE_M2:
G.opcode |= read_d() & 0xF;
/* Latch STP and INT at M2 ([M40] p. 1-10, p. 1-12).
STP wins over INT when both are asserted ([M40] p. 1-13). */
G.stp_latched = vx_pin_read(G.stp) ? true : false;
G.int_latched = (G.iff_enable && !G.stp_latched && !G.inta_ff
&& vx_pin_read(G.intn)) ? true : false;
break;
case PHASE_X1:
/* CY output reflects the carry/link FF; per [M40] p. 1-6
"updated at X1". */
vx_pin_write(G.cy_pin, G.cy ? 1 : 0);
break;
case PHASE_X2:
break;
case PHASE_X3:
/* End-of-cycle: act on latched control signals.
[M40] p. 1-10: STOP FF set at X3 if STP was latched at M2. */
if (G.stp_latched) {
G.stop_ff = true;
vx_pin_write(G.stpa, 1);
} else if (!G.stop_ff) {
/* Resume from STOP: STP=0 latched at M2 → STOP FF reset
at X3. [M40] p. 1-10: "Normal processor operation
resumes at instruction cycle N+1." */
vx_pin_write(G.stpa, 0);
}
if (G.int_latched && !G.stop_ff) {
/* Forced JMS to page 0, location 3. [M40] p. 1-12 */
if (G.sp < 7) G.stack[G.sp++] = G.pc;
G.pc = 0x003;
G.iff_enable = false;
G.inta_ff = true;
vx_pin_write(G.inta, 1);
} else if (!G.stop_ff) {
/* Normal NOP-equivalent: advance PC. */
G.pc = (G.pc + 1) & 0xFFF;
}
G.stp_latched = false;
G.int_latched = false;
break;
}
G.phase = (G.phase + 1) & 7;
}
/* ─── RESET pin watch ────────────────────────────────────────────────────── */
static void on_reset(void* user_data, vx_pin pin, int value) {
(void)user_data; (void)pin;
if (value) {
G.reset_active = true;
reset_state();
} else {
G.reset_active = false;
}
}
/* ─── Setup ──────────────────────────────────────────────────────────────── */
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.dpin[i] = vx_pin_register(name, VX_INPUT);
}
G.sync = vx_pin_register("SYNC", VX_OUTPUT_LOW);
G.reset = vx_pin_register("RESET", VX_INPUT);
G.test = vx_pin_register("TEST", VX_INPUT);
G.cmrom[0] = vx_pin_register("CMROM0", VX_OUTPUT_LOW);
G.cmrom[1] = vx_pin_register("CMROM1", VX_OUTPUT_LOW);
G.cmram[0] = vx_pin_register("CMRAM0", VX_OUTPUT_LOW);
G.cmram[1] = vx_pin_register("CMRAM1", VX_OUTPUT_LOW);
G.cmram[2] = vx_pin_register("CMRAM2", VX_OUTPUT_LOW);
G.cmram[3] = vx_pin_register("CMRAM3", VX_OUTPUT_LOW);
G.clk1 = vx_pin_register("CLK1", VX_INPUT);
G.clk2 = vx_pin_register("CLK2", VX_INPUT);
G.stp = vx_pin_register("STP", VX_INPUT);
G.stpa = vx_pin_register("STPA", VX_OUTPUT_LOW);
G.intn = vx_pin_register("INT", VX_INPUT);
G.inta = vx_pin_register("INTA", VX_OUTPUT_LOW);
G.cy_pin = vx_pin_register("CY", VX_OUTPUT_LOW);
G.vdd = vx_pin_register("VDD", VX_INPUT);
G.vdd1 = vx_pin_register("VDD1", VX_INPUT);
G.vdd2 = vx_pin_register("VDD2", VX_INPUT);
G.vss = vx_pin_register("VSS", VX_INPUT);
reset_state();
G.reset_active = false;
vx_pin_watch(G.reset, VX_EDGE_BOTH, on_reset, 0);
/* Same nominal clock as 4004: 740 kHz → ~1351 ns per phase. */
G.cycle_timer = vx_timer_create(on_phase, 0);
vx_timer_start(G.cycle_timer, 1351, true);
}

View File

@ -24,17 +24,22 @@ const skip = !chipWasmExists(CHIP);
const CLOCK_HZ = 740_000;
const CLOCK_NS = Math.round(1e9 / CLOCK_HZ);
/**
* Pin names match the Intel MCS-40 User's Manual (Nov 1974) pin-description
* table on pages 1-5/1-6. Φ1/Φ2 are renamed CLK1/CLK2 (no Greek letters in
* C identifiers); the three 15 V supply pins (Vdd, Vdd1, Vdd2) are kept
* separate even though velxio is digital and treats them all as power.
*/
function fullPinMap() {
// NOTE: pin names below are best-known; cross-check against an Intel
// 4040 datasheet before the chip implementation locks them down.
const m = {
SYNC: 'SYNC', RESET: 'RESET', TEST: 'TEST',
CMROM0: 'CMROM0', CMROM1: 'CMROM1',
CMRAM0: 'CMRAM0', CMRAM1: 'CMRAM1', CMRAM2: 'CMRAM2', CMRAM3: 'CMRAM3',
CLK1: 'CLK1', CLK2: 'CLK2',
INT: 'INT',
STOP: 'STOP', STOPACK: 'STOPACK',
VDD: 'VDD', VSS: 'VSS',
STP: 'STP', STPA: 'STPA', // Stop input + Stop-acknowledge output
INT: 'INT', INTA: 'INTA', // Interrupt input + ack output
CY: 'CY', // Carry output buffer (open drain)
VDD: 'VDD', VDD1: 'VDD1', VDD2: 'VDD2', VSS: 'VSS',
};
for (let i = 0; i < 4; i++) m[`D${i}`] = `D${i}`;
return m;
@ -50,30 +55,31 @@ describe('Intel 4040 chip', () => {
});
});
describe('STOP / STOP-ACK', () => {
it.skipIf(skip)('asserting STOP halts SYNC pulses and asserts STOPACK', async () => {
describe('STP / STPA', () => {
it.skipIf(skip)('asserting STP causes STPA to assert within one cycle', async () => {
// Per MCS-40 manual p. 1-10: when STP is latched at M2, the STOP FF
// sets at X3; the CPU then executes NOPs in a loop (clock and SYNC
// KEEP RUNNING) and STPA asserts. So the assertion here is that
// STPA goes high — we deliberately do NOT assert that SYNC stops.
const board = new BoardHarness();
await board.addChip(CHIP, fullPinMap());
// Reset and run a few cycles freely.
board.setNet('RESET', true);
board.advanceNanos(CLOCK_NS * 10);
board.advanceNanos(CLOCK_NS * 12); // ≥96 clk per p. 1-5 RESET min
board.setNet('RESET', false);
for (let i = 0; i < 16; i++) board.advanceNanos(CLOCK_NS);
// Now assert STOP and watch.
board.setNet('STOP', true);
// Wait for the chip to acknowledge.
// Now assert STP (active high per pin description, p. 1-5) and watch.
let acked = false;
board.watchNet('STOPACK', (high) => { if (high) acked = true; });
let syncAfter = 0;
board.watchNet('SYNC', (high) => { if (high) syncAfter++; });
board.watchNet('STPA', (high) => { if (high) acked = true; });
board.setNet('STP', true);
// Allow up to 2 instruction cycles for the chip to latch STP at M2
// and assert STPA at X3.
for (let i = 0; i < 24; i++) board.advanceNanos(CLOCK_NS);
expect(acked, 'STOPACK must rise within ~one instruction cycle').toBe(true);
expect(syncAfter, 'SYNC pulses must stop after STOPACK').toBeLessThanOrEqual(1);
expect(acked, 'STPA must rise within ~two instruction cycles').toBe(true);
board.dispose();
});
});

View File

@ -5,8 +5,37 @@ for the spec.
## Status
📋 **Spec only.** Will reuse the 4004 emulator core once that's
working.
**Implemented (bus skeleton + STOP/STPA).** 2/5 active tests pass;
3 are `it.todo` deferred. ~250 LOC clean-room in `4040.c` compiled to
`fixtures/4040.wasm`.
Validated against Intel MCS-40 User's Manual (Nov 1974) — PDF at
`autosearch/pdfs/mcs40_users_manual.pdf`. Cross-checked control logic
against `markablov/i40xx` (MIT) and `Kostu96/K4004` (MIT, ships
Busicom 141-PF firmware).
What works:
- Full 24-pin DIP contract per [M40] pp. 1-5/1-6 — STP, STPA, INT,
INTA, CY, dual CMROM (CMROM0/CMROM1), dual standby Vdd1/Vdd2.
- 8-phase frame inherited from 4004 (binary-compatible, [M40] p. 1-22).
- STP latched at M2 → STOP FF set at X3 → STPA asserts within ~2
instruction cycles ([M40] p. 1-10). Clock and SYNC continue per
manual ("CPU executes NOPs in a loop").
- INT latched at M2 with EIN active → forced JMS to PC=0x003 at X3,
INTA asserts ([M40] p. 1-12). Vector address is fixed; no vector
table.
- Index register file extended to 24 × 4 bits (3 banks × 8); SB0/SB1
bank-select FF in place though not exercised by tests.
- 7-deep PC stack ([M40] p. 1-12) — replaces 4004's 3-deep stack.
Deferred:
- 14 new instructions' semantics: HLT, BBS, LCR, OR4/OR5, AN6/AN7,
DB0/DB1, SB0/SB1, EIN/DIN, RPM (currently all decoded as NOP).
- BBS return-from-interrupt behaviour (pop stack, restore SRC, restore
bank FF, clear INTA).
- DB0/DB1 ROM-bank-select with 3-cycle takeover delay.
- HALT FF semantics (HLT opcode).
- Interrupt-during-2-byte-instruction edge cases ([M40] open question).
## Pin contract (24-pin DIP, real silicon)