Skip to main content

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}