From add7ace674c79942969c845a2f30ac898e989517 Mon Sep 17 00:00:00 2001 From: Jan-Peter von Hunnius Date: Tue, 12 May 2026 20:55:26 +0200 Subject: [PATCH] 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 --- plans/intel_4004_opcode_fix.md | 69 ++++++++++++++++-------------- programs/README.md | 18 ++++---- programs/fibonacci.bin | Bin 33 -> 17 bytes programs/fibonacci_output.bin | Bin 55 -> 23 bytes src/components/cpu/intel_4004.rs | 71 ++++++++++++++++++------------- src/main.rs | 65 +++++++++++++++++----------- tests/fibonacci_test.rs | 9 ++-- 7 files changed, 132 insertions(+), 100 deletions(-) diff --git a/plans/intel_4004_opcode_fix.md b/plans/intel_4004_opcode_fix.md index 24f54eb..f83a79d 100644 --- a/plans/intel_4004_opcode_fix.md +++ b/plans/intel_4004_opcode_fix.md @@ -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 --- diff --git a/programs/README.md b/programs/README.md index 4d48a94..750ded9 100644 --- a/programs/README.md +++ b/programs/README.md @@ -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** diff --git a/programs/fibonacci.bin b/programs/fibonacci.bin index f9c38472326a1b2cdd146933aae89f3db2582303..167d5a3c2677f14c31792ae31f0f66e08718830c 100644 GIT binary patch literal 17 ZcmY#jRI2!}u;Ibxg&P)cEMj3`002e_2YUbj literal 33 kcmY#j5MT&UWE5ZwU|=d^U}9hZu^1T`fLsF(2Ob6n05J9fwg3PC diff --git a/programs/fibonacci_output.bin b/programs/fibonacci_output.bin index 26a01d990ccf15580338f9d2371c6edca1e994d3..015bd0a11740b1a306f968fa4137ae2bdb5388c1 100644 GIT binary patch literal 23 fcmY#jRH{&6P+aif;fI9{4>m8{uy|t;8v_FXa>5ED literal 55 zcmWN_F#!ND2n4a?Cq^WIgG#6b)YLCd%9tcUmxXmQ6loz?q9IhUiNVH0==gQ`qbdbH diff --git a/src/components/cpu/intel_4004.rs b/src/components/cpu/intel_4004.rs index c5cff8b..8651302 100644 --- a/src/components/cpu/intel_4004.rs +++ b/src/components/cpu/intel_4004.rs @@ -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 } } diff --git a/src/main.rs b/src/main.rs index 0f9c0f3..ae61ffd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -381,32 +381,47 @@ fn load_program_data(filename: &str) -> Result, String> { fn get_default_fibonacci_program() -> Vec { // 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) ] } diff --git a/tests/fibonacci_test.rs b/tests/fibonacci_test.rs index 0eab495..deb134b 100644 --- a/tests/fibonacci_test.rs +++ b/tests/fibonacci_test.rs @@ -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) ];