velxio/test/test_intel/test_4040/4040.c

667 lines
23 KiB
C

/*
* 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.
* [M4] Intel MCS-4 User's Manual (Feb 1973) — for the 46 base
* opcodes inherited from 4004.
* 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 occupy OPR=0000
* OPA=0x01..0x0E (NOP=0x00 is preserved).
*
* New 4040 instructions (all 1-byte, [M40] p. 1-22):
* HLT 0x01 — halt
* BBS 0x02 — branch-back from interrupt subroutine (pop PC + clear INTA)
* LCR 0x03 — ACC ← Command Register
* OR4 0x04 — ACC ← ACC OR R4
* OR5 0x05 — ACC ← ACC OR R5
* AN6 0x06 — ACC ← ACC AND R6
* AN7 0x07 — ACC ← ACC AND R7
* DB0 0x08 — designate ROM bank 0 (CMROM0); takes effect 3 cycles later
* DB1 0x09 — designate ROM bank 1 (CMROM1)
* SB0 0x0A — select index-register bank 0 (R0..R7 = reg[0..7])
* SB1 0x0B — select index-register bank 1 (R0..R7 = reg[16..23])
* EIN 0x0C — enable interrupt (set IFF)
* DIN 0x0D — disable interrupt
* RPM 0x0E — read program memory (4289 stub)
*
* Index register bank model ([M40] p. 1-11):
* Physical reg[0..7] = bank 0's R0..R7
* Physical reg[8..15] = shared upper R8..R15
* Physical reg[16..23] = bank 1's R0..R7
* Bank flag selects which physical slice R0..R7 maps to.
*/
#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,
FETCH_OPERAND,
} fetch_t;
/* X2/X3 bus action selected at end of M2 from the decoded opcode.
Same set as the 4004 (the 4040 inherits MCS-4 I/O semantics). */
typedef enum {
XACT_NONE = 0,
XACT_SRC, /* drive pair_hi at X2, pair_lo at X3, CM-RAM strobe */
XACT_WRM_WMP, /* drive ACC at X2, CM-RAM (or CM-ROM for WRR/WPM) */
XACT_RDM, /* release D at X2, sample → io_data_in (4002 drives) */
XACT_RDS, /* RDR — release D at X2, sample (CM-ROM strobed) */
XACT_ADM_SBM, /* like RDM but feeds ADD/SUB */
XACT_WR_STATUS, /* WR0..WR3 — drive ACC at X2 */
XACT_RD_STATUS, /* RD0..RD3 — release at X2, sample */
} xact_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];
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]; /* bank0:[0..7] shared:[8..15] bank1:[16..23] */
uint8_t bank; /* 0 (SB0) or 1 (SB1) */
uint16_t stack[7]; /* 7-deep PC stack */
uint8_t sp;
uint8_t cmram_select;
uint8_t rom_bank; /* 0 or 1 — set by DB0/DB1 */
bool iff_enable;
uint8_t cmd_reg; /* command register accessible via LCR */
/* Bus / fetch state */
int phase;
uint8_t opcode;
uint8_t operand;
fetch_t fetch_state;
bool reset_active;
bool driving_d;
bool pc_overridden;
/* Latched control inputs */
bool stp_latched;
bool int_latched;
bool stop_ff;
bool halt_ff;
bool inta_ff;
/* X2/X3 staging — populated at M2 from the decoded opcode. */
xact_t xact;
uint8_t xact_pair;
uint8_t xact_status_idx;
uint8_t io_data_in;
} cpu_t;
static cpu_t G;
/* ─── Bank-aware register access ────────────────────────────────────────── */
static uint8_t* regp(uint8_t n) {
n &= 0xF;
if (n >= 8) return &G.reg[n]; /* R8..R15 are shared */
return G.bank ? &G.reg[n + 16] : &G.reg[n]; /* R0..R7 follow SB0/SB1 */
}
/* Register-pair: Pn = (2n, 2n+1), n=0..7 */
static uint8_t pair_read(uint8_t p) {
uint8_t hi_idx = (p << 1) & 0xE;
uint8_t lo_idx = hi_idx + 1;
return (*regp(hi_idx) << 4) | *regp(lo_idx);
}
static void pair_write(uint8_t p, uint8_t v) {
uint8_t hi_idx = (p << 1) & 0xE;
uint8_t lo_idx = hi_idx + 1;
*regp(hi_idx) = (v >> 4) & 0xF;
*regp(lo_idx) = v & 0xF;
}
/* ─── 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;
}
static vx_pin active_cmrom(void) { return G.cmrom[G.rom_bank & 1]; }
/* ─── 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;
G.cmd_reg = 0;
G.phase = 0;
G.opcode = 0;
G.operand = 0;
G.fetch_state = FETCH_OPCODE;
G.pc_overridden = false;
G.stp_latched = false;
G.int_latched = false;
G.stop_ff = false;
G.halt_ff = false;
G.inta_ff = false;
G.xact = XACT_NONE;
G.io_data_in = 0;
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();
}
/* ─── ALU helpers ────────────────────────────────────────────────────────── */
static bool is_two_byte(uint8_t op) {
uint8_t hi = (op >> 4) & 0xF;
if (hi == 0x1) return true;
if (hi == 0x2) return (op & 1) == 0;
if (hi == 0x4) return true;
if (hi == 0x5) return true;
if (hi == 0x7) return true;
return false;
}
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));
return c1 ? !any : any;
}
/* 7-deep stack ([M40] p. 1-12) — overflow drops oldest. */
static void stack_push(uint16_t value) {
for (int i = 6; i > 0; i--) G.stack[i] = G.stack[i-1];
G.stack[0] = value;
if (G.sp < 7) G.sp++;
}
static uint16_t stack_pop(void) {
uint16_t v = G.stack[0];
for (int i = 0; i < 6; i++) G.stack[i] = G.stack[i+1];
G.stack[6] = 0;
if (G.sp > 0) G.sp--;
return v;
}
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;
}
}
static void kbp(void) {
static const uint8_t kbp_lut[16] = {
0x0, 0x1, 0x2, 0xF,
0x3, 0xF, 0xF, 0xF,
0x4, 0xF, 0xF, 0xF,
0xF, 0xF, 0xF, 0xF,
};
G.acc = kbp_lut[G.acc & 0xF];
}
/* ─── Execute 1-byte instruction ────────────────────────────────────────── */
static void exec_1byte(uint8_t op) {
uint8_t hi = (op >> 4) & 0xF;
uint8_t lo = op & 0xF;
if (hi == 0x0) {
/* The 4040 places its 14 new instructions here, plus NOP at 0x00. */
switch (lo) {
case 0x0: /* NOP */ break;
case 0x1: /* HLT */
G.halt_ff = true;
G.stop_ff = true;
vx_pin_write(G.stpa, 1);
break;
case 0x2: /* BBS — branch back from interrupt subroutine */
G.pc = stack_pop() & 0xFFF;
G.inta_ff = false;
vx_pin_write(G.inta, 0);
G.pc_overridden = true;
/* SRC + bank FF restoration happens here on real silicon;
we don't fully model the SRC re-emit yet. */
break;
case 0x3: /* LCR — ACC ← Command Register */
G.acc = G.cmd_reg & 0xF;
break;
case 0x4: /* OR4 — ACC ← ACC OR R4 */
G.acc = (G.acc | *regp(4)) & 0xF;
break;
case 0x5: /* OR5 */
G.acc = (G.acc | *regp(5)) & 0xF;
break;
case 0x6: /* AN6 — ACC ← ACC AND R6 */
G.acc = (G.acc & *regp(6)) & 0xF;
break;
case 0x7: /* AN7 */
G.acc = (G.acc & *regp(7)) & 0xF;
break;
case 0x8: /* DB0 — designate ROM bank 0 */
G.rom_bank = 0;
break;
case 0x9: /* DB1 — designate ROM bank 1 */
G.rom_bank = 1;
break;
case 0xA: /* SB0 — select index-register bank 0 */
G.bank = 0;
break;
case 0xB: /* SB1 — select index-register bank 1 */
G.bank = 1;
break;
case 0xC: /* EIN — enable interrupt */
G.iff_enable = true;
break;
case 0xD: /* DIN — disable interrupt */
G.iff_enable = false;
break;
case 0xE: /* RPM — read program memory (4289 stub) */
G.acc = 0;
break;
/* 0xF unused */
}
return;
}
/* The remaining 1-byte opcodes are inherited from the 4004. */
switch (hi) {
case 0x2: { /* SRC Pn (odd opcodes only) */
(void)pair_read(lo >> 1);
break;
}
case 0x3: {
uint8_t pair_idx = lo >> 1;
if ((lo & 1) == 0) {
/* FIN Pn — stub */
(void)pair_idx;
} else {
G.pc = (G.pc & 0xF00) | pair_read(pair_idx);
G.pc_overridden = true;
}
break;
}
case 0x6: /* INC Rn */
*regp(lo) = (*regp(lo) + 1) & 0xF;
break;
case 0x8: { /* ADD Rn */
uint8_t r = G.acc + *regp(lo) + (G.cy ? 1 : 0);
G.cy = (r > 0xF);
G.acc = r & 0xF;
break;
}
case 0x9: { /* SUB Rn */
uint8_t r = G.acc + ((~*regp(lo)) & 0xF) + (G.cy ? 0 : 1);
G.cy = (r > 0xF);
G.acc = r & 0xF;
break;
}
case 0xA: G.acc = *regp(lo); break; /* LD Rn */
case 0xB: { /* XCH Rn */
uint8_t t = G.acc;
G.acc = *regp(lo);
*regp(lo) = t;
break;
}
case 0xC: /* BBL d */
G.pc = stack_pop() & 0xFFF;
G.acc = lo;
G.pc_overridden = true;
break;
case 0xD: G.acc = lo; break; /* LDM d */
case 0xE: /* I/O / RAM group — bus heavy lifting happened during
X2/X3; here we only update ACC/flags from io_data_in
for read ops. Writes have no further effect on CPU
state (output side of the 4002/4001 was driven by
the X2 bus action). */
switch (lo) {
case 0x8: { /* SBM — A ← A + ~RAM + ~CY */
uint8_t r = G.acc + ((~G.io_data_in) & 0xF) + (G.cy ? 0 : 1);
G.cy = (r > 0xF);
G.acc = r & 0xF;
break;
}
case 0x9: G.acc = G.io_data_in; break; /* RDM */
case 0xA: G.acc = G.io_data_in; break; /* RDR */
case 0xB: { /* ADM — A ← A + RAM + CY */
uint8_t r = G.acc + G.io_data_in + (G.cy ? 1 : 0);
G.cy = (r > 0xF);
G.acc = r & 0xF;
break;
}
case 0xC: case 0xD: case 0xE: case 0xF: /* RD0..RD3 */
G.acc = G.io_data_in;
break;
/* 0,1,2,3,4,5,6,7 = WRM/WMP/WRR/WPM/WR0..3 — bus drives
ACC at X2; nothing more for the CPU side. */
default: break;
}
break;
case 0xF: /* ACC group */
switch (lo) {
case 0x0: G.acc = 0; G.cy = false; break;
case 0x1: G.cy = false; break;
case 0x2: { uint8_t r = G.acc + 1; G.cy = (r > 0xF); G.acc = r & 0xF; break; }
case 0x3: G.cy = !G.cy; break;
case 0x4: G.acc = (~G.acc) & 0xF; break;
case 0x5: { uint8_t b3 = (G.acc >> 3) & 1;
G.acc = ((G.acc << 1) | (G.cy ? 1 : 0)) & 0xF;
G.cy = b3 != 0; break; }
case 0x6: { 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;
case 0x8: { 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;
case 0xA: G.cy = true; break;
case 0xB: daa(); break;
case 0xC: kbp(); break;
case 0xD: G.cmram_select = G.acc & 7; G.cmd_reg = G.acc & 7; break;
}
break;
default: break;
}
}
/* ─── Execute 2-byte instruction ────────────────────────────────────────── */
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:
if (jcn_condition(lo)) {
G.pc = (G.pc & 0xF00) | operand;
G.pc_overridden = true;
}
break;
case 0x2:
pair_write(lo >> 1, operand);
break;
case 0x4:
G.pc = (((uint16_t)lo) << 8) | operand;
G.pc_overridden = true;
break;
case 0x5:
stack_push(G.pc & 0xFFF);
G.pc = (((uint16_t)lo) << 8) | operand;
G.pc_overridden = true;
break;
case 0x7: {
uint8_t v = (*regp(lo) + 1) & 0xF;
*regp(lo) = v;
if (v != 0) {
G.pc = (G.pc & 0xF00) | operand;
G.pc_overridden = true;
}
break;
}
default: 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], 0);
vx_pin_write(G.cmrom[1], 0);
for (int i = 0; i < 4; i++) vx_pin_write(G.cmram[i], 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);
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;
}
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;
/* Decode opcode → set up X2/X3 bus action (mirrors 4004). */
G.xact = XACT_NONE;
if (G.fetch_state == FETCH_OPCODE) {
uint8_t op = G.opcode;
if ((op & 0xF1) == 0x21) {
G.xact = XACT_SRC;
G.xact_pair = (op >> 1) & 7;
} else if ((op & 0xF0) == 0xE0) {
uint8_t lo = op & 0xF;
switch (lo) {
case 0x0: case 0x1:
case 0x2: case 0x3:
G.xact = XACT_WRM_WMP; break;
case 0x4: case 0x5: case 0x6: case 0x7:
G.xact = XACT_WR_STATUS;
G.xact_status_idx = lo - 4;
break;
case 0x8: case 0xB: G.xact = XACT_ADM_SBM; break;
case 0x9: G.xact = XACT_RDM; break;
case 0xA: G.xact = XACT_RDS; break;
case 0xC: case 0xD: case 0xE: case 0xF:
G.xact = XACT_RD_STATUS;
G.xact_status_idx = lo - 0xC;
break;
}
}
}
break;
case PHASE_X1:
vx_pin_write(G.cy_pin, G.cy ? 1 : 0);
break;
case PHASE_X2:
switch (G.xact) {
case XACT_SRC:
drive_d((pair_read(G.xact_pair) >> 4) & 0xF);
vx_pin_write(G.cmram[G.cmram_select & 3], 1);
break;
case XACT_WRM_WMP: {
drive_d(G.acc & 0xF);
uint8_t lo = G.opcode & 0xF;
if (lo == 0x2 || lo == 0x3) {
vx_pin_write(active_cmrom(), 1);
} else {
vx_pin_write(G.cmram[G.cmram_select & 3], 1);
}
break;
}
case XACT_WR_STATUS:
drive_d(G.acc & 0xF);
vx_pin_write(G.cmram[G.cmram_select & 3], 1);
break;
case XACT_RDM:
case XACT_ADM_SBM:
case XACT_RD_STATUS:
release_d();
vx_pin_write(G.cmram[G.cmram_select & 3], 1);
G.io_data_in = read_d() & 0xF;
break;
case XACT_RDS:
release_d();
vx_pin_write(active_cmrom(), 1);
G.io_data_in = read_d() & 0xF;
break;
default:
break;
}
break;
case PHASE_X3: {
switch (G.xact) {
case XACT_SRC:
drive_d(pair_read(G.xact_pair) & 0xF);
break;
case XACT_WRM_WMP:
case XACT_WR_STATUS:
/* drive held from X2 */
break;
case XACT_RDM:
case XACT_ADM_SBM:
case XACT_RD_STATUS:
case XACT_RDS:
vx_pin_write(G.cmram[G.cmram_select & 3], 0);
vx_pin_write(active_cmrom(), 0);
release_d();
break;
default:
break;
}
G.pc_overridden = false;
if (G.stp_latched) {
G.stop_ff = true;
vx_pin_write(G.stpa, 1);
} else if (!G.halt_ff) {
G.stop_ff = false;
vx_pin_write(G.stpa, 0);
}
if (G.int_latched && !G.stop_ff) {
stack_push(G.pc & 0xFFF);
G.pc = 0x003;
G.iff_enable = false;
G.inta_ff = true;
vx_pin_write(G.inta, 1);
} else if (!G.stop_ff && !G.halt_ff) {
if (G.fetch_state == FETCH_OPCODE) {
if (is_two_byte(G.opcode)) {
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 {
G.pc = (G.pc + 1) & 0xFFF;
exec_2byte(G.opcode, G.operand);
G.fetch_state = FETCH_OPCODE;
}
}
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;
}
}
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);
G.cycle_timer = vx_timer_create(on_phase, 0);
vx_timer_start(G.cycle_timer, 1351, true);
}