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

@ -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)
]
}