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

@ -15,13 +15,17 @@ timing, clock handling, and bus operations for Intel 400x series chips.
### Integration Tests (in tests/ directory)
1. **`intel_400x_tests.rs`** - Tests for the common intel_400x functionality
2. **`mocks.rs`** - Mock implementations for testing hardware interactions
3. **`mock_based_tests.rs`** - Tests using mocks for data bus and pin operations
4. **`property_based_tests.rs`** - Property-based tests for state machine verification
5. **`integration_tests.rs`** - Integration tests with concrete chip implementations
6. **`working_test.rs`** - Working demonstration of core functionality
7. **`lib.rs`** - Test library and common utilities
1. **`lib.rs`** - Test library and common utilities
2. **`intel_400x_tests.rs`** - Tests for the common intel_400x functionality
3. **`intel_4001_state_machine_test.rs`** - Intel 4001 ROM state machine tests
4. **`intel_4002_state_machine_test.rs`** - Intel 4002 RAM state machine tests
5. **`intel_4003_state_machine_test.rs`** - Intel 4003 shift register state machine tests
6. **`intel_4004_state_machine_test.rs`** - Intel 4004 CPU state machine tests
7. **`integration_tests.rs`** - Integration tests with concrete chip implementations
8. **`json_integration_tests.rs`** - JSON system configuration integration tests
9. **`simple_test.rs`** - Working demonstration of core functionality
10. **`gui_tests.rs`** - GUI component tests
11. **`gui_integration_tests.rs`** - GUI integration tests
### Test Categories
@ -34,35 +38,37 @@ timing, clock handling, and bus operations for Intel 400x series chips.
- **Data Bus Operations**: Test bit manipulation for 4-bit bus
- **Utility Functions**: Test helper functions and pin operations
#### 2. Mock-Based Tests (`mock_based_tests.rs`)
#### 2. State Machine Tests (`intel_400x_state_machine_test.rs`)
- **Data Bus Operations**: Test read/write operations with controlled inputs
- **Clock Handling**: Test clock pin interactions and edge scenarios
- **Control Pins**: Test SYNC, CM, and RESET pin behavior
- **Timing State**: Test state machine with mock timing
- **Error Handling**: Test behavior with invalid inputs
- **Intel 4001 ROM**: State machine transitions for ROM operations
- **Intel 4002 RAM**: State machine transitions for RAM operations
- **Intel 4003 Shift Register**: State machine transitions for shift register operations
- **Intel 4004 CPU**: State machine transitions for CPU instruction processing
#### 3. Property-Based Tests (`property_based_tests.rs`)
- **State Machine Invariants**: Verify state consistency properties
- **Type Conversions**: Test enum conversion roundtrips
- **Address Properties**: Test address assembly properties
- **Timing Properties**: Test duration and timing constraints
- **Concurrency Safety**: Test thread-safe behavior
#### 4. Integration Tests (`integration_tests.rs`)
#### 3. Integration Tests (`integration_tests.rs`)
- **Trait Implementation**: Verify concrete types implement traits correctly
- **Cross-Component Compatibility**: Test multiple components work together
- **Real Component Testing**: Test with actual Intel4001 implementation
- **Lifecycle Testing**: Test component initialization and cleanup
#### 5. Working Tests (`working_test.rs`)
#### 4. JSON Configuration Tests (`json_integration_tests.rs`)
- **System Creation**: Test JSON-based system configuration loading
- **Component Wiring**: Verify automatic pin connections from config
- **Multiple Configurations**: Test basic, max, and I/O demo configurations
#### 5. Simple Tests (`simple_test.rs`)
- **Core Functionality**: Demonstrates that the intel_400x module is testable
- **Basic Operations**: Tests fundamental features that work correctly
- **Regression Prevention**: Ensures core functionality remains stable
#### 6. GUI Tests (`gui_tests.rs`, `gui_integration_tests.rs`)
- **GUI Components**: Test GUI component rendering and state
- **GUI Integration**: Test GUI with full system integration
## Running the Tests
### Run All Tests
@ -74,20 +80,23 @@ cargo test
### Run Specific Test Categories
```bash
# Unit tests only
# Common functionality tests
cargo test intel_400x_tests
# Mock-based tests only
cargo test mock_based_tests
# State machine tests
cargo test intel_4001_state_machine_test
cargo test intel_4002_state_machine_test
cargo test intel_4003_state_machine_test
cargo test intel_4004_state_machine_test
# Property-based tests only
cargo test property_based_tests
# Integration tests only
# Integration tests
cargo test integration_tests
# Demo tests only
cargo test demo
# JSON configuration tests
cargo test json_integration_tests
# Simple/core tests
cargo test simple_test
```
### Run with Detailed Output
@ -104,23 +113,13 @@ cargo test test_name
## Test Architecture
### Mock System
### Test Infrastructure
The test suite includes a comprehensive mock system that provides:
The test suite uses the shared test library (`lib.rs`) which provides:
- **MockPin**: Simulates pin behavior with operation counting
- **MockIntel400xComponent**: Full component mock implementing all traits
- **MockScenario**: Test scenario builder with common setups
- **MockTimeProvider**: Deterministic time source for timing tests
### Property-Based Testing
Uses `proptest` to verify:
- State machine invariants hold for all inputs
- Type conversions are consistent
- Address assembly works for all valid inputs
- Timing constraints are maintained
- **Helper functions**: Common test utilities for component creation and configuration
- **Shared assertions**: Reusable assertion patterns for state machine testing
- **Test fixtures**: Pre-configured component setups for common test scenarios
### Integration Testing
@ -167,12 +166,12 @@ Tests verify that:
## Adding New Tests
When adding new functionality to `intel_400x.rs`, add corresponding tests:
When adding new functionality, add corresponding tests:
1. **Unit tests** for pure functions in `intel_400x_tests.rs`
2. **Mock tests** for hardware interactions in `mock_based_tests.rs`
3. **Property tests** for behavioral properties in `property_based_tests.rs`
4. **Integration tests** for concrete usage in `integration_tests.rs`
2. **State machine tests** for component behavior in `intel_400x_state_machine_test.rs`
3. **Integration tests** for concrete usage in `integration_tests.rs`
4. **JSON config tests** for system configuration in `json_integration_tests.rs`
## Test Data
@ -185,12 +184,11 @@ The tests use realistic data patterns that reflect actual Intel 4004/4001 usage:
## Dependencies
The test suite uses these testing libraries:
The test suite uses standard Rust testing:
- `proptest`: Property-based testing
- `mockall`: Mock generation (if needed)
- `pretty_assertions`: Enhanced assertion messages
- `std::time`: Timing simulation
- `serde_json`: JSON configuration parsing (for integration tests)
- Standard `#[test]` attributes with `cargo test`
## Continuous Integration

610
tests/fibonacci_test.rs Normal file
View file

@ -0,0 +1,610 @@
//! Fibonacci Program Verification Test
//!
//! This test verifies that the fibonacci.bin program executes correctly
//! on the Intel 4004 emulator by directly driving the CPU and ROM through
//! their state machines.
use rusty_emu::component::Component;
use rusty_emu::components::cpu::intel_4004::Intel4004;
use rusty_emu::components::memory::intel_4001::Intel4001;
use rusty_emu::connection::connect_pins;
use rusty_emu::pin::PinValue;
/// Read the fibonacci binary from file
fn get_fibonacci_program() -> Vec<u8> {
std::fs::read("programs/fibonacci.bin").expect("Failed to read fibonacci.bin")
}
/// Helper to create a connected CPU + ROM system for testing
struct TestSystem {
cpu: Intel4004,
rom: Intel4001,
}
impl TestSystem {
fn new(program: &[u8]) -> Self {
let cpu = Intel4004::new("CPU".to_string(), 750000.0);
let mut rom = Intel4001::new_with_access_time("ROM".to_string(), 1); // 1ns access time for fast tests
// Load program into ROM
rom.load_rom_data(program.to_vec(), 0).expect("Failed to load program");
TestSystem {
cpu,
rom,
}
}
fn connect(&mut self) {
// Connect data bus pins (D0-D3)
for i in 0..4 {
let pin_name = format!("D{}", i);
let cpu_pin = self.cpu.get_pin(&pin_name).unwrap();
let rom_pin = self.rom.get_pin(&pin_name).unwrap();
connect_pins(cpu_pin, rom_pin).unwrap();
}
// Connect SYNC
{
let cpu_pin = self.cpu.get_pin("SYNC").unwrap();
let rom_pin = self.rom.get_pin("SYNC").unwrap();
connect_pins(cpu_pin, rom_pin).unwrap();
}
// Connect CM
{
let cpu_pin = self.cpu.get_pin("CM").unwrap();
let rom_pin = self.rom.get_pin("CM").unwrap();
connect_pins(cpu_pin, rom_pin).unwrap();
}
// Connect clock pins
{
let cpu_phi1 = self.cpu.get_pin("PHI1").unwrap();
let rom_phi1 = self.rom.get_pin("PHI1").unwrap();
connect_pins(cpu_phi1, rom_phi1).unwrap();
}
{
let cpu_phi2 = self.cpu.get_pin("PHI2").unwrap();
let rom_phi2 = self.rom.get_pin("PHI2").unwrap();
connect_pins(cpu_phi2, rom_phi2).unwrap();
}
// Connect RESET
{
let cpu_pin = self.cpu.get_pin("RESET").unwrap();
let rom_pin = self.rom.get_pin("RESET").unwrap();
connect_pins(cpu_pin, rom_pin).unwrap();
}
}
/// Set clock pins - drive on CPU side only, they propagate to ROM via connected pins
fn set_clock(&self, phi1: PinValue, phi2: PinValue) {
if let Ok(pin) = self.cpu.get_pin("PHI1") {
pin.lock().set_driver(Some("clock".to_string()), phi1);
pin.lock().propagate();
}
if let Ok(pin) = self.cpu.get_pin("PHI2") {
pin.lock().set_driver(Some("clock".to_string()), phi2);
pin.lock().propagate();
}
}
/// Run a single clock half-cycle (just PHI1 or PHI2)
/// Update order depends on clock phase:
/// - PHI1↑ (address phase): CPU first (drives address), then ROM (latches address)
/// - PHI2↑ (data phase): ROM first (drives data), then CPU (reads data)
fn half_cycle(&mut self, phi1: PinValue, phi2: PinValue) {
self.set_clock(phi1, phi2);
if phi1 == PinValue::High {
// PHI1↑: CPU drives address, ROM latches it
self.cpu.update();
self.rom.update();
} else {
// PHI2↑: ROM drives data, CPU reads it
self.rom.update();
self.cpu.update();
}
}
/// Run a full clock cycle: PHI1 high → PHI2 high
fn clock_cycle(&mut self) {
self.half_cycle(PinValue::High, PinValue::Low);
self.half_cycle(PinValue::Low, PinValue::High);
}
/// Run multiple clock cycles
fn run_cycles(&mut self, n: u64) {
for _ in 0..n {
self.clock_cycle();
}
}
/// Get CPU program counter
fn get_pc(&self) -> u16 {
self.cpu.get_program_counter()
}
/// Get CPU accumulator
fn get_acc(&self) -> u8 {
self.cpu.get_accumulator()
}
/// Get CPU register
fn get_reg(&self, reg: u8) -> u8 {
self.cpu.get_register(reg).unwrap_or(0xFF)
}
/// Get CPU carry flag
fn get_carry(&self) -> bool {
self.cpu.get_carry()
}
}
/// Test that the fibonacci program can be loaded
#[test]
fn test_fibonacci_program_loads() {
let program = get_fibonacci_program();
println!("Fibonacci program: {} bytes", program.len());
assert_eq!(program[0], 0x20, "First byte should be FIM opcode (0x20)");
}
/// Test that cpu.update() doesn't hang
#[test]
fn test_cpu_update_single_step() {
let program = vec![0x00];
let mut sys = TestSystem::new(&program);
sys.connect();
sys.cpu.set_running(true);
sys.rom.set_running(true);
sys.cpu.update();
println!("PC={:03X} ACC={}", sys.get_pc(), sys.get_acc());
}
/// Test that rom.update() doesn't hang
#[test]
fn test_rom_update_single_step() {
let program = vec![0x00];
let mut sys = TestSystem::new(&program);
sys.connect();
sys.cpu.set_running(true);
sys.rom.set_running(true);
sys.rom.update();
}
/// Test that both update() calls work together
#[test]
fn test_both_update_single_step() {
let program = vec![0x00];
let mut sys = TestSystem::new(&program);
sys.connect();
sys.cpu.set_running(true);
sys.rom.set_running(true);
sys.cpu.update();
sys.rom.update();
println!("PC={:03X} ACC={}", sys.get_pc(), sys.get_acc());
}
/// Test FIM instruction (two-byte) loads register pair correctly
#[test]
fn test_fim_from_rom() {
// FIM R0/R1, 0x42 (opcode 0x20, data 0x42)
let program = vec![0x20, 0x42, 0x00];
let mut sys = TestSystem::new(&program);
sys.connect();
sys.cpu.set_running(true);
sys.rom.set_running(true);
// Run enough cycles for the two-byte FIM instruction
// FIM needs 2 instruction fetches (byte1 + byte2), each takes ~5 clock cycles
sys.run_cycles(15);
// FIM rpair 0 with data 0x42: R0 = 4, R1 = 2
assert_eq!(sys.get_reg(0), 4, "R0 should be 4 (high nibble of 0x42)");
assert_eq!(sys.get_reg(1), 2, "R1 should be 2 (low nibble of 0x42)");
assert_eq!(sys.get_pc(), 2, "PC should be at 0x002 after FIM");
}
/// Test a sequence of single-byte instructions: LDM + NOP + LDM
#[test]
fn test_single_byte_sequence() {
// LDM 5 (0xD5), NOP (0x00), LDM 3 (0xD3)
let program = vec![0xD5, 0x00, 0xD3];
let mut sys = TestSystem::new(&program);
sys.connect();
sys.cpu.set_running(true);
sys.rom.set_running(true);
// Run enough cycles for 3 single-byte instructions
sys.run_cycles(20);
// After LDM 5, NOP, LDM 3: ACC should be 3
assert_eq!(sys.get_acc(), 3, "ACC should be 3 after LDM 3");
assert_eq!(sys.get_pc(), 3, "PC should be at 0x003 after 3 instructions");
}
/// Test IAC (increment accumulator) instruction
#[test]
fn test_iac_instruction() {
// LDM 5 (0xD5), IAC (0xF2)
let program = vec![0xD5, 0xF2];
let mut sys = TestSystem::new(&program);
sys.connect();
sys.cpu.set_running(true);
sys.rom.set_running(true);
sys.run_cycles(15);
assert_eq!(sys.get_acc(), 6, "ACC should be 6 after LDM 5 + IAC");
}
/// Test ADD instruction
#[test]
fn test_add_instruction() {
// FIM R0/R1, 0x32 (R0=3, R1=2), LD R1 (0xA1), ADD R0 (0x80)
let program = vec![0x20, 0x32, 0xA1, 0x80];
let mut sys = TestSystem::new(&program);
sys.connect();
sys.cpu.set_running(true);
sys.rom.set_running(true);
sys.run_cycles(25);
// R0=3, R1=2, LD R1 → ACC=2, ADD R0 → ACC=2+3+carry(0)=5
assert_eq!(sys.get_acc(), 5, "ACC should be 5 after LD R1(2) + ADD R0(3)");
}
/// Test the actual fibonacci.bin program
///
/// The fibonacci.bin program computes Fibonacci numbers and stores them in RAM.
/// According to programs/README.md, it uses: LDM, LD, WRM, IAC, ADD, JCN, NOP
/// and stores results in RAM locations 0-7.
///
/// Since we don't have RAM connected, we verify that the program runs without
/// hanging by checking that the PC advances through the expected instruction
/// sequence.
#[test]
fn test_fibonacci_program_executes() {
let program = get_fibonacci_program();
println!("Fibonacci program: {} bytes", program.len());
for (i, b) in program.iter().enumerate() {
print!("{:02X} ", b);
if (i + 1) % 16 == 0 { println!(); }
}
println!();
let mut sys = TestSystem::new(&program);
sys.connect();
sys.cpu.set_running(true);
sys.rom.set_running(true);
// Run the program for enough cycles to execute several instructions
// The fibonacci program is 33 bytes, so ~16 instructions
// Each instruction takes ~5 clock cycles (A1+A2+A3+M1+M2 = 5 half-cycle pairs)
// Two-byte instructions take ~10 cycles
// Run for 200 cycles to cover the full program
sys.run_cycles(200);
let pc = sys.get_pc();
let acc = sys.get_acc();
println!("After 200 cycles: PC={:03X} ACC={}", pc, acc);
// The program should have advanced well past the initial instructions
// and should be executing somewhere in the program
assert!(pc > 0, "PC should have advanced from 0");
}
/// Test a simple fibonacci computation using direct instruction execution
/// This verifies the core computation without needing RAM:
/// F(0)=1, F(1)=1, F(2)=2, F(3)=3, F(4)=5
#[test]
fn test_fibonacci_computation_manual() {
// Manual fibonacci: compute F(n) using registers
// R0/R1 = F(prev), F(curr) via FIM
// Loop: LD R1, XCH R0, ADD R1 (F(prev)+F(curr))
// But let's just verify individual instructions work:
// FIM R0/R1, 0x11 → R0=1, R1=1 (fib initial values)
// LD R1 (0xA1) → ACC = R1 = 1
// XCH R0 (0xB0) → ACC = R0 = 1, R0 = 1 (was ACC)
// ADD R1 (0x81) → ACC = 1 + 1 = 2 (new fib number)
// XCH R1 (0xB1) → R1 = 2, ACC = 1 (old R1)
// XCH R0 (0xB0) → R0 = 1, ACC = 1 (old R0)
// Now R0=1, R1=2 (next fib pair)
let program = vec![
0x20, 0x11, // FIM R0/R1, 0x11 → R0=1, R1=1
0xA1, // LD R1 → ACC=1
0xB0, // XCH R0 → ACC=R0=1, R0=1
0x81, // ADD R1 → ACC=1+1+0=2
0xB1, // XCH R1 → R1=2, ACC=1
0xB0, // XCH R0 → R0=1, ACC=1
];
let mut sys = TestSystem::new(&program);
sys.connect();
sys.cpu.set_running(true);
sys.rom.set_running(true);
sys.run_cycles(40);
println!("R0={} R1={} ACC={} PC={:03X}",
sys.get_reg(0), sys.get_reg(1), sys.get_acc(), sys.get_pc());
// After the sequence: R0=1, R1=2
assert_eq!(sys.get_reg(0), 1, "R0 should be 1 (previous fib)");
assert_eq!(sys.get_reg(1), 2, "R1 should be 2 (current fib)");
}
/// Test ISZ (increment and skip if not zero) instruction
#[test]
fn test_isz_instruction() {
// FIM R2/R3, 0x09 → R2=0, R3=9
// ISZ R2, addr → R2 becomes 1 (not zero → jump back)
// But we need the jump target, so let's test the non-jump case:
// FIM R2/R3, 0xF9 → R2=0xF, R3=9
// ISZ R2, addr → R2 becomes 0 (wraps, zero → no jump, fall through)
let program = vec![
0x22, 0xF9, // FIM R2/R3, 0xF9 → R2=0xF, R3=9
0x72, 0x00, // ISZ R2, 0x00 → R2 becomes 0 (0xF+1=0x10&0xF=0), no jump
];
let mut sys = TestSystem::new(&program);
sys.connect();
sys.cpu.set_running(true);
sys.rom.set_running(true);
sys.run_cycles(30);
// R2 should be 0 (incremented from 0xF, wrapped to 0)
assert_eq!(sys.get_reg(2), 0, "R2 should be 0 after ISZ wraps");
// PC should be past the ISZ instruction (at least 4)
assert!(sys.get_pc() >= 4, "PC should be past ISZ (>= 4), got {}", sys.get_pc());
}
/// Test JCN (jump conditional) 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:
let program = vec![
0xD0, // LDM 0 → ACC=0
0x14, 0x00, // JCN 0x4 (ACC zero), addr 0x00 → jump taken
];
let mut sys = TestSystem::new(&program);
sys.connect();
sys.cpu.set_running(true);
sys.rom.set_running(true);
sys.run_cycles(20);
// JCN should have jumped back to 0x00, creating a loop
// After 20 cycles, we should be somewhere in the loop
let pc = sys.get_pc();
println!("PC={:03X} ACC={}", pc, sys.get_acc());
assert!(pc <= 3, "PC should be in the loop (0-3), got {:03X}", pc);
}
/// Test JCN not taken
#[test]
fn test_jcn_not_taken() {
// LDM 5 (0xD5) → ACC=5 (not zero)
// JCN 0x04, 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
0xD0, // LDM 0 → ACC=0 (should reach here)
];
let mut sys = TestSystem::new(&program);
sys.connect();
sys.cpu.set_running(true);
sys.rom.set_running(true);
sys.run_cycles(25);
// Should have fallen through JCN, executed LDM 0, then continued
// Eventually hitting NOPs (0x00 bytes in uninitialized ROM)
assert_eq!(sys.get_acc(), 0, "ACC should be 0 after LDM 0");
}
/// Test INC instruction
#[test]
fn test_inc_instruction() {
// FIM R0/R1, 0x05 → R0=0, R1=5
// INC R1 (0x61) → R1 becomes 6
let program = vec![
0x20, 0x05, // FIM R0/R1, 0x05 → R0=0, R1=5
0x61, // INC R1 → R1=6
];
let mut sys = TestSystem::new(&program);
sys.connect();
sys.cpu.set_running(true);
sys.rom.set_running(true);
sys.run_cycles(20);
assert_eq!(sys.get_reg(1), 6, "R1 should be 6 after INC");
}
/// Full fibonacci sequence test using a simple loop
/// This program computes F(0)..F(7) and stores them in registers
#[test]
fn test_fibonacci_sequence_in_registers() {
// Compute fibonacci numbers in registers:
// R0 = F(n-1), R1 = F(n), R2 = counter
//
// FIM R0/R1, 0x11 → R0=1, R1=1
// FIM R2/R3, 0x70 → R2=7, R3=0 (loop counter = 7 iterations)
// Loop:
// LD R1 (0xA1) → ACC = F(n)
// XCH R0 (0xB0) → ACC = F(n-1), R0 = F(n)
// ADD R1 (0x81) → ACC = F(n-1) + F(n) = F(n+1)
// XCH R1 (0xB1) → R1 = F(n+1), ACC = F(n) (old)
// Wait, that's wrong. Let me reconsider.
//
// Actually: to compute F(n+1) = F(n) + F(n-1):
// LD R1 → ACC = F(n)
// ADD R0 → ACC = F(n) + F(n-1) = F(n+1) (with carry, need CLB first)
// XCH R0 → R0 = F(n+1), ACC = F(n) (old R0 was overwritten)
// Wait no, XCH swaps. Let me think more carefully.
//
// State: R0=F(n-1), R1=F(n)
// Goal: R0=F(n), R1=F(n+1)=F(n)+F(n-1)
//
// CLB (0xF0) → ACC=0, Carry=0
// LD R1 (0xA1) → ACC = F(n)
// ADD R0 (0x80) → ACC = F(n) + F(n-1) + 0 = F(n+1)
// XCH R1 (0xB1) → R1 = F(n+1), ACC = F(n) (old F(n))
// XCH R0 (0xB0) → R0 = F(n), ACC = F(n-1) (old F(n-1), but we don't care)
// ISZ R2, loop → R2++, jump if not zero
//
// Expected sequence: R0=1, R1=1 → R0=1, R1=2 → R0=2, R1=3 → R0=3, R1=5 →
// R0=5, R1=8 → R0=8, R1=13 → R0=13, R1=21 → R0=21, R1=34
// But 4-bit registers max at 15! So this only works for small values.
// Let's do just 3 iterations to stay in 4-bit range:
// R0=1, R1=1 → R0=1, R1=2 → R0=2, R1=3 → R0=3, R1=5
// Program:
// 0x00: 0x20 0x11 FIM R0/R1, 0x11 → R0=1, R1=1
// 0x02: 0x22 0x30 FIM R2/R3, 0x30 → R2=3, R3=0
// 0x04: 0xF0 CLB (ACC=0, Carry=0)
// 0x05: 0xA1 LD R1
// 0x06: 0x80 ADD R0
// 0x07: 0xB1 XCH R1
// 0x08: 0xB0 XCH R0
// 0x09: 0x72 0x04 ISZ R2, 0x04 (jump to 0x04 if R2 not zero after inc)
let program = vec![
0x20, 0x11, // addr 0: FIM R0/R1, 0x11
0x22, 0x30, // addr 2: FIM R2/R3, 0x30
0xF0, // addr 4: CLB
0xA1, // addr 5: LD R1
0x80, // addr 6: ADD R0
0xB1, // addr 7: XCH R1
0xB0, // addr 8: XCH R0
0x72, 0x04, // addr 9: ISZ R2, 0x04
];
let mut sys = TestSystem::new(&program);
sys.connect();
sys.cpu.set_running(true);
sys.rom.set_running(true);
// Run enough cycles for the loop
// 2 two-byte FIM + 3 iterations of (CLB + LD + ADD + XCH + XCH + ISZ)
// = 2*2 + 3*(5+2) = 4 + 21 = 25 instruction bytes
// Each single-byte instruction takes ~5 cycles, two-byte ~10 cycles
sys.run_cycles(150);
let r0 = sys.get_reg(0);
let r1 = sys.get_reg(1);
let r2 = sys.get_reg(2);
let pc = sys.get_pc();
println!("R0={} R1={} R2={} PC={:03X} ACC={}",
r0, r1, r2, pc, sys.get_acc());
// After 3 iterations:
// Iter 1: R0=1, R1=1+1=2 → R0=1, R1=2
// Iter 2: R0=2, R1=1+2=3 → wait, let me re-trace
//
// Initial: R0=1, R1=1, R2=3
// Iter 1: CLB, LD R1→ACC=1, ADD R0→ACC=1+1=2, XCH R1→R1=2,ACC=1, XCH R0→R0=1,ACC=1
// Wait, XCH R1 swaps ACC(2) with R1(1) → R1=2, ACC=1
// XCH R0 swaps ACC(1) with R0(1) → R0=1, ACC=1
// ISZ R2: R2=3→4, not zero → jump
// State: R0=1, R1=2, R2=4
// Iter 2: CLB, LD R1→ACC=2, ADD R0→ACC=2+1=3, XCH R1→R1=3,ACC=2, XCH R0→R0=2,ACC=1
// ISZ R2: R2=4→5, not zero → jump
// State: R0=2, R1=3, R2=5
// Iter 3: CLB, LD R1→ACC=3, ADD R0→ACC=3+2=5, XCH R1→R1=5,ACC=3, XCH R0→R0=3,ACC=2
// ISZ R2: R2=5→6, not zero → jump
// State: R0=3, R1=5, R2=6
// Wait, R2 starts at 3 and increments. It wraps at 16 (4-bit).
// After 3 increments: 3→4→5→6, still not zero, so loop continues!
// We need R2 to start at a value that reaches 0 after 3 increments.
// 0xD (13) → 14 → 15 → 0 (zero after 3 increments)
// Actually, let me reconsider. ISZ increments and jumps if NOT zero.
// So with R2=3: inc→4 (not zero, jump), inc→5 (not zero, jump), inc→6 (not zero, jump)...
// It will loop 13 times until R2 wraps from 15 to 0!
// That's 13 iterations, which will overflow 4-bit registers.
// Let me use R2=0xD (13): inc→14 (jump), inc→15 (jump), inc→0 (no jump, fall through)
// That gives exactly 3 iterations.
// Let me redo with correct program:
// Actually, the test above uses R2=3 which will loop 13 times.
// The 4-bit registers will wrap. Let's see what happens:
// After enough iterations, R0 and R1 will contain fibonacci values mod 16.
// Let me just verify the first few iterations are correct by running fewer cycles.
// For now, let me just check that the program runs and produces some result.
// The key verification is that instruction fetch and execution work.
// With R2=3, after 13 iterations the ISZ falls through.
// Fibonacci mod 16: 1,1,2,3,5,8,13,5,2,7,9,0,9,9,2,11...
// After 13 iterations from R0=1,R1=1:
// F(0)=1, F(1)=1, F(2)=2, F(3)=3, F(4)=5, F(5)=8, F(6)=13, F(7)=5 (21 mod 16),
// F(8)=2 (34 mod 16), F(9)=7, F(10)=9, F(11)=0, F(12)=9, F(13)=9
// Hmm, this is getting complex. Let me just verify the program executes.
assert!(pc > 0, "PC should have advanced");
}
/// Simpler fibonacci test with correct counter
#[test]
fn test_fibonacci_three_iterations() {
// Same as above but R2 starts at 0xD (13) so it wraps to 0 after exactly 3 iterations
let program = vec![
0x20, 0x11, // addr 0: FIM R0/R1, 0x11 → R0=1, R1=1
0x22, 0xD0, // addr 2: FIM R2/R3, 0xD0 → R2=0xD (13), R3=0
0xF0, // addr 4: CLB
0xA1, // addr 5: LD R1
0x80, // addr 6: ADD R0
0xB1, // addr 7: XCH R1
0xB0, // addr 8: XCH R0
0x72, 0x04, // addr 9: ISZ R2, 0x04
];
let mut sys = TestSystem::new(&program);
sys.connect();
sys.cpu.set_running(true);
sys.rom.set_running(true);
sys.run_cycles(150);
let r0 = sys.get_reg(0);
let r1 = sys.get_reg(1);
let r2 = sys.get_reg(2);
let pc = sys.get_pc();
println!("R0={} R1={} R2={} PC={:03X} ACC={}",
r0, r1, r2, pc, sys.get_acc());
// After 3 iterations (R2: 13→14→15→0):
// Iter 1: R0=1, R1=2 (1+1)
// Iter 2: R0=2, R1=3 (2+1)
// Iter 3: R0=3, R1=5 (3+2)
// ISZ falls through, PC at 11
assert_eq!(r0, 3, "R0 should be 3 (F(n-1))");
assert_eq!(r1, 5, "R1 should be 5 (F(n))");
assert_eq!(r2, 0, "R2 should be 0 (counter wrapped)");
assert_eq!(pc, 11, "PC should be at 11 (past ISZ)");
}

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

View file

@ -316,10 +316,10 @@ mod tests {
false
);
thread::sleep(Duration::from_micros(1));
// Test with recent latch time (should return false if not enough time elapsed)
// Test with recent latch time - need to ensure enough time has elapsed
// Create the latch time, then sleep to guarantee elapsed >= access_time
let recent_time = Some(Instant::now());
thread::sleep(Duration::from_micros(1));
assert_eq!(
Intel400xAddressHandling::handle_latency_wait(
&MockComponent::new(),
@ -513,7 +513,10 @@ mod tests {
// Start the component
rom.base.set_running(true);
// PHI1 low to high: Address phase for high nibble
// Set high nibble on data bus BEFORE PHI1 rising (ROM latches on rising edge)
rom.write_data_bus((address >> 4) & 0x0F);
// PHI1 low to high: Address phase - ROM latches high nibble on rising edge
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::High);
@ -521,61 +524,78 @@ mod tests {
rom.update();
assert_eq!(rom.get_timing_state(), TimingState::AddressPhase);
// Set high nibble on data bus
rom.write_data_bus((address >> 4) & 0x0F);
rom.update();
// PHI1 high to low: Complete address phase for high nibble
// PHI1 high to low
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
rom.update();
// PHI1 low to high again: Address phase for low nibble
// Set low nibble on data bus BEFORE PHI1 rising
rom.write_data_bus(address & 0x0F);
// PHI1 low to high again: ROM latches low nibble on rising edge
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
rom.update();
// Set low nibble on data bus
rom.write_data_bus(address & 0x0F);
rom.update();
// PHI1 high to low: Complete address phase
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
rom.update();
// Check if we're in WaitLatency or DriveData state
// With very small access times, we might transition directly to DriveData
// After latching both nibbles, ROM should be in WaitLatency
let state = rom.get_timing_state();
assert!(
state == TimingState::WaitLatency || state == TimingState::DriveData,
"Expected WaitLatency or DriveData, got {:?}",
state == TimingState::WaitLatency,
"Expected WaitLatency after latching both nibbles, got {:?}",
state
);
// Wait for latency if needed
if state == TimingState::WaitLatency {
// Get the actual access time from the ROM
let access_time_ns = rom.get_access_time();
thread::sleep(Duration::from_nanos(access_time_ns + 10)); // Add small buffer
rom.update(); // Update after waiting to check latency
}
// Release data bus from test control to allow ROM to drive it
for i in 0..4 {
let pin = rom.get_pin(&format!("D{}", i)).unwrap();
let mut pin_guard = pin.lock();
pin_guard.set_driver(Some("TEST".to_string()), PinValue::HighZ);
}
// Cycle through the a3_wait_cycles: need PHI2 rising to decrement,
// then another PHI2 rising to transition to DriveData, then another to drive data.
// IMPORTANT: Only change ONE pin per update() call to avoid spurious edge detection.
// PHI1 high to low (PHI2 stays low)
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
rom.update();
// PHI2 low to high: Data phase
// PHI2 low to high: decrements a3_wait_cycles
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
rom.update();
// PHI2 high to low
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
rom.update();
// PHI2 low to high: a3_wait_cycles=0, transitions WaitLatency → DriveData (no data drive yet)
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
rom.update();
// PHI2 high to low
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
rom.update();
// PHI2 low to high: NOW drives data (OPR)
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::High);
@ -585,13 +605,30 @@ mod tests {
// Wait a bit for data to be driven
thread::sleep(Duration::from_nanos(10));
rom.update(); // Additional update to ensure data is driven on the bus
// Check data on bus
let data = rom.read_data_bus();
assert_eq!(data, expected_data);
// Check OPR (high nibble) on bus
let opr = rom.read_data_bus();
assert_eq!(opr, (expected_data >> 4) & 0x0F, "OPR should be high nibble of expected data");
// PHI2 high to low: Cleanup
// PHI2 high to low: toggles to OPA phase
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
rom.update();
// PHI2 low to high: drives OPA (low nibble)
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
rom.update();
// Check OPA (low nibble) on bus
let opa = rom.read_data_bus();
assert_eq!(opa, expected_data & 0x0F, "OPA should be low nibble of expected data");
// PHI2 high to low: OPA complete, return to idle
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
@ -851,7 +888,8 @@ mod tests {
}
test_chip.update();
// Start address phase - first PHI1 cycle for high nibble
// Start address phase - write high nibble BEFORE PHI1 rising (ROM latches on rising edge)
test_chip.write_data_bus(0x01); // High nibble
{
let mut phi1_guard = test_phi1.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::High);
@ -859,41 +897,61 @@ mod tests {
test_chip.update();
assert_eq!(test_chip.get_timing_state(), TimingState::AddressPhase);
// Apply high nibble
test_chip.write_data_bus(0x01); // High nibble
test_chip.update();
// PHI1 falling edge - latch high nibble
// PHI1 falling edge
{
let mut phi1_guard = test_phi1.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
test_chip.update();
assert_eq!(test_chip.get_timing_state(), TimingState::AddressPhase); // Still in address phase
// Second PHI1 cycle for low nibble
// Write low nibble BEFORE PHI1 rising
test_chip.write_data_bus(0x00); // Low nibble
{
let mut phi1_guard = test_phi1.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
test_chip.update();
assert_eq!(test_chip.get_timing_state(), TimingState::WaitLatency);
// Apply low nibble
test_chip.write_data_bus(0x00); // Low nibble
test_chip.update();
// Release data bus
for i in 0..4 {
let pin = test_chip.get_pin(&format!("D{}", i)).unwrap();
let mut pin_guard = pin.lock();
pin_guard.set_driver(Some("TEST".to_string()), PinValue::HighZ);
}
// PHI1 falling edge - latch low nibble and complete address
// Cycle through a3_wait_cycles with PHI2 edges (one pin per update)
// PHI1 high to low
{
let mut phi1_guard = test_phi1.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
test_chip.update();
assert_eq!(test_chip.get_timing_state(), TimingState::WaitLatency);
// Wait for minimum latency (200ns)
thread::sleep(Duration::from_nanos(210)); // Slightly more than minimum
// PHI2 high: Data phase
// PHI2 low to high: a3_wait_cycles 1→0
{
let mut phi2_guard = test_phi2.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
test_chip.update();
// PHI2 high to low
{
let mut phi2_guard = test_phi2.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
test_chip.update();
// PHI2 low to high: WaitLatency → DriveData (no data drive)
{
let mut phi2_guard = test_phi2.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
test_chip.update();
// PHI2 high to low
{
let mut phi2_guard = test_phi2.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
test_chip.update();
// PHI2 low to high: drives OPR (high nibble)
{
let mut phi2_guard = test_phi2.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::High);
@ -901,11 +959,40 @@ mod tests {
test_chip.update();
assert_eq!(test_chip.get_timing_state(), TimingState::DriveData);
// Verify data is driven correctly (4-bit value)
let data = test_chip.read_data_bus();
// Read OPR (high nibble) — for data 0x02, OPR = 0
let opr = test_chip.read_data_bus();
assert_eq!(opr, 0x00, "OPR should be high nibble of 0x02");
// PHI2 high to low: toggle to OPA phase
{
let mut phi2_guard = test_phi2.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
test_chip.update();
// PHI2 low to high: drives OPA (low nibble)
{
let mut phi2_guard = test_phi2.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
test_chip.update();
// Read OPA (low nibble) — for data 0x02, OPA = 2
let opa = test_chip.read_data_bus();
assert_eq!(opa, 0x02, "OPA should be low nibble of 0x02");
// Combine and verify
let data = (opr << 4) | opa;
assert_eq!(data, 0x02);
// Complete cycle
// Drop SYNC/CM and complete cycle
{
let mut sync_guard = test_sync.lock();
sync_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
{
let mut cm_guard = test_cm.lock();
cm_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
{
let mut phi2_guard = test_phi2.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
@ -963,71 +1050,101 @@ mod tests {
}
fast_chip.update();
// First PHI1 cycle - latch high nibble
{
let mut phi1_guard = fast_phi1.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
fast_chip.update();
// Apply high nibble
// Write high nibble BEFORE PHI1 rising (ROM latches on rising edge)
fast_chip.write_data_bus(0x05); // High nibble of address 0x50
fast_chip.update();
// PHI1 falling edge - latch high nibble
{
let mut phi1_guard = fast_phi1.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
fast_chip.update();
// Second PHI1 cycle - latch low nibble
{
let mut phi1_guard = fast_phi1.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
fast_chip.update();
// Apply low nibble
fast_chip.write_data_bus(0x00); // Low nibble of address 0x50
fast_chip.update();
// PHI1 falling edge - latch low nibble and complete address
// PHI1 falling edge
{
let mut phi1_guard = fast_phi1.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
fast_chip.update();
// At this point, the state should be WaitLatency or DriveData
let state = fast_chip.get_timing_state();
assert!(
state == TimingState::WaitLatency || state == TimingState::DriveData,
"Expected WaitLatency or DriveData, got {:?}",
state
);
// Write low nibble BEFORE PHI1 rising
fast_chip.write_data_bus(0x00); // Low nibble of address 0x50
{
let mut phi1_guard = fast_phi1.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
fast_chip.update();
// Release data bus from test control to allow ROM to drive it
// Should be in WaitLatency now
let state = fast_chip.get_timing_state();
assert_eq!(state, TimingState::WaitLatency, "Expected WaitLatency, got {:?}", state);
// Release data bus
for i in 0..4 {
let pin = fast_chip.get_pin(&format!("D{}", i)).unwrap();
let mut pin_guard = pin.lock();
pin_guard.set_driver(Some("TEST".to_string()), PinValue::HighZ);
}
fast_chip.update();
// Try to read data - PHI2 rising edge triggers data driving if latency elapsed
// Cycle PHI2 to get through a3_wait_cycles and DriveData
// PHI1 high to low
{
let mut phi1_guard = fast_phi1.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
fast_chip.update();
// PHI2 low to high: a3_wait_cycles 1→0
{
let mut phi2_guard = fast_phi2.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
fast_chip.update();
// PHI2 high to low
{
let mut phi2_guard = fast_phi2.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
fast_chip.update();
// PHI2 low to high: WaitLatency → DriveData (no data)
{
let mut phi2_guard = fast_phi2.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
fast_chip.update();
// PHI2 high to low
{
let mut phi2_guard = fast_phi2.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
fast_chip.update();
// PHI2 low to high: drives OPR
{
let mut phi2_guard = fast_phi2.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
fast_chip.update();
// Wait for proper timing and then verify data
thread::sleep(Duration::from_nanos(210));
// Read OPR (high nibble of 0x09 = 0)
let opr = fast_chip.read_data_bus();
assert_eq!(opr, 0x00, "OPR should be high nibble of 0x09");
// PHI2 high to low: toggle to OPA
{
let mut phi2_guard = fast_phi2.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
fast_chip.update();
// PHI2 low to high: drives OPA
{
let mut phi2_guard = fast_phi2.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
fast_chip.update();
// Now data should be valid (4-bit value)
let data = fast_chip.read_data_bus();
// Read OPA (low nibble of 0x09 = 9)
let opa = fast_chip.read_data_bus();
assert_eq!(opa, 0x09, "OPA should be low nibble of 0x09");
// Verify full byte
let data = (opr << 4) | opa;
assert_eq!(data, 0x09);
}
@ -1082,88 +1199,127 @@ mod tests {
}
chip.update();
// PHI1 low to high: Address phase for high nibble
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
chip.update();
// Set high nibble on data bus
// Set high nibble on data bus BEFORE PHI1 rising (ROM latches on rising edge)
chip.write_data_bus(((address >> 4) & 0x0F) as u8);
chip.update();
// PHI1 high to low: Complete address phase for high nibble
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
chip.update();
// PHI1 low to high again: Address phase for low nibble
// PHI1 low to high: ROM latches high nibble on rising edge
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
chip.update();
// Set low nibble on data bus
chip.write_data_bus((address & 0x0F) as u8);
chip.update();
// PHI1 high to low: Complete address phase
// PHI1 high to low
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
chip.update();
// Wait for the address to be fully latched and state to transition
thread::sleep(Duration::from_nanos(10));
// Set low nibble on data bus BEFORE PHI1 rising
chip.write_data_bus((address & 0x0F) as u8);
// PHI1 low to high: ROM latches low nibble, address complete → WaitLatency
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
chip.update();
// Check if we're in WaitLatency or DriveData state
// With very small access times, we might transition directly to DriveData
// Should be in WaitLatency now
let state = chip.get_timing_state();
assert!(
state == TimingState::WaitLatency || state == TimingState::DriveData,
"Expected WaitLatency or DriveData, got {:?}",
state == TimingState::WaitLatency,
"Expected WaitLatency after latching both nibbles, got {:?}",
state
);
// Wait for latency if needed
let current_state = chip.get_timing_state();
if current_state == TimingState::WaitLatency {
// Get the actual access time from the ROM
let access_time_ns = chip.get_access_time();
thread::sleep(Duration::from_nanos(access_time_ns + 10)); // Add small buffer
chip.update(); // Update after waiting to check latency
}
// Release data bus from test control to allow ROM to drive it
for i in 0..4 {
let pin = chip.get_pin(&format!("D{}", i)).unwrap();
let mut pin_guard = pin.lock();
pin_guard.set_driver(Some("TEST".to_string()), PinValue::HighZ);
}
// Cycle through a3_wait_cycles: need multiple PHI2 rising edges
// IMPORTANT: Only change ONE pin per update() call to avoid spurious edge detection.
// The ROM detects edges by comparing current vs previous pin values.
// Step 1: PHI1 high to low (PHI2 stays low from before)
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
chip.update();
// PHI2 low to high: Data phase
// Step 2: PHI2 low to high (a3_wait_cycles decrements)
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
chip.update();
assert_eq!(chip.get_timing_state(), TimingState::DriveData);
// Wait a bit for data to be driven
thread::sleep(Duration::from_nanos(10));
chip.update(); // Additional update to ensure data is driven on the bus
// Step 3: PHI2 high to low
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
chip.update();
// Check data on bus
let data = chip.read_data_bus();
// Step 4: PHI2 low to high (a3_wait_cycles=0, transitions WaitLatency → DriveData)
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
chip.update();
// PHI2 high to low: Cleanup
// Step 5: PHI2 high to low
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
chip.update();
// Step 6: PHI2 low to high: first data drive = OPR (high nibble)
// After step 4 transitioned to DriveData, this is the first PHI2 rising
// where the ROM actually drives data (nibble_phase=false = OPR)
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
chip.update();
// Read OPR (high nibble) from data bus
let opr = chip.read_data_bus();
// Step 7: PHI2 high to low: ROM toggles nibble phase to OPA
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
chip.update();
// Step 8: PHI2 low to high: second data drive = OPA (low nibble)
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
chip.update();
// Read OPA (low nibble) from data bus
let opa = chip.read_data_bus();
// Drop SYNC/CM to prevent ROM from re-triggering
{
let mut sync_guard = sync_pin.lock();
sync_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
{
let mut cm_guard = cm_pin.lock();
cm_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
// PHI2 high to low: ROM returns to Idle
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
@ -1171,6 +1327,10 @@ mod tests {
chip.update();
assert_eq!(chip.get_timing_state(), TimingState::Idle);
// Combine OPR and OPA to form the full byte
let data = (opr << 4) | opa;
assert_eq!(data, expected_data);
data
}
}