rusty_emu/src/components/memory/intel_4001.rs
Jan-Peter von Hunnius 08f53d68fe feat: implement virtual clock timing architecture (Phases 5-8)
Introduces a VirtualClock-based timing system replacing wall-clock
Instant-based timing for hardware-accurate simulation.

New modules:
- virtual_clock.rs: Shared virtual time source with condvar sync
- real_time_governor.rs: Thread that advances virtual time
- component_timing.rs: Per-component timing constraints

Key changes:
- Component trait: added update_at_time(), timing(), set_virtual_clock_optional()
- All Intel 400x components migrated to virtual-time-aware operation
- ConfigurableSystem: start() returns Result, spawns governor thread
- GUI: timing controls (speed slider, mode selector, step button)
- Console: speed/mode/step/time commands
- Backward compatible: legacy fallback for components without VirtualClock

Reviewed-by: code review found 2 warnings (clock resume() dead code,
reset() safety docs) — both fixed.
2026-05-14 21:00:22 +02:00

1665 lines
64 KiB
Rust

use parking_lot::Mutex;
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use crate::component::{BaseComponent, Component, RunnableComponent};
use crate::component_timing::ComponentTiming;
use crate::components::common::intel_400x::{
process_event_direct, Intel400xAddressHandling, Intel400xClockHandling,
Intel400xComponentBehavior, Intel400xComponentType, Intel400xControlPins, Intel400xDataBus,
Intel400xEvent, Intel400xResetHandling, Intel400xStateMachine, Intel400xTimingState,
Intel400xUnifiedState, TimingState,
};
use crate::pin::{Pin, PinValue};
use crate::virtual_clock::VirtualClock;
/// Intel 4001 - 256-byte ROM with integrated I/O
/// Part of the MCS-4 family, designed to work with Intel 4004 CPU
/// Features 256 bytes of mask-programmable ROM and 4 I/O pins
///
/// Hardware Deviations:
/// - I/O read results are written to D0-D3 for test convenience
/// (real hardware reads directly from I/O pins)
/// - SYNC logic simplified for unit testing (may need refinement for CPU integration)
pub struct Intel4001 {
pub base: BaseComponent,
memory: Vec<u8>, // 256-byte ROM storage
last_address: u16, // Last accessed memory address
access_time: Duration, // ROM access latency (500ns)
output_latch: u8, // 4-bit output latch for I/O operations
input_latch: u8, // 4-bit input latch for I/O operations
io_mode: IoMode, // Current I/O mode configuration
io_ports: [u8; 4], // 4 I/O ports (4 bits each) - matches datasheet
io_direction: [IoDirection; 4], // I/O direction for each port
selected_io_port: Option<usize>, // Currently selected I/O port (0-3)
// Unified state machine implementation
state_machine: Intel400xStateMachine, // Unified state machine for Intel 400x components
// Clock edge detection
prev_phi1: PinValue, // Previous Φ1 clock state for edge detection
prev_phi2: PinValue, // Previous Φ2 clock state for edge detection
// M1/M2 data phase tracking
// M1: ROM drives instruction high nibble (OPR, bits 7-4)
// M2: ROM drives instruction low nibble (OPA, bits 3-0)
data_nibble_phase: bool, // false = OPR (high nibble), true = OPA (low nibble)
// A3 wait counter: after latching both address nibbles (A1+A2), the ROM
// must wait one more full clock cycle for the CPU's A3 cycle before driving data.
// This counter tracks how many PHI2 rising edges to wait in WaitLatency.
a3_wait_cycles: u8,
// Track whether data was actually driven during this PHI2 high period
data_driven_this_cycle: bool,
// Virtual time support (Phase 6)
virtual_clock: Option<Arc<VirtualClock>>, // Injected by ConfigurableSystem (Phase 7)
/// Virtual time snapshot for the current update cycle.
/// Set at the start of `update_at_time()` so all internal methods use a
/// consistent time value instead of reading `VirtualClock::now()` which
/// may have advanced since the cycle started.
current_update_time: Duration,
}
#[derive(Debug, Clone, Copy, PartialEq)]
/// I/O mode configuration for 4001 ROM
/// Determines how I/O pins are configured during read/write operations
pub enum IoMode {
Input, // I/O pins configured as inputs (RDM instruction)
Output, // I/O pins configured as outputs (WRM instruction)
Bidirectional, // I/O pins bidirectional (not used in 4001)
}
/// I/O direction for each I/O port
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum IoDirection {
Input, // Port configured as input
Output, // Port configured as output
}
impl Intel400xClockHandling for Intel4001 {
fn get_base(&self) -> &BaseComponent {
&self.base
}
}
impl Intel400xDataBus for Intel4001 {
fn get_base(&self) -> &BaseComponent {
&self.base
}
}
impl Intel400xAddressHandling for Intel4001 {
fn get_base(&self) -> &BaseComponent {
&self.base
}
}
impl Intel400xControlPins for Intel4001 {
fn get_base(&self) -> &BaseComponent {
&self.base
}
}
impl Intel400xResetHandling for Intel4001 {
fn get_base(&self) -> &BaseComponent {
&self.base
}
fn perform_reset(&mut self) {
// Reset operations specific to Intel4001
// Note: This is called from handle_reset, so we don't need to check reset pin again
self.state_machine.reset();
self.tri_state_data_bus();
self.tri_state_io_pins();
// Reset I/O state
self.io_ports = [0u8; 4];
self.io_direction = [IoDirection::Input; 4];
self.selected_io_port = None;
self.io_mode = IoMode::Input; // Reset I/O mode to Input
// Reset clock edge detection state to ensure proper edge detection after reset
// This is critical for tests that reset the chip between operations
self.prev_phi1 = PinValue::Low;
self.prev_phi2 = PinValue::Low;
// Reset A3 wait counter and data nibble phase
self.a3_wait_cycles = 0;
self.data_nibble_phase = false;
}
}
impl Intel400xTimingState for Intel4001 {
fn get_timing_state(&self) -> TimingState {
// Convert from Intel400xUnifiedState to TimingState
match self.state_machine.get_state() {
Intel400xUnifiedState::Idle => TimingState::Idle,
Intel400xUnifiedState::AddressPhase => TimingState::AddressPhase,
Intel400xUnifiedState::WaitLatency => TimingState::WaitLatency,
Intel400xUnifiedState::DriveData => TimingState::DriveData,
// Intel 4001 doesn't use component-specific states
_ => TimingState::Idle,
}
}
fn set_timing_state(&mut self, state: TimingState) {
// Convert from TimingState to Intel400xUnifiedState
let unified_state = match state {
TimingState::Idle => Intel400xUnifiedState::Idle,
TimingState::AddressPhase => Intel400xUnifiedState::AddressPhase,
TimingState::WaitLatency => Intel400xUnifiedState::WaitLatency,
TimingState::DriveData => Intel400xUnifiedState::DriveData,
};
// Use state machine's reset method to transition to new state
// This is a compatibility method for legacy state handling
self.state_machine.reset();
self.state_machine.current_state = unified_state;
}
fn get_address_latch_time(&self) -> Option<Duration> {
self.state_machine.get_address_latch_time()
}
fn set_address_latch_time(&mut self, time: Option<Duration>) {
self.state_machine.set_address_latch_time(time);
}
fn get_full_address_ready(&self) -> bool {
self.state_machine.is_full_address_ready()
}
fn set_full_address_ready(&mut self, _ready: bool) {
// This is handled by state machine, but keep for compatibility
}
fn get_address_high_nibble(&self) -> Option<u8> {
self.state_machine.get_address_high_nibble()
}
fn set_address_high_nibble(&mut self, nibble: Option<u8>) {
self.state_machine.set_address_high_nibble(nibble);
}
fn get_address_low_nibble(&self) -> Option<u8> {
self.state_machine.get_address_low_nibble()
}
fn set_address_low_nibble(&mut self, nibble: Option<u8>) {
self.state_machine.set_address_low_nibble(nibble);
}
fn get_access_time(&self) -> Duration {
self.state_machine.get_access_time()
}
fn is_latency_elapsed_at(&self, virtual_time: Duration) -> bool {
self.state_machine.is_latency_elapsed_at(virtual_time)
}
}
impl Intel400xComponentBehavior for Intel4001 {
fn get_component_type(&self) -> Intel400xComponentType {
Intel400xComponentType::Intel4001
}
fn on_state_entry(&mut self, state: Intel400xUnifiedState) {
match state {
Intel400xUnifiedState::Idle => {
// Entry actions for Idle state
self.tri_state_data_bus();
// Reset address state for next fetch
self.state_machine.set_address_high_nibble(None);
self.state_machine.set_address_low_nibble(None);
// Reset data driving state for next fetch
self.data_driven_this_cycle = false;
self.data_nibble_phase = false; // Reset to OPR phase
}
Intel400xUnifiedState::AddressPhase => {
// Entry actions for AddressPhase state
// Address latching will happen in execute_state_logic
// Reset address state to ensure clean latch
self.state_machine.set_address_high_nibble(None);
self.state_machine.set_address_low_nibble(None);
}
Intel400xUnifiedState::WaitLatency => {
// Entry actions for WaitLatency state
// Address is already latched, start access timer
}
Intel400xUnifiedState::DriveData => {
// Entry actions for DriveData state
// Data driving will happen in execute_state_logic
}
// Intel 4001 doesn't use component-specific states
_ => {} // Handle all other states
}
}
fn on_state_exit(&mut self, state: Intel400xUnifiedState) {
match state {
Intel400xUnifiedState::DriveData => {
// Exit actions for DriveData state
self.tri_state_data_bus();
}
_ => {} // Handle all other states
}
}
fn handle_component_event(&mut self, event: Intel400xEvent) -> bool {
match event {
Intel400xEvent::ClockPhi1Rising => {
// Handle Φ1 rising edge
self.handle_phi1_rising();
true // Event was handled
}
Intel400xEvent::ClockPhi1Falling => {
// Handle Φ1 falling edge
self.handle_phi1_falling();
true // Event was handled
}
Intel400xEvent::ClockPhi2Rising => {
// Handle Φ2 rising edge
self.handle_phi2_rising();
true // Event was handled
}
Intel400xEvent::ClockPhi2Falling => {
// Handle Φ2 falling edge
self.handle_phi2_falling();
true // Event was handled
}
Intel400xEvent::ResetActive => {
// Handle reset
self.perform_reset();
true // Event was handled
}
_ => false, // Let the generic state machine handle other events
}
}
fn execute_state_logic(&mut self, state: Intel400xUnifiedState) {
match state {
Intel400xUnifiedState::Idle => {
// In idle state, ensure bus is tri-stated
self.tri_state_data_bus();
}
Intel400xUnifiedState::AddressPhase => {
// Handle address latching during address phase
self.handle_address_latching();
}
Intel400xUnifiedState::WaitLatency => {
// Check if latency has elapsed using virtual time.
//
// **With VirtualClock (production):** `address_latch_time` was recorded
// by `handle_address_nibble_at()` in virtual time. We check if enough
// virtual time has elapsed since then (≥ 500ns access time).
//
// **Without VirtualClock (standalone tests):** `current_virtual_time()`
// returns `Duration::ZERO`, so `is_latency_elapsed_at()` would always
// return `false` (since virtual time never advances). The `!has_clock`
// guard short-circuits to skip the wait immediately.
//
// NOTE: The old `Instant::now()`-based code would naturally wait ~500ns
// because wall-clock time always advances. This new code skips the wait
// in legacy mode, making standalone tests less hardware-accurate but
// preventing them from hanging forever in WaitLatency.
let has_clock = self.virtual_clock.is_some();
let vtime = self.current_update_time;
if !has_clock || self.state_machine.is_latency_elapsed_at(vtime) {
self.process_latency_elapsed();
}
}
Intel400xUnifiedState::DriveData => {
// Drive data on bus
self.handle_data_driving();
}
// Intel 4001 doesn't use component-specific states
_ => {}
}
}
fn responds_to_address(&self) -> bool {
true // Intel 4001 responds to address transmission
}
}
impl Intel4001 {
/// Create a new Intel 4001 ROM with specified access time
/// Parameters: name - Component identifier, access_time_ns - Memory access time in nanoseconds
/// Returns: New Intel4001 instance with configurable access timing
pub fn new(name: String) -> Self {
Self::new_with_access_time(name, 500) // Default 500ns access time
}
/// Create a new Intel 4001 ROM with custom access time (for testing)
/// Parameters: name - Component identifier, access_time_ns - Memory access time in nanoseconds
/// Returns: New Intel4001 instance with configurable access timing
pub fn new_with_access_time(name: String, access_time_ns: u64) -> Self {
// Intel 4001 has 256 bytes of ROM
let rom_size = 256;
// Intel 4001 pinout (per datasheet):
// - 4 data pins (D0-D3) for multiplexed address/data
// - 4 I/O pins (IO0-IO3)
// - Control pins: SYNC, RESET, CM, CI
// - Clock pins: Φ1, Φ2 (two-phase clock from 4004 CPU)
//
// Control pin behavior:
// - SYNC: Marks start of instruction cycle
// - RESET: Clears internal state
// - CM: Chip select (must be HIGH for ROM access)
// - CI: I/O select (distinguishes I/O vs memory when chip selected)
let pin_names = vec![
"D0", "D1", "D2", "D3", // Data/Address pins
"IO0", "IO1", "IO2", "IO3", // I/O pins
"SYNC", // Sync signal
"CM", // CM-ROM: ROM/RAM Chip Select
"CI", // CM-RAM: RAM Chip Select (I/O vs ROM access)
"RESET", // Reset
"PHI1", // Clock phase 1
"PHI2", // Clock phase 2
];
let pins = BaseComponent::create_pin_map(&pin_names, &name);
let memory = vec![0u8; rom_size];
Intel4001 {
base: BaseComponent::new(name, pins),
memory,
last_address: 0,
access_time: Duration::from_nanos(access_time_ns),
output_latch: 0,
input_latch: 0,
io_mode: IoMode::Input,
io_ports: [0u8; 4], // Initialize all I/O ports to 0
io_direction: [IoDirection::Input; 4], // Default all ports to input
selected_io_port: None, // No I/O port selected initially
state_machine: Intel400xStateMachine::new(
Intel400xComponentType::Intel4001,
Duration::from_nanos(access_time_ns),
),
prev_phi1: PinValue::Low,
prev_phi2: PinValue::Low,
data_nibble_phase: false, // Start with OPR (high nibble)
a3_wait_cycles: 0, // No A3 wait initially
data_driven_this_cycle: false, // No data driven yet
virtual_clock: None,
current_update_time: Duration::ZERO,
}
}
/// Inject a VirtualClock for virtual time-based operation.
/// When set, `update()` uses virtual time and `run()` uses `wait_until()`.
/// This method is called by ConfigurableSystem in Phase 7.
pub fn set_virtual_clock(&mut self, clock: Arc<VirtualClock>) {
self.virtual_clock = Some(clock);
}
/// Get current virtual time, or Duration::ZERO if no VirtualClock is injected.
///
/// # Zero-Time Invariant
///
/// When no VirtualClock is injected, this returns `Duration::ZERO`. The `WaitLatency`
/// handler checks `vtime == Duration::ZERO` to skip latency enforcement in legacy mode.
/// This is intentional: legacy tests don't use VirtualClock and expect latency to be
/// skipped for backward compatibility. If this ever returns a non-zero value without
/// a proper VirtualClock, the latency check would use zero as the latch time and
/// produce incorrect results.
fn current_virtual_time(&self) -> Duration {
self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now())
}
/// Set memory access time for simulation
/// Parameters: access_time_ns - Access time in nanoseconds
pub fn set_access_time(&mut self, access_time_ns: u64) {
self.access_time = Duration::from_nanos(access_time_ns);
self.state_machine
.set_access_time(Duration::from_nanos(access_time_ns));
}
/// Get current memory access time
/// Returns: Access time in nanoseconds
pub fn get_access_time(&self) -> u64 {
self.access_time.as_nanos() as u64
}
/// Load binary data into ROM at specified offset
/// Parameters: data - Binary data to load, offset - Starting address
/// Returns: Ok(()) on success, Err(String) on failure
pub fn load_rom_data(&mut self, data: Vec<u8>, offset: usize) -> Result<(), String> {
if offset + data.len() > self.memory.len() {
return Err(format!(
"Data exceeds ROM capacity: offset {} + data length {} > ROM size {}",
offset,
data.len(),
self.memory.len()
));
}
self.memory[offset..offset + data.len()].copy_from_slice(&data);
Ok(())
}
/// Load hexadecimal data into ROM at specified offset
/// Parameters: hex_data - Space-separated hex bytes, offset - Starting address
/// Returns: Ok(()) on success, Err(String) on failure
pub fn load_from_hex(&mut self, hex_data: &str, offset: usize) -> Result<(), String> {
let bytes: Result<Vec<u8>, _> = hex_data
.split_whitespace()
.map(|s| u8::from_str_radix(s.trim(), 16))
.collect();
match bytes {
Ok(data) => self.load_rom_data(data, offset),
Err(e) => Err(format!("Invalid hex data: {}", e)),
}
}
/// Data bus methods now use common functionality
fn read_io_pins(&self) -> u8 {
let mut data = 0;
for i in 0..4 {
if let Ok(pin) = self.base.get_pin(&format!("IO{}", i)) {
let pin_guard = pin.lock();
if pin_guard.read() == PinValue::High {
data |= 1 << i;
}
}
}
data
}
/// Handle Φ1 rising edge - Start of address phase
/// Hardware: Φ1 high = CPU starts driving bus with address/control information
/// Memory operations start when SYNC goes high during Φ1 rising edge
/// Focus: Start memory operations, I/O operations
///
/// Intel 4004 Address Timing (CPU drives address during PHI1):
/// - A1 cycle: CPU drives PC high nibble on PHI1 rising
/// - A2 cycle: CPU drives PC low nibble on PHI1 rising
/// - ROM latches each nibble on PHI1 RISING edge (CPU has just driven the address)
fn handle_phi1_rising(&mut self) {
// Handle system reset first (highest priority)
self.handle_reset("RESET");
// Check for memory operation start on Φ1 rising edge with SYNC high
// Hardware: Memory operations start when SYNC goes high during Φ1
// Note: SYNC marks instruction fetch start, but exact logic involves CPU cycle state.
// ROM access: CM=1 (chip_select), CI=0 (!io_select)
// I/O access: CM=1 (chip_select), CI=1 (io_select)
let sync = self.read_sync_pin();
let chip_select = self.read_cm_rom_pin();
let io_select = self.read_ci_pin();
if sync && chip_select && !io_select {
// Start memory address phase on Φ1 rising edge (ROM access)
// Only start a new memory operation if we're currently idle
if self.state_machine.get_state() == Intel400xUnifiedState::Idle {
self.process_chip_select();
}
}
// Latch address nibble on PHI1 rising edge
// The CPU drives the address on PHI1 rising, and since CPU update runs
// before ROM update, the address is already on the bus at this point.
if self.state_machine.get_state() == Intel400xUnifiedState::AddressPhase {
let nibble = self.read_data_bus() & 0x0F;
if self.state_machine.handle_address_nibble_at(nibble, self.current_update_time) {
// Address is complete — virtual time was recorded atomically by handle_address_nibble_at()
// Transition to latency wait
let _addr = self.state_machine.get_full_address().unwrap_or(0xFFFF);
self.process_address_complete();
// Set A3 wait counter
self.a3_wait_cycles = 1;
}
}
// Handle I/O operations (higher priority than memory operations)
// I/O operations: CM=1 (chip_select), CI=1 (io_select)
// Note: SYNC is NOT required for I/O - only marks instruction fetch (ROM access)
if chip_select && io_select {
self.handle_io_operation();
}
}
/// Handle Φ1 falling edge - No specific action needed
/// Address latching is now done on PHI1 rising edge
fn handle_phi1_falling(&mut self) {
// Address latching moved to PHI1 rising edge
}
/// Handle Φ2 rising edge - Data phase
/// Hardware: Φ2 high = Peripherals drive bus with data
/// Focus: Data driving operations
fn handle_phi2_rising(&mut self) {
// Handle A3 wait cycle: after address complete, wait one full clock cycle
// for the CPU's A3 cycle before transitioning to DriveData.
if self.state_machine.get_state() == Intel400xUnifiedState::WaitLatency {
if self.a3_wait_cycles > 0 {
// Still waiting for A3 cycle to complete
self.a3_wait_cycles -= 1;
} else {
// A3 wait complete, transition to DriveData.
// IMPORTANT: Do NOT drive data on this same edge.
// The first data drive happens on the NEXT PHI2 rising edge.
// This ensures the ROM's OPR/OPA phases align with the CPU's M1/M2 cycles.
self.process_latency_elapsed();
return; // Skip data driving on this edge
}
}
// Drive data if in DriveData state (only on subsequent PHI2 rising edges)
if self.state_machine.get_state() == Intel400xUnifiedState::DriveData {
self.handle_data_driving();
}
}
/// Handle Φ2 falling edge - End of data phase
/// Hardware: Φ2 low = End of data driving phase, time to tri-state bus
/// Data is held on bus during Φ2 high, tri-stated when Φ2 falls
/// Focus: Clean up and return to idle state
fn handle_phi2_falling(&mut self) {
match self.state_machine.get_state() {
Intel400xUnifiedState::DriveData => {
if self.data_driven_this_cycle {
// Data was actually driven this cycle - toggle nibble phase
if !self.data_nibble_phase {
// Just finished OPR (M1), switch to OPA (M2)
self.data_nibble_phase = true;
self.tri_state_data_bus();
// Stay in DriveData for M2 cycle
} else {
// Just finished OPA (M2) - full 8-bit instruction sent
// Return to idle so the next fetch can begin
self.data_nibble_phase = false;
self.tri_state_data_bus();
self.process_operation_complete();
}
} else {
// No data driven this cycle (e.g., just entered DriveData)
// Don't toggle - just tri-state
self.tri_state_data_bus();
}
}
Intel400xUnifiedState::Idle
| Intel400xUnifiedState::AddressPhase
| Intel400xUnifiedState::WaitLatency => {
// For other states, just ensure bus is tri-stated
self.tri_state_data_bus();
}
// Intel 4001 doesn't use component-specific states
_ => {
self.tri_state_data_bus();
}
}
}
fn tri_state_io_pins(&self) {
for i in 0..4 {
if let Ok(pin) = self.base.get_pin(&format!("IO{}", i)) {
let mut pin_guard = pin.lock();
pin_guard.set_driver(Some(self.base.name()), PinValue::HighZ);
}
}
}
/// Set I/O mode for WRM/RDM instruction handling
/// Parameters: mode - I/O mode (Input for RDM, Output for WRM)
pub fn set_io_mode(&mut self, mode: IoMode) {
self.io_mode = mode;
}
/// Get current I/O mode
/// Returns: Current I/O mode
pub fn get_io_mode(&self) -> IoMode {
self.io_mode
}
/// Handle I/O port operations according to Intel 4001 datasheet
/// Hardware: I/O operations occur during I/O instructions (WRM/RDM)
/// Direction determined by CPU instruction, not manual configuration
/// Note: I/O pins remain driven after WRM until another WRM or reset.
/// This matches real 4001 hardware where I/O ports are latched.
///
/// I/O Port Addressing (datasheet):
/// - I/O ports are addressed using lower 2 bits of address
/// - Port 0: Address 0x00, Port 1: Address 0x01, etc.
/// - Each port is 4 bits wide (matches data bus width)
///
/// I/O Latch Persistence:
/// - Output latch persists after WRM until next WRM or reset
/// - Input latch overwrites every RDM (I/O input pins are live reads)
fn handle_io_operation(&mut self) {
let chip_select = self.read_cm_rom_pin();
let io_select = self.read_ci_pin(); // Proper CI pin for I/O vs ROM selection
// I/O operations occur when: CM=1 (chip_select), CI=1 (io_select)
// Note: SYNC is NOT required for I/O - only marks instruction fetch (ROM access)
if chip_select && io_select {
let port_address = self.read_data_bus() & 0x03; // Lower 2 bits for port selection (0-3)
self.selected_io_port = Some(port_address as usize);
// Determine operation type based on current instruction phase
// In real hardware, this would be determined by CPU instruction (WRM vs RDM)
match self.io_mode {
IoMode::Input => {
// RDM instruction: Read from I/O port
if let Some(port) = self.selected_io_port {
self.input_latch = self.read_io_port(port);
// Write input data to data bus for CPU to read
self.write_data_bus(self.input_latch);
}
}
IoMode::Output => {
// WRM instruction: Write to I/O port
if let Some(port) = self.selected_io_port {
let data = self.read_data_bus();
self.write_io_port(port, data);
}
}
IoMode::Bidirectional => {
// Not used in 4001, treat as input
if let Some(port) = self.selected_io_port {
self.input_latch = self.read_io_port(port);
self.write_data_bus(self.input_latch);
}
}
}
}
}
/// Read CI (I/O select) pin state
/// Hardware: CI pin distinguishes I/O vs ROM access when chip selected (CM=1)
fn read_ci_pin(&self) -> bool {
if let Ok(pin) = self.base.get_pin("CI") {
let pin_guard = pin.lock();
pin_guard.read() == PinValue::High
} else {
false
}
}
/// Read from a specific I/O port
/// Parameters: port - I/O port number (0-3)
/// Returns: 4-bit value from I/O port
fn read_io_port(&self, port: usize) -> u8 {
if port < 4 {
match self.io_direction[port] {
IoDirection::Input => {
// Read from actual I/O pins
self.read_io_pins()
}
IoDirection::Output => {
// Read from output latch (for read-back capability)
self.io_ports[port]
}
}
} else {
0
}
}
/// Write to a specific I/O port
/// Parameters: port - I/O port number (0-3), data - 4-bit data to write
fn write_io_port(&mut self, port: usize, data: u8) {
if port < 4 {
self.io_ports[port] = data & 0x0F;
self.io_direction[port] = IoDirection::Output;
self.update_io_pins();
}
}
/// Update I/O pins based on current port values and directions
/// Hardware: Only output ports drive I/O pins
fn update_io_pins(&self) {
for i in 0..4 {
if let Ok(pin) = self.base.get_pin(&format!("IO{}", i)) {
let mut pin_guard = pin.lock();
match self.io_direction[i] {
IoDirection::Output => {
// Drive pin with output port value
let bit_value = (self.io_ports[i]) & 1;
let pin_value = if bit_value == 1 {
PinValue::High
} else {
PinValue::Low
};
pin_guard.set_driver(Some(self.base.name()), pin_value);
}
IoDirection::Input => {
// Input mode - tri-state pin
pin_guard.set_driver(Some(self.base.name()), PinValue::HighZ);
}
}
}
}
}
/// Handle memory address-related operations during Φ1
/// Hardware-accurate: Φ1 is when CPU drives address, so we handle address latching
/// Memory operation start is checked in handle_phi1_rising() on SYNC high + Φ1 rising
/// Focus: Address latching and setup operations
#[allow(dead_code)] // This method is kept for compatibility but not currently used
fn handle_memory_address_operations(&mut self) {
match self.state_machine.get_state() {
Intel400xUnifiedState::Idle => {
// In idle state, ensure bus is tri-stated
// Memory operation start is checked in handle_phi1_rising()
self.tri_state_data_bus();
}
Intel400xUnifiedState::AddressPhase => {
// Currently latching address nibbles during Φ1
self.handle_address_latching();
}
Intel400xUnifiedState::WaitLatency => {
// Address latched, waiting for access latency
// Check if latency has elapsed, and we can transition to data phase
self.handle_latency_wait();
}
Intel400xUnifiedState::DriveData => {
// Data phase should be handled by Φ2, not Φ1
// Just ensure bus is tri-stated during wrong phase
self.tri_state_data_bus();
}
// Intel 4001 doesn't use component-specific states
_ => {
self.tri_state_data_bus();
}
}
}
/// Handle memory data-related operations during Φ2
/// Hardware-accurate: Φ2 is when peripherals drive data, so we handle data driving
/// Focus: Data driving operations
#[allow(dead_code)] // This method is kept for compatibility but not currently used
fn handle_memory_data_operations(&mut self) {
// Reduced debug output - only show when actually driving data
match self.state_machine.get_state() {
Intel400xUnifiedState::Idle => {
// During data phase, idle state means tri-state bus
self.tri_state_data_bus();
}
Intel400xUnifiedState::AddressPhase => {
// Address phase should be handled by Φ1, not Φ2
// Tri-state bus during wrong phase
self.tri_state_data_bus();
}
Intel400xUnifiedState::WaitLatency => {
// Address latched, waiting for access latency
// Check if latency has elapsed, and we can transition to data phase
self.handle_latency_wait();
// If we transitioned to DriveData, handle data driving
if self.state_machine.get_state() == Intel400xUnifiedState::DriveData {
self.handle_data_driving();
}
}
Intel400xUnifiedState::DriveData => {
// Latency elapsed, drive data on bus during Φ2
// Data will remain on bus until Φ2 falling edge
self.handle_data_driving();
}
// Intel 4001 doesn't use component-specific states
_ => {
self.tri_state_data_bus();
}
}
}
/// Handle address nibble latching during address phase
/// Hardware: CPU provides 8-bit address as two 4-bit nibbles
fn handle_address_latching(&mut self) {
// Use common address latching functionality
let nibble = self.read_data_bus() & 0x0F; // Only 4 bits are valid for address nibbles
if self.state_machine.handle_address_nibble_at(nibble, self.current_update_time) {
// Address is complete — virtual time was recorded atomically by handle_address_nibble_at()
// Transition to latency wait
self.process_address_complete();
}
}
/// Handle latency timing during wait state
/// Hardware: ROM needs 500ns to access data after address is latched
#[allow(dead_code)] // This method is kept for compatibility with legacy code
fn handle_latency_wait(&mut self) {
// Note: Legacy method — latency check now requires virtual time
// Always transition for backward compatibility
self.process_latency_elapsed();
}
/// Handle data driving during DriveData state
/// Hardware: ROM drives data bus when all conditions are met
/// Data stays on bus during Φ2 high period, tri-stated on Φ2 falling edge
/// IMPORTANT: Do NOT return to idle here after OPA - the data must stay on the bus
/// for the CPU to read. Return to idle happens in handle_phi2_falling().
fn handle_data_driving(&mut self) {
// Reset the data-driven flag before each drive attempt
self.data_driven_this_cycle = false;
// Once we're in DriveData state, we should drive data regardless of current control signals
// The control signals were already checked when we started memory operation
if self.state_machine.is_full_address_ready() {
// Drive data on bus
// Data will remain on bus until Φ2 falling edge
let address = self.state_machine.get_full_address().unwrap_or(0) as u16;
if (address as usize) < self.memory.len() {
let data = self.memory[address as usize];
// Intel 4004 data bus is 4 bits wide. Instructions are 8 bits.
// M1 (data_nibble_phase=false): Drive high nibble (OPR, bits 7-4)
// M2 (data_nibble_phase=true): Drive low nibble (OPA, bits 3-0)
let nibble = if self.data_nibble_phase {
data & 0x0F // OPA: low nibble
} else {
(data >> 4) & 0x0F // OPR: high nibble
};
self.write_data_bus(nibble);
self.data_driven_this_cycle = true;
// Do NOT return to idle here! The data must persist on the bus
// for the CPU to read. Return to idle happens in handle_phi2_falling().
} else {
// Invalid address, tri-state
self.tri_state_data_bus();
}
} else {
// Address not ready, tri-state
self.tri_state_data_bus();
}
}
/// Helper function to process latency elapsed event
fn process_latency_elapsed(&mut self) {
let old_state = self.state_machine.get_state();
let event = Intel400xEvent::LatencyElapsed;
let new_state = process_event_direct(&mut self.state_machine, event);
// Handle state entry/exit manually
if new_state != old_state {
self.on_state_exit(old_state);
self.state_machine.current_state = new_state;
self.on_state_entry(new_state);
}
}
/// Helper function to process chip select event
fn process_chip_select(&mut self) {
let old_state = self.state_machine.get_state();
let event = Intel400xEvent::ChipSelect;
let new_state = process_event_direct(&mut self.state_machine, event);
if new_state != old_state {
self.on_state_exit(old_state);
self.on_state_entry(new_state);
}
}
/// Helper function to process operation complete event
fn process_operation_complete(&mut self) {
let old_state = self.state_machine.get_state();
let event = Intel400xEvent::OperationComplete;
let new_state = process_event_direct(&mut self.state_machine, event);
if new_state != old_state {
self.on_state_exit(old_state);
self.on_state_entry(new_state);
}
}
/// Helper function to process address complete event
fn process_address_complete(&mut self) {
let old_state = self.state_machine.get_state();
let event = Intel400xEvent::AddressComplete;
let new_state = process_event_direct(&mut self.state_machine, event);
if new_state != old_state {
self.on_state_exit(old_state);
self.on_state_entry(new_state);
}
}
/// Helper function to execute unified state machine logic
/// Note: This method is reserved for future use when state machine execution
/// needs to be triggered explicitly rather than through clock edge handlers
#[allow(dead_code)]
fn execute_state_machine(&mut self) {
// Execute the current state logic using the unified state machine
let current_state = self.state_machine.get_state();
self.execute_state_logic(current_state);
}
}
impl Component for Intel4001 {
fn name(&self) -> String {
self.base.name()
}
fn pins(&self) -> HashMap<String, Arc<Mutex<Pin>>> {
self.base.pins()
}
fn get_pin(&self, name: &str) -> Result<Arc<Mutex<Pin>>, String> {
self.base.get_pin(name)
}
/// Main update cycle - delegates to update_at_time with current virtual time
/// Hardware: Responds to Φ1 and Φ2 clock edges from CPU
fn update(&mut self) {
let vtime = self.current_virtual_time();
self.update_at_time(vtime);
}
/// Virtual-time-aware update cycle - handles clock edge detection and operation dispatch
/// Hardware: Responds to Φ1 and Φ2 clock edges from CPU
fn update_at_time(&mut self, virtual_time: Duration) {
// Snapshot virtual_time for the entire update cycle so internal methods
// (handle_phi1_rising, execute_state_logic, etc.) use a consistent time
// instead of reading VirtualClock::now() which may have advanced.
self.current_update_time = virtual_time;
if !self.is_running() {
return;
}
// Handle both rising and falling edges for proper two-phase operation
let phi1_rising = self.is_phi1_rising_edge(self.prev_phi1);
let phi1_falling = self.is_phi1_falling_edge(self.prev_phi1);
let phi2_rising = self.is_phi2_rising_edge(self.prev_phi2);
let phi2_falling = self.is_phi2_falling_edge(self.prev_phi2);
// Update clock states for next edge detection
let (phi1, phi2) = self.read_clock_pins();
self.prev_phi1 = phi1;
self.prev_phi2 = phi2;
// Check for reset first (highest priority, independent of clock edges)
let reset_active = self.read_reset_pin();
if reset_active {
self.perform_reset();
// Skip further processing if reset is active
return;
}
// Process clock events through component-specific handlers
// IMPORTANT ORDER: Handle falling edges BEFORE rising edges.
// When PHI1 rises and PHI2 falls simultaneously (same half-cycle),
// we must complete the previous operation (PHI2 falling) before
// starting a new one (PHI1 rising). This ensures the ROM returns
// to idle from OPA completion before starting a new address phase.
if phi2_falling {
self.handle_phi2_falling();
}
if phi1_falling {
self.handle_phi1_falling();
}
if phi1_rising {
self.handle_phi1_rising();
}
if phi2_rising {
self.handle_phi2_rising();
}
// Note: execute_state_machine() is NOT called here because:
// 1. Address latching is handled directly in handle_phi1_rising()
// 2. Data driving is handled directly in handle_phi2_rising()
// 3. Calling it would cause double-latching of address nibbles
}
/// Run component with VirtualClock support or legacy fallback
/// Hardware: Simulates continuous operation with clock edge detection
fn run(&mut self) {
self.base.set_running(true);
// Initialize clock states for edge detection
let (phi1, phi2) = self.read_clock_pins();
self.prev_phi1 = phi1;
self.prev_phi2 = phi2;
// If no VirtualClock is injected, fall back to wall-clock run loop
let vclock = match self.virtual_clock.as_ref() {
Some(vc) => Arc::clone(vc),
None => {
// Legacy mode: use wall-clock timing
while self.is_running() {
self.update();
thread::sleep(Duration::from_micros(1));
}
return;
}
};
// Virtual time mode: wait for next tick to synchronize with clock
while self.is_running() {
let next_tick = vclock.now() + vclock.tick_size();
if let Some(current_vtime) = vclock.wait_until(next_tick) {
self.update_at_time(current_vtime);
} else {
// Clock was interrupted, exit
break;
}
}
}
/// Get timing constraints for Intel 4001 ROM
/// Returns: ComponentTiming with 500ns access time, 200ns propagation delay
fn timing(&self) -> ComponentTiming {
ComponentTiming::intel_4001()
}
/// Stop component and tri-state all outputs
/// Hardware: Component enters low-power state, all pins high-impedance
fn stop(&mut self) {
self.base.set_running(false);
// Tri-state all outputs when stopped
self.tri_state_data_bus();
self.tri_state_io_pins();
}
fn is_running(&self) -> bool {
self.base.is_running()
}
fn set_running(&mut self, running: bool) {
self.base.set_running(running);
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn set_virtual_clock_optional(&mut self, clock: Option<Arc<VirtualClock>>) {
self.virtual_clock = clock;
}
fn has_virtual_clock(&self) -> bool {
self.virtual_clock.is_some()
}
}
impl RunnableComponent for Intel4001 {}
// Intel 4001 specific methods
impl Intel4001 {
/// Debug: get current state machine state name
pub fn debug_state(&self) -> &'static str {
match self.state_machine.get_state() {
Intel400xUnifiedState::Idle => "Idle",
Intel400xUnifiedState::AddressPhase => "AddrPhase",
Intel400xUnifiedState::WaitLatency => "WaitLatency",
Intel400xUnifiedState::DriveData => "DriveData",
_ => "Unknown",
}
}
/// Debug: get current data nibble phase
pub fn debug_nibble_phase(&self) -> bool {
self.data_nibble_phase
}
/// Get ROM size in bytes
/// Returns: Total number of bytes in ROM (256 for 4001)
pub fn get_rom_size(&self) -> usize {
self.memory.len()
}
/// Read a byte from ROM at specified address
/// Parameters: address - 8-bit address (0-255)
/// Returns: Some(data) if address valid, None if out of bounds
pub fn read_rom(&self, address: u8) -> Option<u8> {
if (address as usize) < self.memory.len() {
Some(self.memory[address as usize])
} else {
None
}
}
/// Get current output latch value
/// Returns: 4-bit value last written to I/O ports
pub fn get_output_latch(&self) -> u8 {
self.output_latch
}
/// Get current input latch value
/// Returns: 4-bit value last read from I/O ports
pub fn get_input_latch(&self) -> u8 {
self.input_latch
}
/// Get I/O port value
/// Parameters: port - I/O port number (0-3)
/// Returns: Some(4-bit value) if port valid, None if invalid
pub fn get_io_port(&self, port: usize) -> Option<u8> {
if port < 4 {
Some(self.io_ports[port])
} else {
None
}
}
/// Set I/O port value (for testing/debugging)
/// Parameters: port - I/O port number (0-3), data - 4-bit data to set
/// Returns: Ok(()) on success, Err(String) on failure
pub fn set_io_port(&mut self, port: usize, data: u8) -> Result<(), String> {
if port < 4 {
self.io_ports[port] = data & 0x0F;
self.io_direction[port] = IoDirection::Output;
self.update_io_pins();
Ok(())
} else {
Err("I/O port number out of range (0-3)".to_string())
}
}
/// Get I/O port direction
/// Parameters: port - I/O port number (0-3)
/// Returns: Some(direction) if port valid, None if invalid
pub fn get_io_direction(&self, port: usize) -> Option<IoDirection> {
if port < 4 {
Some(self.io_direction[port])
} else {
None
}
}
/// Set I/O port direction (for testing/debugging)
/// Parameters: port - I/O port number (0-3), direction - I/O direction
/// Returns: Ok(()) on success, Err(String) on failure
pub fn set_io_direction(&mut self, port: usize, direction: IoDirection) -> Result<(), String> {
if port < 4 {
self.io_direction[port] = direction;
self.update_io_pins();
Ok(())
} else {
Err("I/O port number out of range (0-3)".to_string())
}
}
/// Get currently selected I/O port
/// Returns: Some(port_number) if a port is selected, None otherwise
pub fn get_selected_io_port(&self) -> Option<usize> {
self.selected_io_port
}
/// Debug function to log state transitions for troubleshooting
/// Parameters: test_name - Name of the test for context
pub fn debug_state_transitions(&self, test_name: &str) {
println!(
"{} - State: {:?}, High: {:?}, Low: {:?}, Address: 0x{:x}, Ready: {}, Latch time: {:?}",
test_name,
self.state_machine.get_state(),
self.state_machine.get_address_high_nibble(),
self.state_machine.get_address_low_nibble(),
self.last_address,
self.state_machine.is_full_address_ready(),
self.state_machine.get_address_latch_time()
);
}
}
// Custom formatter for debugging
impl std::fmt::Display for IoMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IoMode::Input => write!(f, "Input"),
IoMode::Output => write!(f, "Output"),
IoMode::Bidirectional => write!(f, "Bidirectional"),
}
}
}
impl std::fmt::Display for IoDirection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IoDirection::Input => write!(f, "Input"),
IoDirection::Output => write!(f, "Output"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_intel4001_creation() {
let rom = Intel4001::new("ROM_4001".to_string());
assert_eq!(rom.name(), "ROM_4001");
assert_eq!(rom.get_rom_size(), 256);
assert_eq!(rom.get_access_time(), 500); // Default 500ns
assert!(!rom.is_running());
}
#[test]
fn test_intel4001_rom_loading() {
let mut rom = Intel4001::new("ROM_4001".to_string());
let test_data = vec![0x12, 0x34, 0x56, 0x78];
assert!(rom.load_rom_data(test_data.clone(), 0).is_ok());
assert_eq!(rom.read_rom(0).unwrap(), 0x12);
assert_eq!(rom.read_rom(1).unwrap(), 0x34);
assert_eq!(rom.read_rom(2).unwrap(), 0x56);
assert_eq!(rom.read_rom(3).unwrap(), 0x78);
}
#[test]
fn test_intel4001_io_modes() {
let rom = Intel4001::new("ROM_4001".to_string());
// Test that I/O mode starts as Input (after reset)
// Note: IoMode is now internal and not exposed via public API
// I/O direction is determined by CPU instructions (WRM/RDM)
assert_eq!(rom.name(), "ROM_4001");
}
#[test]
fn test_intel4001_latches() {
let rom = Intel4001::new("ROM_4001".to_string());
// Initial state
assert_eq!(rom.get_output_latch(), 0);
assert_eq!(rom.get_input_latch(), 0);
// These would be set during actual I/O operations
// The test verifies latch structures exist
}
#[test]
fn test_io_mode_display() {
assert_eq!(IoMode::Input.to_string(), "Input");
assert_eq!(IoMode::Output.to_string(), "Output");
assert_eq!(IoMode::Bidirectional.to_string(), "Bidirectional");
}
#[test]
fn test_configurable_access_time() {
let mut rom = Intel4001::new("ROM_4001".to_string());
// Test default access time
assert_eq!(rom.get_access_time(), 500);
// Test setting custom access time
rom.set_access_time(100);
assert_eq!(rom.get_access_time(), 100);
// Test constructor with custom access time
let fast_rom = Intel4001::new_with_access_time("FAST_ROM".to_string(), 1);
assert_eq!(fast_rom.get_access_time(), 1);
assert_eq!(fast_rom.name(), "FAST_ROM");
}
#[test]
fn test_basic_memory_read() {
let mut rom = Intel4001::new_with_access_time("ROM_4001".to_string(), 1);
// Load test data
let test_data = vec![0x12, 0x34];
rom.load_rom_data(test_data, 0).unwrap();
// Test direct memory read (bypassing pin simulation)
assert_eq!(rom.read_rom(0x00).unwrap(), 0x12);
assert_eq!(rom.read_rom(0x01).unwrap(), 0x34);
}
#[test]
fn test_memory_operation_start() {
let mut rom = Intel4001::new_with_access_time("ROM_4001".to_string(), 1);
// Initially should be in idle state
assert_eq!(rom.state_machine.current_state, Intel400xUnifiedState::Idle);
// Start component so it can process updates
rom.base.set_running(true);
assert!(rom.is_running());
// 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 phi1_pin = rom.get_pin("PHI1").unwrap();
// Initialize PHI1 to Low first
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::Low);
}
// Set up memory read operation: SYNC=1, CM=1, CI=0 (memory access)
{
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); // Memory access
}
// Set Φ1 high (creating rising edge)
{
let mut phi1_guard = phi1_pin.lock();
phi1_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
// Debug: Check conditions that would trigger memory operation
let sync = rom.read_sync_pin();
let chip_select = rom.read_cm_rom_pin();
let io_select = rom.read_ci_pin();
println!(
"DEBUG: Conditions - sync: {}, chip_select: {}, io_select: {}, combined: {}",
sync,
chip_select,
io_select,
sync && chip_select && !io_select
);
// Update to process Φ1 rising edge
rom.update();
// Debug: Check edge detection results
println!("DEBUG: After update - prev_phi1: {:?}", rom.prev_phi1);
// Should have started memory operation and transitioned to AddressPhase
// because SYNC=1, CM=1, CI=0 triggers chip select on PHI1 rising edge
assert_eq!(
rom.state_machine.current_state,
Intel400xUnifiedState::AddressPhase
);
println!(
"Memory state after Φ1 rising: {:?}",
rom.state_machine.current_state
);
}
#[test]
fn test_clock_driven_memory_fetch() {
let mut rom = Intel4001::new_with_access_time("ROM_4001".to_string(), 1); // 1ns for fast testing
// Load test data: address 0x01 should contain 0x34
let test_data = vec![0x12, 0x34, 0x56, 0x78];
rom.load_rom_data(test_data, 0).unwrap();
// Get pin references for simulation
let sync_pin = rom.get_pin("SYNC").unwrap();
let cm_pin = rom.get_pin("CM").unwrap();
let ci_pin = rom.get_pin("CI").unwrap();
// Set up memory read operation: SYNC=1, chip_select=1, io_select=0 (memory access)
{
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); // Memory access, not I/O
}
// Test that memory state machine works correctly
// The state machine should handle two-phase addressing properly
rom.update();
// Verify that ROM correctly read test data
assert_eq!(rom.read_rom(0).unwrap(), 0x12);
assert_eq!(rom.read_rom(1).unwrap(), 0x34);
assert_eq!(rom.read_rom(2).unwrap(), 0x56);
assert_eq!(rom.read_rom(3).unwrap(), 0x78);
}
#[test]
fn test_reset_behavior() {
let mut rom = Intel4001::new_with_access_time("ROM_4001".to_string(), 1); // 1ns for fast testing
// Load test data
let test_data = vec![0x12, 0x34, 0x56, 0x78];
rom.load_rom_data(test_data, 0).unwrap();
// Set up some state first
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();
// Set memory operation active
{
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); // Memory access, not I/O
}
// Trigger some state changes
rom.update();
// Now assert RESET
{
let mut reset_guard = reset_pin.lock();
reset_guard.set_driver(Some("TEST".to_string()), PinValue::High);
}
// Update with RESET high - should clear all state
rom.update();
// Verify all state is cleared
assert_eq!(rom.get_output_latch(), 0);
assert_eq!(rom.get_input_latch(), 0);
// Verify data bus is tri-stated
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();
// After reset, all pins should be HighZ (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);
}
#[test]
fn test_tri_state_behavior() {
let mut rom = Intel4001::new_with_access_time("ROM_4001".to_string(), 1); // 1ns for fast testing
// Load test data
let test_data = vec![0x12, 0x34, 0x56, 0x78];
rom.load_rom_data(test_data, 0).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();
// Initially, no operation - should be 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);
// Set up memory operation but don't complete address phase
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 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); // Memory access, not I/O
// Trigger update - should start address phase but still tri-state
rom.update();
// Should still be tri-stated during address phase
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);
// Complete address phase but don't wait for latency
// Should still be tri-stated
rom.update();
// Should still be tri-stated (waiting for latency)
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_intel4001_state_transitions() {
let mut rom = Intel4001::new_with_access_time("StateTestROM".to_string(), 1);
// Test all state transitions
let states = vec![
TimingState::Idle,
TimingState::AddressPhase,
TimingState::WaitLatency,
TimingState::DriveData,
];
for &state in &states {
rom.set_timing_state(state);
assert_eq!(rom.get_timing_state(), state);
}
// Test return to idle
rom.set_timing_state(TimingState::Idle);
assert_eq!(rom.get_timing_state(), TimingState::Idle);
}
#[test]
fn test_intel4001_address_handling() {
let mut rom = Intel4001::new_with_access_time("AddressTestROM".to_string(), 1);
// Test address nibble handling
rom.set_address_high_nibble(Some(0x12));
rom.set_address_low_nibble(Some(0x34));
rom.set_full_address_ready(true);
assert_eq!(rom.get_address_high_nibble(), Some(0x12));
assert_eq!(rom.get_address_low_nibble(), Some(0x34));
assert_eq!(rom.state_machine.is_full_address_ready(), true);
// Test clearing
rom.set_address_high_nibble(None);
rom.set_address_low_nibble(None);
rom.set_full_address_ready(false);
assert_eq!(rom.get_address_high_nibble(), None);
assert_eq!(rom.get_address_low_nibble(), None);
assert_eq!(rom.state_machine.is_full_address_ready(), false);
}
#[test]
fn test_intel4001_error_conditions() {
let mut rom = Intel4001::new("ErrorTestROM".to_string());
// Test invalid memory access
assert_eq!(rom.read_rom(0xFF), Some(0x00)); // Default value for unmapped memory
// Test invalid data loading
assert!(rom.load_rom_data(vec![0x12], 255).is_ok()); // Valid
assert!(rom.load_rom_data(vec![0x12], 256).is_err()); // Invalid - out of bounds
}
#[test]
fn test_intel4001_io_port_operations() {
let mut rom = Intel4001::new("IOTestROM".to_string());
// Test I/O port initialization
assert_eq!(rom.get_io_port(0).unwrap(), 0);
assert_eq!(rom.get_io_port(1).unwrap(), 0);
assert_eq!(rom.get_io_port(2).unwrap(), 0);
assert_eq!(rom.get_io_port(3).unwrap(), 0);
// Test I/O direction initialization
assert_eq!(rom.get_io_direction(0).unwrap(), IoDirection::Input);
assert_eq!(rom.get_io_direction(1).unwrap(), IoDirection::Input);
// Test setting I/O port values
assert!(rom.set_io_port(0, 0x05).is_ok());
assert_eq!(rom.get_io_port(0).unwrap(), 0x05);
assert_eq!(rom.get_io_direction(0).unwrap(), IoDirection::Output);
// Test invalid port access
assert!(rom.set_io_port(4, 0x0F).is_err());
assert!(rom.get_io_port(4).is_none());
assert!(rom.set_io_direction(4, IoDirection::Output).is_err());
assert!(rom.get_io_direction(4).is_none());
}
#[test]
fn test_intel4001_io_mode_control() {
let mut rom = Intel4001::new("ModeTestROM".to_string());
// Test initial I/O mode
assert_eq!(rom.get_io_mode(), IoMode::Input);
// Test setting I/O modes
rom.set_io_mode(IoMode::Output);
assert_eq!(rom.get_io_mode(), IoMode::Output);
rom.set_io_mode(IoMode::Input);
assert_eq!(rom.get_io_mode(), IoMode::Input);
rom.set_io_mode(IoMode::Bidirectional);
assert_eq!(rom.get_io_mode(), IoMode::Bidirectional);
}
#[test]
fn test_intel4001_io_port_selection() {
let rom = Intel4001::new("SelectionTestROM".to_string());
// Initially no port selected
assert_eq!(rom.get_selected_io_port(), None);
// Test I/O port selection during operation
// This would be set during actual I/O operations
// For testing, we can verify field exists and can be modified
assert_eq!(rom.selected_io_port, None);
}
#[test]
fn test_intel4001_reset_io_behavior() {
let mut rom = Intel4001::new("ResetIOTestROM".to_string());
// Set up some I/O state
rom.set_io_port(0, 0x05).unwrap();
rom.set_io_port(1, 0x0A).unwrap();
rom.set_io_direction(0, IoDirection::Output).unwrap();
rom.set_io_direction(1, IoDirection::Output).unwrap();
rom.set_io_mode(IoMode::Output);
// Verify state is set
assert_eq!(rom.get_io_port(0).unwrap(), 0x05);
assert_eq!(rom.get_io_port(1).unwrap(), 0x0A);
assert_eq!(rom.get_io_direction(0).unwrap(), IoDirection::Output);
assert_eq!(rom.get_io_mode(), IoMode::Output);
// Perform reset
rom.perform_reset();
// Verify I/O state is cleared
assert_eq!(rom.get_io_port(0).unwrap(), 0);
assert_eq!(rom.get_io_port(1).unwrap(), 0);
assert_eq!(rom.get_io_direction(0).unwrap(), IoDirection::Input);
assert_eq!(rom.get_io_mode(), IoMode::Input);
assert_eq!(rom.get_selected_io_port(), None);
}
}