rusty_emu/tests/intel_400x_tests.rs
Jan-Peter von Hunnius 6a335a15d4 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.
2026-05-12 19:32:01 +02:00

1336 lines
47 KiB
Rust

use parking_lot::Mutex;
use rusty_emu::component::{BaseComponent, Component};
use rusty_emu::components::common::intel_400x::*;
use rusty_emu::components::memory::intel_4001::{Intel4001, IoDirection, IoMode};
use rusty_emu::pin::{Pin, PinValue};
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
// Mock component for testing trait implementations
#[derive(Debug)]
struct MockComponent {}
impl MockComponent {
fn new() -> Self {
Self {}
}
}
impl Component for MockComponent {
fn name(&self) -> String {
"MockComponent".to_string()
}
fn pins(&self) -> HashMap<String, Arc<Mutex<rusty_emu::pin::Pin>>> {
HashMap::new()
}
fn get_pin(&self, name: &str) -> Result<Arc<Mutex<rusty_emu::pin::Pin>>, String> {
// For testing, we'll return an error since we don't have real pins
Err(format!("Mock pin not implemented for {}", name))
}
fn update(&mut self) {
// No-op for mock
}
fn run(&mut self) {
// No-op for mock
}
fn stop(&mut self) {
// No-op for mock
}
fn is_running(&self) -> bool {
false
}
fn set_running(&mut self, _running: bool) {
// No-op for mock
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Intel400xAddressHandling for MockComponent {
fn get_base(&self) -> &BaseComponent {
unimplemented!("MockComponent doesn't contain BaseComponent")
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn test_timing_constants() {
// Test that timing constants have expected values
assert_eq!(
TimingConstants::DEFAULT_ACCESS_TIME,
Duration::from_nanos(500)
);
assert_eq!(TimingConstants::FAST_ACCESS_TIME, Duration::from_nanos(200));
assert_eq!(TimingConstants::ADDRESS_SETUP, Duration::from_nanos(100));
assert_eq!(TimingConstants::DATA_VALID, Duration::from_nanos(200));
}
#[test]
fn test_assemble_full_address() {
// Create a mock trait implementation for testing
struct TestAddressHandler {}
impl TestAddressHandler {
fn new() -> Self {
Self {}
}
}
impl Intel400xAddressHandling for TestAddressHandler {
fn get_base(&self) -> &BaseComponent {
unimplemented!("TestAddressHandler doesn't contain BaseComponent")
}
}
let handler = TestAddressHandler::new();
// Test address assembly with valid nibbles
assert_eq!(
handler.assemble_full_address(Some(0x0F), Some(0x03)),
Some(0xF3)
);
assert_eq!(
handler.assemble_full_address(Some(0x02), Some(0x04)),
Some(0x24)
);
assert_eq!(
handler.assemble_full_address(Some(0x00), Some(0x00)),
Some(0x00)
);
assert_eq!(
handler.assemble_full_address(Some(0x0F), Some(0x0F)),
Some(0xFF)
);
// Test with missing nibbles
assert_eq!(handler.assemble_full_address(None, Some(0x23)), None);
assert_eq!(handler.assemble_full_address(Some(0x0F), None), None);
assert_eq!(handler.assemble_full_address(None, None), None);
}
#[test]
fn test_timing_state_machine() {
// Test state machine properties
let idle_state = TimingState::Idle;
let address_state = TimingState::AddressPhase;
let wait_state = TimingState::WaitLatency;
let drive_state = TimingState::DriveData;
// Test state queries
assert!(idle_state.is_idle());
assert!(!idle_state.is_address_phase());
assert!(!idle_state.is_waiting_latency());
assert!(!idle_state.is_driving_data());
assert!(!address_state.is_idle());
assert!(address_state.is_address_phase());
assert!(!address_state.is_waiting_latency());
assert!(!address_state.is_driving_data());
assert!(!wait_state.is_idle());
assert!(!wait_state.is_address_phase());
assert!(wait_state.is_waiting_latency());
assert!(!wait_state.is_driving_data());
assert!(!drive_state.is_idle());
assert!(!drive_state.is_address_phase());
assert!(!drive_state.is_waiting_latency());
assert!(drive_state.is_driving_data());
}
#[test]
fn test_memory_state_conversions() {
// Test conversions between MemoryState and TimingState
let mem_idle = MemoryState::Idle;
let timing_idle: TimingState = mem_idle.into();
assert_eq!(timing_idle, TimingState::Idle);
let mem_address = MemoryState::AddressPhase;
let timing_address: TimingState = mem_address.into();
assert_eq!(timing_address, TimingState::AddressPhase);
let mem_wait = MemoryState::WaitLatency;
let timing_wait: TimingState = mem_wait.into();
assert_eq!(timing_wait, TimingState::WaitLatency);
let mem_drive = MemoryState::DriveData;
let timing_drive: TimingState = mem_drive.into();
assert_eq!(timing_drive, TimingState::DriveData);
// Test reverse conversions
let back_to_mem: MemoryState = timing_idle.into();
assert_eq!(back_to_mem, MemoryState::Idle);
}
#[test]
fn test_ram_state_conversions() {
// Test conversions between RamState and TimingState
let ram_idle = RamState::Idle;
let timing_idle: TimingState = ram_idle.into();
assert_eq!(timing_idle, TimingState::Idle);
let ram_read = RamState::ReadData;
let timing_drive: TimingState = ram_read.into();
assert_eq!(timing_drive, TimingState::DriveData);
let ram_write = RamState::WriteData;
let timing_drive: TimingState = ram_write.into();
assert_eq!(timing_drive, TimingState::DriveData);
let ram_output = RamState::OutputPort;
let timing_drive: TimingState = ram_output.into();
assert_eq!(timing_drive, TimingState::DriveData);
}
#[test]
fn test_utils_create_driver_name() {
assert_eq!(create_driver_name("COMP1", "DATA"), "COMP1_DATA");
assert_eq!(create_driver_name("MEMORY", "ADDR"), "MEMORY_ADDR");
assert_eq!(create_driver_name("CPU", "CONTROL"), "CPU_CONTROL");
}
#[test]
fn test_data_bus_bit_operations() {
// Test bit manipulation for data bus operations
let test_values = vec![
(0x00, [false, false, false, false]),
(0x01, [true, false, false, false]),
(0x02, [false, true, false, false]),
(0x04, [false, false, true, false]),
(0x08, [false, false, false, true]),
(0x05, [true, false, true, false]),
(0x0A, [false, true, false, true]),
(0x0F, [true, true, true, true]),
];
for (value, expected_bits) in test_values {
// Test bit extraction (reading from bus)
for i in 0..4 {
let bit_value = (value >> i) & 1;
assert_eq!(bit_value == 1, expected_bits[i as usize]);
}
// Test bit setting (writing to bus)
let mut reconstructed = 0;
for i in 0..4 {
if expected_bits[i as usize] {
reconstructed |= 1 << i;
}
}
assert_eq!(value, reconstructed);
}
}
#[test]
fn test_address_latching_logic() {
// Test the address latching algorithm
let mut high_nibble: Option<u8> = None;
let mut low_nibble: Option<u8> = None;
let mut full_address_ready = false;
let mut address_latch_time: Option<Instant> = None;
let access_time = Duration::from_nanos(100);
// Mock address handler for testing the algorithm
struct TestAddressHandler {}
impl TestAddressHandler {
fn new() -> Self {
Self {}
}
}
impl Intel400xAddressHandling for TestAddressHandler {
fn get_base(&self) -> &BaseComponent {
unimplemented!("TestAddressHandler doesn't contain BaseComponent")
}
}
let handler = TestAddressHandler::new();
// First cycle: high nibble
handler.handle_address_latching(
0x12,
&mut high_nibble,
&mut low_nibble,
&mut full_address_ready,
&mut address_latch_time,
access_time,
);
assert_eq!(high_nibble, Some(0x12));
assert_eq!(low_nibble, None);
assert_eq!(full_address_ready, false);
assert_eq!(address_latch_time, None);
// Second cycle: low nibble
handler.handle_address_latching(
0x34,
&mut high_nibble,
&mut low_nibble,
&mut full_address_ready,
&mut address_latch_time,
access_time,
);
assert_eq!(high_nibble, None); // Should be cleared
assert_eq!(low_nibble, None); // Should be cleared
assert_eq!(full_address_ready, true);
assert!(address_latch_time.is_some());
// Verify the assembled address
let assembled = handler.assemble_full_address(Some(0x02), Some(0x04));
assert_eq!(assembled, Some(0x24));
}
#[test]
fn test_latency_timing_logic() {
let access_time = Duration::from_nanos(100);
// Test with no latch time (should return false)
assert_eq!(
Intel400xAddressHandling::handle_latency_wait(
&MockComponent::new(),
&None,
access_time
),
false
);
// 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(),
&recent_time,
access_time
),
true
);
// Note: Testing with elapsed time would require sleeping or mocking time
// This demonstrates the logic structure is correct
}
#[test]
fn test_intel4001_integration_debugging() {
println!("=== Intel 4001 Integration Test: Setup ===");
let mut rom = Intel4001::new_with_access_time("TestROM".to_string(), 1); // 1ns for fast testing
rom.base.set_running(true);
// Load test data: simple arithmetic sequence
let test_data = vec![
0x01, 0x02, 0x03, 0x04, 0x05, 0x10, 0x20, 0x30, 0x40, 0x50, 0xFF,
];
rom.load_rom_data(test_data.clone(), 0).unwrap();
// Verify test data loaded
for (i, &data) in test_data.iter().enumerate() {
assert_eq!(rom.read_rom(i as u8).unwrap(), data);
}
// Get pin references
let sync_pin = rom.get_pin("SYNC").unwrap();
let cm_pin = rom.get_pin("CM").unwrap();
let ci_pin = rom.get_pin("CI").unwrap();
let reset_pin = rom.get_pin("RESET").unwrap();
let phi1_pin = rom.get_pin("PHI1").unwrap();
let phi2_pin = rom.get_pin("PHI2").unwrap();
let d0_pin = rom.get_pin("D0").unwrap();
let d1_pin = rom.get_pin("D1").unwrap();
let d2_pin = rom.get_pin("D2").unwrap();
let d3_pin = rom.get_pin("D3").unwrap();
// Set settlement time to 0 for immediate testing
sync_pin.lock().set_settlement_time(Duration::from_nanos(0));
cm_pin.lock().set_settlement_time(Duration::from_nanos(0));
ci_pin.lock().set_settlement_time(Duration::from_nanos(0));
reset_pin
.lock()
.set_settlement_time(Duration::from_nanos(0));
phi1_pin.lock().set_settlement_time(Duration::from_nanos(0));
phi2_pin.lock().set_settlement_time(Duration::from_nanos(0));
d0_pin.lock().set_settlement_time(Duration::from_nanos(0));
d1_pin.lock().set_settlement_time(Duration::from_nanos(0));
d2_pin.lock().set_settlement_time(Duration::from_nanos(0));
d3_pin.lock().set_settlement_time(Duration::from_nanos(0));
// Initially, all data pins should be HighZ
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);
// Test reset (RESET is active LOW)
println!("=== Testing Reset ===");
{
let mut reset_guard = reset_pin.lock();
reset_guard.set_driver(Some("TEST".to_string()), PinValue::Low); // Assert reset
}
rom.update();
assert_eq!(rom.get_timing_state(), TimingState::Idle);
assert_eq!(rom.get_io_mode(), IoMode::Input);
// Release reset (set HIGH for normal operation)
{
let mut reset_guard = reset_pin.lock();
reset_guard.set_driver(Some("TEST".to_string()), PinValue::High); // Release reset
}
rom.update();
// Test memory read at address 0x00
println!("=== Memory Access Testing: Address 0x00 ===");
test_memory_read(
&mut rom, &sync_pin, &cm_pin, &ci_pin, &phi1_pin, &phi2_pin, &d0_pin, &d1_pin, &d2_pin,
&d3_pin, 0x00, 0x01,
);
// Test memory read at address 0xFF
// Load test data at address 0xFF specifically
// Note: Intel 4004 has a 4-bit data bus, so we can only read 4 bits at a time
// The value 0x0F will be read as 0x0F (low nibble only)
rom.load_rom_data(vec![0x0F], 0xFF).unwrap();
println!("=== Memory Access Testing: Address 0xFF ===");
test_memory_read(
&mut rom, &sync_pin, &cm_pin, &ci_pin, &phi1_pin, &phi2_pin, &d0_pin, &d1_pin, &d2_pin,
&d3_pin, 0xFF, 0x0F, // 4-bit value (low nibble only)
);
// Test I/O operations
println!("=== I/O Integration Testing ===");
// Note: I/O operations on Intel 4001 require proper clock timing and control signals.
// The handle_io_operation() method requires:
// - CM (chip_select) = HIGH
// - CI (io_select) = HIGH
// - Proper clock edges to trigger the operation
// For this test, we'll verify the direct I/O port access methods work correctly
// since full I/O timing simulation would require more complex test setup.
// Test direct I/O port write (using set_io_port API)
rom.set_io_mode(IoMode::Output);
rom.set_io_port(0, 0x05).unwrap();
assert_eq!(rom.get_io_port(0).unwrap(), 0x05);
// Test I/O port read (using get_io_port API)
rom.set_io_port(0, 0x0A).unwrap();
assert_eq!(rom.get_io_port(0).unwrap(), 0x0A);
// Test that I/O direction is set to Output when we write
assert_eq!(rom.get_io_direction(0).unwrap(), IoDirection::Output);
// Debugging features
println!("=== Debugging Features ===");
rom.debug_state_transitions("Integration Test");
// Pin state snapshots
println!(
"Pin states: SYNC: {:?}, CM: {:?}, CI: {:?}, PHI1: {:?}, PHI2: {:?}",
sync_pin.lock().read(),
cm_pin.lock().read(),
ci_pin.lock().read(),
phi1_pin.lock().read(),
phi2_pin.lock().read()
);
println!("Test completed successfully!");
}
fn test_memory_read(
rom: &mut Intel4001,
sync_pin: &Arc<Mutex<Pin>>,
cm_pin: &Arc<Mutex<Pin>>,
ci_pin: &Arc<Mutex<Pin>>,
phi1_pin: &Arc<Mutex<Pin>>,
phi2_pin: &Arc<Mutex<Pin>>,
d0_pin: &Arc<Mutex<Pin>>,
d1_pin: &Arc<Mutex<Pin>>,
d2_pin: &Arc<Mutex<Pin>>,
d3_pin: &Arc<Mutex<Pin>>,
address: u8,
expected_data: u8,
) {
// Reset to known state first (RESET is active LOW)
{
let reset_pin = rom.get_pin("RESET").unwrap();
let mut reset_guard = reset_pin.lock();
reset_guard.set_driver(Some("TEST".to_string()), PinValue::Low); // Assert reset (active LOW)
}
rom.update();
{
let reset_pin = rom.get_pin("RESET").unwrap();
let mut reset_guard = reset_pin.lock();
reset_guard.set_driver(Some("TEST".to_string()), PinValue::High); // Release reset (HIGH = normal operation)
}
rom.update();
// Set SYNC=1, CM=1, CI=0 for memory read
{
let mut sync_guard = sync_pin.lock();
let mut cm_guard = cm_pin.lock();
let mut ci_guard = ci_pin.lock();
sync_guard.set_driver(Some("TEST".to_string()), PinValue::High);
cm_guard.set_driver(Some("TEST".to_string()), PinValue::High);
ci_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
rom.update();
// Ensure PHI2 is low initially
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
rom.update();
// Ensure PHI1 is low initially
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
rom.update();
// Start the component
rom.base.set_running(true);
// 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);
}
rom.update();
assert_eq!(rom.get_timing_state(), TimingState::AddressPhase);
// PHI1 high to low
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
rom.update();
// 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();
// After latching both nibbles, ROM should be in WaitLatency
let state = rom.get_timing_state();
assert!(
state == TimingState::WaitLatency,
"Expected WaitLatency after latching both nibbles, got {:?}",
state
);
// 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: 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);
}
rom.update();
assert_eq!(rom.get_timing_state(), TimingState::DriveData);
// Wait a bit for data to be driven
thread::sleep(Duration::from_nanos(10));
// 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: 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);
}
rom.update();
assert_eq!(rom.get_timing_state(), TimingState::Idle);
// Tri-state pins
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);
}
#[test]
fn test_minimum_access_time() {
// Test behavior at minimum access time (200ns) with comprehensive edge cases
let mut chip = Intel4001::new_with_access_time("MIN_TIME_ROM".to_string(), 200); // 200ns minimum access time
chip.base.set_running(true);
// Get pin references
let sync_pin = chip.get_pin("SYNC").unwrap();
let cm_pin = chip.get_pin("CM").unwrap();
let ci_pin = chip.get_pin("CI").unwrap();
let reset_pin = chip.get_pin("RESET").unwrap();
let phi1_pin = chip.get_pin("PHI1").unwrap();
let phi2_pin = chip.get_pin("PHI2").unwrap();
let d0_pin = chip.get_pin("D0").unwrap();
let d1_pin = chip.get_pin("D1").unwrap();
let d2_pin = chip.get_pin("D2").unwrap();
let d3_pin = chip.get_pin("D3").unwrap();
// Set settlement time to 0 for immediate testing
sync_pin.lock().set_settlement_time(Duration::from_nanos(0));
cm_pin.lock().set_settlement_time(Duration::from_nanos(0));
ci_pin.lock().set_settlement_time(Duration::from_nanos(0));
reset_pin
.lock()
.set_settlement_time(Duration::from_nanos(0));
phi1_pin.lock().set_settlement_time(Duration::from_nanos(0));
phi2_pin.lock().set_settlement_time(Duration::from_nanos(0));
d0_pin.lock().set_settlement_time(Duration::from_nanos(0));
d1_pin.lock().set_settlement_time(Duration::from_nanos(0));
d2_pin.lock().set_settlement_time(Duration::from_nanos(0));
d3_pin.lock().set_settlement_time(Duration::from_nanos(0));
// Test memory read at minimum timing
// Note: Intel 4004 has a 4-bit data bus, so we can only read 4 bits at a time
// The full 8-bit read would require M1/M2 cycles (not yet implemented)
let address = 0x12;
let expected_data = 0x04; // 4-bit value (low nibble only for single-cycle read)
// Set up ROM data
chip.load_rom_data(vec![expected_data], address).unwrap();
// Test memory read using the helper function
test_memory_read(
&mut chip,
&sync_pin,
&cm_pin,
&ci_pin,
&phi1_pin,
&phi2_pin,
&d0_pin,
&d1_pin,
&d2_pin,
&d3_pin,
address as u8,
expected_data,
);
// Verify state machine transitions correctly at minimum timing
assert_eq!(chip.get_timing_state(), TimingState::Idle);
// Test multiple consecutive reads at minimum timing
// Note: Intel 4004 has a 4-bit data bus, so we can only read 4 bits at a time
// The expected data must be a 4-bit value (0x00-0x0F)
for addr in 0x10..0x15 {
// Use 4-bit values that fit on the data bus
let expected_data = (addr & 0x0F) as u8; // Low nibble only
let test_data = vec![expected_data];
chip.load_rom_data(test_data, addr).unwrap();
test_memory_read(
&mut chip,
&sync_pin,
&cm_pin,
&ci_pin,
&phi1_pin,
&phi2_pin,
&d0_pin,
&d1_pin,
&d2_pin,
&d3_pin,
addr as u8,
expected_data,
);
}
// Enhanced edge case testing for minimum access time
// Test edge case: consecutive reads at minimum access time
// Note: Intel 4004 has a 4-bit data bus, so expected data must be 0-15
for address in 0x00..0x10 {
// Use addresses 0x00-0x0F where low nibble equals address
let expected_data = (address & 0x0F) as u8; // 4-bit value
let result1 = test_memory_read_edge_case(
&mut chip,
&sync_pin,
&cm_pin,
&ci_pin,
&phi1_pin,
&phi2_pin,
&d0_pin,
&d1_pin,
&d2_pin,
&d3_pin,
address as u16,
expected_data,
);
assert_eq!(
result1, expected_data,
"Failed consecutive read 1 at address {} at minimum access time",
address
);
}
// Test edge case: rapid address changes at minimum access time
// Note: Intel 4004 has a 4-bit data bus, so expected data must be 0-15
let test_addresses = [0, 255, 128, 1, 254, 2, 253, 3];
for &address in &test_addresses {
let expected_data = (address & 0x0F) as u8; // 4-bit value for data bus
chip.load_rom_data(vec![expected_data], address).unwrap();
let result = test_memory_read_edge_case(
&mut chip,
&sync_pin,
&cm_pin,
&ci_pin,
&phi1_pin,
&phi2_pin,
&d0_pin,
&d1_pin,
&d2_pin,
&d3_pin,
address as u16,
expected_data,
);
assert_eq!(
result, expected_data,
"Failed rapid address change test at address {} at minimum access time",
address
);
}
// Test edge case: minimum access time with I/O operations
// Note: Intel 4004 has a 4-bit data bus, so expected data must be 0-15
chip.load_rom_data(vec![0x05], 0x00).unwrap(); // Test pattern (4-bit value)
chip.load_rom_data(vec![0x0A], 0xFF).unwrap(); // Another test pattern (4-bit value)
// Test I/O pin behavior during minimum access time reads
let io_pins = ["IO0", "IO1", "IO2", "IO3"];
// Set I/O pins to output mode
for pin_name in &io_pins {
let _io_pin = chip.get_pin(pin_name).unwrap();
// Note: In the actual implementation, I/O pin mode is controlled through the chip's I/O methods
// For this test, we'll verify the I/O functionality through the chip's methods
}
// Test memory read while I/O operations are active
let result = test_memory_read_edge_case(
&mut chip, &sync_pin, &cm_pin, &ci_pin, &phi1_pin, &phi2_pin, &d0_pin, &d1_pin,
&d2_pin, &d3_pin, 0x00, 0x05,
);
assert_eq!(
result, 0x05,
"Failed memory read with active I/O at minimum access time"
);
// Test edge case: minimum access time with boundary addresses
// Note: Intel 4004 has a 4-bit data bus, so expected data must be 0-15
let boundary_addresses = [0, 1, 254, 255];
for &address in &boundary_addresses {
let expected_data = (address & 0x0F) as u8; // 4-bit value for data bus
chip.load_rom_data(vec![expected_data], address).unwrap();
let result = test_memory_read_edge_case(
&mut chip,
&sync_pin,
&cm_pin,
&ci_pin,
&phi1_pin,
&phi2_pin,
&d0_pin,
&d1_pin,
&d2_pin,
&d3_pin,
address as u16,
expected_data,
);
assert_eq!(
result, expected_data,
"Failed boundary address test at {} at minimum access time",
address
);
}
// Test state machine transitions at minimum access time
// Verify proper state progression through all phases
// Note: Intel 4004 has a 4-bit data bus, so we can only read 4 bits at a time
let mut test_chip = Intel4001::new_with_access_time("STATE_TEST_ROM".to_string(), 200);
test_chip.base.set_running(true);
test_chip.load_rom_data(vec![0x02], 0x10).unwrap(); // 4-bit value (low nibble only)
// Get pins for state machine test
let test_sync = test_chip.get_pin("SYNC").unwrap();
let test_cm = test_chip.get_pin("CM").unwrap();
let test_ci = test_chip.get_pin("CI").unwrap();
let test_phi1 = test_chip.get_pin("PHI1").unwrap();
let test_phi2 = test_chip.get_pin("PHI2").unwrap();
// Set settlement times to 0
test_sync
.lock()
.set_settlement_time(Duration::from_nanos(0));
test_cm.lock().set_settlement_time(Duration::from_nanos(0));
test_ci.lock().set_settlement_time(Duration::from_nanos(0));
test_phi1
.lock()
.set_settlement_time(Duration::from_nanos(0));
test_phi2
.lock()
.set_settlement_time(Duration::from_nanos(0));
// Initialize PHI2 to Low to ensure proper edge detection
{
let mut phi2_guard = test_phi2.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
test_chip.update();
// Manually step through the state machine to verify timing
// Set control pins for memory read
{
let mut sync_guard = test_sync.lock();
let mut cm_guard = test_cm.lock();
let mut ci_guard = test_ci.lock();
sync_guard.set_driver(Some("TEST".to_string()), PinValue::High);
cm_guard.set_driver(Some("TEST".to_string()), PinValue::High);
ci_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
test_chip.update();
assert_eq!(test_chip.get_timing_state(), TimingState::Idle);
// Ensure PHI1 is low initially
{
let mut phi1_guard = test_phi1.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
test_chip.update();
// 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);
}
test_chip.update();
assert_eq!(test_chip.get_timing_state(), TimingState::AddressPhase);
// PHI1 falling edge
{
let mut phi1_guard = test_phi1.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
test_chip.update();
// 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);
// 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);
}
// 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();
// 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);
}
test_chip.update();
assert_eq!(test_chip.get_timing_state(), TimingState::DriveData);
// 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);
// 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);
}
test_chip.update();
assert_eq!(test_chip.get_timing_state(), TimingState::Idle);
// Test edge case: timing violations at minimum access time
// Test what happens when we try to read too quickly
let mut fast_chip = Intel4001::new_with_access_time("FAST_VIOLATION_ROM".to_string(), 200);
fast_chip.base.set_running(true);
fast_chip.load_rom_data(vec![0x09], 0x50).unwrap(); // Use 4-bit value (0x09)
let fast_sync = fast_chip.get_pin("SYNC").unwrap();
let fast_cm = fast_chip.get_pin("CM").unwrap();
let fast_ci = fast_chip.get_pin("CI").unwrap();
let fast_phi1 = fast_chip.get_pin("PHI1").unwrap();
let fast_phi2 = fast_chip.get_pin("PHI2").unwrap();
// Set settlement times to 0
fast_sync
.lock()
.set_settlement_time(Duration::from_nanos(0));
fast_cm.lock().set_settlement_time(Duration::from_nanos(0));
fast_ci.lock().set_settlement_time(Duration::from_nanos(0));
fast_phi1
.lock()
.set_settlement_time(Duration::from_nanos(0));
fast_phi2
.lock()
.set_settlement_time(Duration::from_nanos(0));
// Initialize PHI2 to Low to ensure proper edge detection
{
let mut phi2_guard = fast_phi2.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
fast_chip.update();
// Set up for memory read
{
let mut sync_guard = fast_sync.lock();
let mut cm_guard = fast_cm.lock();
let mut ci_guard = fast_ci.lock();
sync_guard.set_driver(Some("TEST".to_string()), PinValue::High);
cm_guard.set_driver(Some("TEST".to_string()), PinValue::High);
ci_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
fast_chip.update();
// Ensure PHI1 is low initially
{
let mut phi1_guard = fast_phi1.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
fast_chip.update();
// Write high nibble BEFORE PHI1 rising (ROM latches on rising edge)
fast_chip.write_data_bus(0x05); // High nibble of address 0x50
{
let mut phi1_guard = fast_phi1.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
fast_chip.update();
// PHI1 falling edge
{
let mut phi1_guard = fast_phi1.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
fast_chip.update();
// 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();
// 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);
}
// 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();
// 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();
// 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);
}
// Helper function for edge case memory read testing
fn test_memory_read_edge_case(
chip: &mut Intel4001,
sync_pin: &Arc<Mutex<Pin>>,
cm_pin: &Arc<Mutex<Pin>>,
ci_pin: &Arc<Mutex<Pin>>,
phi1_pin: &Arc<Mutex<Pin>>,
phi2_pin: &Arc<Mutex<Pin>>,
_d0_pin: &Arc<Mutex<Pin>>,
_d1_pin: &Arc<Mutex<Pin>>,
_d2_pin: &Arc<Mutex<Pin>>,
_d3_pin: &Arc<Mutex<Pin>>,
address: u16,
expected_data: u8,
) -> u8 {
// Load the expected data into ROM at the specified address
chip.load_rom_data(vec![expected_data], address as usize)
.unwrap();
// Reset to known state
chip.perform_reset();
chip.update();
// Start the component
chip.base.set_running(true);
// Set control pins for memory read
{
let mut sync_guard = sync_pin.lock();
let mut cm_guard = cm_pin.lock();
let mut ci_guard = ci_pin.lock();
sync_guard.set_driver(Some("TEST".to_string()), PinValue::High);
cm_guard.set_driver(Some("TEST".to_string()), PinValue::High);
ci_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
chip.update();
// Ensure PHI2 is low initially
{
let mut phi2_guard = phi2_pin.lock();
phi2_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
chip.update();
// Ensure PHI1 is low initially
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
chip.update();
// Set high nibble on data bus BEFORE PHI1 rising (ROM latches on rising edge)
chip.write_data_bus(((address >> 4) & 0x0F) as u8);
// 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();
// PHI1 high to low
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
chip.update();
// 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();
// Should be in WaitLatency now
let state = chip.get_timing_state();
assert!(
state == TimingState::WaitLatency,
"Expected WaitLatency after latching both nibbles, got {:?}",
state
);
// 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();
// 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();
// 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();
// 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();
// 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);
}
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
}
}