diff --git a/crates/core/src/bus/tick.rs b/crates/core/src/bus/tick.rs index 8c71c998d..f8b9ae354 100644 --- a/crates/core/src/bus/tick.rs +++ b/crates/core/src/bus/tick.rs @@ -756,3 +756,104 @@ impl SystemBus { (interrupts, costs) } } + +#[cfg(test)] +mod walk_free_campaign { + //! Pins the walk-free STM32 campaign's *remaining surface* on the L476 + //! nokia5110-invaders bus as it is actually executed (`from_config` + + //! `configure_cortex_m`, exactly how every e2e/capture harness builds it). + //! The bus is built with any hand `walk_deleted` flag stripped, so the + //! assertion reflects only what the models themselves prove — not a manifest + //! override. + //! + //! The walk-forcing set is `needs_legacy_walk() && !uses_scheduler()` — the + //! exact predicate `derive_walk_deletable` negates (see this module's parent). + //! uart×6 / spi×3 are already event-migrated (`uses_scheduler()==true`) so + //! they carry the default `needs_legacy_walk()==true` but do NOT force the + //! walk; they are correctly excluded here. + //! + //! Why `configure_cortex_m`: the Cortex-M core installs the *real* SCB and + //! NVIC (the chip descriptor only carries inert placeholders for those ids) + //! and appends DWT (CYCCNT), which is not in the descriptor at all. SCB's + //! `tick()` drains software-pended exceptions and DWT's advances CYCCNT — both + //! real walk work. Omitting this step would hide SCB's real tick and DWT + //! entirely and under-report the surface, so the runtime-faithful bus is the + //! honest one to pin. + //! + //! After batch **B0** (Class-A inert sweep) the forcing set is the plan's 21 + //! Class-B instances still awaiting scheduler migration, PLUS the core DWT + //! (a lazy-read CYCCNT counter the plan calls out separately as the purest + //! read-sync case). Each later batch (SysTick+SCB, timers, DMA, …) migrates a + //! slice onto the scheduler, flipping its `uses_scheduler()`, so this expected + //! set shrinks batch by batch. Keep it in lockstep with the plan's inventory. + + use crate::bus::SystemBus; + use crate::system::cortex_m::configure_cortex_m; + use labwired_config::{ChipDescriptor, SystemManifest}; + use std::path::PathBuf; + + /// Walk-forcing ids on the runtime invaders bus after B0: + /// plan Class-B: systick + 11 timers + 2 dma + 3 i2c + adc + exti + scb + bxcan (21) + /// + core DWT (added by `configure_cortex_m`, lazy-read CYCCNT walker) = 22. + const EXPECTED_WALK_FORCING: &[&str] = &[ + "systick", "tim1", "tim2", "tim3", "tim4", "tim5", "tim6", "tim7", "tim8", "tim15", + "tim16", "tim17", "dma1", "dma2", "i2c1", "i2c2", "i2c3", "adc1", "exti", "scb", "can1", + "dwt", + ]; + + fn invaders_bus_walk_stripped() -> SystemBus { + let system_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../examples/nokia5110-invaders-lab/system.yaml"); + let mut manifest = SystemManifest::from_file(&system_path).expect("load invaders manifest"); + // Construct WITHOUT the lab's hand `walk_deleted: true`: the campaign + // surface must come from the models, not the manifest escape hatch. + manifest.walk_deleted = None; + let chip_path = system_path.parent().unwrap().join(&manifest.chip); + let chip = ChipDescriptor::from_file(&chip_path).expect("load l476 chip"); + let mut bus = SystemBus::from_config(&chip, &manifest).expect("build invaders bus"); + // Install the real SCB/NVIC and the core DWT — the executed bus. + let _ = configure_cortex_m(&mut bus); + bus + } + + #[test] + fn b0_remaining_walk_forcing_set_is_exactly_class_b() { + let bus = invaders_bus_walk_stripped(); + + let mut forcing: Vec<&str> = bus + .peripherals + .iter() + .filter(|p| p.dev.needs_legacy_walk() && !p.dev.uses_scheduler()) + .map(|p| p.name.as_str()) + .collect(); + forcing.sort_unstable(); + + let mut expected: Vec<&str> = EXPECTED_WALK_FORCING.to_vec(); + expected.sort_unstable(); + + assert_eq!( + forcing, + expected, + "walk-forcing set (needs_legacy_walk && !uses_scheduler) drifted from the \ + B0 Class-B inventory.\n got ({}): {:?}\n expected ({}): {:?}\n\ + A model newly (un)marked `needs_legacy_walk()` or migrated to the scheduler \ + must update EXPECTED_WALK_FORCING to match the campaign's remaining surface.", + forcing.len(), + forcing, + expected.len(), + expected, + ); + } + + #[test] + fn b0_does_not_flip_walk_deletion() { + // Class B still forces the walk, so B0 provably changes zero runtime + // behaviour: the bus is NOT walk-deletable yet. + let bus = invaders_bus_walk_stripped(); + assert!( + !bus.derive_walk_deletable(), + "invaders bus became walk-deletable after B0, but Class-B walkers remain — \ + B0 must be pure honest bookkeeping with no behaviour change" + ); + } +} diff --git a/crates/core/src/peripherals/comp.rs b/crates/core/src/peripherals/comp.rs index b47139760..e70ce0d09 100644 --- a/crates/core/src/peripherals/comp.rs +++ b/crates/core/src/peripherals/comp.rs @@ -93,6 +93,11 @@ impl Default for Comp { } impl crate::Peripheral for Comp { + // Inert walk: comparator register bank; tick() is the trait-default no-op. + fn needs_legacy_walk(&self) -> bool { + false + } + fn read(&self, offset: u64) -> SimResult { let reg = offset & !3; let byte = (offset % 4) as u32; diff --git a/crates/core/src/peripherals/crc.rs b/crates/core/src/peripherals/crc.rs index 4227baff9..f65fb48c9 100644 --- a/crates/core/src/peripherals/crc.rs +++ b/crates/core/src/peripherals/crc.rs @@ -100,6 +100,11 @@ impl Default for Crc { } impl crate::Peripheral for Crc { + // Inert walk: register bank (CRC computed on write); tick() is the trait-default no-op. + fn needs_legacy_walk(&self) -> bool { + false + } + fn read(&self, offset: u64) -> SimResult { let reg = offset & !3; let byte = (offset % 4) as u32; diff --git a/crates/core/src/peripherals/dac.rs b/crates/core/src/peripherals/dac.rs index f0add8fc2..f2872254a 100644 --- a/crates/core/src/peripherals/dac.rs +++ b/crates/core/src/peripherals/dac.rs @@ -93,6 +93,11 @@ impl Dac { } impl crate::Peripheral for Dac { + // Inert walk: register bank with no time-driven state; tick() is the trait-default no-op. + fn needs_legacy_walk(&self) -> bool { + false + } + fn read(&self, offset: u64) -> SimResult { let reg = offset & !3; let byte = (offset % 4) as u32; diff --git a/crates/core/src/peripherals/dbgmcu.rs b/crates/core/src/peripherals/dbgmcu.rs index 509d2b9e1..68453fc8e 100644 --- a/crates/core/src/peripherals/dbgmcu.rs +++ b/crates/core/src/peripherals/dbgmcu.rs @@ -60,6 +60,11 @@ impl Dbgmcu { } impl crate::Peripheral for Dbgmcu { + // Inert walk: read-only ID / register bank; tick() is the trait-default no-op. + fn needs_legacy_walk(&self) -> bool { + false + } + fn read(&self, offset: u64) -> SimResult { let reg = offset & !3; let byte = (offset % 4) as u32; diff --git a/crates/core/src/peripherals/flash.rs b/crates/core/src/peripherals/flash.rs index 04bc71822..f6bade61f 100644 --- a/crates/core/src/peripherals/flash.rs +++ b/crates/core/src/peripherals/flash.rs @@ -689,6 +689,11 @@ impl Default for Flash { } impl crate::Peripheral for Flash { + // Inert walk: tick() is the trait-default no-op; H5 erase/bank-swap ops drain via requires_cycle_accurate/drain_pending_op per instruction, never the walk. + fn needs_legacy_walk(&self) -> bool { + false + } + fn read(&self, offset: u64) -> SimResult { let reg = offset & !3; let byte = (offset % 4) as u32; diff --git a/crates/core/src/peripherals/fmc.rs b/crates/core/src/peripherals/fmc.rs index 58c5a9597..0a2b9921c 100644 --- a/crates/core/src/peripherals/fmc.rs +++ b/crates/core/src/peripherals/fmc.rs @@ -140,6 +140,11 @@ impl Default for Fmc { } impl crate::Peripheral for Fmc { + // Inert walk: external-memory-controller register bank; tick() is the trait-default no-op. + fn needs_legacy_walk(&self) -> bool { + false + } + fn read(&self, offset: u64) -> SimResult { let reg = offset & !3; let byte = (offset % 4) as u32; diff --git a/crates/core/src/peripherals/gpio.rs b/crates/core/src/peripherals/gpio.rs index 59af5ebc0..58a7b9601 100644 --- a/crates/core/src/peripherals/gpio.rs +++ b/crates/core/src/peripherals/gpio.rs @@ -680,6 +680,11 @@ impl GpioPort { } impl crate::Peripheral for GpioPort { + // Inert walk: pure register + pad bank; pin edges are surfaced by the bus GPIO-diff pass, not tick(). + fn needs_legacy_walk(&self) -> bool { + false + } + fn read(&self, offset: u64) -> SimResult { let reg_offset = offset & !3; let byte_offset = (offset % 4) as u32; diff --git a/crates/core/src/peripherals/iwdg.rs b/crates/core/src/peripherals/iwdg.rs index 199a0aeda..be1c9d333 100644 --- a/crates/core/src/peripherals/iwdg.rs +++ b/crates/core/src/peripherals/iwdg.rs @@ -85,6 +85,11 @@ impl Default for Iwdg { } impl crate::Peripheral for Iwdg { + // Inert walk: stub register bank that does not count down today; tick() is the trait-default no-op. + fn needs_legacy_walk(&self) -> bool { + false + } + fn read(&self, offset: u64) -> SimResult { let reg = offset & !3; let byte = (offset % 4) as u32; diff --git a/crates/core/src/peripherals/lptim.rs b/crates/core/src/peripherals/lptim.rs index 7a122c03e..205974466 100644 --- a/crates/core/src/peripherals/lptim.rs +++ b/crates/core/src/peripherals/lptim.rs @@ -97,6 +97,11 @@ impl Default for Lptim { } impl crate::Peripheral for Lptim { + // Inert walk: register bank with no time-driven state; tick() is the trait-default no-op. + fn needs_legacy_walk(&self) -> bool { + false + } + fn read(&self, offset: u64) -> SimResult { let reg = offset & !3; let byte = (offset % 4) as u32; diff --git a/crates/core/src/peripherals/nvic.rs b/crates/core/src/peripherals/nvic.rs index 2c8ba748d..3a64560ab 100644 --- a/crates/core/src/peripherals/nvic.rs +++ b/crates/core/src/peripherals/nvic.rs @@ -92,6 +92,11 @@ impl Nvic { } impl Peripheral for Nvic { + // Inert walk: enable/pending register bank; the NVIC pend+enable scan runs in the bus tick loop over shared state, not this peripheral's tick() (the trait-default no-op). + fn needs_legacy_walk(&self) -> bool { + false + } + fn read(&self, offset: u64) -> SimResult { let reg_idx = (offset / 4) as usize; let byte_offset = (offset % 4) as usize; diff --git a/crates/core/src/peripherals/pwr.rs b/crates/core/src/peripherals/pwr.rs index dd2f8e743..9f8899563 100644 --- a/crates/core/src/peripherals/pwr.rs +++ b/crates/core/src/peripherals/pwr.rs @@ -156,6 +156,11 @@ impl Default for Pwr { } impl crate::Peripheral for Pwr { + // Inert walk: register bank (voltage scaling resolves in the write path); tick() is the trait-default no-op. + fn needs_legacy_walk(&self) -> bool { + false + } + fn read(&self, offset: u64) -> SimResult { let reg = offset & !3; let byte = (offset % 4) as u32; diff --git a/crates/core/src/peripherals/quadspi.rs b/crates/core/src/peripherals/quadspi.rs index 7c6e99bfc..19243db25 100644 --- a/crates/core/src/peripherals/quadspi.rs +++ b/crates/core/src/peripherals/quadspi.rs @@ -112,6 +112,11 @@ impl Default for Quadspi { } impl crate::Peripheral for Quadspi { + // Inert walk: register bank with no time-driven state; tick() is the trait-default no-op. + fn needs_legacy_walk(&self) -> bool { + false + } + fn read(&self, offset: u64) -> SimResult { let reg = offset & !3; let byte = (offset % 4) as u32; diff --git a/crates/core/src/peripherals/rcc.rs b/crates/core/src/peripherals/rcc.rs index e1ca879a3..b9e7496f8 100644 --- a/crates/core/src/peripherals/rcc.rs +++ b/crates/core/src/peripherals/rcc.rs @@ -1043,6 +1043,11 @@ impl Rcc { } impl crate::Peripheral for Rcc { + // Inert walk: clock-control register bank; tick() is the trait-default no-op. + fn needs_legacy_walk(&self) -> bool { + false + } + fn read(&self, offset: u64) -> SimResult { let reg_offset = offset & !3; let byte_offset = (offset % 4) as u32; diff --git a/crates/core/src/peripherals/rng.rs b/crates/core/src/peripherals/rng.rs index eae375d60..7bdb79f20 100644 --- a/crates/core/src/peripherals/rng.rs +++ b/crates/core/src/peripherals/rng.rs @@ -87,6 +87,11 @@ impl Default for Rng { } impl crate::Peripheral for Rng { + // Inert walk: register bank (output computed on read); tick() is the trait-default no-op. + fn needs_legacy_walk(&self) -> bool { + false + } + fn read(&self, _offset: u64) -> SimResult { // RNG.DR has side-effects (drains a word) so a const-self byte // read isn't ideal. Most firmware does word reads; for byte-level diff --git a/crates/core/src/peripherals/rtc.rs b/crates/core/src/peripherals/rtc.rs index b37f1bf5c..d30e21144 100644 --- a/crates/core/src/peripherals/rtc.rs +++ b/crates/core/src/peripherals/rtc.rs @@ -108,6 +108,11 @@ impl Default for Rtc { } impl crate::Peripheral for Rtc { + // Inert walk: register bank; tick() is the trait-default no-op (the counter-timer RTC is rtc_v3, a separate Class-B type). + fn needs_legacy_walk(&self) -> bool { + false + } + fn read(&self, offset: u64) -> SimResult { let reg = offset & !3; let byte = (offset % 4) as u32; diff --git a/crates/core/src/peripherals/sai.rs b/crates/core/src/peripherals/sai.rs index 235ca801f..072971ccd 100644 --- a/crates/core/src/peripherals/sai.rs +++ b/crates/core/src/peripherals/sai.rs @@ -99,6 +99,11 @@ impl Sai { } impl crate::Peripheral for Sai { + // Inert walk: register bank with no time-driven state; tick() is the trait-default no-op. + fn needs_legacy_walk(&self) -> bool { + false + } + fn read(&self, offset: u64) -> SimResult { let reg = offset & !3; let byte = (offset % 4) as u32; diff --git a/crates/core/src/peripherals/sdmmc.rs b/crates/core/src/peripherals/sdmmc.rs index c92cbc35e..e8b72be30 100644 --- a/crates/core/src/peripherals/sdmmc.rs +++ b/crates/core/src/peripherals/sdmmc.rs @@ -167,6 +167,11 @@ impl Default for Sdmmc { } impl crate::Peripheral for Sdmmc { + // Inert walk: register bank with no time-driven state; tick() is the trait-default no-op. + fn needs_legacy_walk(&self) -> bool { + false + } + fn read(&self, offset: u64) -> SimResult { let reg = offset & !3; let byte = (offset % 4) as u32; diff --git a/crates/core/src/peripherals/tsc.rs b/crates/core/src/peripherals/tsc.rs index da36d58c6..122f870ff 100644 --- a/crates/core/src/peripherals/tsc.rs +++ b/crates/core/src/peripherals/tsc.rs @@ -118,6 +118,11 @@ impl Tsc { } impl crate::Peripheral for Tsc { + // Inert walk: register bank with no time-driven state; tick() is the trait-default no-op. + fn needs_legacy_walk(&self) -> bool { + false + } + fn read(&self, offset: u64) -> SimResult { let reg = offset & !3; let byte = (offset % 4) as u32; diff --git a/crates/core/src/peripherals/usb_otg.rs b/crates/core/src/peripherals/usb_otg.rs index d224b6b86..9e40dfc71 100644 --- a/crates/core/src/peripherals/usb_otg.rs +++ b/crates/core/src/peripherals/usb_otg.rs @@ -122,6 +122,11 @@ impl Default for UsbOtg { } impl crate::Peripheral for UsbOtg { + // Inert walk: register bank with no time-driven state; tick() is the trait-default no-op. + fn needs_legacy_walk(&self) -> bool { + false + } + fn read(&self, offset: u64) -> SimResult { let reg = offset & !3; let byte = (offset % 4) as u32; diff --git a/crates/core/src/peripherals/wwdg.rs b/crates/core/src/peripherals/wwdg.rs index 83b890028..4707a1ad4 100644 --- a/crates/core/src/peripherals/wwdg.rs +++ b/crates/core/src/peripherals/wwdg.rs @@ -55,6 +55,11 @@ impl Default for Wwdg { } impl crate::Peripheral for Wwdg { + // Inert walk: stub register bank that does not count down today; tick() is the trait-default no-op. + fn needs_legacy_walk(&self) -> bool { + false + } + fn read(&self, offset: u64) -> SimResult { let reg = offset & !3; let byte = (offset % 4) as u32;