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.
This commit is contained in:
Jan-Peter von Hunnius 2026-05-12 19:32:01 +02:00
parent 54144d10e9
commit 6a335a15d4
14 changed files with 4125 additions and 1062 deletions

View file

@ -143,8 +143,8 @@ fn test_intel_4001_state_machine_progression() {
// Wait for pins to settle
thread::sleep(Duration::from_millis(1));
// Trigger Φ1 rising edge again - ROM should stay in AddressPhase
// (address nibbles are latched on Φ1 falling edge, not rising)
// Trigger Φ1 rising edge again - ROM latches low nibble on PHI1 rising edge
// (address nibbles are latched on Φ1 RISING edge, same as CPU drives address)
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::High);
@ -153,37 +153,16 @@ fn test_intel_4001_state_machine_progression() {
// Update to process Φ1 rising edge
rom.update();
// Should still be in AddressPhase (latching happens on falling edge)
assert_eq!(
rom.get_timing_state(),
TimingState::AddressPhase,
"Should still be in AddressPhase after Φ1 rising - latching happens on falling edge"
);
println!(
"State after Φ1 rising (before falling): {:?}",
rom.get_timing_state()
);
// Reset Φ1 to Low to trigger falling edge - this is when low nibble is latched
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
// Update to process Φ1 falling edge - low nibble is latched here
rom.update();
// Should have latched low nibble and transitioned to WaitLatency or DriveData
// With 1ns access time, it might transition directly to DriveData
// Should have latched low nibble and transitioned to WaitLatency
// (both nibbles latched on successive PHI1 rising edges)
let state_after_low_nibble = rom.get_timing_state();
assert!(
state_after_low_nibble == TimingState::WaitLatency
|| state_after_low_nibble == TimingState::DriveData,
"Expected WaitLatency or DriveData after latching both nibbles, got {:?}",
state_after_low_nibble == TimingState::WaitLatency,
"Expected WaitLatency after latching both nibbles on PHI1 rising, got {:?}",
state_after_low_nibble
);
println!(
"State after Φ1 falling (low nibble latched): {:?}",
"State after second Φ1 rising (both nibbles latched): {:?}",
state_after_low_nibble
);
@ -199,51 +178,82 @@ fn test_intel_4001_state_machine_progression() {
d3_guard.remove_driver("TEST");
}
// If still in WaitLatency, trigger Φ2 rising edge to check latency and drive data
if rom.get_timing_state() == TimingState::WaitLatency {
// Trigger Φ2 rising edge
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
// Update to process Φ2 rising edge
rom.update();
// Should have transitioned to DriveData
assert_eq!(rom.get_timing_state(), TimingState::DriveData);
println!("State after Φ2 rising: {:?}", rom.get_timing_state());
// ROM is in WaitLatency with a3_wait_cycles=1.
// Need to cycle the clock to get through the wait period and into DriveData.
// The ROM expects PHI2 rising edges to decrement a3_wait_cycles and transition.
// Step 1: PHI1 falling + PHI2 rising (decrements a3_wait_cycles to 0)
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
rom.update();
// a3_wait_cycles decremented from 1 to 0
// Wait for pins to settle after ROM drives data
thread::sleep(Duration::from_millis(1));
// Verify data is being driven on the bus
// Data 0xAB has low nibble 0xB = 1011 (D0=1, D1=1, D2=0, D3=1)
// Note: Due to bit ordering, D0 should be High, D1 should be High, D2 should be Low, D3 should be High
assert_eq!(d0_pin.lock().read(), PinValue::High);
assert_eq!(d1_pin.lock().read(), PinValue::High);
assert_eq!(d2_pin.lock().read(), PinValue::Low);
assert_eq!(d3_pin.lock().read(), PinValue::High);
// Reset Φ2 to Low
// Step 2: PHI2 falling + PHI1 rising (no transition yet, just clock edge)
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
// Update to process Φ2 falling edge
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
rom.update();
// Should have returned to Idle
assert_eq!(rom.get_timing_state(), TimingState::Idle);
println!("Final state: {:?}", rom.get_timing_state());
// Step 3: PHI1 falling + PHI2 rising (a3_wait_cycles=0, transitions to DriveData)
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
rom.update();
// Should now be in DriveData (process_latency_elapsed on this PHI2 rising)
// But return statement means data isn't driven yet
// Verify data bus is tri-stated
assert_eq!(d0_pin.lock().read(), PinValue::HighZ);
assert_eq!(d1_pin.lock().read(), PinValue::HighZ);
assert_eq!(d2_pin.lock().read(), PinValue::HighZ);
assert_eq!(d3_pin.lock().read(), PinValue::HighZ);
// Step 4: PHI2 falling + PHI1 rising
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
rom.update();
// Step 5: PHI1 falling + PHI2 rising - NOW ROM drives data
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
rom.update();
// Should be in DriveData and driving OPR (high nibble of 0xAB = 0xA)
assert_eq!(rom.get_timing_state(), TimingState::DriveData);
println!("State after PHI2 rising (driving data): {:?}", rom.get_timing_state());
// Wait for pins to settle after ROM drives data
thread::sleep(Duration::from_millis(1));
// Verify OPR is being driven on the bus
// Data 0xAB has high nibble 0xA = 1010 (D0=0, D1=1, D2=0, D3=1)
assert_eq!(d0_pin.lock().read(), PinValue::Low, "D0 should be Low for 0xA");
assert_eq!(d1_pin.lock().read(), PinValue::High, "D1 should be High for 0xA");
assert_eq!(d2_pin.lock().read(), PinValue::Low, "D2 should be Low for 0xA");
assert_eq!(d3_pin.lock().read(), PinValue::High, "D3 should be High for 0xA");
println!("OPR data verified: 0xA");
}
/// Test to verify that unified state machine doesn't progress without proper control signals