rusty_emu/plans/intel_4004_opcode_fix.md
Jan-Peter von Hunnius 6a335a15d4 Fix Intel 4004 CPU instruction set and improve MCS-4 timing accuracy
Overhaul the Intel 4004 instruction set to match the real opcode map,
adding missing instructions (NOP, FIM, FIN, JIN, ISZ, KBP, DCL, WR0-3,
RD0-3) and refining two-byte instruction handling with proper M1/M2
data phases.  Rewrite the CPU fetch-execute cycle to correctly process
two-byte opcodes across separate clock cycles.

Improve Intel 4001 ROM timing with M1/M2 nibble phase tracking and an
A3 wait counter so the ROM drives data only when the CPU expects it.
Fix state-machine transition helpers in Intel 4002 and 4003 to capture
the old state before processing events, preventing incorrect exit/entry
callbacks.

Add non-propagating pin driver methods to prevent deadlocks caused by
bidirectional pin connections during signal propagation.

Add Fibonacci integration test and opcode fix plan document.
Update all README files to reflect the new instruction set.
2026-05-12 19:32:01 +02:00

45 KiB

Intel 4004 Opcode Table Fix — Design Document

1. Overview

The current implementation in intel_4004.rs has a completely incorrect opcode mapping. The decoder assigns wrong instructions to nearly every opcode range. This document provides the authoritative reference for rewriting the opcode decoder, instruction enum, and execution logic.

Current Issues Summary

Opcode Range Current Mapping Correct Mapping
0x00-0x0F LD/XCH by register NOP + LD R (duplicate of 0xA0-0xAF)
0x10-0x1F ADD/SUB by register JCN condition, address (2-byte)
0x20-0x2F ADDC/SBC by register FIM/SRC by register pair
0x30-0x3F JCN condition FIN/JIN by register pair
0x40-0x4F LDM immediate JUN address (2-byte)
0x50-0x5F WRM/WMP JMS address (2-byte)
0x60-0x6F WRR/WPM INC R
0x70-0x7F Individual I/O instructions ISZ R, address (2-byte)
0x80-0x8F JUN high nibble ADD R with carry
0x90-0x9F JUN low nibble SUB R with borrow
0xA0-0xAF JMS high nibble LD R (into accumulator)
0xB0-0xBF JMS low nibble XCH R (exchange with register)
0xC0-0xCF INC R BBL data (return from subroutine + load ACC)
0xD0-0xDF INC R LDM data (load immediate into ACC)
0xE0-0xEF Not mapped I/O and RAM instructions
0xF0-0xFC Duplicates of accumulator ops Accumulator group instructions
0xFD-0xFF — Invalid / unused

2. Complete Opcode Table (All 256 Entries)

Legend

  • Size: 1 = one-byte (8 cycles), 2 = two-byte (16 cycles)
  • R = register number 0-15
  • r = register pair number 0-7 (even register index)

2.1 NOP and Machine Instructions

Opcode Mnemonic Size Description
0x00 NOP 1 No operation

2.2 JCN — Jump Conditional (2-byte)

Opcode Mnemonic Size Description
0x10 JCN 0, addr 2 Jump if condition 0 (always false — never jumps)
0x11 JCN 1, addr 2 Jump if TEST = 1
0x12 JCN 2, addr 2 Jump if Carry = 1
0x13 JCN 3, addr 2 Jump if Carry=1 OR TEST=1
0x14 JCN 4, addr 2 Jump if ACC = 0
0x15 JCN 5, addr 2 Jump if ACC=0 OR TEST=1
0x16 JCN 6, addr 2 Jump if ACC=0 OR Carry=1
0x17 JCN 7, addr 2 Jump if ACC=0 OR Carry=1 OR TEST=1
0x18 JCN 8, addr 2 Jump unconditional (invert of always false)
0x19 JCN 9, addr 2 Jump if TEST = 0
0x1A JCN A, addr 2 Jump if Carry = 0
0x1B JCN B, addr 2 Jump if Carry=0 AND TEST=0
0x1C JCN C, addr 2 Jump if ACC ≠ 0
0x1D JCN D, addr 2 Jump if ACC≠0 AND TEST=0
0x1E JCN E, addr 2 Jump if Carry=0 AND ACC≠0
0x1F JCN F, addr 2 Jump if Carry=0 AND ACC≠0 AND TEST=0

Byte 2: 8-bit address within current 256-byte page.
Full address: (PC & 0xF00) | byte2

2.3 FIM / SRC — Register Pair Operations

Opcode Mnemonic Size Description
0x20 FIM R0, data 2 Fetch immediate to R0/R1
0x21 SRC R0 1 Send register control from R0/R1
0x22 FIM R2, data 2 Fetch immediate to R2/R3
0x23 SRC R1 1 Send register control from R2/R3
0x24 FIM R4, data 2 Fetch immediate to R4/R5
0x25 SRC R2 1 Send register control from R4/R5
0x26 FIM R6, data 2 Fetch immediate to R6/R7
0x27 SRC R3 1 Send register control from R6/R7
0x28 FIM R8, data 2 Fetch immediate to R8/R9
0x29 SRC R4 1 Send register control from R8/R9
0x2A FIM R10, data 2 Fetch immediate to R10/R11
0x2B SRC R5 1 Send register control from R10/R11
0x2C FIM R12, data 2 Fetch immediate to R12/R13
0x2D SRC R6 1 Send register control from R12/R13
0x2E FIM R14, data 2 Fetch immediate to R14/R15
0x2F SRC R7 1 Send register control from R14/R15

FIM Byte 2: 8-bit data. High nibble → first register (even), Low nibble → second register (odd).

2.4 FIN / JIN — Register Pair Indirect Operations

Opcode Mnemonic Size Description
0x30 FIN R0 2 Fetch indirect to R0/R1 from ROM
0x31 JIN R0 1 Jump indirect to address in R0/R1
0x32 FIN R2 2 Fetch indirect to R2/R3 from ROM
0x33 JIN R1 1 Jump indirect to address in R2/R3
0x34 FIN R4 2 Fetch indirect to R4/R5 from ROM
0x35 JIN R2 1 Jump indirect to address in R4/R5
0x36 FIN R6 2 Fetch indirect to R6/R7 from ROM
0x37 JIN R3 1 Jump indirect to address in R6/R7
0x38 FIN R8 2 Fetch indirect to R8/R9 from ROM
0x39 JIN R4 1 Jump indirect to address in R8/R9
0x3A FIN R10 2 Fetch indirect to R10/R11 from ROM
0x3B JIN R5 1 Jump indirect to address in R10/R11
0x3C FIN R12 2 Fetch indirect to R12/R13 from ROM
0x3D JIN R6 1 Jump indirect to address in R12/R13
0x3E FIN R14 2 Fetch indirect to R14/R15 from ROM
0x3F JIN R7 1 Jump indirect to address in R14/R15

FIN: Reads ROM at address pointed to by register pair, stores result in register pair.
JIN: Sets PC to 12-bit address formed by register pair (even reg = high 6 bits, odd reg = low 6 bits). See Section 8.

2.5 JUN — Jump Unconditional (2-byte)

Opcode Mnemonic Size Description
0x40 JUN addr 2 Jump to address (high nibble 0)
0x41 JUN addr 2 Jump to address (high nibble 1)
0x42 JUN addr 2 Jump to address (high nibble 2)
0x43 JUN addr 2 Jump to address (high nibble 3)
0x44 JUN addr 2 Jump to address (high nibble 4)
0x45 JUN addr 2 Jump to address (high nibble 5)
0x46 JUN addr 2 Jump to address (high nibble 6)
0x47 JUN addr 2 Jump to address (high nibble 7)
0x48 JUN addr 2 Jump to address (high nibble 8)
0x49 JUN addr 2 Jump to address (high nibble 9)
0x4A JUN addr 2 Jump to address (high nibble A)
0x4B JUN addr 2 Jump to address (high nibble B)
0x4C JUN addr 2 Jump to address (high nibble C)
0x4D JUN addr 2 Jump to address (high nibble D)
0x4E JUN addr 2 Jump to address (high nibble E)
0x4F JUN addr 2 Jump to address (high nibble F)

Full address: ((byte1 & 0x0F) << 8) | byte2

2.6 JMS — Jump to Subroutine (2-byte)

Opcode Mnemonic Size Description
0x50 JMS addr 2 Jump to subroutine (high nibble 0)
0x51 JMS addr 2 Jump to subroutine (high nibble 1)
0x52 JMS addr 2 Jump to subroutine (high nibble 2)
0x53 JMS addr 2 Jump to subroutine (high nibble 3)
0x54 JMS addr 2 Jump to subroutine (high nibble 4)
0x55 JMS addr 2 Jump to subroutine (high nibble 5)
0x56 JMS addr 2 Jump to subroutine (high nibble 6)
0x57 JMS addr 2 Jump to subroutine (high nibble 7)
0x58 JMS addr 2 Jump to subroutine (high nibble 8)
0x59 JMS addr 2 Jump to subroutine (high nibble 9)
0x5A JMS addr 2 Jump to subroutine (high nibble A)
0x5B JMS addr 2 Jump to subroutine (high nibble B)
0x5C JMS addr 2 Jump to subroutine (high nibble C)
0x5D JMS addr 2 Jump to subroutine (high nibble D)
0x5E JMS addr 2 Jump to subroutine (high nibble E)
0x5F JMS addr 2 Jump to subroutine (high nibble F)

Full address: ((byte1 & 0x0F) << 8) | byte2
Pushes current PC to stack before jumping.

2.7 INC — Increment Register

Opcode Mnemonic Size Description
0x60 INC R0 1 Increment register 0
0x61 INC R1 1 Increment register 1
0x62 INC R2 1 Increment register 2
0x63 INC R3 1 Increment register 3
0x64 INC R4 1 Increment register 4
0x65 INC R5 1 Increment register 5
0x66 INC R6 1 Increment register 6
0x67 INC R7 1 Increment register 7
0x68 INC R8 1 Increment register 8
0x69 INC R9 1 Increment register 9
0x6A INC R10 1 Increment register 10
0x6B INC R11 1 Increment register 11
0x6C INC R12 1 Increment register 12
0x6D INC R13 1 Increment register 13
0x6E INC R14 1 Increment register 14
0x6F INC R15 1 Increment register 15

Register = opcode & 0x0F. Wraps from 15 to 0 (4-bit).

2.8 ISZ — Increment and Skip if Not Zero (2-byte)

Opcode Mnemonic Size Description
0x70 ISZ R0, addr 2 Increment R0, jump if not zero
0x71 ISZ R1, addr 2 Increment R1, jump if not zero
0x72 ISZ R2, addr 2 Increment R2, jump if not zero
0x73 ISZ R3, addr 2 Increment R3, jump if not zero
0x74 ISZ R4, addr 2 Increment R4, jump if not zero
0x75 ISZ R5, addr 2 Increment R5, jump if not zero
0x76 ISZ R6, addr 2 Increment R6, jump if not zero
0x77 ISZ R7, addr 2 Increment R7, jump if not zero
0x78 ISZ R8, addr 2 Increment R8, jump if not zero
0x79 ISZ R9, addr 2 Increment R9, jump if not zero
0x7A ISZ R10, addr 2 Increment R10, jump if not zero
0x7B ISZ R11, addr 2 Increment R11, jump if not zero
0x7C ISZ R12, addr 2 Increment R12, jump if not zero
0x7D ISZ R13, addr 2 Increment R13, jump if not zero
0x7E ISZ R14, addr 2 Increment R14, jump if not zero
0x7F ISZ R15, addr 2 Increment R15, jump if not zero

Register = opcode & 0x0F.
Byte 2: 8-bit address within current page.
Full address: (PC & 0xF00) | byte2

2.9 ADD — Add Register to Accumulator with Carry

Opcode Mnemonic Size Description
0x80 ADD R0 1 Add R0 to ACC with carry
0x81 ADD R1 1 Add R1 to ACC with carry
0x82 ADD R2 1 Add R2 to ACC with carry
0x83 ADD R3 1 Add R3 to ACC with carry
0x84 ADD R4 1 Add R4 to ACC with carry
0x85 ADD R5 1 Add R5 to ACC with carry
0x86 ADD R6 1 Add R6 to ACC with carry
0x87 ADD R7 1 Add R7 to ACC with carry
0x88 ADD R8 1 Add R8 to ACC with carry
0x89 ADD R9 1 Add R9 to ACC with carry
0x8A ADD R10 1 Add R10 to ACC with carry
0x8B ADD R11 1 Add R11 to ACC with carry
0x8C ADD R12 1 Add R12 to ACC with carry
0x8D ADD R13 1 Add R13 to ACC with carry
0x8E ADD R14 1 Add R14 to ACC with carry
0x8F ADD R15 1 Add R15 to ACC with carry

ACC = (ACC + R + Carry) & 0x0F, Carry = (ACC + R + Carry) > 0x0F

2.10 SUB — Subtract Register from Accumulator with Borrow

Opcode Mnemonic Size Description
0x90 SUB R0 1 Subtract R0 from ACC with borrow
0x91 SUB R1 1 Subtract R1 from ACC with borrow
0x92 SUB R2 1 Subtract R2 from ACC with borrow
0x93 SUB R3 1 Subtract R3 from ACC with borrow
0x94 SUB R4 1 Subtract R4 from ACC with borrow
0x95 SUB R5 1 Subtract R5 from ACC with borrow
0x96 SUB R6 1 Subtract R6 from ACC with borrow
0x97 SUB R7 1 Subtract R7 from ACC with borrow
0x98 SUB R8 1 Subtract R8 from ACC with borrow
0x99 SUB R9 1 Subtract R9 from ACC with borrow
0x9A SUB R10 1 Subtract R10 from ACC with borrow
0x9B SUB R11 1 Subtract R11 from ACC with borrow
0x9C SUB R12 1 Subtract R12 from ACC with borrow
0x9D SUB R13 1 Subtract R13 from ACC with borrow
0x9E SUB R14 1 Subtract R14 from ACC with borrow
0x9F SUB R15 1 Subtract R15 from ACC with borrow

ACC = (ACC - R - (1 - Carry)) & 0x0F, Carry = (ACC - R - (1 - Carry)) >= 0
Note: Carry = 1 means no borrow, Carry = 0 means borrow occurred.

2.11 LD — Load Register into Accumulator

Opcode Mnemonic Size Description
0xA0 LD R0 1 Load R0 into ACC
0xA1 LD R1 1 Load R1 into ACC
0xA2 LD R2 1 Load R2 into ACC
0xA3 LD R3 1 Load R3 into ACC
0xA4 LD R4 1 Load R4 into ACC
0xA5 LD R5 1 Load R5 into ACC
0xA6 LD R6 1 Load R6 into ACC
0xA7 LD R7 1 Load R7 into ACC
0xA8 LD R8 1 Load R8 into ACC
0xA9 LD R9 1 Load R9 into ACC
0xAA LD R10 1 Load R10 into ACC
0xAB LD R11 1 Load R11 into ACC
0xAC LD R12 1 Load R12 into ACC
0xAD LD R13 1 Load R13 into ACC
0xAE LD R14 1 Load R14 into ACC
0xAF LD R15 1 Load R15 into ACC

ACC = R[opcode & 0x0F]

2.12 XCH — Exchange Accumulator with Register

Opcode Mnemonic Size Description
0xB0 XCH R0 1 Exchange ACC with R0
0xB1 XCH R1 1 Exchange ACC with R1
0xB2 XCH R2 1 Exchange ACC with R2
0xB3 XCH R3 1 Exchange ACC with R3
0xB4 XCH R4 1 Exchange ACC with R4
0xB5 XCH R5 1 Exchange ACC with R5
0xB6 XCH R6 1 Exchange ACC with R6
0xB7 XCH R7 1 Exchange ACC with R7
0xB8 XCH R8 1 Exchange ACC with R8
0xB9 XCH R9 1 Exchange ACC with R9
0xBA XCH R10 1 Exchange ACC with R10
0xBB XCH R11 1 Exchange ACC with R11
0xBC XCH R12 1 Exchange ACC with R12
0xBD XCH R13 1 Exchange ACC with R13
0xBE XCH R14 1 Exchange ACC with R14
0xBF XCH R15 1 Exchange ACC with R15

Swap ACC and R[opcode & 0x0F].

2.13 LDM — Load Immediate into Accumulator

Opcode Mnemonic Size Description
0xD0 LDM 0 1 Load 0 into ACC
0xD1 LDM 1 1 Load 1 into ACC
0xD2 LDM 2 1 Load 2 into ACC
0xD3 LDM 3 1 Load 3 into ACC
0xD4 LDM 4 1 Load 4 into ACC
0xD5 LDM 5 1 Load 5 into ACC
0xD6 LDM 6 1 Load 6 into ACC
0xD7 LDM 7 1 Load 7 into ACC
0xD8 LDM 8 1 Load 8 into ACC
0xD9 LDM 9 1 Load 9 into ACC
0xDA LDM A 1 Load A into ACC
0xDB LDM B 1 Load B into ACC
0xDC LDM C 1 Load C into ACC
0xDD LDM D 1 Load D into ACC
0xDE LDM E 1 Load E into ACC
0xDF LDM F 1 Load F into ACC

ACC = opcode & 0x0F

2.14 I/O and RAM Instructions (0xE0-0xEF)

Opcode Mnemonic Size Description
0xE0 WRM 1 Write ACC to RAM main memory character
0xE1 WMP 1 Write ACC to RAM output port
0xE2 WRR 1 Write ACC to ROM output port
0xE3 WPM 1 Write ACC to program memory (special, for 4008/4009)
0xE4 WR0 1 Write ACC to RAM status character 0
0xE5 WR1 1 Write ACC to RAM status character 1
0xE6 WR2 1 Write ACC to RAM status character 2
0xE7 WR3 1 Write ACC to RAM status character 3
0xE8 SBM 1 Subtract RAM main memory character from ACC with borrow
0xE9 RDM 1 Read RAM main memory character to ACC
0xEA RDR 1 Read ROM input port to ACC
0xEB ADM 1 Add RAM main memory character to ACC with carry
0xEC RD0 1 Read RAM status character 0
0xED RD1 1 Read RAM status character 1
0xEE RD2 1 Read RAM status character 2
0xEF RD3 1 Read RAM status character 3

2.15 Accumulator Group Instructions (0xF0-0xFC)

Opcode Mnemonic Size Description
0xF0 CLB 1 Clear both — ACC and Carry
0xF1 CLC 1 Clear carry
0xF2 IAC 1 Increment accumulator
0xF3 CMC 1 Complement carry
0xF4 CMA 1 Complement accumulator
0xF5 RAL 1 Rotate ACC left through carry
0xF6 RAR 1 Rotate ACC right through carry
0xF7 TCC 1 Transmit carry to ACC and clear ACC
0xF8 TCS 1 Transmit carry set — set ACC to 9 if carry, 10 if not
0xF9 STC 1 Set carry
0xFA DAA 1 Decimal adjust accumulator
0xFB KBP 1 Keyboard process
0xFC DCL 1 Designate command line

2.16 Invalid/Unused Opcodes

Opcode Range Status
0x01-0x0F LD R — duplicate of 0xA0-0xAF. Functionally identical.
0xC0-0xCF BBL data — valid one-byte instruction (return from subroutine + load ACC). See Section 10.
0xD0-0xDF LDM data — valid (see Section 2.13)
0xFD-0xFF Invalid / unused

Note on 0x01-0x0F: The Intel 4004 datasheet shows LD at both 0x01-0x0F and 0xA0-0xAF. Both should be decoded identically as LD R where R = opcode & 0x0F.


3. Correct Rust Instruction Enum

/// Intel 4004 instruction set enumeration
/// Organized by opcode range for clarity
#[derive(Debug, Clone, Copy, PartialEq)]
enum Instruction {
    // === No operation ===
    Nop,                           // 0x00

    // === Register load (duplicated at 0x01-0x0F and 0xA0-0xAF) ===
    Ld(u8),                        // LD R: Load register R into ACC (R = 0..15)

    // === Two-byte instructions ===
    Jcn(u8, u8),                   // 0x10-0x1F: JCN condition, addr_lo
                                   //   condition = byte1 & 0x0F
                                   //   addr_lo = byte2
                                   //   full_addr = (PC & 0xF00) | addr_lo

    Fim(u8, u8),                   // 0x20,0x22,...,0x2E: FIM Rpair, data
                                   //   Rpair = (byte1 >> 1) & 0x07 (register pair 0-7)
                                   //   data = byte2
                                   //   R[Rpair*2] = (data >> 4) & 0x0F
                                   //   R[Rpair*2+1] = data & 0x0F

    Src(u8),                       // 0x21,0x23,...,0x2F: SRC Rpair
                                   //   Rpair = (byte1 >> 1) & 0x07

    Fin(u8, u8),                   // 0x30,0x32,...,0x3E: FIN Rpair, data (data from ROM)
                                   //   Rpair = (byte1 >> 1) & 0x07
                                   //   data = byte2 (read from ROM at address in register pair)

    Jin(u8),                       // 0x31,0x33,...,0x3F: JIN Rpair
                                   //   Rpair = (byte1 >> 1) & 0x07

    Jun(u16),                      // 0x40-0x4F: JUN addr
                                   //   addr = ((byte1 & 0x0F) << 8) | byte2

    Jms(u16),                      // 0x50-0x5F: JMS addr
                                   //   addr = ((byte1 & 0x0F) << 8) | byte2

    Isz(u8, u8),                   // 0x70-0x7F: ISZ R, addr_lo
                                   //   R = byte1 & 0x0F
                                   //   addr_lo = byte2
                                   //   full_addr = (PC & 0xF00) | addr_lo

    // === One-byte register operations ===
    Inc(u8),                       // 0x60-0x6F: INC R (R = 0..15)
    Add(u8),                       // 0x80-0x8F: ADD R with carry (R = 0..15)
    Sub(u8),                       // 0x90-0x9F: SUB R with borrow (R = 0..15)
    Xch(u8),                       // 0xB0-0xBF: XCH R (R = 0..15)

    // === One-byte immediate ===
    Ldm(u8),                       // 0xD0-0xDF: LDM data (data = 0..15)

    // === I/O and RAM instructions ===
    Wrm,                           // 0xE0: Write ACC to RAM main memory
    Wmp,                           // 0xE1: Write ACC to RAM output port
    Wrr,                           // 0xE2: Write ACC to ROM output port
    Wpm,                           // 0xE3: Write ACC to program memory
    Wr0,                           // 0xE4: Write ACC to RAM status character 0
    Wr1,                           // 0xE5: Write ACC to RAM status character 1
    Wr2,                           // 0xE6: Write ACC to RAM status character 2
    Wr3,                           // 0xE7: Write ACC to RAM status character 3
    Sbm,                           // 0xE8: Subtract RAM main memory from ACC with borrow
    Rdm,                           // 0xE9: Read RAM main memory to ACC
    Rdr,                           // 0xEA: Read ROM input port to ACC
    Adm,                           // 0xEB: Add RAM main memory to ACC with carry
    Rd0,                           // 0xEC: Read RAM status character 0
    Rd1,                           // 0xED: Read RAM status character 1
    Rd2,                           // 0xEE: Read RAM status character 2
    Rd3,                           // 0xEF: Read RAM status character 3

    // === Accumulator group ===
    Clb,                           // 0xF0: Clear both (ACC and Carry)
    Clc,                           // 0xF1: Clear carry
    Iac,                           // 0xF2: Increment accumulator
    Cmc,                           // 0xF3: Complement carry
    Cma,                           // 0xF4: Complement accumulator
    Ral,                           // 0xF5: Rotate ACC left through carry
    Rar,                           // 0xF6: Rotate ACC right through carry
    Tcc,                           // 0xF7: Transmit carry and clear ACC
    Tcs,                           // 0xF8: Transmit carry set
    Stc,                           // 0xF9: Set carry
    Daa,                           // 0xFA: Decimal adjust accumulator
    Kbp,                           // 0xFB: Keyboard process
    Dcl,                           // 0xFC: Designate command line

    // === BBL (Branch Back and Load) ===
    Bbl(u8),                       // 0xC0-0xCF: BBL data (return from subroutine + load ACC)
                                   //   data = opcode & 0x0F
                                   //   One-byte instruction. See Section 10.

    // === Invalid ===
    Invalid,                       // Unused opcodes (0xFD-0xFF)
}

Note on BBL: BBL is confirmed at opcode range 0xC0-0xCF. It is a one-byte instruction where the 4-bit immediate value is encoded in the low nibble of the opcode itself (similar to LDM). The instruction pops the return address from the stack and loads the accumulator with the immediate value.


4. Correct decode_instruction() Pseudocode

function decode_instruction(opcode: u8) -> Instruction:
    match opcode:
        // NOP
        0x00 => Nop

        // LD R (duplicate range — same as 0xA0-0xAF)
        0x01..=0x0F => Ld(opcode & 0x0F)

        // JCN — two-byte, condition in low nibble
        0x10..=0x1F => Jcn(opcode & 0x0F, 0)  // addr filled by byte 2 fetch

        // FIM (even) / SRC (odd)
        0x20..=0x2F =>
            if opcode & 0x01 == 0:
                Fim((opcode >> 1) & 0x07, 0)  // data filled by byte 2 fetch
            else:
                Src((opcode >> 1) & 0x07)

        // FIN (even) / JIN (odd)
        0x30..=0x3F =>
            if opcode & 0x01 == 0:
                Fin((opcode >> 1) & 0x07)     // data from ROM filled by byte 2
            else:
                Jin((opcode >> 1) & 0x07)

        // JUN — two-byte
        0x40..=0x4F => Jun(0)  // addr filled by byte 2 fetch

        // JMS — two-byte
        0x50..=0x5F => Jms(0)  // addr filled by byte 2 fetch

        // INC R
        0x60..=0x6F => Inc(opcode & 0x0F)

        // ISZ — two-byte
        0x70..=0x7F => Isz(opcode & 0x0F, 0)  // addr filled by byte 2 fetch

        // ADD R (with carry)
        0x80..=0x8F => Add(opcode & 0x0F)

        // SUB R (with borrow)
        0x90..=0x9F => Sub(opcode & 0x0F)

        // LD R (primary range)
        0xA0..=0xAF => Ld(opcode & 0x0F)

        // XCH R
        0xB0..=0xBF => Xch(opcode & 0x0F)

        // BBL (if confirmed) or Invalid
        0xC0..=0xCF => Bbl(opcode & 0x0F)
                       // OR Invalid if 0xC0-0xCF is truly unused

        // Invalid
        0xC0..=0xCF => Invalid  // (use this if BBL is not at this range)

        // LDM data
        0xD0..=0xDF => Ldm(opcode & 0x0F)

        // I/O and RAM instructions
        0xE0 => Wrm
        0xE1 => Wmp
        0xE2 => Wrr
        0xE3 => Wpm
        0xE4 => Wr0
        0xE5 => Wr1
        0xE6 => Wr2
        0xE7 => Wr3
        0xE8 => Sbm
        0xE9 => Rdm
        0xEA => Rdr
        0xEB => Adm
        0xEC => Rd0
        0xED => Rd1
        0xEE => Rd2
        0xEF => Rd3

        // Accumulator group
        0xF0 => Clb
        0xF1 => Clc
        0xF2 => Iac
        0xF3 => Cmc
        0xF4 => Cma
        0xF5 => Ral
        0xF6 => Rar
        0xF7 => Tcc
        0xF8 => Tcs
        0xF9 => Stc
        0xFA => Daa
        0xFB => Kbp
        0xFC => Dcl

        // Invalid/unused
        0xFD..=0xFF => Invalid

        _ => Invalid

5. Correct execute_instruction() Pseudocode

function execute_instruction():
    match current_op:
        Nop:
            // No operation, just advance PC
            PC.inc()

        Ld(reg):
            ACC = index_registers[reg]
            PC.inc()

        Xch(reg):
            temp = ACC
            ACC = index_registers[reg]
            index_registers[reg] = temp
            PC.inc()

        Add(reg):
            result = ACC + index_registers[reg] + (if carry then 1 else 0)
            carry = result > 0x0F
            ACC = result & 0x0F
            PC.inc()

        Sub(reg):
            // Subtract with borrow: ACC = ACC - R - (1 - Carry)
            borrow = if not carry then 1 else 0
            result = ACC - index_registers[reg] - borrow
            carry = result >= 0  // carry=1 means no borrow
            ACC = result & 0x0F
            PC.inc()

        Inc(reg):
            index_registers[reg] = (index_registers[reg] + 1) & 0x0F
            PC.inc()

        Ldm(data):
            ACC = data & 0x0F
            PC.inc()

        // === Two-byte instructions (operand already fetched) ===

        Jcn(condition, addr_lo):
            full_addr = (PC.value() & 0xF00) | addr_lo
            should_jump = evaluate_jcn_condition(condition)
            if should_jump:
                PC.set(full_addr)
            else:
                PC.inc()  // skip past byte 2 (PC was already inc'd past byte 1)

        Fim(rpair, data):
            even_reg = rpair * 2
            odd_reg = rpair * 2 + 1
            index_registers[even_reg] = (data >> 4) & 0x0F
            index_registers[odd_reg] = data & 0x0F
            PC.inc()  // past byte 2

        Src(rpair):
            // Send register pair contents to address bus for I/O
            even_reg = rpair * 2
            odd_reg = rpair * 2 + 1
            // The 8-bit value from the register pair is sent to RAM/ROM
            // for addressing: high nibble from even reg, low nibble from odd reg
            // This selects a specific RAM character or ROM port
            rom_port = index_registers[even_reg]  // high nibble selects chip
            ram_addr_low = index_registers[odd_reg]  // low nibble selects word
            PC.inc()

        Fin(rpair):
            // Read ROM at address in register pair, store in register pair
            even_reg = rpair * 2
            odd_reg = rpair * 2 + 1
            rom_addr = (index_registers[even_reg] << 4) | index_registers[odd_reg]
            // Read 8 bits from ROM at rom_addr
            // (In practice, this reads from the current ROM page)
            // data = ROM[PC_page_base + rom_addr]  -- simplified
            // index_registers[even_reg] = (data >> 4) & 0x0F
            // index_registers[odd_reg] = data & 0x0F
            PC.inc()

        Jin(rpair):
            even_reg = rpair * 2
            odd_reg = rpair * 2 + 1
            addr = (index_registers[even_reg] << 4) | index_registers[odd_reg]
            // Note: JIN uses 8 bits from register pair as page offset
            // Full address = (PC & 0xF00) | addr  (stays on current page)
            // OR: Full address = (index_registers[even_reg] as high 6 bits,
            //                     index_registers[odd_reg] as low 6 bits)
            // See Section 8 for detailed discussion
            PC.set((PC.value() & 0xF00) | addr)
            // Note: JIN does NOT advance PC normally — it sets PC

        Jun(addr):
            PC.set(addr)
            // PC already past byte 2 from fetch cycle

        Jms(addr):
            // Push return address to stack
            if stack_pointer < 3:
                stack[stack_pointer] = PC  // PC points past the 2-byte instruction
                stack_pointer += 1
            PC.set(addr)

        Isz(reg, addr_lo):
            index_registers[reg] = (index_registers[reg] + 1) & 0x0F
            if index_registers[reg] != 0:
                full_addr = (PC.value() & 0xF00) | addr_lo
                PC.set(full_addr)
            else:
                PC.inc()  // past byte 2

        Bbl(data):
            // Return from subroutine and load ACC
            if stack_pointer > 0:
                stack_pointer -= 1
                PC = stack[stack_pointer]
            ACC = data & 0x0F

        // === I/O and RAM instructions ===

        Wrm:
            // Write ACC to RAM main memory at current SRC address
            // ram_write(current_ram_addr, ACC)
            PC.inc()

        Wmp:
            // Write ACC to RAM output port
            // ram_output(ACC)
            PC.inc()

        Wrr:
            // Write ACC to ROM output port
            // rom_output(ACC)
            PC.inc()

        Wpm:
            // Write ACC to program memory (special)
            // rom_write(ACC)
            PC.inc()

        Wr0:
            // Write ACC to RAM status character 0
            PC.inc()

        Wr1:
            // Write ACC to RAM status character 1
            PC.inc()

        Wr2:
            // Write ACC to RAM status character 2
            PC.inc()

        Wr3:
            // Write ACC to RAM status character 3
            PC.inc()

        Sbm:
            // Subtract RAM main memory from ACC with borrow
            // ram_data = ram_read(current_ram_addr)
            // borrow = if not carry then 1 else 0
            // result = ACC - ram_data - borrow
            // carry = result >= 0
            // ACC = result & 0x0F
            PC.inc()

        Rdm:
            // Read RAM main memory to ACC
            // ACC = ram_read(current_ram_addr)
            PC.inc()

        Rdr:
            // Read ROM input port to ACC
            // ACC = rom_read_input()
            PC.inc()

        Adm:
            // Add RAM main memory to ACC with carry
            // ram_data = ram_read(current_ram_addr)
            // result = ACC + ram_data + (if carry then 1 else 0)
            // carry = result > 0x0F
            // ACC = result & 0x0F
            PC.inc()

        Rd0:
            // Read RAM status character 0 to ACC
            // ACC = ram_read_status(0)
            PC.inc()

        Rd1:
            // Read RAM status character 1 to ACC
            PC.inc()

        Rd2:
            // Read RAM status character 2 to ACC
            PC.inc()

        Rd3:
            // Read RAM status character 3 to ACC
            PC.inc()

        // === Accumulator group ===

        Clb:
            ACC = 0
            carry = false
            PC.inc()

        Clc:
            carry = false
            PC.inc()

        Iac:
            result = ACC + 1
            carry = result > 0x0F
            ACC = result & 0x0F
            PC.inc()

        Cmc:
            carry = not carry
            PC.inc()

        Cma:
            ACC = (~ACC) & 0x0F
            PC.inc()

        Ral:
            new_carry = (ACC & 0x08) != 0
            ACC = ((ACC << 1) | (if carry then 1 else 0)) & 0x0F
            carry = new_carry
            PC.inc()

        Rar:
            new_carry = (ACC & 0x01) != 0
            ACC = (ACC >> 1) | (if carry then 0x08 else 0)
            carry = new_carry
            PC.inc()

        Tcc:
            // Transmit carry to ACC and clear
            ACC = if carry then 1 else 0
            carry = false
            PC.inc()

        Tcs:
            // Set ACC to 9 if carry was set, 10 if not
            // Used for BCD subtraction correction
            ACC = if carry then 9 else 10
            carry = false
            PC.inc()

        Stc:
            carry = true
            PC.inc()

        Daa:
            // Decimal adjust accumulator
            if ACC > 9 or carry:
                ACC = ACC + 6
                if ACC > 0x0F:
                    carry = true
                    ACC = ACC & 0x0F
            PC.inc()

        Kbp:
            // Keyboard process — converts 1-of-N coded key to binary
            // If ACC = 0: ACC = 0
            // If ACC = 1: ACC = 0
            // If ACC = 2: ACC = 1
            // If ACC = 4: ACC = 2
            // If ACC = 8: ACC = 3
            // Otherwise: ACC = ACC (error)
            match ACC:
                0 => ACC = 0
                1 => ACC = 0
                2 => ACC = 1
                4 => ACC = 2
                8 => ACC = 3
                _ => {}  // error, leave as is
            PC.inc()

        Dcl:
            // Designate command line — set RAM bank from ACC
            ram_bank = ACC & 0x07  // 3 bits for bank selection
            PC.inc()

        Invalid:
            // Treat as NOP — advance PC
            PC.inc()

6. One-Byte vs Two-Byte Instruction Classification

One-Byte Instructions (8 clock cycles)

Opcode Range Instruction
0x00 NOP
0x01-0x0F LD R (duplicate)
0x21, 0x23, 0x25, 0x27, 0x29, 0x2B, 0x2D, 0x2F SRC Rpair
0x31, 0x33, 0x35, 0x37, 0x39, 0x3B, 0x3D, 0x3F JIN Rpair
0x60-0x6F INC R
0x80-0x8F ADD R
0x90-0x9F SUB R
0xA0-0xAF LD R
0xB0-0xBF XCH R
0xD0-0xDF LDM data
0xC0-0xCF BBL data
0xE0-0xEF I/O and RAM instructions
0xF0-0xFC Accumulator group

Two-Byte Instructions (16 clock cycles)

Opcode Range Instruction Byte 2 Usage
0x10-0x1F JCN condition, addr 8-bit page address
0x20, 0x22, 0x24, 0x26, 0x28, 0x2A, 0x2C, 0x2E FIM Rpair, data 8-bit immediate data
0x30, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x3C, 0x3E FIN Rpair (reads from ROM)
0x40-0x4F JUN addr Low 8 bits of 12-bit address
0x50-0x5F JMS addr Low 8 bits of 12-bit address
0x70-0x7F ISZ R, addr 8-bit page address

Quick Detection Function

function is_two_byte_instruction(opcode: u8) -> bool:
    // JCN: 0x10-0x1F
    if opcode >= 0x10 && opcode <= 0x1F: return true

    // FIM (even opcodes in 0x20-0x2F)
    if opcode >= 0x20 && opcode <= 0x2E && (opcode & 0x01) == 0: return true

    // FIN (even opcodes in 0x30-0x3F)
    if opcode >= 0x30 && opcode <= 0x3E && (opcode & 0x01) == 0: return true

    // JUN: 0x40-0x4F
    if opcode >= 0x40 && opcode <= 0x4F: return true

    // JMS: 0x50-0x5F
    if opcode >= 0x50 && opcode <= 0x5F: return true

    // ISZ: 0x70-0x7F
    if opcode >= 0x70 && opcode <= 0x7F: return true

    return false

7. Two-Byte Instruction Fetch in update() Cycle

Current Problem

The current implementation uses a flawed "high nibble / low nibble" approach for JUN and JMS, treating them as two separate 1-byte instructions (JunHigh/JunLow, JmsHigh/JmsLow). This is incorrect — JUN and JMS are single 2-byte instructions where:

  • Byte 1 contains the opcode AND the high 4 bits of the address
  • Byte 2 contains the low 8 bits of the address

Correct Fetch State Machine

The instruction fetch should use a state machine with an additional state for "waiting for byte 2":

stateDiagram-v2
    [*] --> FetchByte1
    FetchByte1 --> Decode: byte1 ready
    Decode --> Execute: one-byte instruction
    Decode --> FetchByte2: two-byte instruction
    FetchByte2 --> Execute: byte2 ready
    Execute --> FetchByte1: execution complete

Pseudocode for update() Fetch Logic

enum InstructionPhase:
    FetchByte1,     // Fetching first byte of instruction
    FetchByte2,     // Fetching second byte of 2-byte instruction
    Execute,        // Executing decoded instruction

struct Intel4004:
    // ... existing fields ...
    instruction_phase: InstructionPhase
    byte1: u8,              // First byte of instruction
    byte2: u8,              // Second byte (for 2-byte instructions)
    current_op: Instruction // Decoded instruction

function update():
    // ... clock edge detection, memory state machine ...

    match instruction_phase:
        FetchByte1:
            if memory_state == DriveData:
                byte1 = read_data_bus()
                PC.inc()

                if is_two_byte_instruction(byte1):
                    // Decode partially, then fetch byte 2
                    current_op = decode_instruction(byte1)
                    instruction_phase = FetchByte2
                else:
                    // One-byte instruction: decode and execute
                    current_op = decode_instruction(byte1)
                    instruction_phase = Execute

        FetchByte2:
            if memory_state == DriveData:
                byte2 = read_data_bus()
                PC.inc()

                // Complete the instruction with byte2 data
                current_op = complete_two_byte(current_op, byte2)
                instruction_phase = Execute

        Execute:
            execute_instruction()
            instruction_phase = FetchByte1

function complete_two_byte(partial_op: Instruction, byte2: u8) -> Instruction:
    match partial_op:
        Jcn(condition, _):
            return Jcn(condition, byte2)

        Fim(rpair, _):
            return Fim(rpair, byte2)

        Fin(rpair):
            // FIN reads from ROM at address in register pair
            // byte2 is already read from ROM by the fetch hardware
            // Store byte2 into register pair
            return FinWithData(rpair, byte2)

        Jun(_):
            addr = ((byte1 & 0x0F) << 8) | byte2
            return Jun(addr)

        Jms(_):
            addr = ((byte1 & 0x0F) << 8) | byte2
            return Jms(addr)

        Isz(reg, _):
            return Isz(reg, byte2)

        _:
            return partial_op  // Should not happen

Key Points for Implementation

  1. PC is incremented after each byte fetch — once after byte1, once after byte2
  2. JUN/JMS address encoding: Byte 1 low nibble = address bits 11-8, Byte 2 = address bits 7-0
  3. JCN/ISZ address encoding: Byte 2 = address bits 7-0 within current page. Full address = (PC_before_fetch & 0xF00) | byte2
  4. FIM data encoding: Byte 2 high nibble → even register, low nibble → odd register
  5. FIN: The second byte is read from ROM at the address formed by the register pair contents. The CPU fetches it from the data bus like any other byte, but the ROM address comes from the register pair, not from PC.

Important: Save PC Page Before Fetch

For JCN and ISZ, the page address must be captured before the PC is incremented past byte 1:

// In FetchByte1, before incrementing PC:
saved_page = PC.value() & 0xF00

// Later, when computing full address:
full_addr = saved_page | byte2

8. JCN Condition Decoding Logic

The JCN instruction has a 4-bit condition field. The conditions use a combination of three tests:

  • Bit 0 (0x01): TEST input pin
  • Bit 1 (0x02): Carry flag
  • Bit 2 (0x04): Accumulator zero
  • Bit 3 (0x08): Invert flag

Decoding Algorithm

function evaluate_jcn_condition(condition: u8) -> bool:
    test_pin = read_test_pin()        // Current TEST pin state
    carry_flag = self.carry           // Current carry flag
    acc_zero = (self.accumulator == 0) // ACC zero check

    // Compute the base condition (before invert)
    // Conditions are OR-ed together based on which bits are set
    base = false
    if (condition & 0x01) != 0: base = base || test_pin
    if (condition & 0x02) != 0: base = base || carry_flag
    if (condition & 0x04) != 0: base = base || acc_zero

    // Apply invert (bit 3)
    if (condition & 0x08) != 0:
        return !base
    else:
        return base

Condition Code Table

Condition Binary Meaning
0x0 0000 Always false (never jump)
0x1 0001 TEST = 1
0x2 0010 Carry = 1
0x3 0011 Carry=1 OR TEST=1
0x4 0100 ACC = 0
0x5 0101 ACC=0 OR TEST=1
0x6 0110 ACC=0 OR Carry=1
0x7 0111 ACC=0 OR Carry=1 OR TEST=1
0x8 1000 Always true (NOT false)
0x9 1001 TEST = 0 (NOT TEST=1)
0xA 1010 Carry = 0 (NOT Carry=1)
0xB 1011 Carry=0 AND TEST=0 (NOT of Carry=1 OR TEST=1)
0xC 1100 ACC ≠ 0 (NOT ACC=0)
0xD 1101 ACC≠0 AND TEST=0
0xE 1110 Carry=0 AND ACC≠0
0xF 1111 Carry=0 AND ACC≠0 AND TEST=0

Key Insight: De Morgan's Law

The invert bit applies De Morgan's law. When bit 3 is set:

  • NOT (A OR B OR C) becomes NOT A AND NOT B AND NOT C
  • So condition 0xB (1011) = NOT (Carry=1 OR TEST=1) = Carry=0 AND TEST=0

9. SRC Register Pair Addressing

Register Pair Concept

The 16 index registers (R0-R15) are organized as 8 register pairs:

  • Pair 0: R0 (high nibble) + R1 (low nibble)
  • Pair 1: R2 (high nibble) + R3 (low nibble)
  • Pair 2: R4 (high nibble) + R5 (low nibble)
  • Pair 3: R6 (high nibble) + R7 (low nibble)
  • Pair 4: R8 (high nibble) + R9 (low nibble)
  • Pair 5: R10 (high nibble) + R11 (low nibble)
  • Pair 6: R12 (high nibble) + R13 (low nibble)
  • Pair 7: R14 (high nibble) + R15 (low nibble)

Extracting Register Pair Number from Opcode

For FIM/SRC/FIN/JIN, the register pair number is encoded in bits 4-1 of the opcode:

pair_number = (opcode >> 1) & 0x07
even_register = pair_number * 2
odd_register = pair_number * 2 + 1

SRC Operation

The SRC instruction sends the 8-bit value from a register pair to the address bus:

  • Even register → high nibble (chip select / ROM port)
  • Odd register → low nibble (word select within chip)

This 8-bit value is used by subsequent I/O instructions (WRM, WMP, WRR, RDM, RDR, etc.) to select the target device and address.

JIN Operation

JIN uses the register pair to form a jump target address:

  • The 8-bit value from the pair is used as a page offset
  • Full address = (current_PC & 0xF00) | (R[even] << 4) | R[odd]

Note

: Some sources describe JIN as using all 12 bits from a register pair. However, since each register is only 4 bits wide, a pair provides only 8 bits. The upper 4 bits of the address come from the current PC page.

FIM Operation

FIM loads an 8-bit immediate value into a register pair:

data = byte2
R[even] = (data >> 4) & 0x0F   // High nibble → even register
R[odd] = data & 0x0F            // Low nibble → odd register

FIN Operation

FIN reads 8 bits from ROM and stores in a register pair:

// The ROM address is formed from the register pair contents
rom_addr = (R[even] << 4) | R[odd]
// Read 2 nibbles from ROM at current page + rom_addr offset
data = ROM[(PC & 0xF00) | rom_addr]  // Simplified
R[even] = (data >> 4) & 0x0F
R[odd] = data & 0x0F

10. BBL Instruction Clarification

The BBL (Branch Back and Load) instruction is a one-byte instruction at opcode range 0xC0-0xCF. The 4-bit immediate value is encoded in the low nibble of the opcode (similar to LDM). It:

  1. Pops the return address from the stack
  2. Loads the accumulator with the 4-bit immediate value from the low nibble of the opcode
Bbl(data):
    if stack_pointer > 0:
        stack_pointer -= 1
        PC = stack[stack_pointer]
    ACC = data & 0x0F
    PC.inc()  // advance past the return address

Confirmed: BBL is at 0xC0-0xCF and is a one-byte instruction. The immediate is in the opcode's low nibble, not a second byte.


11. Instruction Execution Semantics Notes

ADD vs. Add-with-Carry

The Intel 4004 ADD instruction (0x80-0x8F) always adds with carry. There is no separate "add without carry" instruction. The carry flag is both an input and output.

SUB vs. Subtract-with-Borrow

Similarly, SUB (0x90-0x9F) always subtracts with borrow. The carry flag acts as the inverse of borrow:

  • Carry = 1 → no borrow occurred
  • Carry = 0 → borrow occurred

To subtract without borrow, set carry to 1 first (using STC) before executing SUB.

TCS Semantics

TCS (Transmit Carry Set) is used for BCD correction after subtraction:

  • If carry = 1 (no borrow): ACC = 9
  • If carry = 0 (borrow occurred): ACC = 10

This value is then added to correct the BCD result.

KBP (Keyboard Process)

Converts a 1-of-N encoded input to binary:

ACC Input ACC Output
0000 (0) 0000 (0)
0001 (1) 0000 (0)
0010 (2) 0001 (1)
0100 (4) 0010 (2)
1000 (8) 0011 (3)
Other Unchanged (error)

12. Implementation Checklist

When rewriting intel_4004.rs, the following changes are needed:

  1. Replace the Instruction enum with the correct one from Section 3
  2. Rewrite decode_instruction() using the correct opcode mapping from Section 4
  3. Rewrite execute_instruction() with correct semantics from Section 5
  4. Add is_two_byte_instruction() helper as described in Section 6
  5. Refactor update() fetch logic to use FetchByte1/FetchByte2/Execute states from Section 7
  6. Remove JunHigh/JunLow/JmsHigh/JmsLow — these are artifacts of the wrong design
  7. Remove pending_operand field — replaced by the FetchByte2 state
  8. Implement correct JCN condition decoding from Section 8
  9. Implement correct SRC register pair addressing from Section 9
  10. Add missing instructions: FIM, FIN, JIN, SRC, BBL, KBP, DCL, RD0-RD3, WR0-WR3
  11. Remove non-existent instructions: AddC, SubC, Dad, Jnt, JntInvert
  12. Fix ADD/SUB semantics — they always use carry/borrow
  13. Fix TCC semantics — should set ACC = carry value, not just clear
  14. Fix TCS semantics — should set ACC = 9 if carry, 10 if not
  15. Update all tests to use correct opcodes
  16. Mark 0xFD-0xFF as invalid (not 0xFA-0xFF — DAA/KBP/DCL are at 0xFA-0xFC)