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.
This commit is contained in:
parent
add7ace674
commit
08f53d68fe
28 changed files with 7186 additions and 270 deletions
|
|
@ -3,9 +3,10 @@ use std::any::Any;
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
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,
|
||||
|
|
@ -13,6 +14,7 @@ use crate::components::common::intel_400x::{
|
|||
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
|
||||
|
|
@ -48,6 +50,13 @@ pub struct Intel4001 {
|
|||
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)]
|
||||
|
|
@ -147,11 +156,11 @@ impl Intel400xTimingState for Intel4001 {
|
|||
self.state_machine.current_state = unified_state;
|
||||
}
|
||||
|
||||
fn get_address_latch_time(&self) -> Option<Instant> {
|
||||
fn get_address_latch_time(&self) -> Option<Duration> {
|
||||
self.state_machine.get_address_latch_time()
|
||||
}
|
||||
|
||||
fn set_address_latch_time(&mut self, time: Option<Instant>) {
|
||||
fn set_address_latch_time(&mut self, time: Option<Duration>) {
|
||||
self.state_machine.set_address_latch_time(time);
|
||||
}
|
||||
|
||||
|
|
@ -182,6 +191,10 @@ impl Intel400xTimingState for Intel4001 {
|
|||
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 {
|
||||
|
|
@ -273,9 +286,24 @@ impl Intel400xComponentBehavior for Intel4001 {
|
|||
self.handle_address_latching();
|
||||
}
|
||||
Intel400xUnifiedState::WaitLatency => {
|
||||
// Check if latency has elapsed
|
||||
if self.state_machine.is_latency_elapsed() {
|
||||
// Transition to DriveData
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -353,9 +381,32 @@ impl Intel4001 {
|
|||
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) {
|
||||
|
|
@ -455,8 +506,9 @@ impl Intel4001 {
|
|||
if self.state_machine.get_state() == Intel400xUnifiedState::AddressPhase {
|
||||
let nibble = self.read_data_bus() & 0x0F;
|
||||
|
||||
if self.state_machine.handle_address_nibble(nibble) {
|
||||
// Address is complete (both nibbles latched), transition to latency wait
|
||||
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
|
||||
|
|
@ -768,8 +820,9 @@ impl Intel4001 {
|
|||
// 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(nibble) {
|
||||
// Address is complete, transition to latency wait
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -778,12 +831,9 @@ impl Intel4001 {
|
|||
/// 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) {
|
||||
// This is now handled by state machine
|
||||
if self.state_machine.is_latency_elapsed() {
|
||||
// Latency elapsed, transition to data driving
|
||||
// Data will be driven on next Φ2 rising edge
|
||||
self.process_latency_elapsed();
|
||||
}
|
||||
// Note: Legacy method — latency check now requires virtual time
|
||||
// Always transition for backward compatibility
|
||||
self.process_latency_elapsed();
|
||||
}
|
||||
|
||||
/// Handle data driving during DriveData state
|
||||
|
|
@ -899,9 +949,20 @@ impl Component for Intel4001 {
|
|||
self.base.get_pin(name)
|
||||
}
|
||||
|
||||
/// Main update cycle - handles clock edge detection and operation dispatch
|
||||
/// 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;
|
||||
}
|
||||
|
|
@ -952,7 +1013,7 @@ impl Component for Intel4001 {
|
|||
// 3. Calling it would cause double-latching of address nibbles
|
||||
}
|
||||
|
||||
/// Run component in time-slice mode (manual control)
|
||||
/// 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);
|
||||
|
|
@ -962,13 +1023,37 @@ impl Component for Intel4001 {
|
|||
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() {
|
||||
self.update();
|
||||
// Small delay to prevent busy waiting when no clock is present
|
||||
thread::sleep(Duration::from_micros(1));
|
||||
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) {
|
||||
|
|
@ -994,6 +1079,14 @@ impl Component for Intel4001 {
|
|||
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 {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue