velxio/test/test_intel/test_4004/4004.c

499 lines
18 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: 4-bit data bus D0..D3 multiplexed across 8 phases per
* machine cycle (A1, A2, A3, M1, M2, X1, X2, X3 — [M4] Fig. 2 p. 6).
* Each timer fire = one phase. PC drives D in A1/A2/A3 (low nibble
* first); ROM drives opcode on D in M1/M2; CPU executes in X1/X2/X3.
*
* ISA: 46 instructions implemented per [M4] Table V pp. 15-16. Two-byte
* instructions (JCN, FIM, JUN, JMS, ISZ) span two consecutive cycles —
* cycle N fetches the opcode, cycle N+1 fetches the operand byte using
* the same bus protocol (PC drives address pointing at the operand,
* ROM drives the byte at M1/M2).
*
* The I/O group (WRM/WMP/WRR/WPM/WR0..3/SBM/RDM/RDR/ADM/RD0..3) is
* decoded but the actual RAM/ROM-port side-effects are stubs — they
* require a 4001 ROM and 4002 RAM chip on the canvas, which are not
* yet implemented. WRR / WMP write a value to no-op storage; reads
* return 0.
*/
#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 enum {
FETCH_OPCODE = 0, /* this cycle is fetching the first/only byte */
FETCH_OPERAND, /* this cycle is fetching the second byte of a 2-byte op */
} fetch_t;
typedef struct {
/* Pin handles */
vx_pin dpin[4];
vx_pin sync, reset, test, cmrom;
vx_pin cmram[4];
vx_pin clk1, clk2, vdd, vss;
vx_timer cycle_timer;
/* CPU state ([M4] §III) */
uint16_t pc;
uint8_t acc;
bool cy;
uint8_t reg[16];
uint16_t stack[3];
uint8_t sp;
uint8_t cmram_select; /* 0..3, set by DCL */
/* Bus-level / fetch state */
int phase;
uint8_t opcode;
uint8_t operand;
fetch_t fetch_state;
bool reset_active;
bool driving_d;
bool pc_overridden; /* set by JCN/JUN/JMS/JIN/BBL/ISZ to suppress
the default PC++ at end of cycle */
/* I/O port writes (stubbed — no real ROM/RAM chips on bus yet) */
uint8_t iomem_wmp; /* last value written by WMP */
uint8_t iomem_wrr; /* last value written by WRR */
} 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;
}
/* ─── Reg-pair helpers (Pn = Rn,Rn+1; n=0..7; even reg is high nibble) ─── */
static uint8_t pair_read(uint8_t p) {
return (G.reg[(p << 1) & 0xE] << 4) | G.reg[((p << 1) & 0xE) + 1];
}
static void pair_write(uint8_t p, uint8_t v) {
G.reg[(p << 1) & 0xE] = (v >> 4) & 0xF;
G.reg[((p << 1) & 0xE) + 1] = v & 0xF;
}
/* ─── 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; /* CMRAM0 selected after RESET ([M4] p. 9) */
G.phase = 0;
G.opcode = 0;
G.operand = 0;
G.fetch_state = FETCH_OPCODE;
G.pc_overridden = false;
G.iomem_wmp = 0;
G.iomem_wrr = 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();
}
/* ─── ALU helpers ────────────────────────────────────────────────────────── */
/* Determine whether `op` is a 2-byte instruction per [M4] Table V. */
static bool is_two_byte(uint8_t op) {
uint8_t hi = (op >> 4) & 0xF;
if (hi == 0x1) return true; /* JCN */
if (hi == 0x2) return (op & 1) == 0; /* FIM (even) — SRC is odd, 1-byte */
if (hi == 0x4) return true; /* JUN */
if (hi == 0x5) return true; /* JMS */
if (hi == 0x7) return true; /* ISZ */
return false;
}
/* JCN condition test ([M4] p. 27-28).
OPA bits: C1 C2 C3 C4 (D3 D2 D1 D0).
C1=1 → invert sense
C2=1 → ACC == 0
C3=1 → CY == 1
C4=1 → TEST pin == 0 (logic-0 = high voltage)
"JUMP = C1·((ACC=0)·C2 + (CY=1)·C3 + TEST·C4) + ~C1·~(...)" */
static bool jcn_condition(uint8_t opa) {
uint8_t c1 = (opa >> 3) & 1;
uint8_t c2 = (opa >> 2) & 1;
uint8_t c3 = (opa >> 1) & 1;
uint8_t c4 = (opa >> 0) & 1;
int test_pin = vx_pin_read(G.test) ? 1 : 0;
bool any = (c2 && (G.acc == 0))
|| (c3 && G.cy)
|| (c4 && (test_pin == 0));
/* C1 inverts: default (C1=0) is "jump if any condition met";
with C1=1 the sense flips to "jump if NO condition met". */
return c1 ? !any : any;
}
/* Stack push (3-deep — overflow drops oldest, [M4] p. 13). */
static void stack_push(uint16_t value) {
G.stack[2] = G.stack[1];
G.stack[1] = G.stack[0];
G.stack[0] = value;
if (G.sp < 3) G.sp++;
}
static uint16_t stack_pop(void) {
uint16_t v = G.stack[0];
G.stack[0] = G.stack[1];
G.stack[1] = G.stack[2];
G.stack[2] = 0;
if (G.sp > 0) G.sp--;
return v;
}
/* DAA ([M4] p. 29; per [M4] Table V row F)
"If ACC > 9 OR CY = 1, ACC ← ACC + 6. CY is set if a carry out of bit 4
occurred during the addition; otherwise unchanged." */
static void daa(void) {
if (G.acc > 9 || G.cy) {
uint8_t r = G.acc + 6;
if (r > 0xF) G.cy = true;
G.acc = r & 0xF;
}
}
/* KBP — keyboard process: encodes ACC bits to a position number.
[M4] Table V row F (KBP=FC). Mapping per p. 30:
0000→0, 0001→1, 0010→2, 0100→3, 1000→4, others→15 (error). */
static void kbp(void) {
static const uint8_t kbp_lut[16] = {
0x0, 0x1, 0x2, 0xF, /* 0,1,2,err */
0x3, 0xF, 0xF, 0xF, /* 3,err,err,err */
0x4, 0xF, 0xF, 0xF, /* 4,err,err,err */
0xF, 0xF, 0xF, 0xF, /* err×4 */
};
G.acc = kbp_lut[G.acc & 0xF];
}
/* ─── Execute 1-byte instruction (opcode is in G.opcode) ────────────────── */
static void exec_1byte(uint8_t op) {
uint8_t hi = (op >> 4) & 0xF;
uint8_t lo = op & 0xF;
switch (hi) {
case 0x0: /* NOP */
break;
case 0x2: { /* SRC Pn — odd opcodes only (FIM is even, handled as 2-byte) */
/* Send register pair to RAM/ROM as address. We're a CPU only;
the ROM/RAM chips on the bus act on this — for now no
connected RAM, so this is a no-op beyond setting an
internal "pending SRC" indicator (not modelled). */
(void)pair_read(lo >> 1);
break;
}
case 0x3: {
uint8_t pair_idx = lo >> 1;
if ((lo & 1) == 0) {
/* FIN Pn — A ← ROM[(PC[11:8] : P0)]. Without a real
ROM chip on the bus we can't fetch the indirect byte;
stub as no-op for now. */
(void)pair_idx;
} else {
/* JIN Pn — PC ← (PC[11:8] : Pn) */
G.pc = (G.pc & 0xF00) | pair_read(pair_idx);
G.pc_overridden = true;
}
break;
}
case 0x6: /* INC Rn */
G.reg[lo] = (G.reg[lo] + 1) & 0xF;
break;
case 0x8: { /* ADD Rn — A ← A + Rn + CY */
uint8_t r = G.acc + G.reg[lo] + (G.cy ? 1 : 0);
G.cy = (r > 0xF);
G.acc = r & 0xF;
break;
}
case 0x9: { /* SUB Rn — A ← A + ~Rn + ~CY (i.e. A Rn CY-borrow) */
uint8_t r = G.acc + ((~G.reg[lo]) & 0xF) + (G.cy ? 0 : 1);
G.cy = (r > 0xF);
G.acc = r & 0xF;
break;
}
case 0xA: /* LD Rn — A ← Rn */
G.acc = G.reg[lo];
break;
case 0xB: { /* XCH Rn — swap A and Rn */
uint8_t t = G.acc;
G.acc = G.reg[lo];
G.reg[lo] = t;
break;
}
case 0xC: /* BBL d — pop stack into PC; A ← d */
G.pc = stack_pop() & 0xFFF;
G.acc = lo;
G.pc_overridden = true;
break;
case 0xD: /* LDM d — A ← d */
G.acc = lo;
break;
case 0xE: /* I/O / RAM group ([M4] p. 30 +) */
switch (lo) {
case 0x0: /* WRM — write A to RAM at SRC addr (stub) */ break;
case 0x1: G.iomem_wmp = G.acc; break; /* WMP */
case 0x2: G.iomem_wrr = G.acc; break; /* WRR */
case 0x3: /* WPM — write program memory (4289 stub) */ break;
case 0x4: /* WR0 */ G.iomem_wmp = G.acc; break;
case 0x5: /* WR1 */ break;
case 0x6: /* WR2 */ break;
case 0x7: /* WR3 */ break;
case 0x8: /* SBM — A ← A + ~RAM[SRC] + ~CY (stub: RAM=0) */ {
uint8_t r = G.acc + 0xF + (G.cy ? 0 : 1);
G.cy = (r > 0xF);
G.acc = r & 0xF;
break;
}
case 0x9: /* RDM — A ← RAM[SRC] (stub: 0) */ G.acc = 0; break;
case 0xA: /* RDR — A ← ROM-port[SRC] (stub: 0) */ G.acc = 0; break;
case 0xB: /* ADM — A ← A + RAM[SRC] + CY (stub: RAM=0) */ {
uint8_t r = G.acc + 0 + (G.cy ? 1 : 0);
G.cy = (r > 0xF);
G.acc = r & 0xF;
break;
}
case 0xC: case 0xD: case 0xE: case 0xF: /* RD0..RD3 (stub) */
G.acc = 0;
break;
}
break;
case 0xF: /* ACC group ([M4] p. 29-30) */
switch (lo) {
case 0x0: G.acc = 0; G.cy = false; break; /* CLB */
case 0x1: G.cy = false; break; /* CLC */
case 0x2: { /* IAC — A++, CY = carry */
uint8_t r = G.acc + 1;
G.cy = (r > 0xF);
G.acc = r & 0xF;
break;
}
case 0x3: G.cy = !G.cy; break; /* CMC */
case 0x4: G.acc = (~G.acc) & 0xF; break; /* CMA */
case 0x5: { /* RAL — rotate A left through CY */
uint8_t b3 = (G.acc >> 3) & 1;
G.acc = ((G.acc << 1) | (G.cy ? 1 : 0)) & 0xF;
G.cy = b3 != 0;
break;
}
case 0x6: { /* RAR — rotate A right through CY */
uint8_t b0 = G.acc & 1;
G.acc = ((G.acc >> 1) | ((G.cy ? 1 : 0) << 3)) & 0xF;
G.cy = b0 != 0;
break;
}
case 0x7: G.acc = G.cy ? 1 : 0; G.cy = false; break; /* TCC */
case 0x8: { /* DAC — A--, CY = !borrow */
/* A + 0xF + 0 (no incoming carry bit involved) */
uint8_t r = G.acc + 0xF;
G.cy = (r > 0xF);
G.acc = r & 0xF;
break;
}
case 0x9: G.acc = G.cy ? 0xA : 0x9; G.cy = false; break; /* TCS */
case 0xA: G.cy = true; break; /* STC */
case 0xB: daa(); break; /* DAA */
case 0xC: kbp(); break; /* KBP */
case 0xD: G.cmram_select = G.acc & 7; break; /* DCL */
/* 0xE, 0xF unused */
}
break;
default:
/* All remaining 1-byte slots in the high-nibble range are
unused on the 4004; treat as NOP. */
break;
}
}
/* ─── Execute 2-byte instruction (opcode + operand) ─────────────────────── */
static void exec_2byte(uint8_t op, uint8_t operand) {
uint8_t hi = (op >> 4) & 0xF;
uint8_t lo = op & 0xF;
switch (hi) {
case 0x1: /* JCN cccc */
if (jcn_condition(lo)) {
/* In-page jump; PC high nibble at the moment of the jump
is post-operand-fetch (PC currently at the instr after
JCN). [M4] p. 28 page-wrap: jumps from words 254/255
land in the next page — modelled correctly because we
use the post-increment PC. */
G.pc = (G.pc & 0xF00) | operand;
G.pc_overridden = true;
}
break;
case 0x2: { /* FIM Pn data */
/* Even opcode: load reg pair Pn (n = (op >> 1) & 7) with
immediate 8-bit operand. */
pair_write(lo >> 1, operand);
break;
}
case 0x4: { /* JUN — 12-bit jump */
G.pc = (((uint16_t)lo) << 8) | operand;
G.pc_overridden = true;
break;
}
case 0x5: { /* JMS — push PC; 12-bit jump */
stack_push(G.pc & 0xFFF); /* PC is post-operand (= return addr) */
G.pc = (((uint16_t)lo) << 8) | operand;
G.pc_overridden = true;
break;
}
case 0x7: { /* ISZ Rn — Rn++; if Rn != 0, jump in-page */
uint8_t v = (G.reg[lo] + 1) & 0xF;
G.reg[lo] = v;
if (v != 0) {
G.pc = (G.pc & 0xF00) | operand;
G.pc_overridden = true;
}
break;
}
default:
/* Unknown 2-byte op; should not happen if is_two_byte() agrees. */
break;
}
}
/* ─── Per-phase action ───────────────────────────────────────────────────── */
static void on_phase(void* user_data) {
(void)user_data;
if (G.reset_active) return;
if (G.phase == PHASE_A1) {
vx_pin_write(G.cmrom, 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(G.cmrom, 1);
if (G.fetch_state == FETCH_OPCODE) {
G.opcode = (read_d() & 0xF) << 4;
} else {
G.operand = (read_d() & 0xF) << 4;
}
break;
case PHASE_M2:
if (G.fetch_state == FETCH_OPCODE) {
G.opcode |= read_d() & 0xF;
} else {
G.operand |= read_d() & 0xF;
}
break;
case PHASE_X1:
/* idle; most ops execute at X2/X3 in real silicon, but for
our cycle-coarse model we do everything at X3 below. */
break;
case PHASE_X2:
break;
case PHASE_X3:
G.pc_overridden = false;
if (G.fetch_state == FETCH_OPCODE) {
if (is_two_byte(G.opcode)) {
/* Cycle 1 of a 2-byte instruction — defer execution.
Advance PC to point at operand. */
G.pc = (G.pc + 1) & 0xFFF;
G.fetch_state = FETCH_OPERAND;
} else {
exec_1byte(G.opcode);
if (!G.pc_overridden) G.pc = (G.pc + 1) & 0xFFF;
}
} else {
/* Cycle 2 of a 2-byte instruction. PC currently points
at the operand byte; advance past it (to next instr)
BEFORE executing — JCN/JUN/JMS/ISZ semantics expect
"PC of next instruction" when computing relative or
absolute targets ([M4] p. 12 footnote (3)). */
G.pc = (G.pc + 1) & 0xFFF;
exec_2byte(G.opcode, G.operand);
G.fetch_state = FETCH_OPCODE;
}
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;
}
}
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 = 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);
G.cycle_timer = vx_timer_create(on_phase, 0);
vx_timer_start(G.cycle_timer, 1351, true);
}