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.
470 lines
17 KiB
Rust
470 lines
17 KiB
Rust
//! Per-component timing constraints for hardware-accurate simulation.
|
|
//!
|
|
//! Each `ComponentTiming` instance describes the real timing characteristics of a
|
|
//! hardware component. These values are used during simulation to enforce that
|
|
//! components behave realistically — for example, ROM won't drive data onto the bus
|
|
//! until its access time has elapsed in virtual time.
|
|
//!
|
|
//! # Hardware Accuracy
|
|
//!
|
|
//! If the clock is overclocked beyond a component's maximum frequency, or if timing
|
|
//! constraints are violated (setup time, hold time, access time), the component will
|
|
//! **not** produce correct results — just like real hardware. This is a feature, not
|
|
//! a bug: it allows users to observe what happens when they push hardware beyond its
|
|
//! limits.
|
|
//!
|
|
//! # Timing Violations
|
|
//!
|
|
//! When a timing violation is detected, it is recorded but does not crash the simulation.
|
|
//! The component simply produces incorrect or undefined results, as real hardware would.
|
|
|
|
use std::fmt;
|
|
use std::time::Duration;
|
|
|
|
/// Timing constraints for a hardware component.
|
|
///
|
|
/// These values are based on datasheet specifications and define the real-world
|
|
/// timing behavior that the simulation enforces.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub struct ComponentTiming {
|
|
/// Human-readable name for this timing profile (e.g., "Intel 4001 ROM").
|
|
pub name: &'static str,
|
|
|
|
/// Time from valid address on the bus to data being available.
|
|
///
|
|
/// For ROM/RAM: how long after the address is latched until the data output
|
|
/// is guaranteed to be valid.
|
|
///
|
|
/// Intel 4001 ROM: ~500ns
|
|
/// Intel 4002 RAM: ~500ns
|
|
/// Intel 4003 SR: ~200ns (faster due to simpler design)
|
|
pub access_time: Duration,
|
|
|
|
/// Time from a clock edge until an output signal changes.
|
|
///
|
|
/// This models the internal gate delay of the component.
|
|
/// After a clock edge is detected, the component's output doesn't change
|
|
/// instantly — it takes `propagation_delay` nanoseconds.
|
|
///
|
|
/// Intel 4001 ROM: ~200ns
|
|
/// Intel 4002 RAM: ~200ns
|
|
/// Intel 4003 SR: ~100ns
|
|
pub propagation_delay: Duration,
|
|
|
|
/// Minimum time that address/data inputs must be stable before a clock edge.
|
|
///
|
|
/// If the address changes within `setup_time` before a clock edge,
|
|
/// the component may latch an incorrect address — just like real hardware.
|
|
///
|
|
/// Intel 4001 ROM: ~100ns
|
|
/// Intel 4002 RAM: ~100ns
|
|
pub setup_time: Duration,
|
|
|
|
/// Minimum time that address/data inputs must remain stable after a clock edge.
|
|
///
|
|
/// If the address changes within `hold_time` after a clock edge,
|
|
/// the component's latched value may be corrupted.
|
|
///
|
|
/// Intel 4001 ROM: ~50ns
|
|
/// Intel 4002 RAM: ~50ns
|
|
pub hold_time: Duration,
|
|
|
|
/// Maximum clock frequency this component supports (in Hz).
|
|
///
|
|
/// If the clock exceeds this frequency, the component may not complete
|
|
/// its internal operations before the next clock edge arrives.
|
|
///
|
|
/// Intel 4001 ROM: 750 kHz
|
|
/// Intel 4002 RAM: 750 kHz
|
|
/// Intel 4003 SR: 1 MHz (can handle faster clocks)
|
|
pub max_clock_frequency: f64,
|
|
|
|
/// How frequently (in virtual time) this component's `update_at_time()`
|
|
/// should be called. Smaller values = more frequent updates = more accurate
|
|
/// but more CPU-intensive.
|
|
///
|
|
/// This determines how often the component thread wakes up to check for
|
|
/// clock edges and timing events.
|
|
///
|
|
/// Intel 4001 ROM: 100ns (check every 100ns of virtual time)
|
|
/// Intel 4003 SR: 50ns (more frequent due to faster timing)
|
|
pub min_update_interval: Duration,
|
|
}
|
|
|
|
impl ComponentTiming {
|
|
/// Timing profile for the Intel 4001 ROM chip.
|
|
///
|
|
/// The 4001 is a 256-byte mask-programmable ROM with 4-bit I/O ports.
|
|
/// Access time: 500ns from address valid to data output valid.
|
|
pub fn intel_4001() -> Self {
|
|
ComponentTiming {
|
|
name: "Intel 4001 ROM",
|
|
access_time: Duration::from_nanos(500),
|
|
propagation_delay: Duration::from_nanos(200),
|
|
setup_time: Duration::from_nanos(100),
|
|
hold_time: Duration::from_nanos(50),
|
|
max_clock_frequency: 750_000.0,
|
|
min_update_interval: Duration::from_nanos(100),
|
|
}
|
|
}
|
|
|
|
/// Timing profile for the Intel 4002 RAM chip.
|
|
///
|
|
/// The 4002 is a 320-bit dynamic RAM (80 x 4-bit words) with 4-bit output port.
|
|
/// Access time: 500ns from address valid to data output valid.
|
|
pub fn intel_4002() -> Self {
|
|
ComponentTiming {
|
|
name: "Intel 4002 RAM",
|
|
access_time: Duration::from_nanos(500),
|
|
propagation_delay: Duration::from_nanos(200),
|
|
setup_time: Duration::from_nanos(100),
|
|
hold_time: Duration::from_nanos(50),
|
|
max_clock_frequency: 750_000.0,
|
|
min_update_interval: Duration::from_nanos(100),
|
|
}
|
|
}
|
|
|
|
/// Timing profile for the Intel 4003 shift register.
|
|
///
|
|
/// The 4003 is a 10-bit static shift register with serial/parallel I/O.
|
|
/// Faster access time (200ns) than ROM/RAM due to simpler design.
|
|
pub fn intel_4003() -> Self {
|
|
ComponentTiming {
|
|
name: "Intel 4003 Shift Register",
|
|
access_time: Duration::from_nanos(200),
|
|
propagation_delay: Duration::from_nanos(100),
|
|
setup_time: Duration::from_nanos(50),
|
|
hold_time: Duration::from_nanos(30),
|
|
max_clock_frequency: 1_000_000.0, // Can handle 1 MHz
|
|
min_update_interval: Duration::from_nanos(50),
|
|
}
|
|
}
|
|
|
|
/// Timing profile for the Intel 4004 CPU.
|
|
///
|
|
/// The 4004 is the central processing unit that generates control signals.
|
|
/// Its timing is defined by the clock it receives rather than access times.
|
|
pub fn intel_4004() -> Self {
|
|
ComponentTiming {
|
|
name: "Intel 4004 CPU",
|
|
access_time: Duration::ZERO, // CPU doesn't have "access time"
|
|
propagation_delay: Duration::from_nanos(150),
|
|
setup_time: Duration::from_nanos(100),
|
|
hold_time: Duration::from_nanos(50),
|
|
max_clock_frequency: 750_000.0, // 740 kHz nominal, 750 kHz max
|
|
min_update_interval: Duration::from_nanos(100),
|
|
}
|
|
}
|
|
|
|
/// Sentinel value representing "no frequency limit".
|
|
///
|
|
/// Using a large finite value instead of `f64::INFINITY` avoids NaN
|
|
/// propagation in downstream arithmetic (e.g., `1.0 / frequency`).
|
|
/// 1 PHz (petahertz) is effectively unlimited for any real-world component.
|
|
pub const NO_FREQUENCY_LIMIT: f64 = 1_000_000_000_000_000.0; // 1 PHz
|
|
|
|
/// Timing profile for a generic clock generator.
|
|
///
|
|
/// Clocks don't have access times — they just toggle at their configured frequency.
|
|
pub fn generic_clock() -> Self {
|
|
ComponentTiming {
|
|
name: "Generic Clock",
|
|
access_time: Duration::ZERO,
|
|
propagation_delay: Duration::from_nanos(10),
|
|
setup_time: Duration::ZERO,
|
|
hold_time: Duration::ZERO,
|
|
max_clock_frequency: Self::NO_FREQUENCY_LIMIT,
|
|
min_update_interval: Duration::from_nanos(50),
|
|
}
|
|
}
|
|
|
|
/// Default timing with zero delays (backward-compatible).
|
|
///
|
|
/// Components that haven't been migrated to the timing model yet
|
|
/// use this profile, which imposes no timing constraints.
|
|
pub fn default_no_constraints() -> Self {
|
|
ComponentTiming {
|
|
name: "Default (no constraints)",
|
|
access_time: Duration::ZERO,
|
|
propagation_delay: Duration::ZERO,
|
|
setup_time: Duration::ZERO,
|
|
hold_time: Duration::ZERO,
|
|
max_clock_frequency: Self::NO_FREQUENCY_LIMIT,
|
|
min_update_interval: Duration::from_nanos(100),
|
|
}
|
|
}
|
|
|
|
/// Calculate the minimum clock period this component supports.
|
|
///
|
|
/// Derived from `max_clock_frequency`: period = 1 / frequency.
|
|
/// Returns `Duration::ZERO` for components with no practical frequency limit
|
|
/// (i.e., `max_clock_frequency >= NO_FREQUENCY_LIMIT`).
|
|
pub fn min_clock_period(&self) -> Duration {
|
|
if self.max_clock_frequency <= 0.0 || self.max_clock_frequency >= Self::NO_FREQUENCY_LIMIT {
|
|
Duration::ZERO
|
|
} else {
|
|
Duration::from_secs_f64(1.0 / self.max_clock_frequency)
|
|
}
|
|
}
|
|
|
|
/// Check if a given clock frequency exceeds this component's maximum.
|
|
///
|
|
/// Returns `true` if the frequency is within spec, `false` if overclocked.
|
|
/// Non-finite values (infinity, NaN) are always rejected.
|
|
pub fn supports_frequency(&self, frequency_hz: f64) -> bool {
|
|
frequency_hz.is_finite() && frequency_hz <= self.max_clock_frequency
|
|
}
|
|
|
|
/// Check if enough virtual time has elapsed since an event.
|
|
///
|
|
/// Returns `Ok(())` if `elapsed >= access_time`, `Err(TimingViolation)` otherwise.
|
|
pub fn check_access_time(&self, elapsed: Duration) -> Result<(), TimingViolation> {
|
|
if elapsed >= self.access_time {
|
|
Ok(())
|
|
} else {
|
|
Err(TimingViolation::AccessTimeNotElapsed {
|
|
component: self.name,
|
|
required: self.access_time,
|
|
actual: elapsed,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Check if a signal was stable for the required setup time before a clock edge.
|
|
///
|
|
/// Returns `Ok(())` if `stability_duration >= setup_time`, `Err(TimingViolation)` otherwise.
|
|
pub fn check_setup_time(&self, stability_duration: Duration) -> Result<(), TimingViolation> {
|
|
if stability_duration >= self.setup_time {
|
|
Ok(())
|
|
} else {
|
|
Err(TimingViolation::SetupTimeViolation {
|
|
component: self.name,
|
|
required: self.setup_time,
|
|
actual: stability_duration,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Check if a signal remained stable for the required hold time after a clock edge.
|
|
///
|
|
/// Returns `Ok(())` if `stability_duration >= hold_time`, `Err(TimingViolation)` otherwise.
|
|
pub fn check_hold_time(&self, stability_duration: Duration) -> Result<(), TimingViolation> {
|
|
if stability_duration >= self.hold_time {
|
|
Ok(())
|
|
} else {
|
|
Err(TimingViolation::HoldTimeViolation {
|
|
component: self.name,
|
|
required: self.hold_time,
|
|
actual: stability_duration,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for ComponentTiming {
|
|
fn default() -> Self {
|
|
Self::default_no_constraints()
|
|
}
|
|
}
|
|
|
|
/// A timing violation detected during simulation.
|
|
///
|
|
/// Timing violations are expected when overclocking or when signals change
|
|
/// too quickly. They don't crash the simulation — instead, the component
|
|
/// produces incorrect results, just like real hardware.
|
|
#[derive(Debug, Clone)]
|
|
pub enum TimingViolation {
|
|
/// The component's access time has not yet elapsed since the address was latched.
|
|
///
|
|
/// The data output is not yet valid. Reading the bus now returns undefined data.
|
|
AccessTimeNotElapsed {
|
|
component: &'static str,
|
|
required: Duration,
|
|
actual: Duration,
|
|
},
|
|
|
|
/// An input signal did not remain stable for the required setup time
|
|
/// before a clock edge.
|
|
///
|
|
/// The component may have latched an incorrect value.
|
|
SetupTimeViolation {
|
|
component: &'static str,
|
|
required: Duration,
|
|
actual: Duration,
|
|
},
|
|
|
|
/// An input signal did not remain stable for the required hold time
|
|
/// after a clock edge.
|
|
///
|
|
/// The component's latched value may be corrupted.
|
|
HoldTimeViolation {
|
|
component: &'static str,
|
|
required: Duration,
|
|
actual: Duration,
|
|
},
|
|
|
|
/// The clock frequency exceeds the component's maximum rated frequency.
|
|
///
|
|
/// The component may not complete its internal operations before the
|
|
/// next clock edge arrives.
|
|
Overclocked {
|
|
component: &'static str,
|
|
max_frequency_hz: f64,
|
|
actual_frequency_hz: f64,
|
|
},
|
|
}
|
|
|
|
impl std::error::Error for TimingViolation {}
|
|
|
|
impl fmt::Display for TimingViolation {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
TimingViolation::AccessTimeNotElapsed {
|
|
component,
|
|
required,
|
|
actual,
|
|
} => write!(
|
|
f,
|
|
"{}: Access time not elapsed (need {:?}, got {:?})",
|
|
component, required, actual
|
|
),
|
|
TimingViolation::SetupTimeViolation {
|
|
component,
|
|
required,
|
|
actual,
|
|
} => write!(
|
|
f,
|
|
"{}: Setup time violation (need {:?}, got {:?})",
|
|
component, required, actual
|
|
),
|
|
TimingViolation::HoldTimeViolation {
|
|
component,
|
|
required,
|
|
actual,
|
|
} => write!(
|
|
f,
|
|
"{}: Hold time violation (need {:?}, got {:?})",
|
|
component, required, actual
|
|
),
|
|
TimingViolation::Overclocked {
|
|
component,
|
|
max_frequency_hz,
|
|
actual_frequency_hz,
|
|
} => write!(
|
|
f,
|
|
"{}: Overclocked (max {:.0} Hz, actual {:.0} Hz)",
|
|
component, max_frequency_hz, actual_frequency_hz
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_intel_4001_timing() {
|
|
let timing = ComponentTiming::intel_4001();
|
|
assert_eq!(timing.access_time, Duration::from_nanos(500));
|
|
assert_eq!(timing.propagation_delay, Duration::from_nanos(200));
|
|
assert_eq!(timing.setup_time, Duration::from_nanos(100));
|
|
assert_eq!(timing.hold_time, Duration::from_nanos(50));
|
|
assert_eq!(timing.max_clock_frequency, 750_000.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_intel_4003_timing_faster() {
|
|
let rom = ComponentTiming::intel_4001();
|
|
let sr = ComponentTiming::intel_4003();
|
|
|
|
// Shift register should be faster than ROM
|
|
assert!(sr.access_time < rom.access_time);
|
|
assert!(sr.max_clock_frequency > rom.max_clock_frequency);
|
|
}
|
|
|
|
#[test]
|
|
fn test_supports_frequency() {
|
|
let timing = ComponentTiming::intel_4001();
|
|
|
|
assert!(timing.supports_frequency(500_000.0)); // 500 kHz — within spec
|
|
assert!(timing.supports_frequency(750_000.0)); // 750 kHz — max rated
|
|
assert!(!timing.supports_frequency(1_000_000.0)); // 1 MHz — overclocked!
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_access_time() {
|
|
let timing = ComponentTiming::intel_4001();
|
|
|
|
// Not enough time elapsed
|
|
let result = timing.check_access_time(Duration::from_nanos(300));
|
|
assert!(result.is_err());
|
|
|
|
// Exactly enough time
|
|
let result = timing.check_access_time(Duration::from_nanos(500));
|
|
assert!(result.is_ok());
|
|
|
|
// More than enough time
|
|
let result = timing.check_access_time(Duration::from_nanos(800));
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_setup_time() {
|
|
let timing = ComponentTiming::intel_4001();
|
|
|
|
assert!(timing.check_setup_time(Duration::from_nanos(50)).is_err());
|
|
assert!(timing.check_setup_time(Duration::from_nanos(100)).is_ok());
|
|
assert!(timing.check_setup_time(Duration::from_nanos(200)).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_overclocking_scenario() {
|
|
let rom = ComponentTiming::intel_4001();
|
|
let sr = ComponentTiming::intel_4003();
|
|
|
|
// At 2 MHz clock (500ns cycle, 250ns per phase):
|
|
let clock_freq = 2_000_000.0;
|
|
let phase_time = Duration::from_nanos(250);
|
|
|
|
// ROM can't handle 2 MHz
|
|
assert!(!rom.supports_frequency(clock_freq));
|
|
// ROM access time is 500ns but only gets 250ns per phase
|
|
assert!(rom.check_access_time(phase_time).is_err());
|
|
|
|
// Shift register's access time (200ns) fits within the 250ns phase at 2 MHz,
|
|
// but it can't actually handle 2 MHz clock frequency (max 1 MHz).
|
|
// We only verify access time here, not frequency support.
|
|
assert!(sr.check_access_time(phase_time).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_min_clock_period() {
|
|
let timing = ComponentTiming::intel_4001();
|
|
let period = timing.min_clock_period();
|
|
assert_eq!(period, Duration::from_secs_f64(1.0 / 750_000.0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_default_no_constraints() {
|
|
let timing = ComponentTiming::default_no_constraints();
|
|
assert_eq!(timing.access_time, Duration::ZERO);
|
|
// Supports any reasonable finite frequency
|
|
assert!(timing.supports_frequency(1_000_000_000.0)); // 1 GHz — well within limit
|
|
assert!(timing.supports_frequency(999_999_999_999_999.0)); // Just under sentinel
|
|
assert!(!timing.supports_frequency(f64::INFINITY)); // Non-finite rejected
|
|
assert!(!timing.supports_frequency(f64::NAN)); // NaN rejected
|
|
}
|
|
|
|
#[test]
|
|
fn test_timing_violation_display() {
|
|
let violation = TimingViolation::AccessTimeNotElapsed {
|
|
component: "Intel 4001 ROM",
|
|
required: Duration::from_nanos(500),
|
|
actual: Duration::from_nanos(250),
|
|
};
|
|
let display = format!("{}", violation);
|
|
assert!(display.contains("Access time not elapsed"));
|
|
assert!(display.contains("Intel 4001 ROM"));
|
|
}
|
|
}
|