- Remap opcodes 0x01-0x0F from duplicate LD to Invalid (undefined on real hardware) - Fix JCN condition bit mapping per datasheet: C1=Carry, C2=ACC zero, C3=TEST pin - Fix DAA instruction to clear carry when no 4-bit overflow occurs - Rewrite fibonacci demo program with correct opcodes (FIM, CLB, LD, ADD, WRM, XCH, ISZ) - Regenerate fibonacci.bin and fibonacci_output.bin with correct opcodes - Update all tests to match corrected JCN condition codes - Update plan document and README to reflect changes
46 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 | NOP + undefined | NOP (0x00) + undefined opcodes (0x01-0x0F) |
| 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 | Invalid — undefined on real Intel 4004 hardware. Mapped to Instruction::Invalid. |
| 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: These opcodes are undefined on real Intel 4004 hardware. The only valid LD instruction range is
0xA0-0xAF. Programs that previously used0x01-0x0Fas LD should be updated to use0xA0+Rinstead.
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 ===
Ld(u8), // LD R: Load register R into ACC (R = 0..15), opcode 0xA0-0xAF
// === 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
// 0x01-0x0F: Undefined on real Intel 4004 hardware
0x01..=0x0F => Invalid
// 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
// Per Intel 4004 datasheet: carry is set if adjustment causes 4-bit overflow,
// otherwise carry is cleared.
if ACC > 9 or carry:
ACC = ACC + 6
if ACC > 0x0F:
carry = true
ACC = ACC & 0x0F
else:
carry = false
else:
carry = false
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 | Invalid (undefined on real hardware) |
| 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
- PC is incremented after each byte fetch — once after byte1, once after byte2
- JUN/JMS address encoding: Byte 1 low nibble = address bits 11-8, Byte 2 = address bits 7-0
- JCN/ISZ address encoding: Byte 2 = address bits 7-0 within current page. Full address =
(PC_before_fetch & 0xF00) | byte2 - FIM data encoding: Byte 2 high nibble → even register, low nibble → odd register
- 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 (C1-C4 in the OPA nibble). Per the Intel 4004 datasheet, the conditions use a combination of three tests:
- C1 / Bit 0 (0x01): Carry flag
- C2 / Bit 1 (0x02): Accumulator zero
- C3 / Bit 2 (0x04): TEST input pin
- C4 / 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 || carry_flag // C1
if (condition & 0x02) != 0: base = base || acc_zero // C2
if (condition & 0x04) != 0: base = base || test_pin // C3
// Apply invert (C4)
if (condition & 0x08) != 0:
return !base
else:
return base
Condition Code Table
| Condition | Binary | Meaning |
|---|---|---|
| 0x0 | 0000 | Always false (never jump) |
| 0x1 | 0001 | Carry = 1 |
| 0x2 | 0010 | ACC = 0 |
| 0x3 | 0011 | Carry=1 OR ACC=0 |
| 0x4 | 0100 | TEST = 1 |
| 0x5 | 0101 | Carry=1 OR TEST=1 |
| 0x6 | 0110 | ACC=0 OR TEST=1 |
| 0x7 | 0111 | Carry=1 OR ACC=0 OR TEST=1 |
| 0x8 | 1000 | Always true (NOT false) |
| 0x9 | 1001 | Carry = 0 (NOT Carry=1) |
| 0xA | 1010 | ACC ≠ 0 (NOT ACC=0) |
| 0xB | 1011 | Carry=0 AND ACC≠0 (NOT of Carry=1 OR ACC=0) |
| 0xC | 1100 | TEST = 0 (NOT TEST=1) |
| 0xD | 1101 | Carry=0 AND TEST=0 |
| 0xE | 1110 | ACC≠0 AND TEST=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)becomesNOT A AND NOT B AND NOT C- So condition 0xB (1011) = NOT (Carry=1 OR ACC=0) = Carry=0 AND ACC≠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:
- Pops the return address from the stack
- 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:
- Replace the
Instructionenum with the correct one from Section 3 - Rewrite
decode_instruction()using the correct opcode mapping from Section 4 - Rewrite
execute_instruction()with correct semantics from Section 5 - Add
is_two_byte_instruction()helper as described in Section 6 - Refactor
update()fetch logic to use FetchByte1/FetchByte2/Execute states from Section 7 - Remove
JunHigh/JunLow/JmsHigh/JmsLow— these are artifacts of the wrong design - Remove
pending_operandfield — replaced by the FetchByte2 state - Implement correct JCN condition decoding from Section 8
- Implement correct SRC register pair addressing from Section 9
- Add missing instructions: FIM, FIN, JIN, SRC, BBL, KBP, DCL, RD0-RD3, WR0-WR3
- Remove non-existent instructions: AddC, SubC, Dad, Jnt, JntInvert
- Fix ADD/SUB semantics — they always use carry/borrow
- Fix TCC semantics — should set ACC = carry value, not just clear
- Fix TCS semantics — should set ACC = 9 if carry, 10 if not
- Update all tests to use correct opcodes
- Mark 0xFD-0xFF as invalid (not 0xFA-0xFF — DAA/KBP/DCL are at 0xFA-0xFC)