feat: implement virtual clock timing architecture (Phases 5-8)
Introduces a VirtualClock-based timing system replacing wall-clock Instant-based timing for hardware-accurate simulation. New modules: - virtual_clock.rs: Shared virtual time source with condvar sync - real_time_governor.rs: Thread that advances virtual time - component_timing.rs: Per-component timing constraints Key changes: - Component trait: added update_at_time(), timing(), set_virtual_clock_optional() - All Intel 400x components migrated to virtual-time-aware operation - ConfigurableSystem: start() returns Result, spawns governor thread - GUI: timing controls (speed slider, mode selector, step button) - Console: speed/mode/step/time commands - Backward compatible: legacy fallback for components without VirtualClock Reviewed-by: code review found 2 warnings (clock resume() dead code, reset() safety docs) — both fixed.
This commit is contained in:
parent
add7ace674
commit
08f53d68fe
28 changed files with 7186 additions and 270 deletions
870
plans/phase6_timing_enforcement.md
Normal file
870
plans/phase6_timing_enforcement.md
Normal file
|
|
@ -0,0 +1,870 @@
|
|||
# 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue