rp2040_emu/lib.rs
1//! RP2040 emulator library.
2//!
3//! Phase 5.A fills in the bus fabric, CLOCKS/RESETS/PLL/XOSC/ROSC
4//! register storage, full SIO (GPIO, CPUID, FIFO, spinlocks, divider,
5//! interpolators — **no** doorbells / MTIME / coprocessor bridge),
6//! IO_BANK0 / PADS_BANK0, XIP_CTRL / SSI stubs, and dual-core stepping
7//! (core 0 runs; core 1 stays halted until woken via the SIO FIFO
8//! protocol).
9//!
10//! Phase 5.B wires the two PIO blocks (`bus.pio[0]`, `bus.pio[1]`) into
11//! AHB at `0x5020_0000` / `0x5030_0000`, steps them once per emulator
12//! step, and merges their pad outputs into `bus.gpio_in` (PIO OE
13//! overrides SIO on a per-pin basis, mirroring `rp2350_emu::Emulator`).
14//!
15//! See `wrk_docs/2026.04.14 - HLD - mdpicoem Workspace Restructure.md`.
16
17use tracing::info;
18
19pub mod bus;
20pub mod core;
21pub mod dma;
22pub mod dreq;
23pub mod irq;
24pub mod memory;
25pub mod peripherals;
26
27// Dual-execution HLD V1 (Stage 3b.2) — threaded runtime scaffolding.
28// The module file internally `#![cfg]`-gates to x86_64 Windows + the
29// `threading` cargo feature, so non-Windows and `--no-default-features`
30// builds compile an empty module and the serial path is unaffected.
31#[cfg(feature = "threading")]
32pub mod threaded;
33
34// -----------------------------------------------------------------------
35// Dual-execution HLD V1 (Stage 3b.1) — public types.
36//
37// Introduces the `ExecutionModel` selector, `ConfigError`, `WorkerName`,
38// and `EmulatorError` to mirror the RP2350 crate. Stage 3b.1 ships the
39// types + the `CoreBus` trait port so later sub-stages (3b.2: threaded/
40// module, 3b.4: builder wiring) can land against a stable surface. The
41// Emulator dispatch path stays Serial-only in 3b.1.
42// -----------------------------------------------------------------------
43
44/// Execution model for an [`Emulator`]. Selected at construction via
45/// [`EmulatorBuilder::execution`]; cannot be switched post-build.
46///
47/// - `Serial` — oracle-validated reference path (QEMU + silicon
48/// differentials). Single-threaded, per-instruction interleave.
49/// Always available.
50/// - `Threaded` — multi-thread runtime; opt-in throughput optimization
51/// on x86_64 Windows hosts with the `threading` cargo feature on.
52/// Not validated against QEMU/silicon oracles. Not yet wired into
53/// [`Emulator::step`] — arrives with Stage 3b.4.
54#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
55pub enum ExecutionModel {
56 #[default]
57 Serial,
58 Threaded,
59}
60
61/// Errors returned by [`EmulatorBuilder::build`] once the Stage 3b.4
62/// wiring lands. The only non-trivial variant today is
63/// `ThreadingUnavailable`, returned when the caller selects
64/// [`ExecutionModel::Threaded`] but the host platform or build
65/// configuration cannot satisfy it.
66#[derive(Clone, Debug, PartialEq, Eq)]
67pub enum ConfigError {
68 /// `ExecutionModel::Threaded` selected but the current build does
69 /// not include a threaded runtime — either the `threading` cargo
70 /// feature is off, or the host is not one of the supported
71 /// platforms (currently x86_64 Windows only).
72 ThreadingUnavailable,
73}
74
75impl std::fmt::Display for ConfigError {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 match self {
78 ConfigError::ThreadingUnavailable => write!(
79 f,
80 "ExecutionModel::Threaded is unavailable (requires x86_64 Windows \
81 with the `threading` cargo feature enabled)"
82 ),
83 }
84 }
85}
86
87impl std::error::Error for ConfigError {}
88
89/// Identifier for a worker thread in the threaded runtime. RP2040
90/// uses a three-worker layout (core0, core1, coordinator) — smaller
91/// than RP2350's six-worker layout because M0+ has no PIO-as-worker
92/// split in the Stage 3b plan. rp2350_emu's `Pio0`/`Pio1`/`Pio2` worker
93/// variants are intentionally omitted here; if PIO becomes a
94/// bottleneck the enum can gain those variants in a follow-up.
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub enum WorkerName {
97 Core0,
98 Core1,
99 Coord,
100}
101
102impl WorkerName {
103 /// Short label for summary tables / error messages. Kept stable so
104 /// harness tooling can scrape diagnostic output.
105 pub fn as_str(self) -> &'static str {
106 match self {
107 WorkerName::Core0 => "core0",
108 WorkerName::Core1 => "core1",
109 WorkerName::Coord => "coord",
110 }
111 }
112}
113
114/// Errors returned by post-construction [`Emulator`] methods once the
115/// Stage 3b.4 wiring lands. Surfaces runtime-model mismatches and
116/// worker panics (dual-execution HLD V1 §5.5).
117///
118/// `WorkerPanicked` is sticky: once an [`Emulator`] observes a worker
119/// panic, every subsequent call on that instance returns the same
120/// error without re-attempting the workers (one-shot-after-panic, HLD
121/// §5.5 item 5). Drop the instance and rebuild from a fresh
122/// [`EmulatorBuilder`].
123#[derive(Clone, Debug, PartialEq, Eq)]
124pub enum EmulatorError {
125 /// Called a Serial-only method on a Threaded emulator, e.g.
126 /// `step()` — Threaded runs in quanta, not single-step. HLD §5.4.
127 NotSupportedInThreadedMode,
128 /// One of the worker threads panicked. The `Emulator` is sticky-
129 /// poisoned after this; drop and rebuild. Only produced on the
130 /// Threaded path.
131 WorkerPanicked { which: WorkerName, message: String },
132 /// The shared [`picoem_common::SpinBarrier`] watchdog fired
133 /// because a worker failed to arrive at the rendezvous within
134 /// [`picoem_common::threaded::DEFAULT_DEADLINE`]. The `Emulator`
135 /// is sticky-poisoned after this; drop and rebuild. HLD V1 §6.6.
136 ///
137 /// Only produced on the Threaded path. `which` is the first worker
138 /// that returned `TimedOut` at its barrier; since the barrier
139 /// cannot identify *which* worker failed to arrive, this field
140 /// names an observer rather than the culprit. `elapsed_ms` is the
141 /// reporting waiter's own wall-clock elapsed time at expiry.
142 BarrierTimeout { which: WorkerName, elapsed_ms: u32 },
143}
144
145impl std::fmt::Display for EmulatorError {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 match self {
148 EmulatorError::NotSupportedInThreadedMode => write!(
149 f,
150 "operation not supported on a Threaded Emulator (Serial-only)"
151 ),
152 EmulatorError::WorkerPanicked { which, message } => {
153 write!(f, "worker {} panicked: {message}", which.as_str())
154 }
155 EmulatorError::BarrierTimeout { which, elapsed_ms } => write!(
156 f,
157 "barrier watchdog fired (observed by worker {}) after {}ms",
158 which.as_str(),
159 elapsed_ms
160 ),
161 }
162 }
163}
164
165impl std::error::Error for EmulatorError {}
166
167#[cfg(test)]
168mod tests;
169
170#[cfg(test)]
171mod pio_tests;
172
173pub use self::bus::Bus;
174pub use self::core::CortexM0Plus;
175pub use self::memory::{Memory, ROM_SIZE, SRAM_SIZE, bank_for_address};
176
177#[cfg(target_arch = "x86_64")]
178pub use picoem_common::Pacer;
179pub use picoem_common::{Clock, PacerSnapshot, PacerStats};
180
181/// ROSC nominal frequency (~6.5 MHz). RP2040 boots on ROSC at the same
182/// nominal rate as RP2350; PLL configuration (if any) happens later in
183/// firmware.
184pub use picoem_common::ROSC_FREQ_HZ;
185
186/// Emulator configuration.
187pub struct Config {
188 /// System clock frequency in Hz. Default: ROSC (~6.5 MHz).
189 pub sys_clk_hz: u32,
190}
191
192impl Default for Config {
193 fn default() -> Self {
194 Self {
195 sys_clk_hz: ROSC_FREQ_HZ,
196 }
197 }
198}
199
200/// Default quantum size in cycles. Matches `rp2350_emu`.
201pub const DEFAULT_STEP_QUANTUM: u32 = 64;
202
203/// Top-level RP2040 emulator. Owns dual Cortex-M0+ cores, bus fabric,
204/// memory, and clock.
205///
206/// Dual-execution HLD V1: an `Emulator` has a fixed [`ExecutionModel`]
207/// picked at construction time via [`EmulatorBuilder::execution`]. In
208/// Serial mode (default) the `cores` / `bus` / `clock` fields are the
209/// authoritative state and the existing per-instruction interleave
210/// applies. In Threaded mode those fields retain their post-seed
211/// snapshot until the first `run_quantum` promotes them into the
212/// threaded runtime; afterwards the flat fields are zero-cost
213/// placeholders and typed accessors fire a debug-assert if touched.
214pub struct Emulator {
215 pub cores: [CortexM0Plus; 2],
216 pub bus: Bus,
217 pub clock: Clock,
218 /// Cycles advanced per call to [`Self::step`].
219 pub step_quantum: u32,
220 /// Total PIO ticks performed in the slow path
221 /// (`tick_pio_and_route_irqs`). Diagnostic-only — used by the
222 /// PicoGUS harness to confirm PIO is actually being driven.
223 /// Bumps by `cycles` per quantum after HLD 2026.04.26 V5 chunked
224 /// refactor (per-quantum granularity is acceptable).
225 pub pio_tick_count: u64,
226 /// Subset of [`Self::pio_tick_count`] where bit 4 (IOW for PicoGUS)
227 /// of `bus.gpio_in` was low at the moment of the tick. If this stays
228 /// at zero while the harness is asserting IOW low, the override
229 /// merge is breaking somewhere in the path.
230 pub pio_tick_iow_low_count: u64,
231 /// Diagnostic — maximum PC value PIO0 SM0 has held during the run
232 /// (observed after each slow-path tick). PicoGUS bring-up: if this
233 /// stays at the WAIT-pin instruction slot, SM0 never escaped its
234 /// wait. If it climbs to a higher slot, SM0 advanced through the
235 /// program. Slow-path-only — fast-path skips PIO when both blocks
236 /// are idle so SM0 wouldn't be moving regardless.
237 pub pio0_sm0_max_pc: u8,
238 /// Diagnostic — number of times PIO0 SM0's PC differed from its
239 /// previous-tick value (advanced or jumped). Slow-path-only.
240 pub pio0_sm0_pc_advances: u64,
241 /// Last observed PC of PIO0 SM0 — internal scratch used by
242 /// [`Self::tick_pio_and_route_irqs`] to decide whether the
243 /// PC moved this tick. Initialised to a sentinel `0xFF` so the
244 /// very first observation always counts as an advance.
245 pub(crate) pio0_sm0_last_pc: u8,
246 /// Execution model chosen at build time; cannot change
247 /// post-construction. Dispatch for [`Self::step`] / [`Self::run`] /
248 /// [`Self::run_quantum`] branches on this. Defaults to
249 /// [`ExecutionModel::Serial`].
250 pub execution_model: ExecutionModel,
251 /// Live 3-thread runtime when `execution_model == Threaded` and the
252 /// first `run` / `run_quantum` has fired. Takes ownership of the
253 /// pre-seeded cores / bus / clock during lazy `promote_to_threaded`.
254 #[cfg(all(
255 feature = "threading",
256 target_arch = "x86_64",
257 any(target_os = "windows", target_os = "linux")
258 ))]
259 pub(crate) threaded: Option<threaded::ThreadedEmulator>,
260 /// Sticky panic record from a Threaded worker. Set once when
261 /// `run_quantum` / `run` observes a worker panic; every subsequent
262 /// call returns this cached error without re-attempting workers.
263 #[cfg(all(
264 feature = "threading",
265 target_arch = "x86_64",
266 any(target_os = "windows", target_os = "linux")
267 ))]
268 pub(crate) panic_info: Option<(WorkerName, String)>,
269 /// Sticky watchdog-timeout record from a Threaded run. Set once
270 /// when `run_quantum` / `run` observes a barrier timeout; every
271 /// subsequent call returns this cached error without re-attempting
272 /// workers. HLD V1 §6.6 Stage 5.
273 #[cfg(all(
274 feature = "threading",
275 target_arch = "x86_64",
276 any(target_os = "windows", target_os = "linux")
277 ))]
278 pub(crate) timeout_info: Option<(WorkerName, u32)>,
279 /// Test-only panic injector. Armed via
280 /// [`Self::inject_panic_for_testing`]; consumed on the next
281 /// `run_quantum` / `run` call which forwards to
282 /// [`threaded::ThreadedEmulator::inject_panic_for_testing`].
283 #[cfg(all(
284 feature = "testing",
285 feature = "threading",
286 target_arch = "x86_64",
287 any(target_os = "windows", target_os = "linux")
288 ))]
289 pub(crate) pending_panic_inject: Option<WorkerName>,
290 /// `true` once `promote_to_threaded` has moved the seeded state
291 /// into `self.threaded` — the flat `cores` / `bus` / `clock` fields
292 /// now hold zero-cost placeholders. Typed accessors
293 /// (`core`, `core_mut`, `peek`, `gpio_read`, …) `debug_assert!` on
294 /// this flag so Serial-only callers trip loudly if they reach for
295 /// the flat fields after a Threaded run.
296 ///
297 /// Known escape: raw field access (`emu.bus.*`) bypasses the
298 /// guarded accessors — documented in `tech_debt.md`.
299 #[cfg(all(
300 feature = "threading",
301 target_arch = "x86_64",
302 any(target_os = "windows", target_os = "linux")
303 ))]
304 pub(crate) bus_is_placeholder: bool,
305}
306
307impl Emulator {
308 /// Create a new Serial-mode emulator with the given configuration.
309 /// Infallible shim: Serial builds always succeed. For Threaded
310 /// construction or to surface `ConfigError` explicitly, use
311 /// [`EmulatorBuilder`] directly.
312 pub fn new(config: Config) -> Self {
313 EmulatorBuilder::new(config)
314 .build()
315 .expect("Serial build is infallible")
316 }
317
318 /// Currently selected execution model. Set at build time; does not
319 /// change post-construction.
320 pub fn execution_model(&self) -> ExecutionModel {
321 self.execution_model
322 }
323
324 /// Cycle counter for core `idx` (0 or 1). Serial reads directly
325 /// from the flat `cores[idx]`; Threaded reads the worker-thread
326 /// snapshot (valid between `run_quantum` calls). Returns 0 on
327 /// Threaded before the first `run_quantum` (cores not yet taken).
328 pub fn core_cycles(&self, idx: u8) -> u64 {
329 #[cfg(all(
330 feature = "threading",
331 target_arch = "x86_64",
332 any(target_os = "windows", target_os = "linux")
333 ))]
334 if let Some(t) = &self.threaded {
335 return t.core_cycles(idx);
336 }
337 match idx {
338 0 | 1 => self.cores[idx as usize].cycles(),
339 _ => panic!("core_cycles: idx must be 0 or 1"),
340 }
341 }
342
343 /// Placeholder-guard message shared by the typed accessors below.
344 #[cfg(all(
345 feature = "threading",
346 target_arch = "x86_64",
347 any(target_os = "windows", target_os = "linux")
348 ))]
349 const PLACEHOLDER_GUARD_MSG: &'static str = "direct field access on cores/bus/clock is Serial-only; emulator is in \
350 Threaded mode — use typed accessors like core_cycles(), master_cycle(), \
351 gpio_read() instead";
352
353 /// Debug-only placeholder assertion. No-op on non-threading
354 /// platforms and in release builds.
355 #[inline(always)]
356 fn assert_not_placeholder(&self) {
357 #[cfg(all(
358 feature = "threading",
359 target_arch = "x86_64",
360 any(target_os = "windows", target_os = "linux")
361 ))]
362 debug_assert!(!self.bus_is_placeholder, "{}", Self::PLACEHOLDER_GUARD_MSG);
363 }
364
365 /// Reset the emulator:
366 /// * Load SP from ROM word 0, PC from ROM word 4 into both cores.
367 /// * Core 0 is the bootstrapped core (runs from reset).
368 /// * Core 1 is halted — the Pico SDK launches it by writing a
369 /// wake sequence through the SIO FIFO; `step` calls
370 /// [`Self::wake_checks`] each quantum to observe the handshake.
371 pub fn reset(&mut self) {
372 self.assert_not_placeholder();
373 let initial_sp = self.bus.memory.rom_read32(0);
374 let reset_vector = self.bus.memory.rom_read32(4);
375
376 for i in 0..2 {
377 self.cores[i] = CortexM0Plus::with_id(i as u8);
378 self.cores[i].regs.msp = initial_sp;
379 self.cores[i].regs.r[13] = initial_sp;
380 self.cores[i].regs.set_pc(reset_vector & !1);
381 self.cores[i].regs.xpsr = 1 << 24; // Thumb bit (XPSR_T)
382 }
383
384 self.bus.sio.reset();
385 self.bus.resets.reset();
386 self.bus.clocks_regs.reset();
387 self.bus.xosc_regs.reset();
388 self.bus.rosc_regs.reset();
389 self.bus.watchdog_tick.reset();
390 self.bus.timer.reset();
391 self.bus.uart0.reset();
392 self.bus.uart1.reset();
393 self.bus.spi0.reset();
394 self.bus.spi1.reset();
395 self.bus.i2c0.reset();
396 self.bus.i2c1.reset();
397 self.bus.adc.reset();
398 self.bus.pwm.reset();
399 self.bus.dma.reset();
400 self.bus.irq_pending = 0;
401 for n in &mut self.bus.nvics {
402 n.reset();
403 }
404 self.bus.pll_sys_regs = bus::clocks::PLL_RESET;
405 self.bus.pll_usb_regs = bus::clocks::PLL_RESET;
406 self.bus.pll_sys_lock_at_cycle = None;
407 self.bus.pll_usb_lock_at_cycle = None;
408 self.bus.master_cycle = 0;
409 self.bus.clock_tree = Default::default();
410 self.bus.io_bank0.reset();
411 self.bus.pads_bank0.reset();
412 for pio in &mut self.bus.pio {
413 pio.reset();
414 }
415 // Diagnostic counters track post-reset behaviour, so zero them
416 // on `reset()` too (the SM `pc` field also resets to 0, hence
417 // the sentinel `0xFF` for `last_pc` to make the first observed
418 // PC count as an advance).
419 self.pio0_sm0_max_pc = 0;
420 self.pio0_sm0_pc_advances = 0;
421 self.pio0_sm0_last_pc = 0xFF;
422 if let Some(ref mut psram) = self.bus.psram {
423 psram.reset_state();
424 }
425 self.bus.clear_bus_fault();
426 self.bus.ppb = [Default::default(), Default::default()];
427 self.bus.event_flag = [false; 2];
428 self.bus.wfe_waiting = [false; 2];
429 self.bus.gpio_in = 0;
430 self.bus.external_gpio_in_override = 0;
431 self.bus.external_gpio_in_mask = 0;
432 self.bus.end_core1_step();
433
434 self.clock = Clock { cycles: 0 };
435
436 // Core 1 stays halted — bootrom on real silicon parks core 1 in
437 // a wait-for-event loop until core 0 sends the wake sequence.
438 // Routed through the wrapper so the SIO handshake FSM `armed`
439 // flag stays in sync with core 1's halt state (HLD §2.1).
440 self.halt_core1();
441 }
442
443 /// Load a raw binary at the given address. ROM writes are honoured
444 /// (test seeding path); SRAM writes land in the SRAM backing store;
445 /// XIP loads use [`Self::load_flash`].
446 pub fn load_image(&mut self, addr: u32, data: &[u8]) {
447 self.assert_not_placeholder();
448 match addr >> 28 {
449 0x0 => {
450 // ROM: bootrom-style loads happen via `load_bootrom`.
451 // Support ROM overlay here for tests that want to place
452 // code at an arbitrary ROM offset without zero-padding.
453 let offset = (addr & 0x0FFF_FFFF) as usize;
454 let mut rom_buf = vec![0u8; ROM_SIZE];
455 // Seed with current ROM content so a partial overlay
456 // preserves whatever was already loaded.
457 for i in 0..ROM_SIZE {
458 rom_buf[i] = self.bus.memory.rom_read8(i as u32);
459 }
460 let end = (offset + data.len()).min(ROM_SIZE);
461 if offset < ROM_SIZE {
462 rom_buf[offset..end].copy_from_slice(&data[..end - offset]);
463 self.bus.memory.load_rom(&rom_buf);
464 }
465 self.invalidate_decode_caches_region(crate::bus::invalidation_regions::ROM);
466 }
467 0x2 => {
468 for (i, &byte) in data.iter().enumerate() {
469 let a = addr.wrapping_add(i as u32);
470 self.bus.memory.sram_write8(a & 0x00FF_FFFF, byte);
471 }
472 self.invalidate_decode_caches_region(crate::bus::invalidation_regions::SRAM);
473 }
474 _ => {}
475 }
476 }
477
478 /// Bulk-invalidate both cores' decode caches for the given region
479 /// bitmask. Used by `load_image` (which writes directly to the
480 /// memory backing store, bypassing `Bus::write*`'s automatic
481 /// per-write invalidation queue) to keep the caches coherent with
482 /// the new bytes. Caller passes a single region bit (ROM / XIP /
483 /// SRAM) or BULK to drain everything.
484 fn invalidate_decode_caches_region(&mut self, region: u8) {
485 self.cores[0].invalidate_decode_cache_regions(region);
486 self.cores[1].invalidate_decode_cache_regions(region);
487 }
488
489 /// Load the 16 KB RP2040 bootrom at address `0x0000_0000`.
490 pub fn load_bootrom(&mut self, data: &[u8]) {
491 self.assert_not_placeholder();
492 self.bus.load_bootrom(data);
493 // Drain the region bit `Bus::load_bootrom` set so the next
494 // `step` doesn't see a stale ROM region flag.
495 let regions = std::mem::take(&mut self.bus.pending_invalidation_regions);
496 if regions != 0 {
497 self.cores[0].invalidate_decode_cache_regions(regions);
498 self.cores[1].invalidate_decode_cache_regions(regions);
499 }
500 }
501
502 /// Load an XIP flash image (appears at XIP address `0x1000_0000`).
503 pub fn load_flash(&mut self, data: &[u8]) {
504 self.assert_not_placeholder();
505 self.bus.load_flash(data);
506 let regions = std::mem::take(&mut self.bus.pending_invalidation_regions);
507 if regions != 0 {
508 self.cores[0].invalidate_decode_cache_regions(regions);
509 self.cores[1].invalidate_decode_cache_regions(regions);
510 }
511 }
512
513 /// Direct-boot into an SDK-style firmware by emulating the boot2 →
514 /// application handoff. On real silicon the boot2 stub does three
515 /// things before jumping to the application reset handler: it loads
516 /// SP from word 0 of the vector table, sets VTOR to the vector
517 /// table's flash address, and branches to the reset handler at word
518 /// 1 (Thumb bit stripped). This helper performs the same three-piece
519 /// handoff — SP, VTOR, PC — into both cores, then parks core 1
520 /// halted as `reset()` does. The vector table is expected at
521 /// `vtor_offset` within flash (typically `0x100` for pico-sdk).
522 ///
523 /// Skipping VTOR is silently fatal for any pico-sdk firmware that
524 /// calls `runtime_init_install_ram_vector_table`, which copies the
525 /// flash vector table into SRAM and then writes the SRAM address to
526 /// VTOR. The copy walks `mem[VTOR + 4*i]` for `i` in 0..48; with
527 /// VTOR left at `0x0000_0000` that reads from the bootrom image —
528 /// garbage bytes get installed as exception handlers and the first
529 /// systick fault sends PC into the weeds.
530 ///
531 /// Why this helper exists at all — the real RP2040 B2 bootrom
532 /// detects an attached QSPI flash chip by sampling six QSPI pads via
533 /// `SIO GPIO_HI_IN` (offset `0x008`) and validates boot2 by CRC of
534 /// the first 252 flash bytes read through the SSI peripheral. Our
535 /// emulator stubs SSI and has no QSPI pad model, so the bootrom
536 /// (correctly) gives up and enters USB MSC boot mode, where it waits
537 /// forever for a UF2 drop. This helper bypasses that check.
538 ///
539 /// The bootrom image remains populated at `0x00000000` so firmware
540 /// can resolve ROM function-table pointers (`rom_func_lookup`,
541 /// `rom_data_lookup`). Call **after** `load_bootrom` + `load_flash`
542 /// + `reset`.
543 pub fn direct_boot_from_flash(&mut self, vtor_offset: u32) {
544 self.assert_not_placeholder();
545 let sp = self.bus.memory.xip_read32(vtor_offset);
546 let pc = self.bus.memory.xip_read32(vtor_offset + 4) & !1;
547 let vtor_addr = bus::XIP_FLASH_BASE + vtor_offset;
548 for core in 0..2 {
549 self.cores[core].regs.msp = sp;
550 self.cores[core].regs.r[13] = sp;
551 self.cores[core].regs.set_pc(pc);
552 }
553 self.bus.ppb[0].vtor = vtor_addr;
554 self.bus.ppb[1].vtor = vtor_addr;
555 // Core 1 stays halted — SDK firmware launches it explicitly via
556 // the SIO FIFO handshake, same as after bootrom hand-off. Route
557 // through the wrapper so the handshake FSM re-arms if the caller
558 // used `direct_boot_from_flash` as a mode-switch (§2.1).
559 self.halt_core1();
560 }
561
562 /// Advance the system by up to `step_quantum` master-clock cycles,
563 /// then tick peripherals once. Returns the number of cycles actually
564 /// consumed in this quantum (may be less than `step_quantum` if both
565 /// cores halt mid-quantum).
566 ///
567 /// Per-instruction interleaving of core 0 and core 1 is preserved so
568 /// that bank contention timing on core 1 (`contention_check_active`)
569 /// still accounts +1 cycle on same-port accesses. Each core is armed
570 /// independently per iteration — core 1 can continue running while
571 /// core 0 is halted, and vice-versa. Per-instruction FIFO wake
572 /// checks (`maybe_wake_core1`) also remain so a FIFO write from
573 /// core 0 wakes core 1 within the same quantum.
574 ///
575 /// Dual-core schedule (per inner-loop iteration):
576 /// 1. If core 0 is not halted, step it — fetch/decode/execute one
577 /// instruction.
578 /// 2. If core 1 is not halted, step it with `contention_check_active`
579 /// so same-bank SRAM accesses incur +1 cycle.
580 /// 3. Advance the master clock by `max(c0, c1)` — both cores share
581 /// one clock on real silicon.
582 ///
583 /// The loop exits when `clock.cycles >= target` or both cores are
584 /// halted. Then advance PIO and the GPIO/PSRAM merge **one system
585 /// cycle at a time** for each consumed cycle — so PIO-driven SPI
586 /// programs (which toggle SCK every 1–2 sysclks) present every edge
587 /// to the off-chip PSRAM model. A bulk `tick_pio(consumed)` followed
588 /// by a single `update_gpio()` would let SCK/CS edges slip between
589 /// the start and end of the quantum — the PSRAM would only ever see
590 /// the quantum's final pin snapshot.
591 ///
592 /// Fast-path: when both PIO blocks have no SM enabled, no GPIO pin
593 /// can change during this peripheral-tick window (SIO writes only
594 /// land inside the core loop above, which has already finished),
595 /// so a second `psram.tick` on the same pin snapshot would be a
596 /// semantic no-op — one bulk `tick_pio(consumed) + update_gpio()`
597 /// suffices. This preserves paced_bench_rp2040's throughput on
598 /// pure-ALU workloads (no PIO activity), which would otherwise pay
599 /// a per-cycle `update_gpio` tax for nothing.
600 ///
601 /// Core 1 halted ⇒ PIO may still be ticking (e.g. SPI PSRAM on core
602 /// 0), so the per-cycle loop runs regardless of core-halt state.
603 /// Differs from `rp2350_emu::Emulator::step`'s quantum-end peripheral
604 /// tick — rp2040_emu has the external PSRAM which is sensitive to
605 /// sub-quantum edge timing; rp2350_emu has no equivalent peripheral.
606 pub fn step(&mut self) -> Result<u64, EmulatorError> {
607 if self.execution_model == ExecutionModel::Threaded {
608 #[cfg(all(
609 feature = "threading",
610 target_arch = "x86_64",
611 any(target_os = "windows", target_os = "linux")
612 ))]
613 if let Some((which, message)) = &self.panic_info {
614 return Err(EmulatorError::WorkerPanicked {
615 which: *which,
616 message: message.clone(),
617 });
618 }
619 #[cfg(all(
620 feature = "threading",
621 target_arch = "x86_64",
622 any(target_os = "windows", target_os = "linux")
623 ))]
624 if let Some((which, elapsed_ms)) = self.timeout_info {
625 return Err(EmulatorError::BarrierTimeout { which, elapsed_ms });
626 }
627 return Err(EmulatorError::NotSupportedInThreadedMode);
628 }
629 Ok(self.step_serial())
630 }
631
632 /// Drain the bus's pending decode-cache invalidations into both
633 /// cores' caches and reset the buffers. Called after each
634 /// `core.step` in [`Self::step_serial`] (mirroring the rp2350_emu
635 /// drain at lib.rs:1356-1373, commit `0c31479`).
636 ///
637 /// Per-instruction queue (`pending_cache_invalidations`) drains
638 /// only into the core that just ran — the runner that wrote the
639 /// bytes is the one most likely to refetch them, and the peer
640 /// core's executable bytes haven't moved this step. Region-scoped
641 /// bulk invalidations (`pending_invalidation_regions`, set by ISB
642 /// inside an instruction or by a mid-step `Bus::load_*`) drain to
643 /// BOTH cores so cross-core SMC observers get evicted on their
644 /// next turn.
645 #[inline]
646 fn drain_cache_invalidations(bus: &mut Bus, cores: &mut [CortexM0Plus; 2]) {
647 if !bus.pending_cache_invalidations.is_empty() {
648 let active = bus.active_core();
649 cores[active].invalidate_decode_cache_entries(&bus.pending_cache_invalidations);
650 bus.pending_cache_invalidations.clear();
651 }
652 if bus.pending_invalidation_regions != 0 {
653 let regions = bus.pending_invalidation_regions;
654 cores[0].invalidate_decode_cache_regions(regions);
655 cores[1].invalidate_decode_cache_regions(regions);
656 bus.pending_invalidation_regions = 0;
657 }
658 }
659
660 /// Serial-mode single-quantum step. Shared by [`Self::step`] and
661 /// [`Self::run_quantum`] on the Serial path.
662 fn step_serial(&mut self) -> u64 {
663 debug_assert!(self.step_quantum > 0, "step_quantum must be >= 1");
664 // Refresh the Bus's view of the master cycle count so any MMIO
665 // reads / writes performed during this quantum (notably PLL CS
666 // lock bit + lock-arm transitions — see
667 // `wrk_docs/2026.04.15 - HLD - PLL LOCK Modelling.md` §6 P2)
668 // observe a current cycle. Staleness is bounded by one quantum.
669 self.bus.master_cycle = self.clock.cycles;
670 let start = self.clock.cycles;
671 let target = start.wrapping_add(self.step_quantum as u64);
672
673 // Per HLD 2026.04.26 V5 §5.2.3: accumulate per-core cycle counts
674 // across the inner loop so the slow-branch SysTick advance
675 // mirrors per-core hardware semantics (each core's SysTick
676 // decrements on its own consumed cycles, not on the active-core
677 // shared register).
678 let mut c0_total: u64 = 0;
679 let mut c1_total: u64 = 0;
680
681 while self.clock.cycles < target
682 && (!self.cores[0].is_halted() || !self.cores[1].is_halted())
683 {
684 let c0 = if !self.cores[0].is_halted() && !self.bus.wfe_waiting[0] {
685 self.bus.set_active_core(0);
686 let c = self.cores[0].step(&mut self.bus) as u64;
687 // Drain decode-cache invalidations recorded by writes
688 // during this step into the core that just ran.
689 // Region-scoped bulk invalidations (load_*) reach BOTH
690 // cores so a peer core fetching from the same region
691 // sees the eviction next quantum. Mirrors rp2350_emu
692 // (commit 0c31479, lib.rs §lookup-and-drain).
693 Self::drain_cache_invalidations(&mut self.bus, &mut self.cores);
694 self.maybe_wake_core1(0);
695 c
696 } else {
697 0
698 };
699
700 let c1 = if !self.cores[1].is_halted() && !self.bus.wfe_waiting[1] {
701 self.bus.set_active_core(1);
702 self.bus.begin_core1_step();
703 let c = self.cores[1].step(&mut self.bus) as u64;
704 Self::drain_cache_invalidations(&mut self.bus, &mut self.cores);
705 self.bus.end_core1_step();
706 self.maybe_wake_core1(1);
707 c
708 } else {
709 // Still clear any leftover bank-tracking state so the
710 // next iteration starts fresh.
711 self.bus.end_core1_step();
712 0
713 };
714
715 if c0 == 0 && c1 == 0 {
716 break;
717 }
718 c0_total = c0_total.wrapping_add(c0);
719 c1_total = c1_total.wrapping_add(c1);
720 self.clock.cycles = self.clock.cycles.wrapping_add(c0.max(c1));
721 }
722
723 let consumed = self.clock.cycles.wrapping_sub(start);
724 // tech_debt §1649: when both cores are blocked (halted-or-WFE)
725 // and the inner loop made no progress, `consumed == 0` and
726 // neither the fast-path nor the slow-path below would advance
727 // the master clock — so a TIMER alarm scheduled in the future
728 // could never fire to wake either core. Detect that exact
729 // state, advance the master clock to the soonest scheduled
730 // IRQ-raising alarm (capped by `step_quantum`), tick lazy
731 // peripherals once, drain the resulting IRQs to the NVICs, and
732 // let `wake_checks` at the tail un-halt the woken core. Take
733 // the early return so we don't double-advance via fast/slow
734 // path. Production fix per HLD V2 / tech_debt §1649 Option 1.
735 if consumed == 0
736 && (self.cores[0].is_halted() || self.bus.wfe_waiting[0])
737 && (self.cores[1].is_halted() || self.bus.wfe_waiting[1])
738 && self.bus.irq_pending == 0
739 && self.bus.nvics[0].pending_and_enabled() == 0
740 && self.bus.nvics[1].pending_and_enabled() == 0
741 && let Some(deadline) = self.bus.next_scheduled_lazy_deadline()
742 && deadline > self.bus.master_cycle
743 {
744 // Cap a single advance at `step_quantum`. If the deadline is
745 // farther out, the caller's `run`/`run_quantum` loop iterates
746 // and the next `step()` re-enters this branch, closing the
747 // gap across multiple quanta — never silently stalls.
748 let max_advance = self.step_quantum as u64;
749 let advance = (deadline - self.bus.master_cycle).min(max_advance);
750 self.clock.cycles = self.clock.cycles.wrapping_add(advance);
751 self.bus.master_cycle = self.clock.cycles;
752 self.bus.tick_peripherals(advance as u32);
753 self.drain_pending_irqs_to_cores();
754 self.wake_checks();
755 return advance;
756 }
757 // See the fn docstring for the rationale on the fast-path and
758 // the per-cycle interleave. Measured impact of the fast-path
759 // gate on paced_bench_rp2040 (pure ALU, PIO disabled): without
760 // it, ~49% throughput regression; with it, neutral.
761 //
762 // HLD V7 §5.5 broadens the gate from "PIO idle" to "PIO idle
763 // AND peripherals idle AND DMA idle AND no IRQ pending".
764 // Phase 1 peripherals are all lazy (TIMER/WATCHDOG_TICK), and
765 // DMA is a Phase 1 always-idle stub, so in practice the gate
766 // still reduces to the PIO check — but the extra conditions
767 // are in place so later phases don't need to reopen this
768 // site.
769 let pio_idle = self.bus.pio_all_idle();
770 let peri_idle = self.bus.all_peripherals_idle();
771 let dma_idle = self.bus.dma.is_idle();
772 let any_irq = self.bus.irq_pending != 0;
773 // SysTick fires by ORing into `bus.ppb[active].icsr` — NOT by
774 // setting `bus.irq_pending` — so the `any_irq` check above does
775 // not gate the fast path on SysTick activity. With SysTick
776 // enabled and no peripheral activity (e.g. the V5 §5.2
777 // tail-chain scenario's `b .` busy-wait after preamble), the
778 // fast path would otherwise trigger and SysTick would never
779 // tick. Drop to the slow path whenever SysTick is enabled on
780 // the active core; SysTick-disabled workloads (almost
781 // everything) keep their fast-path eligibility.
782 let systick_idle = !self.bus.systicks[self.bus.active_core()].is_enabled();
783 if pio_idle && peri_idle && dma_idle && systick_idle && !any_irq {
784 self.tick_pio(consumed as u32);
785 // Advance lazy-scheduled peripherals (TIMER alarms) by the
786 // same window the cores consumed. Any alarm matching inside
787 // the window fires into `bus.irq_pending` and gets drained
788 // in the same breath — so firmware that kicks off an alarm
789 // in one quantum sees the IRQ land by the start of the
790 // next.
791 self.bus.advance_lazy_scheduled(consumed);
792 self.drain_pending_irqs_to_cores();
793 self.update_gpio();
794 } else {
795 // Per HLD 2026.04.26 V5 §5.3: chunked once-per-quantum slow
796 // branch. `master_cycle` advances by `consumed` BEFORE
797 // `tick_peripherals` so TIMER's alarm `>=` poll sees the
798 // window's end-of-quantum cycle. SysTick advances per-core
799 // by each core's actual consumed cycle count (mirrors M0+
800 // hardware: SysTick is per-core, decremented on the cycles
801 // the owning core consumes — see §5.2.3). PIO and IRQ drain
802 // run once at quantum end; net IRQ-delivery latency grows
803 // from ≤1 cycle to ≤step_quantum-1 cycles (see §5.4).
804 self.bus.master_cycle = self.bus.master_cycle.wrapping_add(consumed);
805 self.bus.tick_peripherals(consumed as u32);
806 self.tick_systick(c0_total as u32, c1_total as u32);
807 self.tick_pio_and_route_irqs(consumed as u32);
808 self.update_gpio();
809 self.drain_pending_irqs_to_cores();
810 }
811 self.wake_checks();
812 consumed
813 }
814
815 /// Drain [`Bus::irq_pending`] into both cores' NVIC pending
816 /// latches. Per HLD V7 §5.2 this runs once per slow-path inner
817 /// cycle so level-triggered peripherals have at most one
818 /// architectural cycle of routing lag from assert to NVIC latch.
819 ///
820 /// Both cores see every IRQ — RP2040 has a single NVIC per core
821 /// but shared peripheral IRQ wires, so each line latches
822 /// independently on both cores and firmware routes via
823 /// `NVIC_IPR` / `NVIC_ISER` (modelled in
824 /// `bus/mod.rs::nvic_mmio_write32` + `nvic_mmio_read32`).
825 fn drain_pending_irqs_to_cores(&mut self) {
826 if self.bus.irq_pending != 0 {
827 let raised = std::mem::replace(&mut self.bus.irq_pending, 0);
828 for irq in 0..crate::irq::IRQ_COUNT {
829 if raised & (1u32 << irq) != 0 {
830 self.bus.nvics[0].set_pending(irq as u8);
831 self.bus.nvics[1].set_pending(irq as u8);
832 }
833 }
834 }
835 }
836
837 /// Per HLD 2026.04.26 V5 §5.2.3: advance each core's SysTick by
838 /// its own consumed cycle count for this quantum. Mirrors M0+
839 /// hardware semantics — SysTick is per-core, the active-core
840 /// `Bus` field is just a banked-MMIO selector, not a tick gate.
841 /// PENDSTSET (`ICSR[26]`) latches per-core; `drain_pending_irqs_to_cores`
842 /// runs after this call so the SysTick handler is taken on the
843 /// next quantum boundary.
844 fn tick_systick(&mut self, c0: u32, c1: u32) {
845 for _ in 0..c0 {
846 if self.bus.systicks[0].tick() {
847 self.bus.ppb[0].icsr |= 1 << 26;
848 }
849 }
850 for _ in 0..c1 {
851 if self.bus.systicks[1].tick() {
852 self.bus.ppb[1].icsr |= 1 << 26;
853 }
854 }
855 }
856
857 /// Step both PIO blocks by `cycles` system clocks and route their
858 /// IRQ flags into [`Bus::irq_pending`].
859 ///
860 /// Per HLD V7 §5.5 + Appendix B, each PIO block has 8 internal
861 /// Per-block 12-bit raw status (`IRQ[3:0]` + RXNEMPTY[3:0] +
862 /// TXNFULL[3:0]) is masked through `INT0_INTE` / `INT1_INTE` and
863 /// OR'd with `INT0_INTF` / `INT1_INTF` to derive the effective
864 /// values on each NVIC line. Each block has two lines: PIO0_IRQ_0/1
865 /// at NVIC #7/#8 and PIO1_IRQ_0/1 at NVIC #9/#10. PicoGUS firmware
866 /// enables `RXNEMPTY_SM0` on PIO0 INT0_INTE so its ISA handler
867 /// fires when an autopushed event lands in PIO0 SM0's RX FIFO.
868 ///
869 /// Per HLD 2026.04.26 V5 §5.1: chunked once-per-quantum.
870 fn tick_pio_and_route_irqs(&mut self, cycles: u32) {
871 let gpio_in = self.bus.gpio_in;
872 // Diagnostic counters bump by `cycles` (per-quantum granularity is
873 // acceptable per HLD 2026.04.26 V5 §7 risk row "Per-cycle
874 // observation diagnostics under-count by quantum factor").
875 self.pio_tick_count = self.pio_tick_count.wrapping_add(cycles as u64);
876 if gpio_in & (1u32 << 4) == 0 {
877 self.pio_tick_iow_low_count = self.pio_tick_iow_low_count.wrapping_add(cycles as u64);
878 }
879 self.bus.pio[0].step_n(cycles, gpio_in);
880 self.bus.pio[1].step_n(cycles, gpio_in);
881 // Observe PIO0 SM0's PC after the step. Tracks max PC and the
882 // number of times the PC differs from the prior observation
883 // (counts both linear advances and jumps; sequential same-PC
884 // ticks — e.g. a stalled WAIT — do not increment).
885 let sm0_pc = self.bus.pio[0].sm[0].pc();
886 if sm0_pc > self.pio0_sm0_max_pc {
887 self.pio0_sm0_max_pc = sm0_pc;
888 }
889 if sm0_pc != self.pio0_sm0_last_pc {
890 self.pio0_sm0_pc_advances = self.pio0_sm0_pc_advances.wrapping_add(1);
891 self.pio0_sm0_last_pc = sm0_pc;
892 }
893 for (block, line0_bit) in [(0usize, 7u32), (1usize, 9u32)] {
894 if self.bus.pio[block].int0_ints_rp2040() != 0 {
895 self.bus.irq_pending |= 1u32 << line0_bit;
896 }
897 if self.bus.pio[block].int1_ints_rp2040() != 0 {
898 self.bus.irq_pending |= 1u32 << (line0_bit + 1);
899 }
900 }
901 }
902
903 /// Advance both PIO blocks by `cycles` system-clock cycles.
904 ///
905 /// PIO reads `bus.gpio_in` as its view of external pin state — feed it
906 /// the pre-step merge so programs sampling GPIO (e.g. IN PINS) see the
907 /// value SIO / the previous PIO step wrote last. The post-step
908 /// `update_gpio()` then refreshes `bus.gpio_in` from `pad_out`/`pad_oe`.
909 fn tick_pio(&mut self, cycles: u32) {
910 if cycles == 0 {
911 return;
912 }
913 let gpio_in = self.bus.gpio_in;
914 for pio in &mut self.bus.pio {
915 pio.step_n(cycles, gpio_in);
916 }
917 }
918
919 /// Run for at least `cycles` virtual cycles. Returns the number of
920 /// cycles actually executed. May overshoot by up to `step_quantum - 1`
921 /// cycles (one quantum's worth), matching the documented overshoot
922 /// behaviour of [`Self::step`].
923 ///
924 /// Dispatches to the selected [`ExecutionModel`]. In Threaded mode
925 /// this rounds up to the nearest quantum boundary (HLD V1 §5.4)
926 /// and returns `Err(EmulatorError::WorkerPanicked)` sticky on
927 /// worker panic.
928 pub fn run(&mut self, cycles: u64) -> Result<u64, EmulatorError> {
929 if self.execution_model == ExecutionModel::Serial {
930 let start = self.clock.cycles;
931 while self.clock.cycles.wrapping_sub(start) < cycles {
932 let consumed = self.step_serial();
933 if consumed == 0 {
934 break;
935 }
936 }
937 return Ok(self.clock.cycles.wrapping_sub(start));
938 }
939 #[cfg(all(
940 feature = "threading",
941 target_arch = "x86_64",
942 any(target_os = "windows", target_os = "linux")
943 ))]
944 {
945 if let Some((which, message)) = &self.panic_info {
946 return Err(EmulatorError::WorkerPanicked {
947 which: *which,
948 message: message.clone(),
949 });
950 }
951 if let Some((which, elapsed_ms)) = self.timeout_info {
952 return Err(EmulatorError::BarrierTimeout { which, elapsed_ms });
953 }
954 if self.threaded.is_none() {
955 self.promote_to_threaded();
956 }
957 self.apply_pending_panic_inject();
958 let step_q = self.step_quantum as u64;
959 let quanta = cycles.div_ceil(step_q.max(1));
960 let threaded = self.threaded.as_mut().expect("threaded promoted above");
961 match threaded.run_quanta_checked(quanta) {
962 Ok(()) => Ok(quanta.saturating_mul(step_q)),
963 Err(threaded::RunError::Panic { which, message }) => {
964 self.panic_info = Some((which, message.clone()));
965 Err(EmulatorError::WorkerPanicked { which, message })
966 }
967 Err(threaded::RunError::Timeout { which, elapsed_ms }) => {
968 self.timeout_info = Some((which, elapsed_ms));
969 Err(EmulatorError::BarrierTimeout { which, elapsed_ms })
970 }
971 }
972 }
973 #[cfg(not(all(
974 feature = "threading",
975 target_arch = "x86_64",
976 any(target_os = "windows", target_os = "linux")
977 )))]
978 {
979 let _ = cycles;
980 Err(EmulatorError::NotSupportedInThreadedMode)
981 }
982 }
983
984 /// Advance the emulator by exactly one quantum (`step_quantum`
985 /// cycles). Primary entry point for the Threaded path; on Serial
986 /// this is the same as [`Self::step`] and returns the cycles
987 /// consumed. HLD V1 §5.4.
988 ///
989 /// Returns `Err(EmulatorError::WorkerPanicked)` sticky on worker
990 /// panic in Threaded mode. One-shot-after-panic: subsequent calls
991 /// return the cached error without re-attempting workers.
992 pub fn run_quantum(&mut self) -> Result<u64, EmulatorError> {
993 match self.execution_model {
994 ExecutionModel::Serial => Ok(self.step_serial()),
995 ExecutionModel::Threaded => self.run_quantum_threaded(),
996 }
997 }
998
999 #[cfg(all(
1000 feature = "threading",
1001 target_arch = "x86_64",
1002 any(target_os = "windows", target_os = "linux")
1003 ))]
1004 fn run_quantum_threaded(&mut self) -> Result<u64, EmulatorError> {
1005 if let Some((which, message)) = &self.panic_info {
1006 return Err(EmulatorError::WorkerPanicked {
1007 which: *which,
1008 message: message.clone(),
1009 });
1010 }
1011 if let Some((which, elapsed_ms)) = self.timeout_info {
1012 return Err(EmulatorError::BarrierTimeout { which, elapsed_ms });
1013 }
1014 if self.threaded.is_none() {
1015 self.promote_to_threaded();
1016 }
1017 self.apply_pending_panic_inject();
1018 let step_q = self.step_quantum as u64;
1019 let threaded = self.threaded.as_mut().expect("threaded promoted above");
1020 match threaded.run_quanta_checked(1) {
1021 Ok(()) => Ok(step_q),
1022 Err(threaded::RunError::Panic { which, message }) => {
1023 self.panic_info = Some((which, message.clone()));
1024 Err(EmulatorError::WorkerPanicked { which, message })
1025 }
1026 Err(threaded::RunError::Timeout { which, elapsed_ms }) => {
1027 self.timeout_info = Some((which, elapsed_ms));
1028 Err(EmulatorError::BarrierTimeout { which, elapsed_ms })
1029 }
1030 }
1031 }
1032
1033 #[cfg(not(all(
1034 feature = "threading",
1035 target_arch = "x86_64",
1036 any(target_os = "windows", target_os = "linux")
1037 )))]
1038 fn run_quantum_threaded(&mut self) -> Result<u64, EmulatorError> {
1039 Err(EmulatorError::NotSupportedInThreadedMode)
1040 }
1041
1042 /// Forward any pending `inject_panic_for_testing` target into the
1043 /// live `ThreadedEmulator`. No-op on non-testing builds.
1044 #[cfg(all(
1045 feature = "threading",
1046 target_arch = "x86_64",
1047 any(target_os = "windows", target_os = "linux")
1048 ))]
1049 #[inline]
1050 fn apply_pending_panic_inject(&mut self) {
1051 #[cfg(feature = "testing")]
1052 if let Some(which) = self.pending_panic_inject.take()
1053 && let Some(t) = self.threaded.as_mut()
1054 {
1055 t.inject_panic_for_testing(which);
1056 }
1057 }
1058
1059 /// Move the seeded Serial state into a fresh `ThreadedEmulator`.
1060 /// Called lazily on the first `run_quantum` / `run` so harness
1061 /// setup that poked `emu.bus` / `emu.core_mut(...)` pre-run is
1062 /// carried over. After promotion, the top-level `cores` / `bus` /
1063 /// `clock` fields hold zero-cost placeholders and must not be
1064 /// inspected mid-run.
1065 #[cfg(all(
1066 feature = "threading",
1067 target_arch = "x86_64",
1068 any(target_os = "windows", target_os = "linux")
1069 ))]
1070 fn promote_to_threaded(&mut self) {
1071 let placeholder_bus = Bus::new();
1072 let placeholder_cores = [CortexM0Plus::with_id(0), CortexM0Plus::with_id(1)];
1073 let seeded_bus = std::mem::replace(&mut self.bus, placeholder_bus);
1074 let seeded_cores = std::mem::replace(&mut self.cores, placeholder_cores);
1075 let seeded_clock = std::mem::replace(&mut self.clock, Clock { cycles: 0 });
1076 let seeded = Emulator {
1077 cores: seeded_cores,
1078 bus: seeded_bus,
1079 clock: seeded_clock,
1080 step_quantum: self.step_quantum,
1081 pio_tick_count: self.pio_tick_count,
1082 pio_tick_iow_low_count: self.pio_tick_iow_low_count,
1083 pio0_sm0_max_pc: self.pio0_sm0_max_pc,
1084 pio0_sm0_pc_advances: self.pio0_sm0_pc_advances,
1085 pio0_sm0_last_pc: self.pio0_sm0_last_pc,
1086 execution_model: ExecutionModel::Serial,
1087 threaded: None,
1088 panic_info: None,
1089 timeout_info: None,
1090 #[cfg(feature = "testing")]
1091 pending_panic_inject: None,
1092 bus_is_placeholder: false,
1093 };
1094 self.threaded = Some(threaded::ThreadedEmulator::from_emulator(seeded));
1095 self.bus_is_placeholder = true;
1096 }
1097
1098 /// Test-only: arm a panic injection for the next `run_quantum` /
1099 /// `run` call. The matching worker panics on its first barrier
1100 /// entry; the emulator surfaces `Err(EmulatorError::WorkerPanicked)`
1101 /// and becomes sticky-poisoned.
1102 ///
1103 /// Feature-gated behind `testing` so release consumers cannot brick
1104 /// their emulator by calling an internal hook.
1105 #[cfg(all(
1106 feature = "testing",
1107 feature = "threading",
1108 target_arch = "x86_64",
1109 any(target_os = "windows", target_os = "linux")
1110 ))]
1111 pub fn inject_panic_for_testing(&mut self, which: WorkerName) {
1112 self.pending_panic_inject = Some(which);
1113 }
1114
1115 /// Merge SIO and PIO GPIO outputs into `bus.gpio_in`.
1116 ///
1117 /// SIO `gpio_out & gpio_oe` is the base; each PIO block's
1118 /// `pad_out & pad_oe` overrides SIO on the pins it drives (PIO wins
1119 /// wherever `pad_oe` has a bit set — mirrors `rp2350_emu::Emulator::
1120 /// update_gpio`). The result is masked to the RP2040 30-pin range
1121 /// (GPIO0..GPIO29).
1122 ///
1123 /// Next, the off-chip SPI PSRAM observes the post-merge pin state
1124 /// on its CS/SCK/MOSI pins and, if it is currently driving MISO,
1125 /// splices its bit into `gpio_in` bit 0. MISO override happens after
1126 /// SIO/PIO so MOSI/SCK/CS seen by the PSRAM reflect the actual pin
1127 /// levels driven by PIO / SIO on this tick (no feedback from the
1128 /// override into the PSRAM's observation on the same tick).
1129 ///
1130 /// Finally, any [`Bus::external_gpio_in_mask`] bits override the
1131 /// merged value with [`Bus::external_gpio_in_override`]. External
1132 /// drivers (e.g. the `picogus_diff_rp2040` harness injecting a
1133 /// synthetic ISA waveform) win over the on-chip merge for the pins
1134 /// they claim — without this final override step, harness pokes to
1135 /// `bus.gpio_in` would be silently clobbered the next time
1136 /// `update_gpio` ran.
1137 pub(crate) fn update_gpio(&mut self) {
1138 let mut out = self.bus.sio.gpio_out & self.bus.sio.gpio_oe;
1139 for pio in &self.bus.pio {
1140 let pio_mask = pio.pad_oe;
1141 out = (out & !pio_mask) | (pio.pad_out & pio_mask);
1142 }
1143 out &= 0x3FFF_FFFF;
1144 if let Some(ref mut psram) = self.bus.psram
1145 && let Some(miso) = psram.tick(out)
1146 {
1147 let pin = psram.pin_miso();
1148 let mask = 1u32 << pin;
1149 out = (out & !mask) | ((miso as u32) << pin);
1150 }
1151 let ext_mask = self.bus.external_gpio_in_mask;
1152 if ext_mask != 0 {
1153 out = (out & !ext_mask) | (self.bus.external_gpio_in_override & ext_mask);
1154 }
1155 self.bus.gpio_in = out;
1156 }
1157
1158 /// WFE/SEV / WFI quantum-end wake check. See `wrk_docs/2026.04.26
1159 /// - HLD - RP2040 WFE-SEV Wake Mechanics V1.md` §4.4.
1160 ///
1161 /// Per-core:
1162 /// - **WFE wake** — if the core is parked on `wfe_waiting` and an
1163 /// `event_flag` is latched, consume the latch and un-park. The
1164 /// latch is intentionally preserved (one-shot wake) when no
1165 /// waiter is parked: the SEV-before-WFE idiom requires the latch
1166 /// to survive until the next WFE consumes it.
1167 /// - **WFI wake** — if the core is halted and an enabled+pending
1168 /// IRQ exists on its NVIC, un-halt. The pending bit is consumed
1169 /// on the next `step()` via `try_take_any_pending_exception`. The
1170 /// `halted` flag is shared with BKPT/debug halt by design (matches
1171 /// rp2350_emu precedent).
1172 ///
1173 /// Crucially: this function no longer unconditionally clears
1174 /// `event_flag[0]`. A latched event with no waiter survives until
1175 /// the next WFE on that core consumes it. The launch consumer's
1176 /// explicit `event_flag[1] = false` reset (`maybe_wake_core1`) is
1177 /// preserved verbatim — that's an intentional clean-launch reset,
1178 /// not part of the WFE/SEV protocol.
1179 fn wake_checks(&mut self) {
1180 for core in 0..2 {
1181 // WFE wake: parked core + latched event = consume + un-park.
1182 if self.bus.wfe_waiting[core] && self.bus.event_flag[core] {
1183 self.bus.event_flag[core] = false;
1184 self.bus.wfe_waiting[core] = false;
1185 }
1186 // WFI wake: halted core + pending+enabled IRQ = un-halt.
1187 // Reuses `halted` so this also wakes a BKPT-halted core if
1188 // an IRQ asserts; matches the rp2350_emu design wart.
1189 if self.cores[core].is_halted() && self.bus.nvics[core].pending_and_enabled() != 0 {
1190 self.cores[core].wake();
1191 }
1192 }
1193 }
1194
1195 /// Halt core 1 and synchronously re-arm the multicore-launch FSM.
1196 ///
1197 /// This is the ONLY sanctioned path for halting core 1 from
1198 /// production code. Direct `cores[1].halt()` skips the `armed`
1199 /// sync and will silently drift the FSM state against the core's
1200 /// actual halt status. See HLD 2026.04.16 §5 (invariants).
1201 pub fn halt_core1(&mut self) {
1202 self.assert_not_placeholder();
1203 self.cores[1].halt();
1204 self.bus.sio.set_handshake_armed(true);
1205 }
1206
1207 /// Wake core 1 and synchronously disarm the multicore-launch FSM.
1208 ///
1209 /// This is the ONLY sanctioned path for waking core 1 from
1210 /// production code. The launch consumer in [`Self::maybe_wake_core1`]
1211 /// calls this after applying VTOR / MSP / PC; external callers
1212 /// (tests simulating a mode switch; future reset-path code) also
1213 /// route through here.
1214 ///
1215 /// `wake_core1` does not touch CPU register state. Callers that need
1216 /// a clean architectural baseline (e.g. the launch consumer after a
1217 /// re-halt) must call [`CortexM0Plus::reset_control_for_launch`]
1218 /// before this.
1219 pub fn wake_core1(&mut self) {
1220 self.assert_not_placeholder();
1221 self.cores[1].wake();
1222 self.bus.sio.set_handshake_armed(false);
1223 }
1224
1225 /// Observe the Pico SDK multicore-launch handshake. The armed-path
1226 /// FSM in [`crate::bus::Sio::fifo_wr`] consumes core-0 FIFO pushes
1227 /// while core 1 is halted; on the 6th valid word the FSM produces a
1228 /// [`crate::bus::sio::Core1Launch`] token. This consumer applies
1229 /// VTOR / MSP / PC to core 1, resets CONTROL/PSP/xPSR/IPSR/PRIMASK
1230 /// to a clean launch baseline, clears any stale `event_flag[1]`,
1231 /// and wakes the core via the [`Self::wake_core1`] wrapper (which
1232 /// synchronously disarms the FSM).
1233 ///
1234 /// Called once after each core-0 step so that a pushed-then-popped
1235 /// handshake within a single quantum still wakes core 1 in that
1236 /// quantum. The `writer_core` argument is unused on this branch —
1237 /// the FSM is only armed while core 0 pushes, so a core-1 step
1238 /// cannot produce a pending_launch. Kept for call-site-compatibility
1239 /// with the replaced placeholder.
1240 fn maybe_wake_core1(&mut self, _writer_core: usize) {
1241 let Some(launch) = self.bus.sio.take_pending_launch() else {
1242 return;
1243 };
1244 // Invariant: the FSM only arms while core 1 is halted; launch
1245 // tokens can only be produced in that state. If this fails we
1246 // have a logic bug in the arming mechanism (HLD §2.5).
1247 debug_assert!(
1248 self.cores[1].is_halted(),
1249 "pending_launch emitted against an awake core 1 — arming bug"
1250 );
1251
1252 self.bus.ppb[1].vtor = launch.vtor;
1253 self.cores[1].regs.msp = launch.sp;
1254 self.cores[1].regs.r[13] = launch.sp;
1255 // `entry & !1` matches `direct_boot_from_flash` (silent strip).
1256 // On real silicon a Thumb-bit-clear BLX target HardFaults; this
1257 // asymmetry is logged in tech_debt.md alongside direct_boot.
1258 self.cores[1].regs.set_pc(launch.entry & !1);
1259 self.cores[1].reset_control_for_launch();
1260 self.bus.event_flag[1] = false; // clear any stale wake signal
1261 self.wake_core1();
1262 }
1263
1264 /// Read a GPIO pin from the merged pin state. Debug-only: asserts
1265 /// the emulator has not been promoted into Threaded mode.
1266 pub fn gpio_read(&self, pin: u8) -> bool {
1267 self.assert_not_placeholder();
1268 if pin >= 30 {
1269 return false;
1270 }
1271 (self.bus.gpio_in >> pin) & 1 != 0
1272 }
1273
1274 /// Write a GPIO pin. Sets the SIO GPIO_OUT bit and asserts output
1275 /// enable so the pin state becomes observable via [`Self::gpio_read`].
1276 /// Useful as a test-shim to inject a pin level without hand-rolling
1277 /// the SIO register poking.
1278 pub fn gpio_write(&mut self, pin: u8, value: bool) {
1279 self.assert_not_placeholder();
1280 if pin >= 30 {
1281 return;
1282 }
1283 let mask = 1u32 << pin;
1284 self.bus.sio.gpio_oe |= mask;
1285 if value {
1286 self.bus.sio.gpio_out |= mask;
1287 } else {
1288 self.bus.sio.gpio_out &= !mask;
1289 }
1290 self.update_gpio();
1291 }
1292
1293 /// Read all GPIO pins as a bitmask. Debug-only: asserts the
1294 /// emulator has not been promoted into Threaded mode.
1295 pub fn gpio_read_all(&self) -> u64 {
1296 self.assert_not_placeholder();
1297 self.bus.gpio_in as u64
1298 }
1299
1300 /// Access core state. Debug-only: asserts the emulator has not
1301 /// been promoted into Threaded mode (the flat `cores` field would
1302 /// be a placeholder).
1303 pub fn core(&self, id: usize) -> &CortexM0Plus {
1304 self.assert_not_placeholder();
1305 &self.cores[id]
1306 }
1307
1308 /// Mutable accessor; same debug-only placeholder assertion.
1309 pub fn core_mut(&mut self, id: usize) -> &mut CortexM0Plus {
1310 self.assert_not_placeholder();
1311 &mut self.cores[id]
1312 }
1313
1314 /// Direct memory read (bypasses bus timing). Debug-only: asserts
1315 /// the emulator has not been promoted into Threaded mode.
1316 pub fn peek(&self, addr: u32) -> u32 {
1317 self.assert_not_placeholder();
1318 self.bus.peek32(addr)
1319 }
1320
1321 /// Direct memory write (bypasses bus timing). Debug-only: asserts
1322 /// the emulator has not been promoted into Threaded mode.
1323 pub fn poke(&mut self, addr: u32, value: u32) {
1324 self.assert_not_placeholder();
1325 self.bus.poke32(addr, value);
1326 // poke32 bypasses the Bus::write* invalidation hooks
1327 // (memory.sram_write32 / xip_sram direct slice). Conservative
1328 // bulk invalidation here keeps the cache coherent with any
1329 // pre-step `poke` of executable bytes, with negligible overhead
1330 // (callers typically poke before the first step).
1331 self.bus.pending_invalidation_regions |= crate::bus::invalidation_regions::BULK;
1332 self.cores[0].invalidate_decode_cache_all();
1333 self.cores[1].invalidate_decode_cache_all();
1334 self.bus.pending_invalidation_regions = 0;
1335 self.bus.pending_cache_invalidations.clear();
1336 }
1337
1338 /// Current master cycle count. Debug-only: asserts the emulator
1339 /// has not been promoted into Threaded mode — Threaded callers
1340 /// read the live master cycle via the value returned from
1341 /// [`Self::run_quantum`] / [`Self::run`].
1342 pub fn cycles(&self) -> u64 {
1343 self.assert_not_placeholder();
1344 self.clock.cycles
1345 }
1346
1347 /// Write a 32-bit word to an MMIO address via the bus. Charges zero
1348 /// emulator cycles (intended for setup code running outside `run()`).
1349 ///
1350 /// Delegates to [`Bus::write32`], so alias bits (`(addr >> 12) & 3`)
1351 /// are honoured: base address = normal, XOR alias = `|0x1000`, SET
1352 /// alias = `|0x2000`, CLR alias = `|0x3000`. Useful for poking PIO
1353 /// INSTR_MEM, configuring SIO GPIO_OE/_OUT, releasing RESETS bits,
1354 /// etc., without hand-rolling the bus machinery.
1355 pub fn mmio_write32(&mut self, addr: u32, value: u32) {
1356 self.assert_not_placeholder();
1357 // Mirror the `step()` stash so PLL write-time lock-arm transitions
1358 // observe the current cycle count when the harness pokes MMIO
1359 // outside the step path. See HLD §6 P2.
1360 self.bus.master_cycle = self.clock.cycles;
1361 self.bus.write32(addr, value);
1362 }
1363
1364 /// Read a 32-bit word from an MMIO address via the bus. Charges zero
1365 /// emulator cycles (intended for setup code running outside `run()`).
1366 ///
1367 /// **Warning: reads may have side effects.** Several RP2040 MMIO
1368 /// registers mutate state on read — e.g. PIO `RXFn` pops the receive
1369 /// FIFO, SIO divider `QUOTIENT` / `REMAINDER` clear the CSR dirty
1370 /// bit, and a handful of W1C sticky flags are cleared by reads. Setup
1371 /// code should therefore be write-heavy; reads through this method
1372 /// are for confirmation only and should be chosen carefully to avoid
1373 /// disturbing the peripheral's state.
1374 pub fn mmio_read32(&mut self, addr: u32) -> u32 {
1375 self.assert_not_placeholder();
1376 // Mirror the `step()` stash so PLL CS reads observe the current
1377 // cycle count when the harness reads MMIO outside the step path.
1378 self.bus.master_cycle = self.clock.cycles;
1379 self.bus.read32(addr)
1380 }
1381
1382 /// Harness-only diagnostic: drain every byte firmware has written to
1383 /// UART0 `DR` since the previous call. Returns empty if idle.
1384 pub fn drain_uart0_tx_log(&mut self) -> Vec<u8> {
1385 self.assert_not_placeholder();
1386 self.bus.drain_uart0_tx_log()
1387 }
1388}
1389
1390/// Builder for assembling the emulator. Seeds the Bus clock tree from
1391/// `Config::sys_clk_hz` — the first CLOCKS / PLL register write
1392/// replaces the seed with the derived value.
1393pub struct EmulatorBuilder {
1394 config: Config,
1395 step_quantum: u32,
1396 flash: Option<Vec<u8>>,
1397 psram: Option<picoem_devices::Psram>,
1398 execution: ExecutionModel,
1399}
1400
1401impl EmulatorBuilder {
1402 pub fn new(config: Config) -> Self {
1403 Self {
1404 config,
1405 step_quantum: DEFAULT_STEP_QUANTUM,
1406 flash: None,
1407 psram: None,
1408 execution: ExecutionModel::default(),
1409 }
1410 }
1411
1412 /// Override the per-step quantum (default [`DEFAULT_STEP_QUANTUM`]).
1413 ///
1414 /// Clamps `0 -> 1`. Previously a `debug_assert!` here meant
1415 /// `step_quantum(0)` silently advanced 0 cycles per `step()` in
1416 /// release builds, a guaranteed infinite-loop footgun for `run()`.
1417 pub fn step_quantum(mut self, n: u32) -> Self {
1418 self.step_quantum = n.max(1);
1419 self
1420 }
1421
1422 /// Pre-load an XIP flash image. Applied at [`Self::build`] time via
1423 /// [`Emulator::load_flash`]; oversize images are silently clamped to
1424 /// the 2 MB flash window.
1425 pub fn flash(mut self, bytes: Vec<u8>) -> Self {
1426 self.flash = Some(bytes);
1427 self
1428 }
1429
1430 /// Attach an off-chip SPI PSRAM device to the emulator. When set,
1431 /// [`Emulator::update_gpio`] feeds the device's `tick()` method on
1432 /// every GPIO merge and splices its MISO output back into `gpio_in`.
1433 pub fn psram(mut self, psram: picoem_devices::Psram) -> Self {
1434 self.psram = Some(psram);
1435 self
1436 }
1437
1438 /// Select the runtime [`ExecutionModel`]. Defaults to
1439 /// `ExecutionModel::Serial` (the oracle-validated reference path).
1440 /// `ExecutionModel::Threaded` requires the `threading` cargo feature
1441 /// and an x86_64 Windows host; otherwise [`Self::build`] returns
1442 /// `Err(ConfigError::ThreadingUnavailable)`.
1443 pub fn execution(mut self, model: ExecutionModel) -> Self {
1444 self.execution = model;
1445 self
1446 }
1447
1448 pub fn build(self) -> Result<Emulator, ConfigError> {
1449 // Threading availability gate — dual-execution HLD V1 §5.2.
1450 if self.execution == ExecutionModel::Threaded {
1451 #[cfg(not(all(
1452 feature = "threading",
1453 target_arch = "x86_64",
1454 any(target_os = "windows", target_os = "linux")
1455 )))]
1456 return Err(ConfigError::ThreadingUnavailable);
1457 #[cfg(all(
1458 feature = "threading",
1459 target_arch = "x86_64",
1460 any(target_os = "windows", target_os = "linux")
1461 ))]
1462 {
1463 let n = std::thread::available_parallelism()
1464 .map(|n| n.get())
1465 .unwrap_or(1);
1466 if n < 3 {
1467 return Err(ConfigError::ThreadingUnavailable);
1468 }
1469 }
1470 }
1471
1472 let mut bus = Bus::new();
1473 bus.seed_sys_clk_hz(self.config.sys_clk_hz);
1474 bus.psram = self.psram;
1475 let mut emu = Emulator {
1476 cores: [CortexM0Plus::with_id(0), CortexM0Plus::with_id(1)],
1477 bus,
1478 clock: Clock { cycles: 0 },
1479 step_quantum: self.step_quantum,
1480 pio_tick_count: 0,
1481 pio_tick_iow_low_count: 0,
1482 pio0_sm0_max_pc: 0,
1483 pio0_sm0_pc_advances: 0,
1484 pio0_sm0_last_pc: 0xFF,
1485 execution_model: self.execution,
1486 #[cfg(all(
1487 feature = "threading",
1488 target_arch = "x86_64",
1489 any(target_os = "windows", target_os = "linux")
1490 ))]
1491 threaded: None,
1492 #[cfg(all(
1493 feature = "threading",
1494 target_arch = "x86_64",
1495 any(target_os = "windows", target_os = "linux")
1496 ))]
1497 panic_info: None,
1498 #[cfg(all(
1499 feature = "threading",
1500 target_arch = "x86_64",
1501 any(target_os = "windows", target_os = "linux")
1502 ))]
1503 timeout_info: None,
1504 #[cfg(all(
1505 feature = "testing",
1506 feature = "threading",
1507 target_arch = "x86_64",
1508 any(target_os = "windows", target_os = "linux")
1509 ))]
1510 pending_panic_inject: None,
1511 #[cfg(all(
1512 feature = "threading",
1513 target_arch = "x86_64",
1514 any(target_os = "windows", target_os = "linux")
1515 ))]
1516 bus_is_placeholder: false,
1517 };
1518 // Default: core 1 halted — Pico SDK wakes it via SIO FIFO.
1519 // Route through the wrapper so the SIO handshake FSM `armed`
1520 // flag is in sync (HLD 2026.04.16 §2.1 / §5 invariant).
1521 emu.halt_core1();
1522 if let Some(bytes) = self.flash {
1523 emu.load_flash(&bytes);
1524 }
1525 info!(
1526 rom_size = ROM_SIZE,
1527 sram_size = SRAM_SIZE,
1528 step_quantum = self.step_quantum,
1529 sys_clk_hz = self.config.sys_clk_hz,
1530 execution = ?self.execution,
1531 "emulator constructed"
1532 );
1533 Ok(emu)
1534 }
1535}
1536
1537// ---------------------------------------------------------------------------
1538// Stage 4: residue branch coverage for the top-level `lib.rs` (Emulator,
1539// EmulatorBuilder, Config, ConfigError, EmulatorError, ExecutionModel,
1540// WorkerName). Pure append-only — does not modify any production code.
1541// ---------------------------------------------------------------------------
1542#[cfg(test)]
1543mod stage4_lib_residue {
1544 use super::*;
1545
1546 // ------------------- ConfigError -------------------
1547
1548 #[test]
1549 fn config_error_display_threading_unavailable() {
1550 let s = format!("{}", ConfigError::ThreadingUnavailable);
1551 assert!(s.contains("Threaded"));
1552 assert!(s.contains("unavailable"));
1553 }
1554
1555 #[test]
1556 fn config_error_debug_and_clone_eq() {
1557 let e1 = ConfigError::ThreadingUnavailable;
1558 let e2 = e1.clone();
1559 assert_eq!(e1, e2);
1560 let _ = format!("{:?}", e1);
1561 }
1562
1563 #[test]
1564 fn config_error_is_std_error() {
1565 fn assert_err<E: std::error::Error>(_: &E) {}
1566 assert_err(&ConfigError::ThreadingUnavailable);
1567 }
1568
1569 // ------------------- EmulatorError -------------------
1570
1571 #[test]
1572 fn emulator_error_display_not_supported_in_threaded() {
1573 let s = format!("{}", EmulatorError::NotSupportedInThreadedMode);
1574 assert!(s.contains("Threaded"));
1575 }
1576
1577 #[test]
1578 fn emulator_error_display_worker_panicked() {
1579 let e = EmulatorError::WorkerPanicked {
1580 which: WorkerName::Core0,
1581 message: String::from("boom"),
1582 };
1583 let s = format!("{}", e);
1584 assert!(s.contains("panicked"));
1585 assert!(s.contains("boom"));
1586 assert!(s.contains("core0"));
1587 }
1588
1589 #[test]
1590 fn emulator_error_display_barrier_timeout() {
1591 let e = EmulatorError::BarrierTimeout {
1592 which: WorkerName::Coord,
1593 elapsed_ms: 1_234,
1594 };
1595 let s = format!("{}", e);
1596 assert!(s.contains("barrier"));
1597 assert!(s.contains("1234"));
1598 assert!(s.contains("coord"));
1599 }
1600
1601 #[test]
1602 fn emulator_error_clone_eq_debug() {
1603 let e1 = EmulatorError::NotSupportedInThreadedMode;
1604 let e2 = e1.clone();
1605 assert_eq!(e1, e2);
1606 let _ = format!("{:?}", e1);
1607 }
1608
1609 // ------------------- WorkerName -------------------
1610
1611 #[test]
1612 fn worker_name_as_str_all_variants() {
1613 assert_eq!(WorkerName::Core0.as_str(), "core0");
1614 assert_eq!(WorkerName::Core1.as_str(), "core1");
1615 assert_eq!(WorkerName::Coord.as_str(), "coord");
1616 }
1617
1618 #[test]
1619 fn worker_name_clone_eq_debug() {
1620 let w = WorkerName::Core1;
1621 assert_eq!(w, w.clone());
1622 let _ = format!("{:?}", w);
1623 }
1624
1625 // ------------------- ExecutionModel -------------------
1626
1627 #[test]
1628 fn execution_model_default_is_serial() {
1629 assert_eq!(ExecutionModel::default(), ExecutionModel::Serial);
1630 }
1631
1632 #[test]
1633 fn execution_model_eq_and_debug() {
1634 assert_eq!(ExecutionModel::Threaded, ExecutionModel::Threaded);
1635 assert_ne!(ExecutionModel::Serial, ExecutionModel::Threaded);
1636 let _ = format!("{:?}", ExecutionModel::Serial);
1637 let _ = format!("{:?}", ExecutionModel::Threaded);
1638 }
1639
1640 // ------------------- Builder: ConfigError::ThreadingUnavailable -------------------
1641
1642 #[cfg(not(feature = "threading"))]
1643 #[test]
1644 fn builder_threaded_no_feature_returns_threading_unavailable() {
1645 let res = EmulatorBuilder::new(Config::default())
1646 .execution(ExecutionModel::Threaded)
1647 .build();
1648 match res {
1649 Err(ConfigError::ThreadingUnavailable) => {}
1650 Ok(_) => panic!("Threaded should fail without `threading` feature"),
1651 }
1652 }
1653
1654 #[cfg(not(all(
1655 feature = "threading",
1656 target_arch = "x86_64",
1657 any(target_os = "windows", target_os = "linux")
1658 )))]
1659 #[test]
1660 fn builder_threaded_off_platform_returns_threading_unavailable() {
1661 let res = EmulatorBuilder::new(Config::default())
1662 .execution(ExecutionModel::Threaded)
1663 .build();
1664 match res {
1665 Err(ConfigError::ThreadingUnavailable) => {}
1666 Ok(_) => panic!("Threaded should fail on unsupported platforms"),
1667 }
1668 }
1669
1670 // ------------------- Builder defaults / overrides -------------------
1671
1672 #[test]
1673 fn builder_default_step_quantum() {
1674 let emu = EmulatorBuilder::new(Config::default()).build().unwrap();
1675 assert_eq!(emu.step_quantum, DEFAULT_STEP_QUANTUM);
1676 }
1677
1678 #[test]
1679 fn builder_custom_step_quantum() {
1680 let emu = EmulatorBuilder::new(Config::default())
1681 .step_quantum(8)
1682 .build()
1683 .unwrap();
1684 assert_eq!(emu.step_quantum, 8);
1685 }
1686
1687 #[test]
1688 fn step_quantum_zero_clamps_to_one() {
1689 // Regression: `EmulatorBuilder::step_quantum(0)` previously
1690 // tripped a `debug_assert!` (and silently advanced 0 cycles
1691 // per `step()` in release builds — an infinite-loop footgun
1692 // for `run()`). The clamp at the builder entry point keeps
1693 // the runtime contract `step_quantum >= 1` intact.
1694 let mut emu = EmulatorBuilder::new(Config::default())
1695 .step_quantum(0)
1696 .build()
1697 .unwrap();
1698 assert_eq!(emu.step_quantum, 1);
1699 // `step()` must make forward progress (advance >= 1 master
1700 // cycle) and not loop forever.
1701 let advanced = emu.step().unwrap();
1702 assert!(advanced >= 1);
1703 }
1704
1705 #[test]
1706 fn builder_custom_sysclk() {
1707 let cfg = Config {
1708 sys_clk_hz: 125_000_000,
1709 };
1710 let emu = EmulatorBuilder::new(cfg).build().unwrap();
1711 // The clock tree is recomputed from sys_clk_hz; simplest check is
1712 // that the emulator builds and the master cycle starts at zero.
1713 assert_eq!(emu.cycles(), 0);
1714 }
1715
1716 #[test]
1717 fn builder_execution_serial_explicit() {
1718 let emu = EmulatorBuilder::new(Config::default())
1719 .execution(ExecutionModel::Serial)
1720 .build()
1721 .unwrap();
1722 assert_eq!(emu.execution_model(), ExecutionModel::Serial);
1723 }
1724
1725 #[test]
1726 fn builder_with_flash_pre_loads_xip() {
1727 let mut flash = vec![0u8; 32];
1728 flash[0..4].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes());
1729 let emu = EmulatorBuilder::new(Config::default())
1730 .flash(flash)
1731 .build()
1732 .unwrap();
1733 // XIP base is 0x1000_0000 — the flash loader writes there.
1734 assert_eq!(emu.bus.memory.xip_read32(0), 0xDEAD_BEEF);
1735 }
1736
1737 // ------------------- Emulator basics -------------------
1738
1739 #[test]
1740 fn execution_model_accessor_returns_selected() {
1741 let emu = Emulator::new(Config::default());
1742 assert_eq!(emu.execution_model(), ExecutionModel::Serial);
1743 }
1744
1745 #[test]
1746 fn core_cycles_default_zero() {
1747 let emu = Emulator::new(Config::default());
1748 assert_eq!(emu.core_cycles(0), 0);
1749 assert_eq!(emu.core_cycles(1), 0);
1750 }
1751
1752 #[test]
1753 #[should_panic(expected = "core_cycles: idx must be 0 or 1")]
1754 fn core_cycles_invalid_idx_panics() {
1755 let emu = Emulator::new(Config::default());
1756 let _ = emu.core_cycles(2);
1757 }
1758
1759 #[test]
1760 fn cycles_starts_at_zero() {
1761 let emu = Emulator::new(Config::default());
1762 assert_eq!(emu.cycles(), 0);
1763 }
1764
1765 #[test]
1766 fn core_and_core_mut_accessors() {
1767 let mut emu = Emulator::new(Config::default());
1768 // No id() public method available like rp2350_emu's, but we can
1769 // exercise both accessors and the placeholder-guard path.
1770 let _ = emu.core(0);
1771 let _ = emu.core_mut(1);
1772 }
1773
1774 // ------------------- Emulator::run / step -------------------
1775
1776 #[test]
1777 fn run_zero_cycles_serial_is_noop() {
1778 let mut emu = Emulator::new(Config::default());
1779 let executed = emu.run(0).unwrap();
1780 // run() returns the delta in cycles. With cycles=0 the inner loop
1781 // condition `clock.cycles - start < 0` is false on first check, so
1782 // no quanta run.
1783 assert_eq!(executed, 0);
1784 }
1785
1786 #[test]
1787 fn run_quantum_serial_returns_ok() {
1788 let mut emu = Emulator::new(Config::default());
1789 let r = emu.run_quantum().unwrap();
1790 // run_quantum returns the number of cycles consumed in this
1791 // quantum; with both cores halted (core 1 always halted, core 0
1792 // running zero-data ROM) this is bounded by step_quantum.
1793 assert!(r <= emu.step_quantum as u64);
1794 }
1795
1796 #[test]
1797 fn step_serial_returns_ok() {
1798 let mut emu = Emulator::new(Config::default());
1799 // Both cores halt quickly with zero-data ROM, but step still
1800 // returns Ok(consumed) on Serial.
1801 let _ = emu.step().unwrap();
1802 }
1803
1804 // ------------------- gpio bounds -------------------
1805
1806 #[test]
1807 fn gpio_read_pin_30_returns_false() {
1808 let emu = Emulator::new(Config::default());
1809 assert!(!emu.gpio_read(30));
1810 assert!(!emu.gpio_read(31));
1811 }
1812
1813 #[test]
1814 fn gpio_write_pin_out_of_range_is_noop() {
1815 let mut emu = Emulator::new(Config::default());
1816 // Pin 30 is past the valid range. The function bails early — no
1817 // GPIO_OE bit gets set.
1818 emu.gpio_write(30, true);
1819 assert_eq!(emu.bus.sio.gpio_oe & (1u32 << 30), 0);
1820 }
1821
1822 #[test]
1823 fn gpio_write_in_range_sets_oe_and_out() {
1824 let mut emu = Emulator::new(Config::default());
1825 emu.gpio_write(5, true);
1826 assert_ne!(emu.bus.sio.gpio_oe & (1u32 << 5), 0);
1827 assert_ne!(emu.bus.sio.gpio_out & (1u32 << 5), 0);
1828 emu.gpio_write(5, false);
1829 assert_eq!(emu.bus.sio.gpio_out & (1u32 << 5), 0);
1830 }
1831
1832 #[test]
1833 fn gpio_read_all_default_zero() {
1834 let emu = Emulator::new(Config::default());
1835 assert_eq!(emu.gpio_read_all(), 0);
1836 }
1837
1838 // ------------------- load_image / load_bootrom / load_flash -------------------
1839
1840 #[test]
1841 fn load_image_sram_writes_through() {
1842 let mut emu = Emulator::new(Config::default());
1843 let data = [0x11u8, 0x22, 0x33, 0x44];
1844 emu.load_image(0x2000_0100, &data);
1845 assert_eq!(emu.peek(0x2000_0100), 0x4433_2211);
1846 }
1847
1848 #[test]
1849 fn load_image_rom_region_overlay() {
1850 let mut emu = Emulator::new(Config::default());
1851 let data = [0xAAu8, 0xBB, 0xCC, 0xDD];
1852 emu.load_image(0x0000_0000, &data);
1853 assert_eq!(emu.bus.memory.rom_read8(0), 0xAA);
1854 assert_eq!(emu.bus.memory.rom_read8(3), 0xDD);
1855 }
1856
1857 #[test]
1858 fn load_image_unknown_region_silently_dropped() {
1859 let mut emu = Emulator::new(Config::default());
1860 let data = [0xFFu8; 4];
1861 // 0x4 region — no match arm, falls through.
1862 emu.load_image(0x4000_0000, &data);
1863 }
1864
1865 #[test]
1866 fn load_bootrom_loads_first_word() {
1867 let mut emu = Emulator::new(Config::default());
1868 let mut data = vec![0u8; 32];
1869 data[0..4].copy_from_slice(&0x2000_8000u32.to_le_bytes());
1870 emu.load_bootrom(&data);
1871 assert_eq!(emu.bus.memory.rom_read32(0), 0x2000_8000);
1872 }
1873
1874 #[test]
1875 fn load_flash_drains_invalidations() {
1876 let mut emu = Emulator::new(Config::default());
1877 emu.load_flash(&[0u8; 16]);
1878 assert_eq!(emu.bus.pending_invalidation_regions, 0);
1879 }
1880
1881 // ------------------- direct_boot_from_flash -------------------
1882
1883 #[test]
1884 fn direct_boot_from_flash_seeds_sp_pc_vtor() {
1885 let mut emu = Emulator::new(Config::default());
1886 // Build a minimal vector table at flash offset 0x100 (pico-sdk).
1887 let sp = 0x2002_0000u32;
1888 let entry = 0x1000_0301u32; // Thumb-bit set
1889 let vtor_offset = 0x100u32;
1890 let mut flash = vec![0u8; 0x200];
1891 flash[(vtor_offset as usize)..(vtor_offset as usize + 4)]
1892 .copy_from_slice(&sp.to_le_bytes());
1893 flash[(vtor_offset as usize + 4)..(vtor_offset as usize + 8)]
1894 .copy_from_slice(&entry.to_le_bytes());
1895 emu.load_flash(&flash);
1896 emu.direct_boot_from_flash(vtor_offset);
1897 for c in 0..2 {
1898 assert_eq!(emu.cores[c].regs.msp, sp);
1899 assert_eq!(emu.cores[c].regs.pc(), entry & !1);
1900 }
1901 assert_eq!(emu.bus.ppb[0].vtor, 0x1000_0000 + vtor_offset);
1902 // Core 1 stays halted by halt_core1.
1903 assert!(emu.cores[1].is_halted());
1904 }
1905
1906 // ------------------- halt_core1 / wake_core1 -------------------
1907
1908 #[test]
1909 fn halt_and_wake_core1_round_trip() {
1910 let mut emu = Emulator::new(Config::default());
1911 // Initial state: core 1 halted.
1912 assert!(emu.cores[1].is_halted());
1913 emu.wake_core1();
1914 assert!(!emu.cores[1].is_halted());
1915 emu.halt_core1();
1916 assert!(emu.cores[1].is_halted());
1917 }
1918
1919 // ------------------- mmio_read32 / mmio_write32 -------------------
1920
1921 #[test]
1922 fn mmio_write_read_roundtrip_sram() {
1923 let mut emu = Emulator::new(Config::default());
1924 emu.mmio_write32(0x2000_0000, 0xDEAD_BEEF);
1925 assert_eq!(emu.mmio_read32(0x2000_0000), 0xDEAD_BEEF);
1926 }
1927
1928 // ------------------- reset -------------------
1929
1930 #[test]
1931 fn reset_clears_clock() {
1932 let mut emu = Emulator::new(Config::default());
1933 let _ = emu.step().unwrap();
1934 emu.reset();
1935 assert_eq!(emu.cycles(), 0);
1936 assert_eq!(emu.pio_tick_count, 0);
1937 }
1938
1939 // ------------------- drain_uart0_tx_log -------------------
1940
1941 #[test]
1942 fn drain_uart0_tx_log_default_empty() {
1943 let mut emu = Emulator::new(Config::default());
1944 let v = emu.drain_uart0_tx_log();
1945 assert!(v.is_empty());
1946 }
1947
1948 // ------------------- poke / peek -------------------
1949
1950 #[test]
1951 fn poke_and_peek_round_trip_sram() {
1952 let mut emu = Emulator::new(Config::default());
1953 emu.poke(0x2000_2000, 0xCAFE_F00D);
1954 assert_eq!(emu.peek(0x2000_2000), 0xCAFE_F00D);
1955 }
1956}
1957
1958// ---------------------------------------------------------------------------
1959// Stage 5: branch-coverage residue not hit by Stage 4. Targets specific
1960// `if` arms inside `reset`, `load_image`, `step_serial`, `tick_systick`,
1961// `tick_pio_and_route_irqs`, `tick_pio`, `update_gpio`, and `wake_checks`.
1962// Pure append-only — does not modify production code.
1963// ---------------------------------------------------------------------------
1964#[cfg(test)]
1965mod stage5_lib_residue {
1966 use super::*;
1967 use picoem_devices::Psram;
1968
1969 // ------------------- reset: psram present (line 422) -------------------
1970
1971 /// Drives the true branch of `if let Some(ref mut psram) = self.bus.psram`
1972 /// (line 422) inside `reset()` by attaching a PSRAM device first.
1973 #[test]
1974 fn reset_with_psram_attached() {
1975 let psram = Psram::new(0, 1, 2, 3);
1976 let mut emu = EmulatorBuilder::new(Config::default())
1977 .psram(psram)
1978 .build()
1979 .unwrap();
1980 emu.reset();
1981 assert_eq!(emu.cycles(), 0);
1982 }
1983
1984 // ------------------- load_image: ROM offset boundary (line 461) -------------------
1985
1986 /// Drives the false branch of `if offset < ROM_SIZE` inside the ROM
1987 /// arm of `load_image` (line 461) by passing an offset past ROM end.
1988 /// `offset = addr & 0x0FFF_FFFF` so `0x0FFF_FFFF` is well past the
1989 /// 16 KB ROM_SIZE.
1990 #[test]
1991 fn load_image_rom_offset_past_end_is_skipped() {
1992 let mut emu = Emulator::new(Config::default());
1993 let data = [0x55u8; 4];
1994 // offset = 0x0FFF_FFFF >= ROM_SIZE so the inner copy is skipped.
1995 emu.load_image(0x0FFF_FFFF, &data);
1996 // ROM byte 0 untouched (was 0).
1997 assert_eq!(emu.bus.memory.rom_read8(0), 0);
1998 }
1999
2000 /// Drives the true branch of the same `if offset < ROM_SIZE` (line 461)
2001 /// — the existing test suite hits this through `load_image_rom_region_overlay`,
2002 /// repeated here for explicit branch attribution.
2003 #[test]
2004 fn load_image_rom_offset_inside_overlays() {
2005 let mut emu = Emulator::new(Config::default());
2006 let data = [0xAAu8, 0xBB, 0xCC, 0xDD];
2007 emu.load_image(0x0000_0010, &data);
2008 assert_eq!(emu.bus.memory.rom_read8(0x10), 0xAA);
2009 }
2010
2011 // ------------------- step_serial: while-loop arms + c0/c1 paths -------------------
2012
2013 /// Drives the true arm of the c1 step `if !self.cores[1].is_halted()
2014 /// && !self.bus.wfe_waiting[1]` (line 700) by waking core 1 first.
2015 /// Also covers line 681/682 (while predicate evaluates with both
2016 /// cores live).
2017 #[test]
2018 fn step_serial_runs_core1_when_awake() {
2019 let mut emu = Emulator::new(Config::default());
2020 emu.wake_core1();
2021 assert!(!emu.cores[1].is_halted());
2022 let _ = emu.step().unwrap();
2023 }
2024
2025 /// Drives the WFE-waiting branch in the c1 selection (line 700 false
2026 /// arm via wfe_waiting=true).
2027 #[test]
2028 fn step_serial_skips_core1_when_wfe_waiting() {
2029 let mut emu = Emulator::new(Config::default());
2030 emu.wake_core1();
2031 emu.bus.wfe_waiting[1] = true;
2032 let _ = emu.step().unwrap();
2033 }
2034
2035 /// Drives the true arm of `if c0 == 0 && c1 == 0` (line 715) by
2036 /// halting core 0 and parking core 1 in WFE so neither contributes
2037 /// cycles, but the while predicate still fires once because halted
2038 /// flags can be reset between iterations. Best-effort branch-visit
2039 /// test — actual entry depends on inner-loop timing.
2040 #[test]
2041 fn step_serial_break_on_zero_cycles_smoke() {
2042 let mut emu = Emulator::new(Config::default());
2043 // Both cores halted so the while predicate gates entry to the
2044 // loop body. The loop exits before producing cycles, so the
2045 // post-loop bookkeeping still runs.
2046 emu.cores[0].halt();
2047 emu.halt_core1();
2048 let _ = emu.step().unwrap();
2049 }
2050
2051 /// Production fix for tech_debt §1649: when both cores are blocked
2052 /// (halted/WFI here) and a TIMER alarm with INTE is scheduled in the
2053 /// future, the master clock must still advance to the alarm's fire
2054 /// cycle so the IRQ raises and `wake_checks` un-halts a core.
2055 ///
2056 /// Pre-fix behaviour: `consumed == 0` every quantum → master_cycle
2057 /// never moves → poll_alarms never matches → core stays parked
2058 /// forever.
2059 /// Post-fix behaviour: the both-blocked branch advances the clock
2060 /// to the soonest scheduled fire cycle (capped by `step_quantum`),
2061 /// raises the IRQ, drains it to NVICs, and `wake_checks` un-halts.
2062 #[test]
2063 fn step_serial_advances_clock_when_both_cores_blocked_with_armed_alarm() {
2064 use crate::peripherals::timer::{ALARM0_OFFSET, INTE_OFFSET};
2065
2066 // Use a generous step_quantum so a single step() can cover the
2067 // whole armed-alarm interval in one go.
2068 let mut emu = EmulatorBuilder::new(Config::default())
2069 .step_quantum(2_000_000)
2070 .build()
2071 .expect("Serial build is infallible");
2072
2073 // Both cores parked. Core 1 is halt_core1 (correct production
2074 // path); core 0 we explicitly halt to emulate WFI.
2075 emu.cores[0].halt();
2076 emu.halt_core1();
2077 assert!(emu.cores[0].is_halted());
2078 assert!(emu.cores[1].is_halted());
2079
2080 // Arm TIMER ALARM0 200 µs into the future and enable INTE so the
2081 // fire raises NVIC line 0. Reach into bus.timer directly — same
2082 // crate, `pub(crate)` field.
2083 let sys_hz = emu.bus.clock_tree.sys_clk_hz;
2084 emu.bus
2085 .timer
2086 .write32(INTE_OFFSET, 0x1, 0, emu.bus.master_cycle, sys_hz);
2087 emu.bus
2088 .timer
2089 .write32(ALARM0_OFFSET, 200, 0, emu.bus.master_cycle, sys_hz);
2090
2091 // Sanity: nothing pending yet — the bus IRQ vector is clean,
2092 // both NVICs are clean, and both cores are halted. Without the
2093 // fix, this state is a permanent dead-end.
2094 assert_eq!(emu.bus.irq_pending, 0);
2095 assert_eq!(emu.bus.nvics[0].pending_and_enabled(), 0);
2096 assert_eq!(emu.bus.nvics[1].pending_and_enabled(), 0);
2097
2098 // One step suffices when step_quantum >> alarm horizon.
2099 let _ = emu.step().unwrap();
2100
2101 // Post-fix: TIMER alarm fired, IRQ drained to at least one NVIC,
2102 // and `wake_checks` un-halted the core(s) it landed on. Any of
2103 // these three observations is sufficient — and on the pre-fix
2104 // code, none of them holds.
2105 let nvic_pending = emu.bus.nvics[0].is_pending(0) || emu.bus.nvics[1].is_pending(0);
2106 let core_woke = !emu.cores[0].is_halted() || !emu.cores[1].is_halted();
2107 assert!(
2108 nvic_pending || core_woke,
2109 "Both-blocked clock-advance branch did not deliver TIMER alarm: \
2110 nvic0_pend0={} nvic1_pend0={} c0_halted={} c1_halted={} \
2111 master_cycle={} alarm_fire={:?}",
2112 emu.bus.nvics[0].is_pending(0),
2113 emu.bus.nvics[1].is_pending(0),
2114 emu.cores[0].is_halted(),
2115 emu.cores[1].is_halted(),
2116 emu.bus.master_cycle,
2117 emu.bus.next_scheduled_lazy_deadline(),
2118 );
2119 }
2120
2121 // ------------------- step_serial: slow-path / fast-path gating (line 750) -------------------
2122
2123 /// Drives the slow-path arm of the fast-path gate (line 750 false)
2124 /// by enabling SysTick on the active core. The predicate
2125 /// `systick_idle = !systicks[active].is_enabled()` becomes false,
2126 /// forcing the slow branch.
2127 #[test]
2128 fn step_serial_drops_to_slow_path_when_systick_enabled() {
2129 let mut emu = Emulator::new(Config::default());
2130 // Enable SysTick on core 0 (CSR.ENABLE = bit 0).
2131 emu.bus.systicks[0].csr |= 1;
2132 let _ = emu.step().unwrap();
2133 }
2134
2135 /// Drives the fast-path arm of the same gate (line 750 true). Default
2136 /// state has no SysTick, no IRQ, no PIO — the existing
2137 /// `step_serial_returns_ok` already exercises this; making it explicit
2138 /// here for branch attribution.
2139 #[test]
2140 fn step_serial_takes_fast_path_when_idle() {
2141 let mut emu = Emulator::new(Config::default());
2142 let _ = emu.step().unwrap();
2143 }
2144
2145 // ------------------- drain_pending_irqs_to_cores (line 792, 795) -------------------
2146
2147 /// Drives the true arm of `if self.bus.irq_pending != 0` (line 792)
2148 /// AND the inner per-IRQ scan (line 795 true) by pre-staging a bus
2149 /// irq_pending bit. Forces the slow path via SysTick enable so the
2150 /// drain runs.
2151 #[test]
2152 fn drain_pending_irqs_routes_to_nvics() {
2153 let mut emu = Emulator::new(Config::default());
2154 // Force slow path.
2155 emu.bus.systicks[0].csr |= 1;
2156 // Pre-stage a pending IRQ on bus.irq_pending.
2157 emu.bus.irq_pending = 0x1; // line 0
2158 let _ = emu.step().unwrap();
2159 // After the slow path drains, both NVICs see line 0 pending.
2160 assert!(emu.bus.nvics[0].is_pending(0) || emu.bus.nvics[1].is_pending(0));
2161 }
2162
2163 // ------------------- tick_systick (lines 812, 817) -------------------
2164
2165 /// Drives the true branches of `if systicks[0].tick()` (line 812) and
2166 /// `if systicks[1].tick()` (line 817) by enabling SysTick with
2167 /// CVR=0, RVR=0 so the very first tick fires.
2168 #[test]
2169 fn tick_systick_fires_on_both_cores_when_enabled() {
2170 let mut emu = Emulator::new(Config::default());
2171 // Wake core 1 so it consumes cycles → tick_systick(c0, c1) with
2172 // both > 0.
2173 emu.wake_core1();
2174 // Enable SysTick (ENABLE=1, TICKINT=1) on both cores; CVR=0, RVR=0
2175 // → first tick reloads + fires.
2176 emu.bus.systicks[0].csr = 0b11;
2177 emu.bus.systicks[1].csr = 0b11;
2178 emu.bus.systicks[0].cvr = 0;
2179 emu.bus.systicks[1].cvr = 0;
2180 let _ = emu.step().unwrap();
2181 }
2182
2183 // ------------------- tick_pio_and_route_irqs (lines 842, 852, 855, 860, 863) -------------------
2184
2185 /// Drives the false arm of `if gpio_in & (1u32 << 4) == 0` (line 842)
2186 /// by seeding `bus.gpio_in` with bit 4 high before the slow-path
2187 /// tick. tick_pio_and_route_irqs reads gpio_in directly so we must
2188 /// pre-set it; update_gpio runs after tick_pio so the external mask
2189 /// alone doesn't apply early enough.
2190 #[test]
2191 fn tick_pio_iow_high_does_not_count_as_low() {
2192 let mut emu = Emulator::new(Config::default());
2193 // Force slow path so tick_pio_and_route_irqs runs.
2194 emu.bus.systicks[0].csr |= 1;
2195 // Pre-seed gpio_in with bit 4 high AND set the external mask so
2196 // update_gpio's post-tick rewrite preserves it across the cycle.
2197 emu.bus.gpio_in = 1u32 << 4;
2198 emu.bus.external_gpio_in_mask = 1u32 << 4;
2199 emu.bus.external_gpio_in_override = 1u32 << 4;
2200 let _ = emu.step().unwrap();
2201 // The branch was visited; whether the count stays zero depends
2202 // on whether tick_pio runs again after update_gpio. The goal is
2203 // line coverage of the false arm of the IOW gate.
2204 }
2205
2206 /// Drives the true arms of the PIO INTF routing `if int0_ints != 0`
2207 /// (line 860) and `int1_ints != 0` (line 863) for both PIO blocks.
2208 /// Uses the slow-path forcing trick so tick_pio_and_route_irqs runs.
2209 #[test]
2210 fn tick_pio_routes_intf_to_irq_pending() {
2211 let mut emu = Emulator::new(Config::default());
2212 // Force slow path.
2213 emu.bus.systicks[0].csr |= 1;
2214 // Release every peripheral from reset so MMIO writes to PIO land.
2215 emu.bus.resets.state = 0;
2216 // Write INT0_INTF / INT1_INTF on both PIO blocks. RP2040 PIO
2217 // INT0_INTF offset 0x034, INT1_INTF offset 0x040 (datasheet
2218 // §3.7). bus offsets: PIO0=0x5020_0000, PIO1=0x5030_0000.
2219 emu.bus.write32(0x5020_0034, 0x1);
2220 emu.bus.write32(0x5020_0040, 0x1);
2221 emu.bus.write32(0x5030_0034, 0x1);
2222 emu.bus.write32(0x5030_0040, 0x1);
2223 let _ = emu.step().unwrap();
2224 }
2225
2226 // ------------------- tick_pio early-return (line 876) -------------------
2227
2228 /// Drives the true arm of `if cycles == 0` inside `tick_pio`
2229 /// (line 876) by stepping with cores idle. This is best-effort —
2230 /// the step path may always pass non-zero cycles. We instead invoke
2231 /// the situation by halting both cores so `consumed` may be zero.
2232 #[test]
2233 fn tick_pio_zero_cycles_is_noop_smoke() {
2234 let mut emu = Emulator::new(Config::default());
2235 emu.cores[0].halt();
2236 emu.halt_core1();
2237 let _ = emu.step().unwrap();
2238 }
2239
2240 // ------------------- update_gpio: psram path + external mask (lines 1110, 1111, 1118) -------------------
2241
2242 /// Drives the true branch of `if let Some(ref mut psram) = self.bus.psram`
2243 /// (line 1110) plus the inner `if let Some(miso) = psram.tick(out)`
2244 /// (line 1111) by attaching a PSRAM and calling update_gpio via a
2245 /// public path (gpio_write triggers it).
2246 #[test]
2247 fn update_gpio_with_psram_attached() {
2248 let psram = Psram::new(0, 1, 2, 3);
2249 let mut emu = EmulatorBuilder::new(Config::default())
2250 .psram(psram)
2251 .build()
2252 .unwrap();
2253 // gpio_write calls update_gpio internally.
2254 emu.gpio_write(5, true);
2255 }
2256
2257 /// Drives the true branch of `if ext_mask != 0` (line 1118) by
2258 /// asserting an external GPIO mask before update_gpio runs.
2259 #[test]
2260 fn update_gpio_external_mask_overrides() {
2261 let mut emu = Emulator::new(Config::default());
2262 emu.bus.external_gpio_in_mask = 1u32 << 5;
2263 emu.bus.external_gpio_in_override = 1u32 << 5;
2264 // Trigger update_gpio via gpio_write.
2265 emu.gpio_write(0, false);
2266 assert!(emu.gpio_read(5));
2267 }
2268
2269 // ------------------- wake_checks (lines 1148, 1155) -------------------
2270
2271 /// Drives the true arm of `if self.bus.wfe_waiting[core] &&
2272 /// self.bus.event_flag[core]` (line 1148): pre-park core 0 on WFE
2273 /// with an event latched.
2274 #[test]
2275 fn wake_checks_consumes_wfe_event_core0() {
2276 let mut emu = Emulator::new(Config::default());
2277 emu.bus.wfe_waiting[0] = true;
2278 emu.bus.event_flag[0] = true;
2279 emu.cores[0].halt();
2280 let _ = emu.step().unwrap();
2281 assert!(!emu.bus.wfe_waiting[0]);
2282 assert!(!emu.bus.event_flag[0]);
2283 }
2284
2285 /// Drives the true arm of `if self.cores[core].is_halted() &&
2286 /// self.bus.nvics[core].pending_and_enabled() != 0` (line 1155)
2287 /// by enabling and pending an IRQ on core 0 while halted.
2288 #[test]
2289 fn wake_checks_unhalts_on_pending_enabled_irq_core0() {
2290 let mut emu = Emulator::new(Config::default());
2291 emu.cores[0].halt();
2292 emu.bus.nvics[0].set_enabled(0);
2293 emu.bus.nvics[0].set_pending(0);
2294 let _ = emu.step().unwrap();
2295 assert!(!emu.cores[0].is_halted());
2296 }
2297
2298 // ------------------- core_cycles: fall-through after core 1 wake -------------------
2299
2300 /// Indirect coverage for the cycle-counter idx==1 arm in core_cycles
2301 /// after the core has actually consumed a cycle. The default-zero
2302 /// case is covered by `core_cycles_default_zero`; this test forces
2303 /// a non-zero counter to pair with it.
2304 #[test]
2305 fn core_cycles_idx1_after_step() {
2306 let mut emu = Emulator::new(Config::default());
2307 emu.wake_core1();
2308 let _ = emu.step().unwrap();
2309 // No exact assertion — accessor evaluates the idx==1 arm.
2310 let _ = emu.core_cycles(1);
2311 }
2312}