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.
1665 lines
64 KiB
Rust
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);
|
|
}
|
|
}
|