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:
parent
fd93c703c6
commit
ef812c3d9b
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue