Fix Intel 4004 CPU emulation bugs: opcode mapping, JCN conditions, DAA carry

- 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
This commit is contained in:
Jan-Peter von Hunnius 2026-05-12 20:55:26 +02:00
parent 6a335a15d4
commit add7ace674
7 changed files with 132 additions and 100 deletions

View file

@ -8,7 +8,7 @@ The current implementation in [`intel_4004.rs`](src/components/cpu/intel_4004.rs
| Opcode Range | Current Mapping | Correct Mapping |
|---|---|---|
| 0x00-0x0F | LD/XCH by register | NOP + LD R (duplicate of 0xA0-0xAF) |
| 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 |
@ -366,12 +366,12 @@ Swap `ACC` and `R[opcode & 0x0F]`.
| Opcode Range | Status |
|--------------|--------|
| 0x01-0x0F | **LD R** — duplicate of 0xA0-0xAF. Functionally identical. |
| 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**: 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`.
> **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 used `0x01-0x0F` as LD should be updated to use `0xA0+R` instead.
---
@ -385,8 +385,8 @@ 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)
// === 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
@ -485,8 +485,8 @@ function decode_instruction(opcode: u8) -> Instruction:
// NOP
0x00 => Nop
// LD R (duplicate range — same as 0xA0-0xAF)
0x01..=0x0F => Ld(opcode & 0x0F)
// 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
@ -837,11 +837,17 @@ function execute_instruction():
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:
@ -880,7 +886,7 @@ function execute_instruction():
| Opcode Range | Instruction |
|---|---|
| 0x00 | NOP |
| 0x01-0x0F | LD R (duplicate) |
| 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 |
@ -1052,12 +1058,13 @@ 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:
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:
- **Bit 0 (0x01)**: TEST input pin
- **Bit 1 (0x02)**: Carry flag
- **Bit 2 (0x04)**: Accumulator zero
- **Bit 3 (0x08)**: Invert flag
- **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
@ -1070,11 +1077,11 @@ function evaluate_jcn_condition(condition: u8) -> bool:
// 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
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 (bit 3)
// Apply invert (C4)
if (condition & 0x08) != 0:
return !base
else:
@ -1086,27 +1093,27 @@ function evaluate_jcn_condition(condition: u8) -> bool:
| 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 |
| 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 | 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 |
| 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)` becomes `NOT A AND NOT B AND NOT C`
- So condition 0xB (1011) = NOT (Carry=1 OR TEST=1) = Carry=0 AND TEST=0
- So condition 0xB (1011) = NOT (Carry=1 OR ACC=0) = Carry=0 AND ACC≠0
---

View file

@ -5,18 +5,18 @@ This directory contains binary program files for the Intel MCS-4 emulator.
## Available Programs
### fibonacci.bin
- **Size:** 33 bytes (16 instructions × 2 bytes each + 1 extra byte)
- **Description:** Basic Fibonacci sequence generator (stores results in RAM)
- **Algorithm:** Calculates first 8 Fibonacci numbers using registers and RAM
- **Instructions Used:** LDM, LD, WRM, IAC, ADD, JCN, NOP
- **Output:** Results stored in RAM locations 0-7 (not visible to user)
- **Size:** 17 bytes
- **Description:** Fibonacci sequence generator (stores results in RAM)
- **Algorithm:** Calculates first 8 Fibonacci numbers using registers R0-R3 and RAM
- **Instructions Used:** FIM, CLB, LD, ADD, WRM, XCH, ISZ, NOP
- **Output:** Results stored in RAM via WRM
### fibonacci_output.bin ✨ **RECOMMENDED**
- **Size:** 55 bytes (27 instructions × 2 bytes each + 1 extra byte)
- **Size:** 23 bytes
- **Description:** Enhanced Fibonacci sequence generator with output port visibility
- **Algorithm:** Calculates first 8 Fibonacci numbers and outputs them to output ports
- **Instructions Used:** LDM, LD, WRM, IAC, ADD, JCN, NOP, SRC
- **Output:** Each Fibonacci number visible on output ports 0-7 during execution
- **Algorithm:** Calculates first 8 Fibonacci numbers and outputs them to RAM + output ports
- **Instructions Used:** FIM, CLB, LD, ADD, WRM, WMP, XCH, SRC, ISZ, NOP
- **Output:** Each Fibonacci number visible on output ports during execution
- **Features:** Real-time visibility of calculation progress
### io_demo.bin ✨ **NEW**

Binary file not shown.

Binary file not shown.

View file

@ -716,8 +716,9 @@ impl Intel4004 {
// NOP
0x00 => Instruction::Nop,
// LD R (duplicate range — same as 0xA0-0xAF)
0x01..=0x0F => Instruction::Ld(opcode & 0x0F),
// 0x01-0x0F: Undefined on real Intel 4004 hardware
// Mapped to Invalid to catch accidental use of undefined opcodes
0x01..=0x0F => Instruction::Invalid,
// JCN — two-byte, condition in low nibble
0x10..=0x1F => Instruction::Jcn(opcode & 0x0F, 0),
@ -852,11 +853,14 @@ impl Intel4004 {
/// Parameters: condition - 4-bit condition code from JCN instruction
/// Returns: true if the condition is met (should jump), false otherwise
///
/// Condition encoding:
/// - Bit 0 (0x01): TEST input pin
/// - Bit 1 (0x02): Carry flag
/// - Bit 2 (0x04): Accumulator zero
/// - Bit 3 (0x08): Invert flag
/// Condition encoding per Intel 4004 datasheet (OPA bits C1-C4):
/// - C1 / Bit 0 (0x01): Carry flag (inverted: NOT carry)
/// - C2 / Bit 1 (0x02): Accumulator zero (inverted: ACC ≠ 0)
/// - C3 / Bit 2 (0x04): TEST input pin (inverted: TEST = 0)
/// - C4 / Bit 3 (0x08): Invert flag
///
/// When multiple condition bits are set, they are OR-ed together.
/// Bit 3 inverts the entire result.
fn evaluate_jcn_condition(&self, condition: u8) -> bool {
let test_pin = self.read_test_pin();
let acc_zero = self.accumulator == 0;
@ -864,14 +868,14 @@ impl Intel4004 {
// Compute the base condition (before invert) by OR-ing active conditions
let mut base = false;
if (condition & 0x01) != 0 {
base = base || test_pin;
}
if (condition & 0x02) != 0 {
base = base || self.carry;
}
if (condition & 0x04) != 0 {
if (condition & 0x02) != 0 {
base = base || acc_zero;
}
if (condition & 0x04) != 0 {
base = base || test_pin;
}
// Apply invert (bit 3)
if (condition & 0x08) != 0 {
@ -1281,12 +1285,19 @@ impl Intel4004 {
Instruction::Daa => {
// Decimal adjust accumulator
// Per Intel 4004 datasheet: if ACC > 9 or carry is set, add 6.
// Carry is set if the adjustment causes a 4-bit overflow,
// otherwise carry is cleared.
if self.accumulator > 9 || self.carry {
self.accumulator += 6;
if self.accumulator > 0x0F {
self.carry = true;
self.accumulator &= 0x0F;
} else {
self.carry = false;
}
} else {
self.carry = false;
}
self.program_counter.inc();
}
@ -1432,10 +1443,10 @@ mod tests {
#[test]
fn test_decode_ld() {
let cpu = Intel4004::new("TEST_CPU".to_string(), 750000.0);
// LD at 0x01-0x0F (duplicate range)
assert_eq!(cpu.decode_instruction(0x01), Instruction::Ld(1));
assert_eq!(cpu.decode_instruction(0x0F), Instruction::Ld(15));
// LD at 0xA0-0xAF (primary range)
// 0x01-0x0F are undefined opcodes (mapped to Invalid)
assert_eq!(cpu.decode_instruction(0x01), Instruction::Invalid);
assert_eq!(cpu.decode_instruction(0x0F), Instruction::Invalid);
// LD at 0xA0-0xAF (primary and only valid range)
assert_eq!(cpu.decode_instruction(0xA0), Instruction::Ld(0));
assert_eq!(cpu.decode_instruction(0xAF), Instruction::Ld(15));
}
@ -1809,29 +1820,29 @@ mod tests {
// Condition 0x8: NOT always false = always true
assert!(cpu.evaluate_jcn_condition(0x8));
// Condition 0x4: ACC = 0 (ACC is 0 after reset)
assert!(cpu.evaluate_jcn_condition(0x4));
// Condition 0x2: ACC = 0 (ACC is 0 after reset)
assert!(cpu.evaluate_jcn_condition(0x2));
// Condition 0xC: NOT ACC = 0 → ACC != 0
assert!(!cpu.evaluate_jcn_condition(0xC));
// Condition 0xA: NOT ACC = 0 → ACC != 0
assert!(!cpu.evaluate_jcn_condition(0xA));
// Set ACC to non-zero
cpu.set_accumulator(5);
// Condition 0x4: ACC = 0 → false (ACC is 5)
assert!(!cpu.evaluate_jcn_condition(0x4));
// Condition 0x2: ACC = 0 → false (ACC is 5)
assert!(!cpu.evaluate_jcn_condition(0x2));
// Condition 0xC: NOT ACC = 0 → ACC != 0 → true
assert!(cpu.evaluate_jcn_condition(0xC));
// Condition 0xA: NOT ACC = 0 → ACC != 0 → true
assert!(cpu.evaluate_jcn_condition(0xA));
// Test carry conditions
// Test carry conditions (bit 0 = carry)
cpu.carry = true;
assert!(cpu.evaluate_jcn_condition(0x2)); // Carry = 1
assert!(!cpu.evaluate_jcn_condition(0xA)); // NOT Carry = 1 → Carry = 0
assert!(cpu.evaluate_jcn_condition(0x1)); // Carry = 1
assert!(!cpu.evaluate_jcn_condition(0x9)); // NOT Carry = 1 → Carry = 0
cpu.carry = false;
assert!(!cpu.evaluate_jcn_condition(0x2)); // Carry = 1 → false
assert!(cpu.evaluate_jcn_condition(0xA)); // NOT Carry = 1 → true
assert!(!cpu.evaluate_jcn_condition(0x1)); // Carry = 1 → false
assert!(cpu.evaluate_jcn_condition(0x9)); // NOT Carry = 1 → true
}
#[test]
@ -2181,13 +2192,13 @@ mod tests {
assert_eq!(cpu.get_accumulator(), 2); // 12 + 6 = 18 → 18 & 0xF = 2
assert_eq!(cpu.get_carry(), true);
// DAA: adjustment needed (carry set)
// DAA: adjustment needed (carry set, no 4-bit overflow)
cpu.set_accumulator(5);
cpu.carry = true;
cpu.current_op = Instruction::Daa;
cpu.execute_instruction();
assert_eq!(cpu.get_accumulator(), 0xB); // 5 + 6 = 11
assert_eq!(cpu.get_carry(), true); // carry was set, stays set
assert_eq!(cpu.get_carry(), false); // adjustment made, no 4-bit overflow → carry cleared
}
}

View file

@ -381,32 +381,47 @@ fn load_program_data(filename: &str) -> Result<Vec<u8>, String> {
fn get_default_fibonacci_program() -> Vec<u8> {
// Fibonacci program for Intel 4004
// This implements a simple Fibonacci sequence generator
// Computes Fibonacci numbers using registers R0-R3 and writes to RAM.
//
// Register usage:
// R0 = F(n-2) (two steps back)
// R1 = F(n-1) (one step back)
// R2 = loop counter (counts up, exit when ISZ wraps to 0)
// R3 = temp for register rotation
//
// Opcode reference (per Intel 4004 datasheet):
// FIM Rp,data = 0x20+2*Rp, data (load register pair immediate, 2-byte)
// LD R = 0xA0+R (load register R into ACC)
// XCH R = 0xB0+R (exchange ACC ↔ register R)
// ADD R = 0x80+R (ACC = ACC + R + Carry)
// CLB = 0xF0 (clear both ACC and Carry)
// WRM = 0xE0 (write ACC to RAM at current pointer)
// ISZ R,addr = 0x70+R, addr (increment R; jump to addr if R ≠ 0, 2-byte)
vec![
0x20, 0x00, // LDM 0 (Load accumulator with 0) - First Fibonacci number
0x10, 0x00, // LD 0 (Load accumulator from register 0)
0x50, // WRM (Write accumulator to RAM at current pointer)
0x21, 0x01, // LDM 1 (Load accumulator with 1) - Second Fibonacci number
0x10, 0x01, // LD 1 (Load accumulator from register 1)
0x50, // WRM (Write accumulator to RAM)
// Fibonacci calculation loop
0x00, 0x02, // LD 2 (Load register 2 into accumulator) - Loop counter
0x76, // IAC (Increment accumulator)
0x00, 0x02, // LD 2 (Store back to register 2)
// Calculate next Fibonacci number: F(n) = F(n-1) + F(n-2)
0x00, 0x00, // LD 0 (Load F(n-2) into accumulator)
0x10, 0x01, // ADD 1 (Add F(n-1) to accumulator)
0x50, // WRM (Store result to RAM)
// Update registers for next iteration
0x00, 0x01, // LD 1 (Load F(n-1) into accumulator)
0x00, 0x00, // LD 0 (Store F(n-1) to register 0)
0x50, // WRM (Write to RAM - this will be read back as F(n-2))
// Check if we've calculated enough numbers (8 iterations)
0x00, 0x02, // LD 2 (Load loop counter)
0x30, 0x08, // JCN 8 (Jump if accumulator == 8) - Exit condition
0x40, 0x0C, // JCN 12 (Jump back to loop start)
// Exit - halt program
0x00, 0x00, // NOP (placeholder for halt)
// === Address 0x00: Initialize registers ===
0x20, 0x01, // FIM R0/R1, 0x01 → R0=0, R1=1 (F(0)=0, F(1)=1)
0x22, 0x78, // FIM R2/R3, 0x78 → R2=7 (loop counter, ISZ wraps 7→8≠0→jumps, 8→9, ... 15→0→falls through = 8 iterations), R3=8
// === Address 0x04: Loop — compute F(n) = F(n-1) + F(n-2) ===
0xF0, // CLB (ACC=0, Carry=0)
0xA1, // LD R1 → ACC = F(n-1)
0x80, // ADD R0 → ACC = F(n-1) + F(n-2) + 0 = F(n)
0xE0, // WRM (write F(n) to RAM)
// === Address 0x08: Rotate registers: R0 ← R1, R1 ← F(n) ===
// F(n) is still in ACC after WRM (WRM doesn't change ACC)
0xB3, // XCH R3 → ACC = old R3, R3 = F(n)
0xA1, // LD R1 → ACC = F(n-1)
0xB0, // XCH R0 → ACC = old F(n-2), R0 = F(n-1)
0xA3, // LD R3 → ACC = F(n)
0xB1, // XCH R1 → ACC = old F(n-1), R1 = F(n)
// === Address 0x0E: Loop control ===
0x72, 0x04, // ISZ R2, 0x04 → R2++; if R2 ≠ 0 jump to 0x04 (loop)
// === Address 0x10: Exit ===
0x00, // NOP (halt placeholder)
0x00, // NOP (halt placeholder)
]
}

View file

@ -374,12 +374,11 @@ fn test_isz_instruction() {
#[test]
fn test_jcn_instruction() {
// LDM 0 (0xD0) → ACC=0
// JCN 0x04, 0x00 → jump if ACC zero → should jump to 0x00
// But we want to test fall-through too:
// JCN 0x02, 0x00 → jump if ACC zero (bit 1 = ACC zero per datasheet)
let program = vec![
0xD0, // LDM 0 → ACC=0
0x14, 0x00, // JCN 0x4 (ACC zero), addr 0x00 → jump taken
0x12, 0x00, // JCN 0x2 (ACC zero), addr 0x00 → jump taken
];
let mut sys = TestSystem::new(&program);
@ -400,11 +399,11 @@ fn test_jcn_instruction() {
#[test]
fn test_jcn_not_taken() {
// LDM 5 (0xD5) → ACC=5 (not zero)
// JCN 0x04, 0x00 → jump if ACC zero → should NOT jump
// JCN 0x02, 0x00 → jump if ACC zero → should NOT jump
let program = vec![
0xD5, // LDM 5 → ACC=5
0x14, 0x00, // JCN 0x4 (ACC zero), addr 0x00 → NOT taken
0x12, 0x00, // JCN 0x2 (ACC zero), addr 0x00 → NOT taken
0xD0, // LDM 0 → ACC=0 (should reach here)
];