velxio/test/test_intel/test_4004/4004.c

212 lines
7.8 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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);
}