docs: clean up completed timing plans, add VIC-20 architecture plan
Remove 6 completed timing architecture phase plans (5-9 + overview). Add comprehensive VIC-20 architecture plan covering: - 6502 CPU family trait hierarchy (MOS 6502, WDC 65C02) - Generic SRAM/ROM memory components - 6560/6561 VIC video chip design - 6522 VIA I/O chip design - Address bus architecture for memory-mapped I/O - Expansion port design - GUI adaptations for multi-system support - 6-phase implementation roadmap
This commit is contained in:
parent
08f53d68fe
commit
80ef0d4475
7 changed files with 1010 additions and 2709 deletions
|
|
@ -1,898 +0,0 @@
|
||||||
# Phase 5: Migrate Clock Components to Virtual Time
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Phase 5 replaces `Instant::now()` and `thread::sleep()` in both clock components with virtual time from `Arc<VirtualClock>`. This is the first phase that modifies existing components rather than adding new infrastructure.
|
|
||||||
|
|
||||||
**Files modified:**
|
|
||||||
- [`generic_clock.rs`](src/components/clock/generic_clock.rs) — GenericClock
|
|
||||||
- [`two_phase_clock.rs`](src/components/clock/two_phase_clock.rs) — TwoPhaseClock
|
|
||||||
- [`system_config.rs`](src/system_config.rs) — Clock factory closures (minor)
|
|
||||||
|
|
||||||
**Files NOT modified** (Phase 7 responsibility):
|
|
||||||
- [`virtual_clock.rs`](src/virtual_clock.rs) — Already complete
|
|
||||||
- [`real_time_governor.rs`](src/real_time_governor.rs) — Already complete
|
|
||||||
- [`component.rs`](src/component.rs) — Already has `update_at_time()` default
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Order
|
|
||||||
|
|
||||||
**Migrate `GenericClock` first, then `TwoPhaseClock`.**
|
|
||||||
|
|
||||||
Rationale:
|
|
||||||
1. `GenericClock` is simpler — single-phase, single CLK output
|
|
||||||
2. `TwoPhaseClock` has dual outputs (PHI1/PHI2) and inter-phase dependencies
|
|
||||||
3. Both follow the same pattern, so GenericClock establishes the template
|
|
||||||
4. `GenericClock` has existing unit tests to validate the migration
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part A: GenericClock Migration
|
|
||||||
|
|
||||||
### A1. Current Timing Mechanism
|
|
||||||
|
|
||||||
[`GenericClock`](src/components/clock/generic_clock.rs:11) uses wall-clock time in 5 locations:
|
|
||||||
|
|
||||||
| Location | Line | Current Code | Purpose |
|
|
||||||
|----------|------|-------------|---------|
|
|
||||||
| [`last_transition`](src/components/clock/generic_clock.rs:16) field | 16 | `last_transition: Instant` | Track when last phase change occurred |
|
|
||||||
| [`pause_start`](src/components/clock/generic_clock.rs:21) field | 21 | `pause_start: Option<Instant>` | Track when pause began |
|
|
||||||
| [`new()`](src/components/clock/generic_clock.rs:34) constructor | 34 | `last_transition: Instant::now()` | Initialize transition timer |
|
|
||||||
| [`enable()`](src/components/clock/generic_clock.rs:68) | 68 | `self.last_transition = Instant::now()` | Reset timer on enable |
|
|
||||||
| [`pause()`](src/components/clock/generic_clock.rs:80) | 80 | `self.pause_start = Some(Instant::now())` | Record pause start |
|
|
||||||
| [`resume()`](src/components/clock/generic_clock.rs:88) | 88 | `pause_start.elapsed()` | Calculate pause duration |
|
|
||||||
| [`should_transition()`](src/components/clock/generic_clock.rs:131) | 131 | `self.last_transition.elapsed()` | Check if phase duration elapsed |
|
|
||||||
| [`perform_transition()`](src/components/clock/generic_clock.rs:149) | 149 | `self.last_transition = Instant::now()` | Record transition time |
|
|
||||||
| [`run()`](src/components/clock/generic_clock.rs:205) | 205 | `thread::sleep(sleep_time)` | Busy-wait loop with sleep |
|
|
||||||
|
|
||||||
### A2. Required Changes
|
|
||||||
|
|
||||||
#### A2.1 — Add `VirtualClock` field (optional, gradual integration)
|
|
||||||
|
|
||||||
Replace `Instant`-based fields with virtual-time equivalents. Add `Option<Arc<VirtualClock>>` for gradual integration — clocks work without a VirtualClock (falling back to wall-clock), but use virtual time when one is injected.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE (line 11-22):
|
|
||||||
pub struct GenericClock {
|
|
||||||
base: BaseComponent,
|
|
||||||
frequency: f64,
|
|
||||||
duty_cycle: f64,
|
|
||||||
current_state: PinValue,
|
|
||||||
last_transition: Instant,
|
|
||||||
high_time: Duration,
|
|
||||||
low_time: Duration,
|
|
||||||
enabled: bool,
|
|
||||||
paused: bool,
|
|
||||||
pause_start: Option<Instant>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
pub struct GenericClock {
|
|
||||||
base: BaseComponent,
|
|
||||||
frequency: f64,
|
|
||||||
duty_cycle: f64,
|
|
||||||
current_state: PinValue,
|
|
||||||
last_transition_vtime: Duration, // Virtual time of last transition
|
|
||||||
high_time: Duration,
|
|
||||||
low_time: Duration,
|
|
||||||
enabled: bool,
|
|
||||||
paused: bool,
|
|
||||||
pause_start_vtime: Option<Duration>, // Virtual time when pause started
|
|
||||||
virtual_clock: Option<Arc<VirtualClock>>, // Injected in Phase 7
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key change:** `last_transition: Instant` → `last_transition_vtime: Duration` (virtual nanoseconds since sim start). `pause_start: Option<Instant>` → `pause_start_vtime: Option<Duration>`.
|
|
||||||
|
|
||||||
#### A2.2 — Update constructor
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE (line 29-43):
|
|
||||||
let mut clock = GenericClock {
|
|
||||||
base: BaseComponent::new(name, pins),
|
|
||||||
frequency,
|
|
||||||
duty_cycle: 0.5,
|
|
||||||
current_state: PinValue::Low,
|
|
||||||
last_transition: Instant::now(),
|
|
||||||
high_time: Duration::from_secs_f64(0.5 / frequency),
|
|
||||||
low_time: Duration::from_secs_f64(0.5 / frequency),
|
|
||||||
enabled: true,
|
|
||||||
paused: false,
|
|
||||||
pause_start: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
let mut clock = GenericClock {
|
|
||||||
base: BaseComponent::new(name, pins),
|
|
||||||
frequency,
|
|
||||||
duty_cycle: 0.5,
|
|
||||||
current_state: PinValue::Low,
|
|
||||||
last_transition_vtime: Duration::ZERO, // Virtual time 0
|
|
||||||
high_time: Duration::from_secs_f64(0.5 / frequency),
|
|
||||||
low_time: Duration::from_secs_f64(0.5 / frequency),
|
|
||||||
enabled: true,
|
|
||||||
paused: false,
|
|
||||||
pause_start_vtime: None,
|
|
||||||
virtual_clock: None, // Set later via set_virtual_clock()
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
#### A2.3 — Add `set_virtual_clock()` method
|
|
||||||
|
|
||||||
New method for Phase 7 injection:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
/// Inject a VirtualClock for virtual-time-based operation.
|
|
||||||
/// If not set, the clock falls back to wall-clock Instant-based timing.
|
|
||||||
/// This method is called by ConfigurableSystem in Phase 7.
|
|
||||||
pub fn set_virtual_clock(&mut self, clock: Arc<VirtualClock>) {
|
|
||||||
self.virtual_clock = Some(clock);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### A2.4 — Update `enable()` method (line 64-69)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE:
|
|
||||||
pub fn enable(&mut self) {
|
|
||||||
self.enabled = true;
|
|
||||||
self.current_state = PinValue::Low;
|
|
||||||
self.last_transition = Instant::now();
|
|
||||||
}
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
pub fn enable(&mut self) {
|
|
||||||
self.enabled = true;
|
|
||||||
self.current_state = PinValue::Low;
|
|
||||||
self.last_transition_vtime = self.current_virtual_time();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Where `current_virtual_time()` is a new helper:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
/// Get current virtual time, or fall back to Duration::ZERO if no VirtualClock.
|
|
||||||
fn current_virtual_time(&self) -> Duration {
|
|
||||||
self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### A2.5 — Update `pause()` and `resume()` (lines 78-94)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE:
|
|
||||||
pub fn pause(&mut self) {
|
|
||||||
self.paused = true;
|
|
||||||
self.pause_start = Some(Instant::now());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resume(&mut self) {
|
|
||||||
if let Some(pause_start) = self.pause_start {
|
|
||||||
let pause_duration = pause_start.elapsed();
|
|
||||||
self.last_transition += pause_duration;
|
|
||||||
}
|
|
||||||
self.paused = false;
|
|
||||||
self.pause_start = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
pub fn pause(&mut self) {
|
|
||||||
self.paused = true;
|
|
||||||
self.pause_start_vtime = Some(self.current_virtual_time());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resume(&mut self) {
|
|
||||||
if let Some(pause_start_vtime) = self.pause_start_vtime {
|
|
||||||
let pause_duration = self.current_virtual_time().saturating_sub(pause_start_vtime);
|
|
||||||
self.last_transition_vtime = self.last_transition_vtime.saturating_add(pause_duration);
|
|
||||||
}
|
|
||||||
self.paused = false;
|
|
||||||
self.pause_start_vtime = None;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### A2.6 — Update `should_transition()` (line 130-138)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE:
|
|
||||||
fn should_transition(&self) -> bool {
|
|
||||||
let elapsed = self.last_transition.elapsed();
|
|
||||||
match self.current_state {
|
|
||||||
PinValue::High => elapsed >= self.high_time,
|
|
||||||
PinValue::Low => elapsed >= self.low_time,
|
|
||||||
PinValue::HighZ => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
fn should_transition(&self, virtual_time: Duration) -> bool {
|
|
||||||
let elapsed = virtual_time.saturating_sub(self.last_transition_vtime);
|
|
||||||
match self.current_state {
|
|
||||||
PinValue::High => elapsed >= self.high_time,
|
|
||||||
PinValue::Low => elapsed >= self.low_time,
|
|
||||||
PinValue::HighZ => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note:** `should_transition()` now takes `virtual_time: Duration` as a parameter instead of computing elapsed from `Instant`.
|
|
||||||
|
|
||||||
#### A2.7 — Update `perform_transition()` (line 140-150)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE:
|
|
||||||
fn perform_transition(&mut self) {
|
|
||||||
let new_state = match self.current_state {
|
|
||||||
PinValue::High => PinValue::Low,
|
|
||||||
PinValue::Low => PinValue::High,
|
|
||||||
PinValue::HighZ => PinValue::Low,
|
|
||||||
};
|
|
||||||
self.current_state = new_state;
|
|
||||||
self.set_clock_output(new_state);
|
|
||||||
self.last_transition = Instant::now();
|
|
||||||
}
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
fn perform_transition(&mut self, virtual_time: Duration) {
|
|
||||||
let new_state = match self.current_state {
|
|
||||||
PinValue::High => PinValue::Low,
|
|
||||||
PinValue::Low => PinValue::High,
|
|
||||||
PinValue::HighZ => PinValue::Low,
|
|
||||||
};
|
|
||||||
self.current_state = new_state;
|
|
||||||
self.set_clock_output(new_state);
|
|
||||||
self.last_transition_vtime = virtual_time;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### A2.8 — Implement `update_at_time()` (new, replaces `update()` for virtual-time path)
|
|
||||||
|
|
||||||
Add the virtual-time-aware update method. This overrides the default from the `Component` trait:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
impl Component for GenericClock {
|
|
||||||
// ... existing methods unchanged ...
|
|
||||||
|
|
||||||
fn update_at_time(&mut self, virtual_time: Duration) {
|
|
||||||
// Check pause state first - don't update if paused
|
|
||||||
if self.paused {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read external enable control
|
|
||||||
let external_enable = self.read_enable_pin();
|
|
||||||
let should_be_enabled = self.enabled && external_enable;
|
|
||||||
|
|
||||||
if !should_be_enabled {
|
|
||||||
if self.current_state != PinValue::Low {
|
|
||||||
self.set_clock_output(PinValue::Low);
|
|
||||||
self.current_state = PinValue::Low;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's time to transition (using virtual time)
|
|
||||||
if self.should_transition(virtual_time) {
|
|
||||||
self.perform_transition(virtual_time);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep update() for backward compatibility — delegates to update_at_time
|
|
||||||
// using current virtual time (or Duration::ZERO if no VirtualClock)
|
|
||||||
fn update(&mut self) {
|
|
||||||
let vtime = self.current_virtual_time();
|
|
||||||
self.update_at_time(vtime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### A2.9 — Rewrite `run()` to use `wait_until()` (line 190-210)
|
|
||||||
|
|
||||||
This is the most significant change. Replace the `thread::sleep()` busy-loop with `virtual_clock.wait_until()`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE:
|
|
||||||
fn run(&mut self) {
|
|
||||||
self.base.set_running(true);
|
|
||||||
self.enable();
|
|
||||||
|
|
||||||
while self.is_running() {
|
|
||||||
self.update();
|
|
||||||
let sleep_time = if self.frequency > 0.0 {
|
|
||||||
Duration::from_secs_f64(1.0 / self.frequency / 100.0)
|
|
||||||
.max(Duration::from_micros(1))
|
|
||||||
} else {
|
|
||||||
Duration::from_micros(100)
|
|
||||||
};
|
|
||||||
thread::sleep(sleep_time);
|
|
||||||
}
|
|
||||||
self.disable();
|
|
||||||
}
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
fn run(&mut self) {
|
|
||||||
self.base.set_running(true);
|
|
||||||
self.enable();
|
|
||||||
|
|
||||||
// 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 path: use old Instant-based timing
|
|
||||||
return self.run_legacy();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
while self.is_running() {
|
|
||||||
// Calculate next transition time
|
|
||||||
let next_transition = self.next_transition_time();
|
|
||||||
|
|
||||||
// Block until virtual time reaches next transition
|
|
||||||
match vclock.wait_until(next_transition) {
|
|
||||||
Some(current_vtime) => {
|
|
||||||
if !self.is_running() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
self.update_at_time(current_vtime);
|
|
||||||
}
|
|
||||||
None => break, // Simulation stopped
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.disable();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Add helper methods:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
impl GenericClock {
|
|
||||||
/// Calculate the virtual time of the next phase transition.
|
|
||||||
fn next_transition_time(&self) -> Duration {
|
|
||||||
let phase_duration = match self.current_state {
|
|
||||||
PinValue::High => self.high_time,
|
|
||||||
PinValue::Low => self.low_time,
|
|
||||||
PinValue::HighZ => Duration::from_nanos(1),
|
|
||||||
};
|
|
||||||
self.last_transition_vtime.saturating_add(phase_duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Legacy run loop using wall-clock time (backward compatibility).
|
|
||||||
fn run_legacy(&mut self) {
|
|
||||||
while self.is_running() {
|
|
||||||
self.update();
|
|
||||||
let sleep_time = if self.frequency > 0.0 {
|
|
||||||
Duration::from_secs_f64(1.0 / self.frequency / 100.0)
|
|
||||||
.max(Duration::from_micros(1))
|
|
||||||
} else {
|
|
||||||
Duration::from_micros(100)
|
|
||||||
};
|
|
||||||
thread::sleep(sleep_time);
|
|
||||||
}
|
|
||||||
self.disable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### A2.10 — Add `timing()` override
|
|
||||||
|
|
||||||
```rust
|
|
||||||
impl Component for GenericClock {
|
|
||||||
// ... existing methods ...
|
|
||||||
|
|
||||||
fn timing(&self) -> ComponentTiming {
|
|
||||||
ComponentTiming {
|
|
||||||
name: "Generic Clock",
|
|
||||||
access_time: Duration::ZERO,
|
|
||||||
propagation_delay: Duration::ZERO,
|
|
||||||
setup_time: Duration::ZERO,
|
|
||||||
hold_time: Duration::ZERO,
|
|
||||||
max_clock_frequency: f64::INFINITY,
|
|
||||||
min_update_interval: self.high_time.min(self.low_time),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### A2.11 — Update imports
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE (line 1-9):
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use std::any::Any;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::thread;
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
use crate::component::{BaseComponent, Component};
|
|
||||||
use crate::pin::{Pin, PinValue};
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
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};
|
|
||||||
use crate::component_timing::ComponentTiming;
|
|
||||||
use crate::pin::{Pin, PinValue};
|
|
||||||
use crate::virtual_clock::VirtualClock;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key change:** Remove `Instant` from imports, add `VirtualClock` and `ComponentTiming`.
|
|
||||||
|
|
||||||
### A3. GenericClock Test Impact
|
|
||||||
|
|
||||||
The existing tests in [`generic_clock.rs:238-258`](src/components/clock/generic_clock.rs:238) are:
|
|
||||||
|
|
||||||
| Test | What it checks | Impact |
|
|
||||||
|------|---------------|--------|
|
|
||||||
| [`test_clock_creation`](src/components/clock/generic_clock.rs:243) | Name and running state | **No impact** — doesn't use `Instant` |
|
|
||||||
| [`test_clock_timing_calculation`](src/components/clock/generic_clock.rs:250) | `frequency_to_duration()` | **No impact** — static calculation |
|
|
||||||
|
|
||||||
These tests will pass unchanged. No new tests needed for the migration itself — the existing tests validate construction and static calculations. Virtual-time behavior tests should be added in Phase 9.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part B: TwoPhaseClock Migration
|
|
||||||
|
|
||||||
### B1. Current Timing Mechanism
|
|
||||||
|
|
||||||
[`TwoPhaseClock`](src/components/clock/two_phase_clock.rs:11) uses wall-clock time in 5 locations:
|
|
||||||
|
|
||||||
| Location | Line | Current Code | Purpose |
|
|
||||||
|----------|------|-------------|---------|
|
|
||||||
| [`last_transition`](src/components/clock/two_phase_clock.rs:15) field | 15 | `last_transition: Instant` | Track when last phase swap occurred |
|
|
||||||
| [`pause_start`](src/components/clock/two_phase_clock.rs:19) field | 19 | `pause_start: Option<Instant>` | Track when pause began |
|
|
||||||
| [`new()`](src/components/clock/two_phase_clock.rs:38) constructor | 38 | `last_transition: Instant::now()` | Initialize transition timer |
|
|
||||||
| [`enable()`](src/components/clock/two_phase_clock.rs:64) | 64 | `self.last_transition = Instant::now()` | Reset timer on enable |
|
|
||||||
| [`pause()`](src/components/clock/two_phase_clock.rs:80) | 80 | `self.pause_start = Some(Instant::now())` | Record pause start |
|
|
||||||
| [`resume()`](src/components/clock/two_phase_clock.rs:88) | 88 | `pause_start.elapsed()` | Calculate pause duration |
|
|
||||||
| [`should_transition()`](src/components/clock/two_phase_clock.rs:137) | 137 | `self.last_transition.elapsed()` | Check if phase_time elapsed |
|
|
||||||
| [`perform_transition()`](src/components/clock/two_phase_clock.rs:160) | 160 | `self.last_transition = Instant::now()` | Record transition time |
|
|
||||||
| [`run()`](src/components/clock/two_phase_clock.rs:203) | 203 | `thread::sleep(Duration::from_micros(100))` | Fixed 100µs busy-wait |
|
|
||||||
|
|
||||||
### B2. Required Changes
|
|
||||||
|
|
||||||
#### B2.1 — Add `VirtualClock` field (optional, gradual integration)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE (line 11-21):
|
|
||||||
pub struct TwoPhaseClock {
|
|
||||||
base: BaseComponent,
|
|
||||||
phi1_state: PinValue,
|
|
||||||
phi2_state: PinValue,
|
|
||||||
last_transition: Instant,
|
|
||||||
phase_time: Duration,
|
|
||||||
enabled: bool,
|
|
||||||
paused: bool,
|
|
||||||
pause_start: Option<Instant>,
|
|
||||||
frequency: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
pub struct TwoPhaseClock {
|
|
||||||
base: BaseComponent,
|
|
||||||
phi1_state: PinValue,
|
|
||||||
phi2_state: PinValue,
|
|
||||||
last_transition_vtime: Duration, // Virtual time of last phase swap
|
|
||||||
phase_time: Duration,
|
|
||||||
enabled: bool,
|
|
||||||
paused: bool,
|
|
||||||
pause_start_vtime: Option<Duration>, // Virtual time when pause started
|
|
||||||
frequency: f64,
|
|
||||||
virtual_clock: Option<Arc<VirtualClock>>, // Injected in Phase 7
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B2.2 — Update constructor (line 24-45)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE:
|
|
||||||
TwoPhaseClock {
|
|
||||||
base: BaseComponent::new(name, pins),
|
|
||||||
phi1_state: PinValue::High,
|
|
||||||
phi2_state: PinValue::Low,
|
|
||||||
last_transition: Instant::now(),
|
|
||||||
phase_time,
|
|
||||||
enabled: true,
|
|
||||||
paused: false,
|
|
||||||
pause_start: None,
|
|
||||||
frequency,
|
|
||||||
}
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
TwoPhaseClock {
|
|
||||||
base: BaseComponent::new(name, pins),
|
|
||||||
phi1_state: PinValue::High,
|
|
||||||
phi2_state: PinValue::Low,
|
|
||||||
last_transition_vtime: Duration::ZERO,
|
|
||||||
phase_time,
|
|
||||||
enabled: true,
|
|
||||||
paused: false,
|
|
||||||
pause_start_vtime: None,
|
|
||||||
frequency,
|
|
||||||
virtual_clock: None,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B2.3 — Add `set_virtual_clock()` and `current_virtual_time()` helpers
|
|
||||||
|
|
||||||
```rust
|
|
||||||
impl TwoPhaseClock {
|
|
||||||
/// Inject a VirtualClock for virtual-time-based operation.
|
|
||||||
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.
|
|
||||||
fn current_virtual_time(&self) -> Duration {
|
|
||||||
self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B2.4 — Update `enable()` (line 60-69)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE:
|
|
||||||
pub fn enable(&mut self) {
|
|
||||||
self.enabled = true;
|
|
||||||
self.phi1_state = PinValue::High;
|
|
||||||
self.phi2_state = PinValue::Low;
|
|
||||||
self.last_transition = Instant::now();
|
|
||||||
self.update_outputs();
|
|
||||||
self.update_outputs();
|
|
||||||
}
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
pub fn enable(&mut self) {
|
|
||||||
self.enabled = true;
|
|
||||||
self.phi1_state = PinValue::High;
|
|
||||||
self.phi2_state = PinValue::Low;
|
|
||||||
self.last_transition_vtime = self.current_virtual_time();
|
|
||||||
self.update_outputs();
|
|
||||||
self.update_outputs();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B2.5 — Update `pause()` and `resume()` (lines 79-95)
|
|
||||||
|
|
||||||
Same pattern as GenericClock:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub fn pause(&mut self) {
|
|
||||||
self.paused = true;
|
|
||||||
self.pause_start_vtime = Some(self.current_virtual_time());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resume(&mut self) {
|
|
||||||
if let Some(pause_start_vtime) = self.pause_start_vtime {
|
|
||||||
let pause_duration = self.current_virtual_time().saturating_sub(pause_start_vtime);
|
|
||||||
self.last_transition_vtime = self.last_transition_vtime.saturating_add(pause_duration);
|
|
||||||
}
|
|
||||||
self.paused = false;
|
|
||||||
self.pause_start_vtime = None;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B2.6 — Update `should_transition()` (line 136-138)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE:
|
|
||||||
fn should_transition(&self) -> bool {
|
|
||||||
self.last_transition.elapsed() >= self.phase_time
|
|
||||||
}
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
fn should_transition(&self, virtual_time: Duration) -> bool {
|
|
||||||
let elapsed = virtual_time.saturating_sub(self.last_transition_vtime);
|
|
||||||
elapsed >= self.phase_time
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B2.7 — Update `perform_transition()` (line 140-161)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE:
|
|
||||||
fn perform_transition(&mut self) {
|
|
||||||
match (self.phi1_state, self.phi2_state) {
|
|
||||||
(PinValue::High, PinValue::Low) => {
|
|
||||||
self.phi1_state = PinValue::Low;
|
|
||||||
self.phi2_state = PinValue::High;
|
|
||||||
}
|
|
||||||
(PinValue::Low, PinValue::High) => {
|
|
||||||
self.phi1_state = PinValue::High;
|
|
||||||
self.phi2_state = PinValue::Low;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
self.phi1_state = PinValue::High;
|
|
||||||
self.phi2_state = PinValue::Low;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.update_outputs();
|
|
||||||
self.last_transition = Instant::now();
|
|
||||||
}
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
fn perform_transition(&mut self, virtual_time: Duration) {
|
|
||||||
match (self.phi1_state, self.phi2_state) {
|
|
||||||
(PinValue::High, PinValue::Low) => {
|
|
||||||
self.phi1_state = PinValue::Low;
|
|
||||||
self.phi2_state = PinValue::High;
|
|
||||||
}
|
|
||||||
(PinValue::Low, PinValue::High) => {
|
|
||||||
self.phi1_state = PinValue::High;
|
|
||||||
self.phi2_state = PinValue::Low;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
self.phi1_state = PinValue::High;
|
|
||||||
self.phi2_state = PinValue::Low;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.update_outputs();
|
|
||||||
self.last_transition_vtime = virtual_time;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B2.8 — Implement `update_at_time()` and update `update()`
|
|
||||||
|
|
||||||
```rust
|
|
||||||
impl Component for TwoPhaseClock {
|
|
||||||
// ... existing name/pins/get_pin unchanged ...
|
|
||||||
|
|
||||||
fn update_at_time(&mut self, virtual_time: Duration) {
|
|
||||||
if self.paused {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !self.enabled {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.update_outputs();
|
|
||||||
if self.should_transition(virtual_time) {
|
|
||||||
self.perform_transition(virtual_time);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self) {
|
|
||||||
let vtime = self.current_virtual_time();
|
|
||||||
self.update_at_time(vtime);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... rest unchanged ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B2.9 — Rewrite `run()` to use `wait_until()` (line 195-212)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE:
|
|
||||||
fn run(&mut self) {
|
|
||||||
self.base.set_running(true);
|
|
||||||
self.enable();
|
|
||||||
while self.is_running() {
|
|
||||||
self.update();
|
|
||||||
thread::sleep(Duration::from_micros(100));
|
|
||||||
if !self.is_running() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.disable();
|
|
||||||
}
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
fn run(&mut self) {
|
|
||||||
self.base.set_running(true);
|
|
||||||
self.enable();
|
|
||||||
|
|
||||||
let vclock = match self.virtual_clock.as_ref() {
|
|
||||||
Some(vc) => Arc::clone(vc),
|
|
||||||
None => return self.run_legacy(),
|
|
||||||
};
|
|
||||||
|
|
||||||
while self.is_running() {
|
|
||||||
let next_transition = self.last_transition_vtime + self.phase_time;
|
|
||||||
match vclock.wait_until(next_transition) {
|
|
||||||
Some(current_vtime) => {
|
|
||||||
if !self.is_running() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
self.update_at_time(current_vtime);
|
|
||||||
}
|
|
||||||
None => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.disable();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Add legacy helper:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
impl TwoPhaseClock {
|
|
||||||
/// Legacy run loop using wall-clock time (backward compatibility).
|
|
||||||
fn run_legacy(&mut self) {
|
|
||||||
while self.is_running() {
|
|
||||||
self.update();
|
|
||||||
thread::sleep(Duration::from_micros(100));
|
|
||||||
if !self.is_running() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.disable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B2.10 — Add `timing()` override
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn timing(&self) -> ComponentTiming {
|
|
||||||
ComponentTiming {
|
|
||||||
name: "Two-Phase Clock",
|
|
||||||
access_time: Duration::ZERO,
|
|
||||||
propagation_delay: Duration::ZERO,
|
|
||||||
setup_time: Duration::ZERO,
|
|
||||||
hold_time: Duration::ZERO,
|
|
||||||
max_clock_frequency: f64::INFINITY,
|
|
||||||
min_update_interval: self.phase_time,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B2.11 — Update imports
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE (line 1-9):
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use std::any::Any;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::thread;
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
use crate::component::{BaseComponent, Component};
|
|
||||||
use crate::pin::{Pin, PinValue};
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
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};
|
|
||||||
use crate::component_timing::ComponentTiming;
|
|
||||||
use crate::pin::{Pin, PinValue};
|
|
||||||
use crate::virtual_clock::VirtualClock;
|
|
||||||
```
|
|
||||||
|
|
||||||
### B3. TwoPhaseClock Test Impact
|
|
||||||
|
|
||||||
No tests exist for `TwoPhaseClock` (no `#[cfg(test)]` module in the file). No test breakage risk.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part C: System Config Changes
|
|
||||||
|
|
||||||
### C1. Factory Closure Updates
|
|
||||||
|
|
||||||
In [`system_config.rs`](src/system_config.rs:241-277), the factory closures create clocks without a `VirtualClock`. In Phase 5, these remain unchanged — the `virtual_clock` field is `None` by default, and clocks use the legacy `run_legacy()` path.
|
|
||||||
|
|
||||||
In **Phase 7**, `ConfigurableSystem` will:
|
|
||||||
1. Create `Arc<VirtualClock>`
|
|
||||||
2. After constructing each clock, call `set_virtual_clock(Arc::clone(&virtual_clock))`
|
|
||||||
3. This requires changing the factory closure signature or adding a post-construction hook
|
|
||||||
|
|
||||||
For Phase 5, no changes to [`system_config.rs`](src/system_config.rs) are needed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part D: Backward Compatibility Analysis
|
|
||||||
|
|
||||||
### D1. What stays the same
|
|
||||||
|
|
||||||
| Aspect | Compatibility |
|
|
||||||
|--------|--------------|
|
|
||||||
| `new(name, frequency)` constructor signature | **Unchanged** — no new required parameters |
|
|
||||||
| `Component` trait impl | **Unchanged** — `update()` still works via delegation |
|
|
||||||
| `enable()` / `disable()` / `pause()` / `resume()` | **Unchanged** — public API identical |
|
|
||||||
| `set_frequency()` / `get_frequency()` | **Unchanged** |
|
|
||||||
| Pin names (CLK, PHI1, PHI2, ENABLE) | **Unchanged** |
|
|
||||||
| `run()` without VirtualClock | **Works** — falls back to `run_legacy()` with `thread::sleep()` |
|
|
||||||
| Existing tests | **Pass** — no Instant dependency in test assertions |
|
|
||||||
|
|
||||||
### D2. What changes (internal only)
|
|
||||||
|
|
||||||
| Change | Visible externally? |
|
|
||||||
|--------|-------------------|
|
|
||||||
| `last_transition: Instant` → `last_transition_vtime: Duration` | No — private field |
|
|
||||||
| `pause_start: Option<Instant>` → `pause_start_vtime: Option<Duration>` | No — private field |
|
|
||||||
| `should_transition()` takes `Duration` param | No — private method |
|
|
||||||
| `perform_transition()` takes `Duration` param | No — private method |
|
|
||||||
| New `virtual_clock: Option<Arc<VirtualClock>>` field | No — private, `None` by default |
|
|
||||||
| New `set_virtual_clock()` method | Yes — but opt-in, not required |
|
|
||||||
|
|
||||||
### D3. Risk assessment
|
|
||||||
|
|
||||||
**Risk: LOW.** All changes are internal field replacements. The public API is unchanged. The `Option<Arc<VirtualClock>>` pattern ensures clocks work identically without a VirtualClock injected.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part E: Execution Checklist
|
|
||||||
|
|
||||||
These are the implementation steps in order:
|
|
||||||
|
|
||||||
- [ ] **E1.** Update `GenericClock` imports — remove `Instant`, add `VirtualClock` + `ComponentTiming`
|
|
||||||
- [ ] **E2.** Replace `GenericClock` struct fields: `last_transition: Instant` → `last_transition_vtime: Duration`, `pause_start: Option<Instant>` → `pause_start_vtime: Option<Duration>`, add `virtual_clock: Option<Arc<VirtualClock>>`
|
|
||||||
- [ ] **E3.** Update `GenericClock::new()` — initialize `last_transition_vtime: Duration::ZERO`, `virtual_clock: None`
|
|
||||||
- [ ] **E4.** Add `GenericClock::set_virtual_clock()` and `GenericClock::current_virtual_time()` helper methods
|
|
||||||
- [ ] **E5.** Update `GenericClock::enable()` — use `current_virtual_time()` instead of `Instant::now()`
|
|
||||||
- [ ] **E6.** Update `GenericClock::pause()` — use `current_virtual_time()` instead of `Instant::now()`
|
|
||||||
- [ ] **E7.** Update `GenericClock::resume()` — compute pause duration from virtual time deltas
|
|
||||||
- [ ] **E8.** Update `GenericClock::should_transition()` — accept `virtual_time: Duration` parameter
|
|
||||||
- [ ] **E9.** Update `GenericClock::perform_transition()` — accept `virtual_time: Duration`, store in `last_transition_vtime`
|
|
||||||
- [ ] **E10.** Implement `GenericClock::update_at_time()` in `Component` impl
|
|
||||||
- [ ] **E11.** Update `GenericClock::update()` to delegate to `update_at_time(current_virtual_time())`
|
|
||||||
- [ ] **E12.** Add `GenericClock::next_transition_time()` and `GenericClock::run_legacy()` helper methods
|
|
||||||
- [ ] **E13.** Rewrite `GenericClock::run()` — use `wait_until()` when VirtualClock present, else `run_legacy()`
|
|
||||||
- [ ] **E14.** Add `GenericClock::timing()` override in `Component` impl
|
|
||||||
- [ ] **E15.** Verify `GenericClock` existing tests still pass
|
|
||||||
- [ ] **E16.** Update `TwoPhaseClock` imports — remove `Instant`, add `VirtualClock` + `ComponentTiming`
|
|
||||||
- [ ] **E17.** Replace `TwoPhaseClock` struct fields: same pattern as GenericClock
|
|
||||||
- [ ] **E18.** Update `TwoPhaseClock::new()` — initialize virtual time fields
|
|
||||||
- [ ] **E19.** Add `TwoPhaseClock::set_virtual_clock()` and `TwoPhaseClock::current_virtual_time()` helpers
|
|
||||||
- [ ] **E20.** Update `TwoPhaseClock::enable()` — use `current_virtual_time()`
|
|
||||||
- [ ] **E21.** Update `TwoPhaseClock::pause()` and `resume()` — virtual time deltas
|
|
||||||
- [ ] **E22.** Update `TwoPhaseClock::should_transition()` — accept `virtual_time: Duration`
|
|
||||||
- [ ] **E23.** Update `TwoPhaseClock::perform_transition()` — accept `virtual_time: Duration`
|
|
||||||
- [ ] **E24.** Implement `TwoPhaseClock::update_at_time()` in `Component` impl
|
|
||||||
- [ ] **E25.** Update `TwoPhaseClock::update()` to delegate to `update_at_time()`
|
|
||||||
- [ ] **E26.** Add `TwoPhaseClock::run_legacy()` helper
|
|
||||||
- [ ] **E27.** Rewrite `TwoPhaseClock::run()` — use `wait_until()` when VirtualClock present
|
|
||||||
- [ ] **E28.** Add `TwoPhaseClock::timing()` override in `Component` impl
|
|
||||||
- [ ] **E29.** Run `cargo build` to verify compilation
|
|
||||||
- [ ] **E30.** Run `cargo test` to verify all existing tests pass
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Flow Diagram: Clock Thread with Virtual Time
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
A[run called] --> B{virtual_clock set?}
|
|
||||||
B -->|No| C[run_legacy - thread::sleep loop]
|
|
||||||
B -->|Yes| D[enable - reset phase state]
|
|
||||||
D --> E[Calculate next_transition_time]
|
|
||||||
E --> F[wait_until next_transition]
|
|
||||||
F --> G{wait_until returned}
|
|
||||||
G -->|None - stopped| H[disable and exit]
|
|
||||||
G -->|Some vtime| I{is_running?}
|
|
||||||
I -->|No| H
|
|
||||||
I -->|Yes| J[update_at_time with vtime]
|
|
||||||
J --> K[Transition phase if needed]
|
|
||||||
K --> E
|
|
||||||
```
|
|
||||||
|
|
||||||
## Flow Diagram: Phase Transition Logic in Virtual Time
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
subgraph GenericClock
|
|
||||||
G1[last_transition_vtime = 0ns] --> G2{vtime >= high_time?}
|
|
||||||
G2 -->|Yes| G3[High to Low transition]
|
|
||||||
G3 --> G4[last_transition_vtime = vtime]
|
|
||||||
G4 --> G5{vtime >= low_time?}
|
|
||||||
G5 -->|Yes| G6[Low to High transition]
|
|
||||||
G6 --> G4
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph TwoPhaseClock
|
|
||||||
T1[last_transition_vtime = 0ns] --> T2{elapsed >= phase_time?}
|
|
||||||
T2 -->|Yes| T3[PHI1 High+PHI2 Low to PHI1 Low+PHI2 High]
|
|
||||||
T3 --> T4[last_transition_vtime = vtime]
|
|
||||||
T4 --> T5{elapsed >= phase_time?}
|
|
||||||
T5 -->|Yes| T6[PHI1 Low+PHI2 High to PHI1 High+PHI2 Low]
|
|
||||||
T6 --> T4
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
@ -1,870 +0,0 @@
|
||||||
# Phase 6: Add Timing Enforcement to Memory/CPU Components
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Phase 6 adds virtual-time-based timing enforcement to the four Intel 400x series components: ROM (4001), RAM (4002), Shift Register (4003), and CPU (4004). These components currently use `Instant::now()` for address latch timestamps and check latency via `Instant::elapsed()`. This phase replaces those with virtual time from `Arc<VirtualClock>`, adds `ComponentTiming` enforcement checks, and records timing violations.
|
|
||||||
|
|
||||||
**Files modified:**
|
|
||||||
- [`intel_400x.rs`](src/components/common/intel_400x.rs) — Shared state machine timing (highest priority)
|
|
||||||
- [`intel_4001.rs`](src/components/memory/intel_4001.rs) — Intel 4001 ROM
|
|
||||||
- [`intel_4002.rs`](src/components/memory/intel_4002.rs) — Intel 4002 RAM
|
|
||||||
- [`intel_4003.rs`](src/components/memory/intel_4003.rs) — Intel 4003 Shift Register
|
|
||||||
- [`intel_4004.rs`](src/components/cpu/intel_4004.rs) — Intel 4004 CPU
|
|
||||||
|
|
||||||
**Files NOT modified** (Phase 7 responsibility):
|
|
||||||
- [`system_config.rs`](src/system_config.rs) — Will inject `Arc<VirtualClock>` into components
|
|
||||||
- [`virtual_clock.rs`](src/virtual_clock.rs) — Already complete
|
|
||||||
- [`component_timing.rs`](src/component_timing.rs) — Already complete
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Order
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph LR
|
|
||||||
A[Step 0: intel_400x.rs shared code] --> B[Step 1: Intel 4003 SR]
|
|
||||||
B --> C[Step 2: Intel 4001 ROM]
|
|
||||||
C --> D[Step 3: Intel 4002 RAM]
|
|
||||||
D --> E[Step 4: Intel 4004 CPU]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Rationale:**
|
|
||||||
1. **`intel_400x.rs` first** — The shared state machine contains `address_latch_time: Option<Instant>` and `is_latency_elapsed()` used by all three memory components. Changing this first unblocks all component migrations.
|
|
||||||
2. **Intel 4003 second** — Simplest component; no address latching in the traditional sense, uses shift/load operations. Good validation of the shared code changes.
|
|
||||||
3. **Intel 4001 third** — ROM is simpler than RAM (read-only, no write operations). Validates the full address→wait→drive flow.
|
|
||||||
4. **Intel 4002 fourth** — RAM adds write operations and I/O port states on top of the ROM pattern.
|
|
||||||
5. **Intel 4004 last** — CPU is the most complex (98K chars, own MemoryState enum, own `address_latch_time` field). Benefits from all patterns established by the memory components.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 0: Migrate Shared State Machine (`intel_400x.rs`)
|
|
||||||
|
|
||||||
### Current Timing Mechanism
|
|
||||||
|
|
||||||
The shared [`Intel400xStateMachine`](src/components/common/intel_400x.rs:830) uses `Instant` in two critical locations:
|
|
||||||
|
|
||||||
| Location | Line | Current Code | Purpose |
|
|
||||||
|----------|------|-------------|---------|
|
|
||||||
| [`address_latch_time`](src/components/common/intel_400x.rs:824) field | 824 | `address_latch_time: Option<Instant>` | Timestamp when address was fully latched |
|
|
||||||
| [`handle_address_nibble()`](src/components/common/intel_400x.rs:958) | 958 | `self.address_latch_time = Some(Instant::now())` | Record latch time when both nibbles received |
|
|
||||||
| [`is_latency_elapsed()`](src/components/common/intel_400x.rs:967) | 969 | `latch_time.elapsed() >= self.access_time` | Check if access time has passed |
|
|
||||||
|
|
||||||
Additionally, the [`Intel400xTimingState`](src/components/common/intel_400x.rs:320) trait exposes `Instant` in its interface:
|
|
||||||
|
|
||||||
| Location | Line | Signature |
|
|
||||||
|----------|------|-----------|
|
|
||||||
| [`get_address_latch_time()`](src/components/common/intel_400x.rs:324) | 324 | `fn get_address_latch_time(&self) -> Option<Instant>` |
|
|
||||||
| [`set_address_latch_time()`](src/components/common/intel_400x.rs:325) | 325 | `fn set_address_latch_time(&mut self, time: Option<Instant>)` |
|
|
||||||
|
|
||||||
And the [`Intel400xAddressHandling`](src/components/common/intel_400x.rs:196) trait's [`handle_address_latching()`](src/components/common/intel_400x.rs:206) method takes `&mut Option<Instant>`:
|
|
||||||
|
|
||||||
| Location | Line | Current Code |
|
|
||||||
|----------|------|-------------|
|
|
||||||
| [`handle_address_latching()`](src/components/common/intel_400x.rs:212) | 212 | `address_latch_time: &mut Option<Instant>` parameter |
|
|
||||||
| | 223 | `*address_latch_time = Some(Instant::now())` |
|
|
||||||
| [`handle_latency_wait()`](src/components/common/intel_400x.rs:236) | 236 | `address_latch_time: &Option<Instant>` parameter |
|
|
||||||
| | 240 | `latch_time.elapsed() >= access_time` |
|
|
||||||
|
|
||||||
### Required Changes
|
|
||||||
|
|
||||||
#### 0.1 — Change `address_latch_time` from `Instant` to `Duration`
|
|
||||||
|
|
||||||
Replace `Instant`-based timing with virtual time (`Duration` = nanoseconds since simulation start).
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE (line 824):
|
|
||||||
address_latch_time: Option<Instant>,
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
address_latch_time: Option<Duration>, // Virtual time when address was latched
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 0.2 — Add `is_latency_elapsed_at()` method (new, virtual-time-aware)
|
|
||||||
|
|
||||||
Add a new method that takes the current virtual time as a parameter, replacing the `Instant::elapsed()`-based check:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// NEW METHOD — add after is_latency_elapsed() at line ~973:
|
|
||||||
/// Check if latency has elapsed at a given virtual time.
|
|
||||||
/// Returns true if `virtual_time - address_latch_time >= access_time`.
|
|
||||||
pub fn is_latency_elapsed_at(&self, virtual_time: Duration) -> bool {
|
|
||||||
if let Some(latch_time) = self.address_latch_time {
|
|
||||||
virtual_time.saturating_sub(latch_time) >= self.access_time
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Keep the existing `is_latency_elapsed()` method** for backward compatibility — it will continue to work with `Instant::now()` for legacy code paths. Mark it with a comment that it's deprecated for virtual-time usage.
|
|
||||||
|
|
||||||
#### 0.3 — Add `set_address_latch_vtime()` method (new)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
/// Set address latch time using virtual time directly.
|
|
||||||
/// Called from update_at_time() when both address nibbles are latched.
|
|
||||||
pub fn set_address_latch_vtime(&mut self, vtime: Duration) {
|
|
||||||
self.address_latch_time = Some(vtime);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 0.4 — Add `handle_address_nibble_at()` method (new, virtual-time-aware)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
/// Handle address nibble latching with virtual time.
|
|
||||||
/// Returns true only when BOTH nibbles have been latched.
|
|
||||||
/// Records address_latch_time as virtual_time when complete.
|
|
||||||
pub fn handle_address_nibble_at(&mut self, nibble: u8, virtual_time: Duration) -> bool {
|
|
||||||
if self.address_high_nibble.is_none() {
|
|
||||||
self.address_high_nibble = Some(nibble);
|
|
||||||
false
|
|
||||||
} else if self.address_low_nibble.is_none() {
|
|
||||||
self.address_low_nibble = Some(nibble);
|
|
||||||
self.address_latch_time = Some(virtual_time); // Virtual time, not Instant::now()
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 0.5 — Update `Intel400xTimingState` trait (additive only)
|
|
||||||
|
|
||||||
Add new virtual-time-aware methods to the trait alongside existing ones:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub trait Intel400xTimingState {
|
|
||||||
// ... existing methods unchanged ...
|
|
||||||
|
|
||||||
/// Get address latch time as virtual time (Duration)
|
|
||||||
fn get_address_latch_vtime(&self) -> Option<Duration>;
|
|
||||||
|
|
||||||
/// Set address latch time using virtual time
|
|
||||||
fn set_address_latch_vtime(&mut self, time: Option<Duration>);
|
|
||||||
|
|
||||||
/// Check if latency has elapsed at a given virtual time
|
|
||||||
fn is_latency_elapsed_at(&self, virtual_time: Duration) -> bool;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 0.6 — Update `Intel400xAddressHandling` trait (additive only)
|
|
||||||
|
|
||||||
Add virtual-time-aware versions alongside existing methods:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub trait Intel400xAddressHandling {
|
|
||||||
// ... existing methods unchanged ...
|
|
||||||
|
|
||||||
/// Handle latency wait check using virtual time
|
|
||||||
fn handle_latency_wait_at(
|
|
||||||
&self,
|
|
||||||
address_latch_time: &Option<Duration>,
|
|
||||||
access_time: Duration,
|
|
||||||
virtual_time: Duration,
|
|
||||||
) -> bool {
|
|
||||||
if let Some(latch_time) = address_latch_time {
|
|
||||||
virtual_time.saturating_sub(*latch_time) >= access_time
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 0.7 — Update getter/setter for `address_latch_time`
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Change signatures (lines 916-923):
|
|
||||||
pub fn get_address_latch_time(&self) -> Option<Duration> { // was Option<Instant>
|
|
||||||
self.address_latch_time
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_address_latch_time(&mut self, time: Option<Duration>) { // was Option<Instant>
|
|
||||||
self.address_latch_time = time;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Impact:** This changes the return type of `get_address_latch_time()` from `Option<Instant>` to `Option<Duration>`. All callers (the `Intel400xTimingState` impls in each component) must be updated to match.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 1: Intel 4003 Shift Register
|
|
||||||
|
|
||||||
### Current Timing Mechanism
|
|
||||||
|
|
||||||
[`Intel4003`](src/components/memory/intel_4003.rs:24) is the simplest component:
|
|
||||||
- Uses [`Intel400xStateMachine`](src/components/memory/intel_4003.rs:32) for state management
|
|
||||||
- Implements [`Intel400xTimingState`](src/components/memory/intel_4003.rs:94) trait
|
|
||||||
- Has no direct `Instant::now()` calls — all timing goes through the shared state machine
|
|
||||||
- Has `thread::sleep(Duration::from_micros(1))` in [`run()`](src/components/memory/intel_4003.rs:586) (line 597)
|
|
||||||
- Uses `prev_phi1`/`prev_phi2` for edge detection (same as ROM/RAM)
|
|
||||||
|
|
||||||
### Timing Enforcement Points
|
|
||||||
|
|
||||||
The 4003 has simpler timing than ROM/RAM:
|
|
||||||
- **No address latching** — `responds_to_address() = false`
|
|
||||||
- **Shift operations** triggered by clock edges
|
|
||||||
- **Access time: 200ns** — faster than ROM/RAM
|
|
||||||
|
|
||||||
### Required Changes
|
|
||||||
|
|
||||||
#### 1.1 — Add `VirtualClock` field and helper
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Add to struct Intel4003 (after state_machine field):
|
|
||||||
virtual_clock: Option<Arc<VirtualClock>>,
|
|
||||||
|
|
||||||
// Add helper method:
|
|
||||||
fn current_virtual_time(&self) -> Duration {
|
|
||||||
self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 1.2 — Add `set_virtual_clock()` method
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub fn set_virtual_clock(&mut self, clock: Arc<VirtualClock>) {
|
|
||||||
self.virtual_clock = Some(clock);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 1.3 — Update `Intel400xTimingState` impl
|
|
||||||
|
|
||||||
Change `get_address_latch_time` / `set_address_latch_time` to use `Duration`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE (line 124):
|
|
||||||
fn get_address_latch_time(&self) -> Option<Instant> {
|
|
||||||
self.state_machine.get_address_latch_time()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_address_latch_time(&mut self, time: Option<Instant>) {
|
|
||||||
self.state_machine.set_address_latch_time(time);
|
|
||||||
}
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// New virtual-time methods:
|
|
||||||
fn get_address_latch_vtime(&self) -> Option<Duration> {
|
|
||||||
self.state_machine.get_address_latch_time()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_address_latch_vtime(&mut self, time: Option<Duration>) {
|
|
||||||
self.state_machine.set_address_latch_time(time);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_latency_elapsed_at(&self, virtual_time: Duration) -> bool {
|
|
||||||
self.state_machine.is_latency_elapsed_at(virtual_time)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 1.4 — Implement `update_at_time()`
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn update_at_time(&mut self, virtual_time: Duration) {
|
|
||||||
if !self.is_running() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
let (phi1, phi2) = self.read_clock_pins();
|
|
||||||
self.prev_phi1 = phi1;
|
|
||||||
self.prev_phi2 = phi2;
|
|
||||||
|
|
||||||
let reset_active = self.read_reset_pin();
|
|
||||||
if reset_active {
|
|
||||||
self.perform_reset();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timing enforcement: check if clock frequency is within spec
|
|
||||||
// (frequency check happens at clock edge detection time)
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 1.5 — Update `update()` to delegate to `update_at_time()`
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn update(&mut self) {
|
|
||||||
let vtime = self.current_virtual_time();
|
|
||||||
self.update_at_time(vtime);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 1.6 — Update `run()` with VirtualClock wait pattern
|
|
||||||
|
|
||||||
```rust
|
|
||||||
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, fall back to legacy busy-wait loop
|
|
||||||
let vclock = match self.virtual_clock.as_ref() {
|
|
||||||
Some(vc) => Arc::clone(vc),
|
|
||||||
None => {
|
|
||||||
while self.is_running() {
|
|
||||||
self.update();
|
|
||||||
thread::sleep(Duration::from_micros(1));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Virtual-time run loop
|
|
||||||
let timing = ComponentTiming::intel_4003();
|
|
||||||
while self.is_running() {
|
|
||||||
let next_update = vclock.now() + timing.min_update_interval;
|
|
||||||
match vclock.wait_until(next_update) {
|
|
||||||
Some(current_vtime) => {
|
|
||||||
if !self.is_running() { break; }
|
|
||||||
self.update_at_time(current_vtime);
|
|
||||||
}
|
|
||||||
None => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 1.7 — Implement `timing()` trait method
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn timing(&self) -> ComponentTiming {
|
|
||||||
ComponentTiming::intel_4003()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 1.8 — Update constructor
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Add to new_with_access_time():
|
|
||||||
virtual_clock: None,
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 1.9 — Add import
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use crate::virtual_clock::VirtualClock;
|
|
||||||
use crate::component_timing::ComponentTiming;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 2: Intel 4001 ROM
|
|
||||||
|
|
||||||
### Current Timing Mechanism
|
|
||||||
|
|
||||||
[`Intel4001`](src/components/memory/intel_4001.rs:25) uses timing in several places:
|
|
||||||
|
|
||||||
| Location | Line | Current Code | Purpose |
|
|
||||||
|----------|------|-------------|---------|
|
|
||||||
| [`access_time`](src/components/memory/intel_4001.rs:29) field | 29 | `access_time: Duration` | ROM access latency (500ns) |
|
|
||||||
| [`state_machine.is_latency_elapsed()`](src/components/memory/intel_4001.rs:277) | 277 | In `execute_state_logic()` WaitLatency | Check if ROM access time elapsed |
|
|
||||||
| [`state_machine.is_latency_elapsed()`](src/components/memory/intel_4001.rs:782) | 782 | In legacy `handle_latency_wait()` | Same check in legacy path |
|
|
||||||
| [`thread::sleep()`](src/components/memory/intel_4001.rs:968) | 968 | In `run()` | Busy-wait loop |
|
|
||||||
|
|
||||||
The ROM state machine flow:
|
|
||||||
```
|
|
||||||
Idle → AddressPhase → WaitLatency → DriveData → Idle
|
|
||||||
```
|
|
||||||
|
|
||||||
### Timing Enforcement Points
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
A[PHI1 Rising: Address Latching] -->|address_latch_time = virtual_time| B[WaitLatency State]
|
|
||||||
B -->|Check: virtual_time - latch_time >= access_time| C{Access Time Elapsed?}
|
|
||||||
C -->|Yes| D[PHI2 Rising: Drive Data onto bus]
|
|
||||||
C -->|No - Timing Violation| E[Record violation - data undefined]
|
|
||||||
D --> F[PHI2 Falling: Tri-state bus, return to Idle]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key enforcement point:** In `WaitLatency` state, when checking if latency has elapsed, use virtual time comparison. If the clock is overclocked (phase time < access_time), the ROM will NOT produce valid data — just like real hardware.
|
|
||||||
|
|
||||||
### Required Changes
|
|
||||||
|
|
||||||
#### 2.1 — Add `VirtualClock` field and helper
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Add to struct Intel4001:
|
|
||||||
virtual_clock: Option<Arc<VirtualClock>>,
|
|
||||||
|
|
||||||
// Add helper:
|
|
||||||
fn current_virtual_time(&self) -> Duration {
|
|
||||||
self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.2 — Add `set_virtual_clock()` method
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub fn set_virtual_clock(&mut self, clock: Arc<VirtualClock>) {
|
|
||||||
self.virtual_clock = Some(clock);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.3 — Update `Intel400xTimingState` impl
|
|
||||||
|
|
||||||
Same pattern as 4003 — change `Instant` to `Duration` in `get/set_address_latch_time()`, add new virtual-time trait methods.
|
|
||||||
|
|
||||||
#### 2.4 — Implement `update_at_time()`
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn update_at_time(&mut self, virtual_time: Duration) {
|
|
||||||
if !self.is_running() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
let (phi1, phi2) = self.read_clock_pins();
|
|
||||||
self.prev_phi1 = phi1;
|
|
||||||
self.prev_phi2 = phi2;
|
|
||||||
|
|
||||||
let reset_active = self.read_reset_pin();
|
|
||||||
if reset_active {
|
|
||||||
self.perform_reset();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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(); }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.5 — Update `update()` to delegate
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn update(&mut self) {
|
|
||||||
let vtime = self.current_virtual_time();
|
|
||||||
self.update_at_time(vtime);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.6 — Update `run()` with VirtualClock wait pattern
|
|
||||||
|
|
||||||
Same pattern as 4003 — check for `virtual_clock`, fall back to legacy busy-wait if absent.
|
|
||||||
|
|
||||||
#### 2.7 — Implement `timing()` trait method
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn timing(&self) -> ComponentTiming {
|
|
||||||
ComponentTiming::intel_4001()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.8 — Update `handle_phi2_rising()` for timing enforcement
|
|
||||||
|
|
||||||
The key timing enforcement happens in [`handle_phi2_rising()`](src/components/memory/intel_4001.rs:484) where `WaitLatency` state checks `is_latency_elapsed()`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE (line 487-493):
|
|
||||||
if self.state_machine.get_state() == Intel400xUnifiedState::WaitLatency {
|
|
||||||
if self.a3_wait_cycles > 0 {
|
|
||||||
self.a3_wait_cycles -= 1;
|
|
||||||
} else {
|
|
||||||
// A3 wait complete, transition to DriveData.
|
|
||||||
self.process_latency_elapsed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AFTER — add timing check:
|
|
||||||
if self.state_machine.get_state() == Intel400xUnifiedState::WaitLatency {
|
|
||||||
if self.a3_wait_cycles > 0 {
|
|
||||||
self.a3_wait_cycles -= 1;
|
|
||||||
} else {
|
|
||||||
// Check access time using virtual time
|
|
||||||
let virtual_time = self.current_virtual_time();
|
|
||||||
if self.state_machine.is_latency_elapsed_at(virtual_time) {
|
|
||||||
self.process_latency_elapsed();
|
|
||||||
} else {
|
|
||||||
// TIMING VIOLATION: Access time not yet elapsed
|
|
||||||
// Component produces undefined results (just like real hardware)
|
|
||||||
// The ROM will NOT drive data — the bus remains undefined
|
|
||||||
// Violation is recorded but simulation continues
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.9 — Update `execute_state_logic()` WaitLatency branch
|
|
||||||
|
|
||||||
Similarly update the [`execute_state_logic()`](src/components/memory/intel_4001.rs:275) WaitLatency branch:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE (line 275-280):
|
|
||||||
Intel400xUnifiedState::WaitLatency => {
|
|
||||||
if self.state_machine.is_latency_elapsed() {
|
|
||||||
self.process_latency_elapsed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
Intel400xUnifiedState::WaitLatency => {
|
|
||||||
let virtual_time = self.current_virtual_time();
|
|
||||||
if self.state_machine.is_latency_elapsed_at(virtual_time) {
|
|
||||||
self.process_latency_elapsed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.10 — Update constructor and imports
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Add to new_with_access_time():
|
|
||||||
virtual_clock: None,
|
|
||||||
|
|
||||||
// Add imports:
|
|
||||||
use crate::virtual_clock::VirtualClock;
|
|
||||||
use crate::component_timing::ComponentTiming;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 3: Intel 4002 RAM
|
|
||||||
|
|
||||||
### Current Timing Mechanism
|
|
||||||
|
|
||||||
[`Intel4002`](src/components/memory/intel_4002.rs) follows the same pattern as ROM but adds:
|
|
||||||
- **Read/Write/OutputPort** states (vs. just DriveData)
|
|
||||||
- [`is_latency_elapsed()`](src/components/memory/intel_4002.rs:268) check in `execute_state_logic()` WaitLatency
|
|
||||||
- [`is_latency_elapsed()`](src/components/memory/intel_4002.rs:480) check in `handle_phi2_rising()`
|
|
||||||
- [`thread::sleep()`](src/components/memory/intel_4002.rs:726) in `run()` (line 726)
|
|
||||||
|
|
||||||
State machine flow:
|
|
||||||
```
|
|
||||||
Idle → AddressPhase → WaitLatency → ReadData/WriteData/OutputPort → Idle
|
|
||||||
```
|
|
||||||
|
|
||||||
### Timing Enforcement Points
|
|
||||||
|
|
||||||
Same as ROM — the critical check is in `WaitLatency` state when transitioning to data operations.
|
|
||||||
|
|
||||||
### Required Changes
|
|
||||||
|
|
||||||
Identical pattern to ROM (Step 2):
|
|
||||||
|
|
||||||
#### 3.1 — Add `virtual_clock: Option<Arc<VirtualClock>>` field + helper
|
|
||||||
#### 3.2 — Add `set_virtual_clock()` method
|
|
||||||
#### 3.3 — Update `Intel400xTimingState` impl (Instant → Duration)
|
|
||||||
#### 3.4 — Implement `update_at_time()` (same structure as ROM)
|
|
||||||
#### 3.5 — Update `update()` to delegate to `update_at_time()`
|
|
||||||
#### 3.6 — Update `run()` with VirtualClock wait pattern
|
|
||||||
#### 3.7 — Implement `timing()` returning `ComponentTiming::intel_4002()`
|
|
||||||
#### 3.8 — Update `handle_phi2_rising()` WaitLatency check (line ~479-481):
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE:
|
|
||||||
if self.state_machine.is_latency_elapsed() {
|
|
||||||
self.process_latency_elapsed();
|
|
||||||
}
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
let virtual_time = self.current_virtual_time();
|
|
||||||
if self.state_machine.is_latency_elapsed_at(virtual_time) {
|
|
||||||
self.process_latency_elapsed();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3.9 — Update `execute_state_logic()` WaitLatency branch (line ~266-269)
|
|
||||||
#### 3.10 — Update constructor and imports
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 4: Intel 4004 CPU
|
|
||||||
|
|
||||||
### Current Timing Mechanism
|
|
||||||
|
|
||||||
[`Intel4004`](src/components/cpu/intel_4004.rs:124) has its **own** `MemoryState` enum and timing fields, separate from the shared `Intel400xStateMachine`:
|
|
||||||
|
|
||||||
| Location | Line | Current Code | Purpose |
|
|
||||||
|----------|------|-------------|---------|
|
|
||||||
| [`address_latch_time`](src/components/cpu/intel_4004.rs:168) field | 168 | `address_latch_time: Option<Instant>` | Timestamp when address was latched |
|
|
||||||
| [`access_time`](src/components/cpu/intel_4004.rs:170) field | 170 | `access_time: Duration` | Memory access time (500ns) |
|
|
||||||
| [`advance_address_cycle()`](src/components/cpu/intel_4004.rs:596) | 596 | `self.address_latch_time = Some(Instant::now())` | Record latch time at A2→A3 transition |
|
|
||||||
| [`thread::sleep()`](src/components/cpu/intel_4004.rs:2387) | 2387 | In `run()` | Busy-wait loop |
|
|
||||||
|
|
||||||
**Key difference:** The CPU does NOT use the shared `Intel400xStateMachine`. It has its own `MemoryState` enum and `AddressCycle` enum. The `address_latch_time` field is directly on the CPU struct.
|
|
||||||
|
|
||||||
### Timing Enforcement Points
|
|
||||||
|
|
||||||
The CPU's timing enforcement is different from memory components:
|
|
||||||
- **Propagation delay** (150ns): After the CPU drives address/data onto the bus, its outputs don't change instantly
|
|
||||||
- **Setup/hold time checks**: The CPU should verify that data from ROM/RAM was stable for the required setup time before the clock edge
|
|
||||||
- **Frequency check**: The CPU should verify the clock frequency is within spec (750 kHz max)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
A[A1: Drive addr high nibble] --> B[A2: Drive addr low nibble]
|
|
||||||
B -->|address_latch_time = virtual_time| C[A3: Wait for ROM/RAM]
|
|
||||||
C -->|Wait access_time| D[M1: Read OPR from bus]
|
|
||||||
D --> E[M2: Read OPA from bus]
|
|
||||||
E --> F[Execute instruction]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Required Changes
|
|
||||||
|
|
||||||
#### 4.1 — Change `address_latch_time` from `Instant` to `Duration`
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE (line 168):
|
|
||||||
address_latch_time: Option<Instant>,
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
address_latch_time: Option<Duration>, // Virtual time when address was latched
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4.2 — Add `VirtualClock` field and helper
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Add to struct Intel4004:
|
|
||||||
virtual_clock: Option<Arc<VirtualClock>>,
|
|
||||||
|
|
||||||
fn current_virtual_time(&self) -> Duration {
|
|
||||||
self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4.3 — Add `set_virtual_clock()` method
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub fn set_virtual_clock(&mut self, clock: Arc<VirtualClock>) {
|
|
||||||
self.virtual_clock = Some(clock);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4.4 — Update `advance_address_cycle()` (line 596)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// BEFORE:
|
|
||||||
self.address_latch_time = Some(Instant::now());
|
|
||||||
|
|
||||||
// AFTER:
|
|
||||||
self.address_latch_time = Some(self.current_virtual_time());
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4.5 — Implement `update_at_time()`
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn update_at_time(&mut self, virtual_time: Duration) {
|
|
||||||
if !self.is_running() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... same clock edge detection logic as current update() ...
|
|
||||||
// The key difference: address_latch_time is now Duration-based,
|
|
||||||
// so any elapsed time checks use virtual_time - address_latch_time
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4.6 — Update `update()` to delegate
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn update(&mut self) {
|
|
||||||
let vtime = self.current_virtual_time();
|
|
||||||
self.update_at_time(vtime);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4.7 — Update `run()` with VirtualClock wait pattern
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn run(&mut self) {
|
|
||||||
self.base.set_running(true);
|
|
||||||
|
|
||||||
let vclock = match self.virtual_clock.as_ref() {
|
|
||||||
Some(vc) => Arc::clone(vc),
|
|
||||||
None => {
|
|
||||||
// Legacy busy-wait loop
|
|
||||||
while self.is_running() {
|
|
||||||
self.update();
|
|
||||||
thread::sleep(Duration::from_micros(1));
|
|
||||||
if self.cycle_count > 100000 {
|
|
||||||
// ... safety check ...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let timing = ComponentTiming::intel_4004();
|
|
||||||
while self.is_running() {
|
|
||||||
let next_update = vclock.now() + timing.min_update_interval;
|
|
||||||
match vclock.wait_until(next_update) {
|
|
||||||
Some(current_vtime) => {
|
|
||||||
if !self.is_running() { break; }
|
|
||||||
self.update_at_time(current_vtime);
|
|
||||||
if self.cycle_count > 100000 {
|
|
||||||
// ... safety check ...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4.8 — Implement `timing()` trait method
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn timing(&self) -> ComponentTiming {
|
|
||||||
ComponentTiming::intel_4004()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4.9 — Update constructor and reset methods
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// In new() constructor:
|
|
||||||
address_latch_time: None, // Already None, just type change
|
|
||||||
virtual_clock: None,
|
|
||||||
|
|
||||||
// In reset methods (lines 278, 497, 705):
|
|
||||||
self.address_latch_time = None; // No change needed — already None
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4.10 — Add imports
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use crate::virtual_clock::VirtualClock;
|
|
||||||
use crate::component_timing::ComponentTiming;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Timing Violation Handling Strategy
|
|
||||||
|
|
||||||
Timing violations are **recorded, not fatal**. When a violation is detected:
|
|
||||||
|
|
||||||
1. **Access time not elapsed** (ROM/RAM): The component stays in `WaitLatency` and does NOT drive data onto the bus. The bus remains in its previous state (possibly undefined). The CPU reads garbage — just like real hardware.
|
|
||||||
|
|
||||||
2. **Overclocked frequency**: Detected when the clock period is shorter than `min_clock_period()`. The component continues operating but may produce incorrect results. Violation is logged.
|
|
||||||
|
|
||||||
3. **Setup/hold time violation**: The latched value may be incorrect. The component continues with the corrupted value — just like real hardware.
|
|
||||||
|
|
||||||
### Violation Recording (Future Enhancement)
|
|
||||||
|
|
||||||
For Phase 6, violations are detected at the enforcement points but **not yet recorded to a global log**. The component simply behaves incorrectly (doesn't drive data, latches wrong values, etc.). A `TimingViolationLog` can be added in a future phase to collect violations for display in the GUI.
|
|
||||||
|
|
||||||
The `ComponentTiming` check methods already return `Result<(), TimingViolation>`:
|
|
||||||
- [`check_access_time(elapsed)`](src/component_timing.rs:221) — Returns `Err(TimingViolation::AccessTimeNotElapsed)` if not enough time
|
|
||||||
- [`check_setup_time(stability_duration)`](src/component_timing.rs:236) — Returns `Err(TimingViolation::SetupTimeViolation)`
|
|
||||||
- [`check_hold_time(stability_duration)`](src/component_timing.rs:251) — Returns `Err(TimingViolation::HoldTimeViolation)`
|
|
||||||
- [`supports_frequency(freq_hz)`](src/component_timing.rs:214) — Returns `bool`
|
|
||||||
|
|
||||||
These can be used at enforcement points to generate violations for logging when that infrastructure is added.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary of Changes by File
|
|
||||||
|
|
||||||
### [`intel_400x.rs`](src/components/common/intel_400x.rs) (Shared)
|
|
||||||
| Change | Type |
|
|
||||||
|--------|------|
|
|
||||||
| `address_latch_time: Option<Instant>` → `Option<Duration>` | Field type change |
|
|
||||||
| `get/set_address_latch_time()` return/param type | Signature change |
|
|
||||||
| Add `is_latency_elapsed_at(virtual_time: Duration)` | New method |
|
|
||||||
| Add `handle_address_nibble_at(nibble, virtual_time)` | New method |
|
|
||||||
| Add `set_address_latch_vtime(time: Duration)` | New method |
|
|
||||||
| Add virtual-time methods to `Intel400xTimingState` trait | Trait extension |
|
|
||||||
| Add `handle_latency_wait_at()` to `Intel400xAddressHandling` trait | Trait extension |
|
|
||||||
| Keep existing `is_latency_elapsed()` for backward compat | No change |
|
|
||||||
|
|
||||||
### [`intel_4001.rs`](src/components/memory/intel_4001.rs) (ROM)
|
|
||||||
| Change | Type |
|
|
||||||
|--------|------|
|
|
||||||
| Add `virtual_clock: Option<Arc<VirtualClock>>` field | New field |
|
|
||||||
| Add `set_virtual_clock()` method | New method |
|
|
||||||
| Add `current_virtual_time()` helper | New method |
|
|
||||||
| Implement `update_at_time(virtual_time: Duration)` | New method |
|
|
||||||
| Update `update()` to delegate to `update_at_time()` | Modify |
|
|
||||||
| Update `run()` with VirtualClock wait pattern | Modify |
|
|
||||||
| Implement `timing()` returning `ComponentTiming::intel_4001()` | New method |
|
|
||||||
| Update `Intel400xTimingState` impl (Instant → Duration) | Modify |
|
|
||||||
| Update `handle_phi2_rising()` WaitLatency check | Modify |
|
|
||||||
| Update `execute_state_logic()` WaitLatency check | Modify |
|
|
||||||
| Update constructor (`virtual_clock: None`) | Modify |
|
|
||||||
| Add imports for `VirtualClock`, `ComponentTiming` | Modify |
|
|
||||||
|
|
||||||
### [`intel_4002.rs`](src/components/memory/intel_4002.rs) (RAM)
|
|
||||||
| Change | Type |
|
|
||||||
|--------|------|
|
|
||||||
| Same changes as ROM, but with `ComponentTiming::intel_4002()` | Same pattern |
|
|
||||||
| Update `handle_phi2_rising()` WaitLatency check | Modify |
|
|
||||||
| Update `execute_state_logic()` WaitLatency check | Modify |
|
|
||||||
|
|
||||||
### [`intel_4003.rs`](src/components/memory/intel_4003.rs) (Shift Register)
|
|
||||||
| Change | Type |
|
|
||||||
|--------|------|
|
|
||||||
| Same pattern as ROM, but with `ComponentTiming::intel_4003()` | Same pattern |
|
|
||||||
| No `handle_phi2_rising()` WaitLatency changes needed (4003 doesn't wait for access time the same way) | N/A |
|
|
||||||
|
|
||||||
### [`intel_4004.rs`](src/components/cpu/intel_4004.rs) (CPU)
|
|
||||||
| Change | Type |
|
|
||||||
|--------|------|
|
|
||||||
| `address_latch_time: Option<Instant>` → `Option<Duration>` | Field type change |
|
|
||||||
| Add `virtual_clock: Option<Arc<VirtualClock>>` field | New field |
|
|
||||||
| Add `set_virtual_clock()` method | New method |
|
|
||||||
| Add `current_virtual_time()` helper | New method |
|
|
||||||
| Implement `update_at_time(virtual_time: Duration)` | New method |
|
|
||||||
| Update `update()` to delegate to `update_at_time()` | Modify |
|
|
||||||
| Update `run()` with VirtualClock wait pattern | Modify |
|
|
||||||
| Implement `timing()` returning `ComponentTiming::intel_4004()` | New method |
|
|
||||||
| Update `advance_address_cycle()` latch time assignment | Modify |
|
|
||||||
| Update constructor (`virtual_clock: None`) | Modify |
|
|
||||||
| Add imports for `VirtualClock`, `ComponentTiming` | Modify |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backward Compatibility
|
|
||||||
|
|
||||||
All changes follow the Phase 5 pattern:
|
|
||||||
- `Option<Arc<VirtualClock>>` — components work without a VirtualClock
|
|
||||||
- `run()` falls back to legacy `thread::sleep()` busy-wait when no VirtualClock is injected
|
|
||||||
- `update()` delegates to `update_at_time()` using `current_virtual_time()` (returns `Duration::ZERO` when no clock)
|
|
||||||
- `update_at_time()` is a new override; existing `update()` callers continue to work
|
|
||||||
- Existing `is_latency_elapsed()` method kept for backward compat (uses `Instant::elapsed()` internally)
|
|
||||||
|
|
||||||
Phase 7 (`ConfigurableSystem`) will create the `Arc<VirtualClock>` and inject it via `set_virtual_clock()` on each component.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
After each component migration, verify:
|
|
||||||
1. **Legacy path still works** — Run existing tests without VirtualClock injected
|
|
||||||
2. **Virtual time path works** — Inject a VirtualClock, advance time, verify correct behavior
|
|
||||||
3. **Timing violation behavior** — Overclock the clock and verify ROM/RAM produce incorrect results while the faster shift register still works
|
|
||||||
4. **State machine transitions** — Verify Idle → AddressPhase → WaitLatency → DriveData cycle completes correctly in virtual time
|
|
||||||
|
|
@ -1,174 +0,0 @@
|
||||||
# Phase 7: System Integration — ConfigurableSystem + VirtualClock
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Phase 7 wires the `VirtualClock` and `RealTimeGovernor` into `ConfigurableSystem`, completing the transition from `thread::sleep()`-based timing to virtual-time-synchronized per-component threads.
|
|
||||||
|
|
||||||
## Current Architecture (Pre-Phase 7)
|
|
||||||
|
|
||||||
```
|
|
||||||
ConfigurableSystem::start()
|
|
||||||
→ for each component:
|
|
||||||
thread::spawn(|| {
|
|
||||||
loop {
|
|
||||||
if !running → break
|
|
||||||
if paused → sleep(10ms), continue
|
|
||||||
comp.try_lock() → comp.update()
|
|
||||||
adaptive sleep (yield or µs-sleep based on frequency)
|
|
||||||
}
|
|
||||||
comp.stop()
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
Problems:
|
|
||||||
- No timing accuracy — OS `nanosleep` has ~100µs granularity
|
|
||||||
- No synchronization between components
|
|
||||||
- No speed control
|
|
||||||
- Components don't use virtual time
|
|
||||||
|
|
||||||
## Target Architecture (Post-Phase 7)
|
|
||||||
|
|
||||||
```
|
|
||||||
ConfigurableSystem::start()
|
|
||||||
1. Create Arc<VirtualClock> with tick_size derived from clock frequency
|
|
||||||
2. Inject VirtualClock into all components via set_virtual_clock()
|
|
||||||
3. Spawn RealTimeGovernor thread → governor.run() calls clock.start()
|
|
||||||
4. for each component:
|
|
||||||
thread::spawn(|| {
|
|
||||||
comp.run() // Component's own run() handles VirtualClock wait
|
|
||||||
})
|
|
||||||
5. Store governor handle for clean shutdown
|
|
||||||
```
|
|
||||||
|
|
||||||
## Changes Required
|
|
||||||
|
|
||||||
### 1. `src/system_config.rs` — `ConfigurableSystem`
|
|
||||||
|
|
||||||
#### New Fields
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct ConfigurableSystem {
|
|
||||||
// ... existing fields ...
|
|
||||||
virtual_clock: Arc<VirtualClock>,
|
|
||||||
governor_handle: Arc<Mutex<Option<std::thread::JoinHandle<()>>>>,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Modified `new()` Constructor
|
|
||||||
|
|
||||||
- Derive tick_size from clock frequency in metadata
|
|
||||||
- Create `Arc<VirtualClock>` with that tick_size
|
|
||||||
- Store it in the struct
|
|
||||||
|
|
||||||
#### Modified `start()` Method (Inherent)
|
|
||||||
|
|
||||||
The current `start()` spawns threads with a manual loop that calls `comp.update()`.
|
|
||||||
The new `start()` will:
|
|
||||||
|
|
||||||
1. Inject `Arc<VirtualClock>` into all components via `set_virtual_clock()` (downcast to each concrete type)
|
|
||||||
2. Spawn `RealTimeGovernor` in its own thread — `governor.run()` calls `clock.start()` internally
|
|
||||||
3. For each component, spawn a thread that calls `comp.run()` — the component's own `run()` method handles the VirtualClock wait loop (already implemented in Phases 5-6)
|
|
||||||
4. Store the governor thread handle
|
|
||||||
|
|
||||||
#### Modified `cleanup()` Method
|
|
||||||
|
|
||||||
1. Call `clock.stop()` to unblock all component threads waiting in `wait_until()` / `wait_for_start()`
|
|
||||||
2. Set `is_running = false`
|
|
||||||
3. Wait for component threads to finish (join)
|
|
||||||
4. Wait for governor thread to finish (join)
|
|
||||||
|
|
||||||
#### Modified `SystemController::stop()` (Trait Impl)
|
|
||||||
|
|
||||||
- Call `clock.stop()` before existing cleanup
|
|
||||||
- This wakes all component threads, which see `None` from `wait_until()` and exit
|
|
||||||
|
|
||||||
#### Modified `SystemController::pause()` / `resume()` (Trait Impl)
|
|
||||||
|
|
||||||
- Delegate to `clock.pause()` / `clock.resume()` instead of (or in addition to) component-level pause
|
|
||||||
- This is more efficient — all threads block on the condvar instead of polling
|
|
||||||
|
|
||||||
### 2. `src/main.rs` — No Changes Required
|
|
||||||
|
|
||||||
The `main.rs` calls `ConfigurableSystem::start()` or `system.run()`, both of which will now use VirtualClock internally. No changes needed.
|
|
||||||
|
|
||||||
### 3. Tick Size Derivation
|
|
||||||
|
|
||||||
For a 750 kHz two-phase clock:
|
|
||||||
- Full cycle = 1/750000 ≈ 1333ns
|
|
||||||
- Each phase = 1333ns / 2 ≈ 666ns
|
|
||||||
- Phase transition = 666ns / 2 = 333ns (quarter cycle)
|
|
||||||
- tick_size = 333ns
|
|
||||||
|
|
||||||
General formula: `tick_size = 1 / (4 * frequency)` seconds
|
|
||||||
|
|
||||||
For non-two-phase clocks (generic_clock):
|
|
||||||
- tick_size = 1 / (2 * frequency) seconds (half cycle)
|
|
||||||
|
|
||||||
We use the conservative (smaller) tick_size: `1 / (4 * frequency)`.
|
|
||||||
|
|
||||||
### 4. Backward Compatibility
|
|
||||||
|
|
||||||
If `set_virtual_clock()` is not called on a component (e.g., a component type that doesn't have the method), the component's `run()` method falls back to `run_legacy()` which uses `thread::sleep()`. This is already implemented in each component's `run()` method from Phases 5-6.
|
|
||||||
|
|
||||||
The `Component` trait's `run()` method has a default implementation that uses `thread::sleep()`, so any component that hasn't been migrated will still work.
|
|
||||||
|
|
||||||
### 5. Thread Lifecycle
|
|
||||||
|
|
||||||
```
|
|
||||||
Component Thread:
|
|
||||||
1. comp.run() is called
|
|
||||||
2. If virtual_clock is Some:
|
|
||||||
a. Loop: wait_until(next_time) → update_at_time(vtime) → schedule next
|
|
||||||
b. Exit when wait_until returns None (clock stopped)
|
|
||||||
3. If virtual_clock is None:
|
|
||||||
a. run_legacy() — original thread::sleep loop
|
|
||||||
|
|
||||||
Governor Thread:
|
|
||||||
1. governor.run() is called
|
|
||||||
2. Calls clock.start() — wakes all component threads in wait_for_start()
|
|
||||||
3. Loop: advance virtual time based on mode/speed
|
|
||||||
4. Exit when clock.is_running() == false
|
|
||||||
5. Drop guard calls clock.stop()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. Downcasting Strategy
|
|
||||||
|
|
||||||
Since `set_virtual_clock()` is a method on each concrete type (not on the `Component` trait), we need to downcast from `Box<dyn Component>` to each concrete type. The order of attempts:
|
|
||||||
|
|
||||||
1. `GenericClock` — `comp.as_any_mut().downcast_mut::<GenericClock>()`
|
|
||||||
2. `TwoPhaseClock` — `comp.as_any_mut().downcast_mut::<TwoPhaseClock>()`
|
|
||||||
3. `Intel4004` — `comp.as_any_mut().downcast_mut::<Intel4004>()`
|
|
||||||
4. `Intel4001` — `comp.as_any_mut().downcast_mut::<Intel4001>()`
|
|
||||||
5. `Intel4002` — `comp.as_any_mut().downcast_mut::<Intel4002>()`
|
|
||||||
6. `Intel4003` — `comp.as_any_mut().downcast_mut::<Intel4003>()`
|
|
||||||
|
|
||||||
This is the same pattern already used in `pause()`, `resume()`, `set_clock_frequency()`, etc.
|
|
||||||
|
|
||||||
## Implementation Steps
|
|
||||||
|
|
||||||
1. Add imports for `VirtualClock`, `RealTimeGovernor`, and concrete component types
|
|
||||||
2. Add `virtual_clock` and `governor_handle` fields to `ConfigurableSystem`
|
|
||||||
3. Update `ConfigurableSystem::new()` to create `VirtualClock` from frequency
|
|
||||||
4. Add `inject_virtual_clock()` helper method
|
|
||||||
5. Update `ConfigurableSystem::start()` (inherent) to:
|
|
||||||
- Inject VirtualClock into all components
|
|
||||||
- Spawn governor thread
|
|
||||||
- Spawn component threads that call `comp.run()`
|
|
||||||
6. Update `ConfigurableSystem::cleanup()` to stop clock and join governor
|
|
||||||
7. Update `SystemController::pause()`/`resume()` to use clock.pause()/resume()
|
|
||||||
8. Build and test
|
|
||||||
|
|
||||||
## Risk Assessment
|
|
||||||
|
|
||||||
- **Low Risk**: Adding new fields and methods to `ConfigurableSystem`
|
|
||||||
- **Medium Risk**: Changing the thread spawn model from external loop to `comp.run()`
|
|
||||||
- Mitigated by the fact that `comp.run()` already works (tested in Phases 5-6)
|
|
||||||
- **Low Risk**: Downcast-based injection — same pattern already used extensively
|
|
||||||
- **Low Risk**: Governor thread — already tested independently
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
1. `cargo build` — must compile
|
|
||||||
2. `cargo test` — all existing tests must pass
|
|
||||||
3. Manual test: `cargo run -- --system basic` — system should start, run, and stop cleanly
|
|
||||||
4. Manual test: `cargo run -- --gui --system basic` — GUI should work with VirtualClock
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
# Phase 8: Speed/Step Controls for GUI and Console
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Add simulation speed, mode, and step controls to both the egui-based GUI and the ratatui-based console interface. These controls leverage the existing `VirtualClock` and `RealTimeGovernor` APIs.
|
|
||||||
|
|
||||||
## Files to Modify
|
|
||||||
|
|
||||||
1. **`src/gui/state.rs`** — Add `TimingState` struct to `GuiState` for virtual time display data
|
|
||||||
2. **`src/gui/components.rs`** — Add `TimingControls` component (speed slider, mode selector, step button, time display)
|
|
||||||
3. **`src/gui/mod.rs`** — Integrate `TimingControls` into the main render loop
|
|
||||||
4. **`src/gui/console.rs`** — Add console commands: `speed`, `mode`, `step`, `time`
|
|
||||||
|
|
||||||
## Detailed Design
|
|
||||||
|
|
||||||
### 1. GUI State (`src/gui/state.rs`)
|
|
||||||
|
|
||||||
Add a `TimingState` struct to `GuiState`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct TimingState {
|
|
||||||
/// Current virtual time in nanoseconds
|
|
||||||
pub virtual_time_ns: u64,
|
|
||||||
/// Total ticks elapsed
|
|
||||||
pub tick_count: u64,
|
|
||||||
/// Current speed multiplier (from VirtualClock)
|
|
||||||
pub target_speed: f64,
|
|
||||||
/// Actual measured speed (from RealTimeGovernor snapshots)
|
|
||||||
pub actual_speed: f64,
|
|
||||||
/// Current simulation mode: "RealTime", "MaxSpeed", "SingleStep"
|
|
||||||
pub mode: String,
|
|
||||||
/// Whether a step is pending (SingleStep mode)
|
|
||||||
pub step_pending: bool,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The `update_from_system()` method will be extended to:
|
|
||||||
- Access `ConfigurableSystem::get_virtual_clock()` → `Arc<VirtualClock>`
|
|
||||||
- Read `clock.now_ns()`, `clock.tick_count()`, `clock.speed_multiplier()`, `clock.mode()`, `clock.is_step_pending()`
|
|
||||||
- Compute actual speed using `RealTimeGovernor::take_speed_snapshot()` / `compute_speed()` across frames
|
|
||||||
|
|
||||||
To support speed snapshot computation across frames, `GuiState` will hold two `SpeedSnapshot` instances:
|
|
||||||
```rust
|
|
||||||
pub speed_snap_prev: Option<SpeedSnapshot>,
|
|
||||||
pub speed_snap_curr: Option<SpeedSnapshot>,
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. TimingControls Component (`src/gui/components.rs`)
|
|
||||||
|
|
||||||
New component `TimingControls` with:
|
|
||||||
|
|
||||||
#### Speed Slider (Logarithmic Scale)
|
|
||||||
- Slider range: logarithmic from 0.001× to 100.0×
|
|
||||||
- Uses `egui::Slider` with custom logarithmic mapping
|
|
||||||
- Position 0.0 → 0.001×, position 1.0 → 100.0×
|
|
||||||
- Special "MaxSpeed" position at the far right
|
|
||||||
- Calls `clock.set_speed_multiplier()` on change
|
|
||||||
- Display: "Speed: 1.00× (actual: 0.98×)"
|
|
||||||
|
|
||||||
#### Mode Selector
|
|
||||||
- Three radio buttons: RealTime / MaxSpeed / SingleStep
|
|
||||||
- Calls `clock.set_mode()` on selection change
|
|
||||||
|
|
||||||
#### Step Controls
|
|
||||||
- "Step" button, enabled only in SingleStep mode
|
|
||||||
- Calls `clock.trigger_step()` on click
|
|
||||||
- Shows "Step pending..." indicator when step is pending
|
|
||||||
|
|
||||||
#### Virtual Time Display
|
|
||||||
- Human-readable format: auto-selects µs, ms, or s
|
|
||||||
- Shows tick count
|
|
||||||
- Shows target vs actual speed
|
|
||||||
|
|
||||||
### 3. Integration (`src/gui/mod.rs`)
|
|
||||||
|
|
||||||
Add `timing_controls: TimingControls` to `GuiComponents` and call `render_timing_controls()` between the control panel and memory sections.
|
|
||||||
|
|
||||||
### 4. Console Commands (`src/gui/console.rs`)
|
|
||||||
|
|
||||||
New commands in `execute_command()`:
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
|---------|-------------|
|
|
||||||
| `speed <multiplier>` | Set speed multiplier (e.g., `speed 0.5`) |
|
|
||||||
| `speed` | Show current speed |
|
|
||||||
| `mode <realtime\|maxspeed\|step>` | Set simulation mode |
|
|
||||||
| `mode` | Show current mode |
|
|
||||||
| `step` | Trigger a single step (SingleStep mode) |
|
|
||||||
| `time` | Show virtual time and tick count |
|
|
||||||
|
|
||||||
Also add keyboard shortcuts:
|
|
||||||
- `+`/`-` already exist for frequency; add new shortcuts for speed:
|
|
||||||
- Keep existing `+`/`-` for frequency control (unchanged)
|
|
||||||
|
|
||||||
### Logarithmic Slider Mapping
|
|
||||||
|
|
||||||
The slider position `t` (0.0 to 1.0) maps to speed multiplier via:
|
|
||||||
```
|
|
||||||
t=0.00 → 0.001×
|
|
||||||
t=0.25 → 0.01×
|
|
||||||
t=0.50 → 0.1×
|
|
||||||
t=0.75 → 1.0×
|
|
||||||
t=0.90 → 10.0×
|
|
||||||
t=1.00 → 100.0×
|
|
||||||
```
|
|
||||||
|
|
||||||
Formula: `speed = 10^(t * 5 - 3)` gives range [0.001, 100.0]
|
|
||||||
|
|
||||||
For MaxSpeed mode, we'll use a separate toggle rather than a slider position.
|
|
||||||
|
|
||||||
## Implementation Order
|
|
||||||
|
|
||||||
1. Add `TimingState` to `src/gui/state.rs`
|
|
||||||
2. Add `TimingControls` component to `src/gui/components.rs`
|
|
||||||
3. Integrate into `src/gui/mod.rs`
|
|
||||||
4. Add console commands to `src/gui/console.rs`
|
|
||||||
5. Build and test
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
# Phase 9: Update Tests for Virtual Time
|
|
||||||
|
|
||||||
## Current State
|
|
||||||
|
|
||||||
All 226 existing tests pass (142 unit tests, 16 fibonacci tests, 62 integration tests, 5 simple tests, 15 doc-tests). No tests are broken by the virtual time migration — the existing tests use `update()` (without virtual time) and direct component manipulation, which still works.
|
|
||||||
|
|
||||||
## Plan
|
|
||||||
|
|
||||||
### 1. Update `tests/lib.rs` — Add Virtual Time Test Utilities
|
|
||||||
|
|
||||||
Add helper functions to the `test_utils` module:
|
|
||||||
- `create_maxspeed_clock()` — Creates a VirtualClock in MaxSpeed mode with a 333ns tick size
|
|
||||||
- `create_singlestep_clock()` — Creates a VirtualClock in SingleStep mode
|
|
||||||
- `spawn_governor()` — Spawns a RealTimeGovernor thread for a given clock
|
|
||||||
- `stop_and_wait()` — Stops the clock and waits for governor thread to finish
|
|
||||||
|
|
||||||
### 2. New Test File: `tests/virtual_time_tests.rs`
|
|
||||||
|
|
||||||
#### MaxSpeed Mode Tests
|
|
||||||
- `test_maxspeed_clock_advances` — Verify VirtualClock advances in MaxSpeed mode with governor
|
|
||||||
- `test_maxspeed_component_updates` — Verify Intel4001 updates correctly at virtual time
|
|
||||||
- `test_maxspeed_rom_access_timing` — Verify ROM data is valid after access_time elapsed in virtual time
|
|
||||||
|
|
||||||
#### SingleStep Mode Tests
|
|
||||||
- `test_singlestep_advances_one_tick` — Verify each trigger_step() advances exactly one tick
|
|
||||||
- `test_singlestep_rom_state_at_specific_time` — Verify ROM state at specific virtual times:
|
|
||||||
- Before access_time: data not ready
|
|
||||||
- After access_time: data valid
|
|
||||||
|
|
||||||
#### Overclocking Tests
|
|
||||||
- `test_overclocking_rom_fails_at_2mhz` — Set 2 MHz clock (250ns phase), verify ROM (500ns access) fails
|
|
||||||
- `test_overclocking_ram_fails_at_2mhz` — Same for RAM
|
|
||||||
- `test_shift_register_works_at_higher_freq` — Intel4003 (200ns access) works at 2 MHz (250ns phase >= 200ns)
|
|
||||||
- `test_component_timing_check_access_time` — Direct ComponentTiming API tests
|
|
||||||
|
|
||||||
#### Virtual Clock Integration Tests
|
|
||||||
- `test_configurable_system_creates_virtual_clock` — Verify ConfigurableSystem has a VirtualClock
|
|
||||||
- `test_configurable_system_maxspeed_execution` — Run system in MaxSpeed mode briefly, verify it works
|
|
||||||
- `test_virtual_clock_stop_exits_threads` — Verify clock.stop() causes governor to exit
|
|
||||||
- `test_pause_resume_behavior` — Verify pause stops time advancement, resume continues
|
|
||||||
|
|
||||||
### 3. Implementation Notes
|
|
||||||
|
|
||||||
- All new tests go in a separate file to avoid modifying existing passing tests
|
|
||||||
- Tests use `Arc<VirtualClock>` and `RealTimeGovernor` directly (not through ConfigurableSystem) for precise control
|
|
||||||
- Overclocking tests use `ComponentTiming::check_access_time()` directly since component-internal timing checks happen via `update_at_time()` which requires the full component thread infrastructure
|
|
||||||
- Tests that spawn governor threads must ensure proper cleanup (stop + join)
|
|
||||||
|
|
@ -1,603 +0,0 @@
|
||||||
# Timing-Accurate Simulation Architecture
|
|
||||||
|
|
||||||
## Problem Statement
|
|
||||||
|
|
||||||
The current emulator spawns each component in its own OS thread with independent `thread::sleep()` calls. This means:
|
|
||||||
|
|
||||||
1. **No timing accuracy** — `nanosleep` has ~100µs granularity but components need nanosecond-level timing
|
|
||||||
2. **No synchronization** — Components update independently with no guaranteed ordering
|
|
||||||
3. **No speed control** — No way to slow down, speed up, or single-step
|
|
||||||
4. **No hardware timing enforcement** — Timing constants exist but aren't checked against simulation time
|
|
||||||
|
|
||||||
## How Other Emulators/Simulators Handle Timing
|
|
||||||
|
|
||||||
### UAE (Ultimate Amiga Emulator)
|
|
||||||
|
|
||||||
UAE uses a **line-based timing model**:
|
|
||||||
- The Amiga's custom chips are synchronized to the video beam position (horizontal line)
|
|
||||||
- Each emulated line takes a fixed number of CPU cycles (e.g., 228 cycles for PAL)
|
|
||||||
- The CPU runs for one line's worth of cycles, then the custom chips update
|
|
||||||
- A **host synchronization point** occurs once per frame (50/60 Hz) — UAE sleeps to match real-time at the frame boundary, not per-cycle
|
|
||||||
- Sound output drives real-time sync: the audio buffer must be filled at the correct rate
|
|
||||||
- Key insight: **UAE batches many cycles of work, then syncs to wall-clock at coarse granularity (frame rate)**
|
|
||||||
|
|
||||||
### VICE (Versatile Commodore Emulator)
|
|
||||||
|
|
||||||
VICE uses a **cycle-accurate but batched** model:
|
|
||||||
- The CPU and CIA/VIC chips each track their own cycle count
|
|
||||||
- The main loop runs the CPU for one cycle, then lets the VIC/CIA advance one cycle
|
|
||||||
- **No per-cycle sleeping** — VICE runs cycles as fast as possible
|
|
||||||
- Real-time sync happens at the **frame boundary** (50/60 Hz = every ~20ms)
|
|
||||||
- VICE uses the host's audio subsystem or `usleep()` at frame boundaries to maintain real-time
|
|
||||||
- The SID (sound chip) and VIC (video) generate their output in batches, then the host presents it at the right time
|
|
||||||
- Key insight: **Cycle-accurate internally, wall-clock synced at frame boundaries**
|
|
||||||
|
|
||||||
### Verilog/VHDL Simulators (Icarus Verilog, ModelSim, Verilator)
|
|
||||||
|
|
||||||
- **Event-driven scheduling** at virtual time points
|
|
||||||
- Components register callbacks: "wake me at virtual time T"
|
|
||||||
- No wall-clock sleeping at all — pure computation
|
|
||||||
- The simulator maintains an **event queue** sorted by virtual time
|
|
||||||
- Key insight: **Virtual time is just a number in a priority queue**
|
|
||||||
|
|
||||||
### QEMU
|
|
||||||
|
|
||||||
- Uses **instruction counting** for timing
|
|
||||||
- Each emulated instruction advances a virtual clock counter
|
|
||||||
- For real-time workloads (audio, video), QEMU sleeps at coarse boundaries
|
|
||||||
- Key insight: **Count instructions, sync at coarse intervals**
|
|
||||||
|
|
||||||
### SPICE (Analog Simulator)
|
|
||||||
|
|
||||||
- **Adaptive timestep** integration
|
|
||||||
- Time advances in variable-size steps based on signal activity
|
|
||||||
- Smaller steps when signals change rapidly, larger steps when stable
|
|
||||||
- Key insight: **Variable granularity based on activity**
|
|
||||||
|
|
||||||
### Common Pattern
|
|
||||||
|
|
||||||
All these emulators share the same core pattern:
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph LR
|
|
||||||
A[Run N cycles of virtual time] --> B[Batch: process all events for those cycles]
|
|
||||||
B --> C[Sync: sleep to match wall-clock if needed]
|
|
||||||
C --> A
|
|
||||||
```
|
|
||||||
|
|
||||||
The **sync point granularity** varies:
|
|
||||||
| Emulator | Sync granularity | Virtual time per sync |
|
|
||||||
|----------|-----------------|----------------------|
|
|
||||||
| UAE | Per video frame | ~20ms |
|
|
||||||
| VICE | Per video frame | ~20ms |
|
|
||||||
| QEMU | Per instruction batch | ~1-10ms |
|
|
||||||
| Verilog | Never (pure computation) | N/A |
|
|
||||||
| SPICE | Never (pure computation) | N/A |
|
|
||||||
|
|
||||||
**The key insight for our emulator:** We should use **virtual time internally** (like all of them) and **sync to wall-clock at coarse boundaries** (like UAE/VICE), not try to `nanosleep` per cycle.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Proposed Architecture: Virtual-Time-Synchronized Per-Component Threads
|
|
||||||
|
|
||||||
### Core Idea
|
|
||||||
|
|
||||||
Keep each component in its own thread (the project's signature feature), but replace `thread::sleep()`-based timing with a **shared virtual clock** that all components synchronize against. Components sleep until the virtual clock reaches their next scheduled update time.
|
|
||||||
|
|
||||||
### Architecture Diagram
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TB
|
|
||||||
subgraph "Global Synchronization"
|
|
||||||
VC[VirtualClock - shared Arc atomic u64 nanoseconds]
|
|
||||||
RTG[RealTimeGovernor - advances virtual clock]
|
|
||||||
SC[SpeedControl - multiplier + mode]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "Component Threads - each runs independently"
|
|
||||||
CT1[Clock Thread]
|
|
||||||
CT2[CPU Thread]
|
|
||||||
CT3[ROM Thread]
|
|
||||||
CT4[RAM Thread]
|
|
||||||
CT5[ShiftReg Thread]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "Per-Component Timing"
|
|
||||||
T1[ComponentTiming - access_time, setup_time, etc.]
|
|
||||||
T2[TimingChecker - validates constraints against virtual time]
|
|
||||||
end
|
|
||||||
|
|
||||||
RTG --> VC
|
|
||||||
SC --> RTG
|
|
||||||
VC --> CT1
|
|
||||||
VC --> CT2
|
|
||||||
VC --> CT3
|
|
||||||
VC --> CT4
|
|
||||||
VC --> CT5
|
|
||||||
T1 --> CT2
|
|
||||||
T1 --> CT3
|
|
||||||
T2 --> CT3
|
|
||||||
```
|
|
||||||
|
|
||||||
### How It Works — Virtual Time Synchronization
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant Gov as Governor Thread
|
|
||||||
participant VC as VirtualClock
|
|
||||||
participant CLK as Clock Thread
|
|
||||||
participant CPU as CPU Thread
|
|
||||||
participant ROM as ROM Thread
|
|
||||||
|
|
||||||
Note over Gov: Governor advances virtual clock in steps
|
|
||||||
Gov->>VC: advance to 333ns
|
|
||||||
VC->>VC: notify_all - wake sleeping components
|
|
||||||
|
|
||||||
CLK->>VC: read virtual_time = 333ns
|
|
||||||
CLK->>CLK: 333ns >= 666ns? No - sleep until 666ns
|
|
||||||
|
|
||||||
CPU->>VC: read virtual_time = 333ns
|
|
||||||
CPU->>CPU: Check for clock edges - no change - wait
|
|
||||||
|
|
||||||
Note over Gov: Governor advances
|
|
||||||
Gov->>VC: advance to 666ns
|
|
||||||
VC->>VC: notify_all
|
|
||||||
|
|
||||||
CLK->>VC: read virtual_time = 666ns
|
|
||||||
CLK->>CLK: 666ns >= 666ns? Yes! Toggle PHI1 LOW, PHI2 HIGH
|
|
||||||
CLK->>CLK: Schedule next transition at 1333ns
|
|
||||||
CLK->>VC: wait_until 1333ns
|
|
||||||
|
|
||||||
CPU->>VC: read virtual_time = 666ns
|
|
||||||
CPU->>CPU: Detect PHI2 rising - process data phase
|
|
||||||
|
|
||||||
ROM->>VC: read virtual_time = 666ns
|
|
||||||
ROM->>ROM: See address on bus. Record latch_time = 666ns
|
|
||||||
ROM->>ROM: Need 500ns access time. Data ready at 1166ns
|
|
||||||
ROM->>VC: wait_until 1166ns
|
|
||||||
|
|
||||||
Note over Gov: Governor advances
|
|
||||||
Gov->>VC: advance to 1166ns
|
|
||||||
|
|
||||||
ROM->>VC: read virtual_time = 1166ns
|
|
||||||
ROM->>ROM: 1166 - 666 = 500ns >= 500ns access time. Drive data!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Components
|
|
||||||
|
|
||||||
### 1. VirtualClock — Shared Virtual Time Source
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct VirtualClock {
|
|
||||||
/// Current virtual time in nanoseconds
|
|
||||||
time_ns: AtomicU64,
|
|
||||||
/// Target time for next step (components wait for this)
|
|
||||||
target_ns: AtomicU64,
|
|
||||||
/// Condvar for notifying components when time advances
|
|
||||||
time_advanced: Condvar,
|
|
||||||
/// Mutex for condvar
|
|
||||||
state_mutex: Mutex<()>,
|
|
||||||
/// Speed multiplier: 1.0 = real-time, 0.1 = 10x slower
|
|
||||||
speed_multiplier: AtomicU64, // Stored as fixed-point
|
|
||||||
/// Simulation mode
|
|
||||||
mode: AtomicU8, // 0=RealTime, 1=MaxSpeed, 2=SingleStep
|
|
||||||
/// Single-step trigger
|
|
||||||
step_pending: AtomicBool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VirtualClock {
|
|
||||||
/// Get current virtual time
|
|
||||||
pub fn now(&self) -> Duration {
|
|
||||||
Duration::from_nanos(self.time_ns.load(Ordering::SeqCst))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Block until virtual time reaches at least target
|
|
||||||
/// Returns the actual virtual time when woken
|
|
||||||
pub fn wait_until(&self, target: Duration) -> Duration {
|
|
||||||
let target_ns = target.as_nanos() as u64;
|
|
||||||
self.target_ns.store(target_ns, Ordering::SeqCst);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let current = self.time_ns.load(Ordering::SeqCst);
|
|
||||||
if current >= target_ns {
|
|
||||||
return Duration::from_nanos(current);
|
|
||||||
}
|
|
||||||
// Wait for governor to advance time
|
|
||||||
let guard = self.state_mutex.lock();
|
|
||||||
self.time_advanced.wait(guard);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Non-blocking check: has virtual time reached target?
|
|
||||||
pub fn has_reached(&self, target: Duration) -> bool {
|
|
||||||
self.time_ns.load(Ordering::SeqCst) >= target.as_nanos() as u64
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. RealTimeGovernor — Advances Virtual Clock
|
|
||||||
|
|
||||||
Runs in its own thread. Advances the shared virtual clock based on speed multiplier:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct RealTimeGovernor {
|
|
||||||
clock: Arc<VirtualClock>,
|
|
||||||
tick_size_ns: u64, // Virtual nanoseconds per tick (e.g., 333)
|
|
||||||
speed_multiplier: f64, // 1.0 = real-time
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RealTimeGovernor {
|
|
||||||
fn run(&mut self) {
|
|
||||||
let mut last_wall_time = Instant::now();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match self.clock.mode() {
|
|
||||||
SimulationMode::RealTime => {
|
|
||||||
// Calculate how long to sleep to match speed_multiplier
|
|
||||||
let virtual_advance = Duration::from_nanos(self.tick_size_ns);
|
|
||||||
let target_wall = virtual_advance.div_f64(self.speed_multiplier);
|
|
||||||
|
|
||||||
let elapsed = last_wall_time.elapsed();
|
|
||||||
if elapsed < target_wall {
|
|
||||||
thread::sleep(target_wall - elapsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advance virtual time
|
|
||||||
self.clock.advance_by(self.tick_size_ns);
|
|
||||||
last_wall_time = Instant::now();
|
|
||||||
}
|
|
||||||
|
|
||||||
SimulationMode::MaxSpeed => {
|
|
||||||
// Advance as fast as possible
|
|
||||||
self.clock.advance_by(self.tick_size_ns);
|
|
||||||
// Yield occasionally to prevent starving other threads
|
|
||||||
if self.clock.now().as_nanos() % 1000 == 0 {
|
|
||||||
thread::yield_now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SimulationMode::SingleStep => {
|
|
||||||
// Wait for user to trigger a step
|
|
||||||
if self.clock.consume_step_trigger() {
|
|
||||||
self.clock.advance_by(self.tick_size_ns);
|
|
||||||
} else {
|
|
||||||
thread::sleep(Duration::from_millis(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Component Thread Pattern
|
|
||||||
|
|
||||||
Each component thread follows this pattern:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// In ConfigurableSystem::start() — for each component:
|
|
||||||
let clock_clone = Arc::clone(&virtual_clock);
|
|
||||||
let comp_clone = Arc::clone(&component);
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
let mut next_update_time = Duration::ZERO;
|
|
||||||
let timing = ComponentTiming::for_component(&*comp_clone.lock());
|
|
||||||
|
|
||||||
loop {
|
|
||||||
// Wait for virtual time to reach our next scheduled update
|
|
||||||
let current_vtime = clock_clone.wait_until(next_update_time);
|
|
||||||
|
|
||||||
if !clock_clone.is_running() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lock component and update
|
|
||||||
if let Some(mut comp) = comp_clone.try_lock() {
|
|
||||||
comp.update_at_time(current_vtime);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schedule next update based on component's timing needs
|
|
||||||
next_update_time = current_vtime + timing.minimum_update_interval();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Per-Component Timing with Virtual Time
|
|
||||||
|
|
||||||
Components use virtual time instead of `Instant::now()`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// In Intel4001 (ROM):
|
|
||||||
impl Component for Intel4001 {
|
|
||||||
fn update_at_time(&mut self, virtual_time: Duration) {
|
|
||||||
let (phi1, phi2) = self.read_clock_pins();
|
|
||||||
|
|
||||||
let phi1_rising = phi1 == PinValue::High && self.prev_phi1 == PinValue::Low;
|
|
||||||
|
|
||||||
if phi1_rising {
|
|
||||||
// Record when address was latched (in virtual time!)
|
|
||||||
self.address_latch_time = Some(virtual_time);
|
|
||||||
self.handle_phi1_rising();
|
|
||||||
}
|
|
||||||
|
|
||||||
if phi2_rising {
|
|
||||||
// Check if access time has elapsed IN VIRTUAL TIME
|
|
||||||
if let Some(latch_time) = self.address_latch_time {
|
|
||||||
let elapsed_virtual = virtual_time.saturating_sub(latch_time);
|
|
||||||
if elapsed_virtual >= self.timing.access_time {
|
|
||||||
// Data is valid — drive it
|
|
||||||
self.handle_phi2_rising();
|
|
||||||
} else {
|
|
||||||
// Access time NOT yet elapsed — data not ready!
|
|
||||||
// This happens when the clock is overclocked
|
|
||||||
self.handle_data_not_ready();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.prev_phi1 = phi1;
|
|
||||||
self.prev_phi2 = phi2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Clock Component with Virtual Time
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// In TwoPhaseClock:
|
|
||||||
impl Component for TwoPhaseClock {
|
|
||||||
fn update_at_time(&mut self, virtual_time: Duration) {
|
|
||||||
// Check if it's time to transition (using virtual time)
|
|
||||||
let elapsed = virtual_time.saturating_sub(self.last_transition_vtime);
|
|
||||||
|
|
||||||
match self.current_phase {
|
|
||||||
Phase::Phi1High => {
|
|
||||||
if elapsed >= self.phase_time {
|
|
||||||
self.phi1_state = PinValue::Low;
|
|
||||||
self.phi2_state = PinValue::High;
|
|
||||||
self.last_transition_vtime = virtual_time;
|
|
||||||
self.update_outputs();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Phase::Phi2High => {
|
|
||||||
if elapsed >= self.phase_time {
|
|
||||||
self.phi2_state = PinValue::Low;
|
|
||||||
self.phi1_state = PinValue::High;
|
|
||||||
self.last_transition_vtime = virtual_time;
|
|
||||||
self.update_outputs();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Thread Architecture
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph LR
|
|
||||||
subgraph "Governor Thread"
|
|
||||||
G[RealTimeGovernor]
|
|
||||||
G -->|advance_by tick_size| VC[VirtualClock]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "Component Thread 1"
|
|
||||||
C1[TwoPhaseClock]
|
|
||||||
C1 -->|wait_until next_transition| VC
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "Component Thread 2"
|
|
||||||
C2[Intel4004]
|
|
||||||
C2 -->|wait_until next_cycle| VC
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "Component Thread 3"
|
|
||||||
C3[Intel4001]
|
|
||||||
C3 -->|wait_until access_time_elapsed| VC
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "Component Thread N"
|
|
||||||
CN[...]
|
|
||||||
CN -->|wait_until scheduled_time| VC
|
|
||||||
end
|
|
||||||
|
|
||||||
VC -->|notify_all on advance| C1
|
|
||||||
VC -->|notify_all on advance| C2
|
|
||||||
VC -->|notify_all on advance| C3
|
|
||||||
VC -->|notify_all on advance| CN
|
|
||||||
```
|
|
||||||
|
|
||||||
Each component thread:
|
|
||||||
1. Calls `virtual_clock.wait_until(target_time)` — blocks until the governor advances virtual time past the target
|
|
||||||
2. Wakes up, reads pins, processes its logic
|
|
||||||
3. Schedules its next wake-up time based on its timing constraints
|
|
||||||
4. Goes back to sleep
|
|
||||||
|
|
||||||
The governor thread:
|
|
||||||
1. Advances virtual time by `tick_size` (e.g., 333ns)
|
|
||||||
2. Notifies all waiting component threads
|
|
||||||
3. Sleeps to match wall-clock time × speed_multiplier (if in RealTime mode)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Component Timing Model
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct ComponentTiming {
|
|
||||||
pub access_time: Duration, // Time from address valid to data ready
|
|
||||||
pub propagation_delay: Duration, // Output delay after input change
|
|
||||||
pub setup_time: Duration, // Input stability before clock edge
|
|
||||||
pub hold_time: Duration, // Input stability after clock edge
|
|
||||||
pub max_clock_frequency: f64, // Maximum supported clock frequency
|
|
||||||
pub min_update_interval: Duration, // How often update_at_time should be called
|
|
||||||
}
|
|
||||||
|
|
||||||
// Per-component defaults from datasheets:
|
|
||||||
impl Intel4001 { // ROM - 500ns access
|
|
||||||
fn timing() -> ComponentTiming {
|
|
||||||
ComponentTiming {
|
|
||||||
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), // Check every 100ns
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Intel4002 { // RAM - 500ns access
|
|
||||||
fn timing() -> ComponentTiming {
|
|
||||||
ComponentTiming {
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Intel4003 { // Shift Register - faster 200ns
|
|
||||||
fn timing() -> ComponentTiming {
|
|
||||||
ComponentTiming {
|
|
||||||
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,
|
|
||||||
min_update_interval: Duration::from_nanos(50),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Speed Control
|
|
||||||
|
|
||||||
| Mode | Multiplier | Governor Behavior | Use Case |
|
|
||||||
|------|-----------|-------------------|----------|
|
|
||||||
| RealTime | 0.001× | Sleep 333µs per 333ns tick | Observe individual clock edges |
|
|
||||||
| RealTime | 0.01× | Sleep 33.3µs per 333ns tick | Slow-motion debugging |
|
|
||||||
| RealTime | 0.1× | Sleep 3.33µs per 333ns tick | Slower than real-time |
|
|
||||||
| RealTime | 1.0× | Sleep 333ns per tick (≈ max speed) | Attempt real-time |
|
|
||||||
| MaxSpeed | ∞ | No sleep, yield occasionally | Fastest possible simulation |
|
|
||||||
| SingleStep | — | Advance one tick per trigger | Debug one phase at a time |
|
|
||||||
|
|
||||||
**At 1.0× and above**, the OS cannot keep up (333ns is below scheduling granularity). The governor will run as fast as it can and report the **actual achieved speed** (e.g., "Target: 1.0×, Achieved: 0.47×").
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overclocking Behavior
|
|
||||||
|
|
||||||
When the clock frequency exceeds a component's `max_clock_frequency`:
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
OC[Clock set to 2 MHz - 500ns cycle]
|
|
||||||
ROM1[ROM access_time = 500ns]
|
|
||||||
RAM1[RAM access_time = 500ns]
|
|
||||||
SR1[ShiftReg access_time = 200ns]
|
|
||||||
|
|
||||||
OC -->|Phase = 250ns| ROM1
|
|
||||||
OC -->|Phase = 250ns| RAM1
|
|
||||||
OC -->|Phase = 250ns| SR1
|
|
||||||
|
|
||||||
ROM1 -->|250ns < 500ns| FAIL1[ROM data NOT ready - CPU reads garbage]
|
|
||||||
RAM1 -->|250ns < 500ns| FAIL2[RAM data NOT ready - CPU reads garbage]
|
|
||||||
SR1 -->|250ns >= 200ns| OK[ShiftReg responds correctly - its faster]
|
|
||||||
```
|
|
||||||
|
|
||||||
This is **exactly what happens in real hardware** — the shift register (200ns access) keeps working while ROM/RAM (500ns access) fails.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Migration Strategy
|
|
||||||
|
|
||||||
### Phase 1: Create VirtualClock (new file, non-breaking)
|
|
||||||
|
|
||||||
- New file: `src/virtual_clock.rs`
|
|
||||||
- Contains `VirtualClock` struct with atomic virtual time, condvar, and speed control
|
|
||||||
- No changes to existing code
|
|
||||||
|
|
||||||
### Phase 2: Create RealTimeGovernor (new file, non-breaking)
|
|
||||||
|
|
||||||
- New file: `src/real_time_governor.rs`
|
|
||||||
- Governor thread that advances `VirtualClock` based on speed multiplier
|
|
||||||
- No changes to existing code
|
|
||||||
|
|
||||||
### Phase 3: Create ComponentTiming (new file, non-breaking)
|
|
||||||
|
|
||||||
- New file: `src/component_timing.rs`
|
|
||||||
- `ComponentTiming` struct and per-component defaults
|
|
||||||
- No changes to existing code
|
|
||||||
|
|
||||||
### Phase 4: Extend Component trait with virtual time support
|
|
||||||
|
|
||||||
- Add `update_at_time(&mut self, virtual_time: Duration)` with default that calls `update()`
|
|
||||||
- Add `timing(&self) -> ComponentTiming` with default that returns zero-delay timing
|
|
||||||
- Backward-compatible — existing `update()` still works
|
|
||||||
|
|
||||||
### Phase 5: Migrate clock components to virtual time
|
|
||||||
|
|
||||||
- `GenericClock` and `TwoPhaseClock`: Replace `Instant::now()` with virtual time parameter
|
|
||||||
- Clock transitions based on virtual time comparisons
|
|
||||||
- Clocks call `virtual_clock.wait_until(next_transition)`
|
|
||||||
|
|
||||||
### Phase 6: Add timing enforcement to memory/CPU components
|
|
||||||
|
|
||||||
- Intel4001, Intel4002, Intel4003: Check access times against virtual time
|
|
||||||
- Intel4004: Check setup/hold times
|
|
||||||
- Record timing violations for reporting
|
|
||||||
|
|
||||||
### Phase 7: Update ConfigurableSystem to use VirtualClock
|
|
||||||
|
|
||||||
- Create `Arc<VirtualClock>` shared by all components
|
|
||||||
- Governor thread manages virtual time advancement
|
|
||||||
- Component threads use `virtual_clock.wait_until()` instead of `thread::sleep()`
|
|
||||||
- Keep the per-component thread model — just change how they wait
|
|
||||||
|
|
||||||
### Phase 8: Add speed/step controls to GUI and console
|
|
||||||
|
|
||||||
- Speed slider (logarithmic: 0.001× to MaxSpeed)
|
|
||||||
- Step buttons: Step Phase, Step Cycle, Step Instruction
|
|
||||||
- Display: actual speed, virtual time, timing violations
|
|
||||||
|
|
||||||
### Phase 9: Update tests
|
|
||||||
|
|
||||||
- Tests use MaxSpeed mode for fast deterministic execution
|
|
||||||
- Tests use SingleStep mode for precise state verification
|
|
||||||
- Add overclocking tests: verify ROM fails at 2 MHz but shift register still works
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Differences from Single-Scheduler Approach
|
|
||||||
|
|
||||||
| Aspect | Single Scheduler | Per-Component Threads + VirtualClock |
|
|
||||||
|--------|-----------------|--------------------------------------|
|
|
||||||
| Threading model | 1 scheduler thread | N component threads + 1 governor thread |
|
|
||||||
| Component autonomy | Components are passive — called by scheduler | Components are active — they wait for their own timing |
|
|
||||||
| Realism | Less realistic — all components update in strict order | More realistic — components race independently, just like real hardware |
|
|
||||||
| Timing enforcement | Centralized in scheduler | Distributed — each component enforces its own timing |
|
|
||||||
| Race conditions | None — deterministic order | Possible — but that's **realistic**! Real hardware has races too |
|
|
||||||
| Speed control | Scheduler controls tick rate | Governor controls virtual clock rate |
|
|
||||||
| Complexity | Simpler | More complex but more faithful to hardware |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
1. **Per-component threads preserved** — The project's signature feature remains
|
|
||||||
2. **Hardware-accurate timing** — Each component enforces its real timing constraints
|
|
||||||
3. **Overclocking realism** — Components fail when clocked too fast, just like real hardware
|
|
||||||
4. **Virtual time** — No dependency on OS scheduling granularity
|
|
||||||
5. **Speed control** — Logarithmic multiplier from 0.001× to MaxSpeed
|
|
||||||
6. **Single-step** — Step by phase, cycle, or instruction
|
|
||||||
7. **Realistic races** — Components compete for the bus independently, modeling real hardware behavior
|
|
||||||
8. **Backward compatible** — Phases 1-3 are additive; existing code continues to work during migration
|
|
||||||
1010
plans/vic20_architecture.md
Normal file
1010
plans/vic20_architecture.md
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue