velxio/test/test_intel/test_4040/4040.c

275 lines
9.9 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 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);
}