From 840085bd1808d39596fb8fd66b257f82de7b6ba3 Mon Sep 17 00:00:00 2001 From: BFE Engineering Date: Mon, 24 Aug 2026 17:49:44 -0600 Subject: [PATCH] sim: CRU reset-ladder model on MemBus (CruSim) Models the RV1106 reset ladder + boot-mode register on the MemBus seam, so flared's devmem::hard_reset ladder and the boot-mode -> MaskRom recovery maneuver are testable entirely on the host. Bakes in the two hardware facts that cost real bench time as regression tests: - the CRU global-reset register is 0xff3b0c08/0xfdb9; the magic at the wrong offset 0xff3a0614 (from other Rockchip SoCs) is a SILENT NO-OP here; - the boot-mode register 0xff020200 survives a warm reset (the mechanism that makes "set MaskRom, then reset" drop the SoC into BootROM download), and a power-on reset clears it. 6 tests (both rungs, pet, the wrong-offset no-op, MaskRom-survives-warm-reset, POR-clears-request); the sim crate is 14/14 green. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf --- docs/architecture.md | 2 +- sim/src/cru.rs | 234 +++++++++++++++++++++++++++++++++++++++++++ sim/src/lib.rs | 2 + 3 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 sim/src/cru.rs diff --git a/docs/architecture.md b/docs/architecture.md index a2de722..5c8fec3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -60,7 +60,7 @@ supervisor logic runs in CI with no panel. let the boot-loaded-watchdog logic be validated before the flash that bricked a bench unit (though the *layout* fault — a load address in unreserved kernel RAM — is a target-config check, §5, not a sim property). -- **Next:** a **CRU reset-ladder** model on `MemBus` (so `flared::devmem::hard_reset`'s +- **`cru` — reset ladder.** Done. `CruSim` on `MemBus` (so `flared::devmem::hard_reset`'s ladder is host-tested against the known glb_srst_fst / DW-watchdog registers); an **NPU** load model behind the path seam; a **GPIO/relay** sysfs model; a **modbus device** model unifying the existing `mbsim.py` corpus into the same framework; diff --git a/sim/src/cru.rs b/sim/src/cru.rs new file mode 100644 index 0000000..5251e78 --- /dev/null +++ b/sim/src/cru.rs @@ -0,0 +1,234 @@ +//! CRU reset-ladder model on the [`MemBus`] seam. +//! +//! `reboot -f` does NOT reset the RV1106 (no PSCI/restart handler). The canonical +//! reset is the CRU global-first software reset (rung 1), with the DesignWare +//! watchdog as a backstop (rung 2) — the ladder in flared's `devmem::hard_reset`. +//! This model lets that ladder, and the boot-mode → MaskRom recovery maneuver, be +//! exercised entirely on the host: run the pokes against a [`SimBus`], then +//! [`CruSim::poll`] to see which rung fired and what boot mode a warm reset lands in. +//! +//! It bakes in the two hardware facts that cost real hardware time: +//! * the CRU global-reset register is `0xff3b0c08` magic `0xfdb9` — the offset +//! `0xff3a0614` from *other* Rockchip SoCs is a **silent no-op** here (the model +//! ignores it, so a regression that reverts to the wrong offset fails a test); +//! * the boot-mode register `0xff020200` **survives a warm reset** and is cleared +//! only by a power-on reset — the mechanism that makes "set MaskRom, then reset" +//! drop the SoC into BootROM download without the BOOT button. + +use crate::membus::MemBus; + +/// Correct RV1106 global-first software reset (confirmed on hardware 2026-08-14). +pub const CRU_GLB_SRST_FST: u64 = 0xff3b_0c08; +pub const CRU_GLB_SRST_MAGIC: u32 = 0xfdb9; +/// Wrong offset carried over from other Rockchip SoCs — a silent no-op on RV1106. +pub const CRU_WRONG_OFFSET: u64 = 0xff3a_0614; + +/// DesignWare watchdog (rung 2 backstop). +pub const DW_WDT_BASE: u64 = 0xff5a_0000; +pub const DW_WDT_CR: u64 = DW_WDT_BASE; // bit0 = enable +pub const DW_WDT_TORR: u64 = DW_WDT_BASE + 0x4; // timeout range select +pub const DW_WDT_CRR: u64 = DW_WDT_BASE + 0xc; // write 0x76 to pet +pub const DW_WDT_PET: u32 = 0x76; +pub const DW_WDT_EN: u32 = 0x1; + +/// Boot-mode register: survives warm reset, cleared by POR. +pub const BOOT_MODE_REG: u64 = 0xff02_0200; +pub const BOOT_NORMAL: u32 = 0x5242_c300; +pub const BOOT_LOADER: u32 = 0x5242_c301; // U-Boot rockusb download +pub const BOOT_MASKROM: u32 = 0xef08_a53c; // BootROM MaskRom (db-able) + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum ResetCause { + Cru, + Watchdog, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum BootMode { + Normal, + Loader, + Maskrom, +} + +impl BootMode { + fn from_reg(v: u32) -> BootMode { + match v { + BOOT_LOADER => BootMode::Loader, + BOOT_MASKROM => BootMode::Maskrom, + _ => BootMode::Normal, // unknown/empty/BOOT_NORMAL all boot normally + } + } +} + +/// Models the reset ladder + boot-mode register over a [`MemBus`]. Poll it after +/// running reset code against the same bus. +pub struct CruSim { + bus: B, + reset_count: u64, + last_cause: Option, + boot_mode: BootMode, + /// WDT timeout in `now`-ticks (derived from TORR on enable). None = disabled. + wdt_deadline: Option, +} + +impl CruSim { + pub fn new(bus: B) -> Self { + Self { + bus, + reset_count: 0, + last_cause: None, + boot_mode: BootMode::Normal, + wdt_deadline: None, + } + } + + pub fn reset_count(&self) -> u64 { + self.reset_count + } + pub fn last_cause(&self) -> Option { + self.last_cause + } + /// Boot mode the most recent (warm) reset landed in. MaskRom/Loader stick until + /// a power-on reset; a plain reset with no boot-mode set lands in Normal. + pub fn boot_mode(&self) -> BootMode { + self.boot_mode + } + + /// A power-on reset: clears the boot-mode register (the one thing a warm reset + /// preserves), so the next boot is Normal regardless of a stale MaskRom request. + pub fn power_on_reset(&mut self) { + self.bus.poke32(BOOT_MODE_REG, BOOT_NORMAL); + self.boot_mode = BootMode::Normal; + self.wdt_deadline = None; + } + + /// Advance the model to time `now` and apply any pending reset. Returns the + /// cause if a reset fired this tick. A reset consumes its trigger (the CRU + /// magic / the WDT deadline) and reads the *preserved* boot-mode register. + pub fn poll(&mut self, now: u64) -> Option { + // Rung 1: CRU global-first software reset. ONLY the correct register fires; + // the magic at the wrong offset is a silent no-op (the register just holds + // the value and nothing happens). + if self.bus.peek32(CRU_GLB_SRST_FST) == CRU_GLB_SRST_MAGIC { + self.bus.poke32(CRU_GLB_SRST_FST, 0); // reset consumes the request + return Some(self.fire(ResetCause::Cru)); + } + + // Rung 2: DesignWare watchdog. Enabled (CR bit0) + deadline passed with no + // intervening pet => reset. + let cr = self.bus.peek32(DW_WDT_CR); + if cr & DW_WDT_EN != 0 { + // A pet (CRR == 0x76) rearms the timer; consume it so we detect the next. + if self.bus.peek32(DW_WDT_CRR) == DW_WDT_PET { + self.bus.poke32(DW_WDT_CRR, 0); + self.arm_wdt(now); + } else if self.wdt_deadline.is_none() { + self.arm_wdt(now); // just enabled: start the timer + } + if let Some(deadline) = self.wdt_deadline { + if now >= deadline { + self.wdt_deadline = None; + return Some(self.fire(ResetCause::Watchdog)); + } + } + } else { + self.wdt_deadline = None; + } + None + } + + fn arm_wdt(&mut self, now: u64) { + // TORR selects the timeout; model it as 1< 1 tick so the reset-ladder backstop fires promptly). + let torr = self.bus.peek32(DW_WDT_TORR) & 0xf; + self.wdt_deadline = Some(now + (1u64 << torr)); + } + + fn fire(&mut self, cause: ResetCause) -> ResetCause { + self.reset_count += 1; + self.last_cause = Some(cause); + // A warm reset preserves the boot-mode register: that is exactly how + // "poke MaskRom then reset" reaches BootROM download. + self.boot_mode = BootMode::from_reg(self.bus.peek32(BOOT_MODE_REG)); + cause + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::membus::SimBus; + + /// Rung 1: the correct CRU global-reset register + magic fires a CRU reset. + #[test] + fn cru_global_reset_fires() { + let bus = SimBus::new(); + let mut cru = CruSim::new(bus.clone()); + bus.poke32(CRU_GLB_SRST_FST, CRU_GLB_SRST_MAGIC); + assert_eq!(cru.poll(0), Some(ResetCause::Cru)); + assert_eq!(cru.reset_count(), 1); + assert_eq!(cru.boot_mode(), BootMode::Normal); + } + + /// The hardware lesson as a regression: the magic at the WRONG offset does + /// nothing (a silent no-op), so the ladder must fall through to the watchdog. + #[test] + fn wrong_offset_is_a_silent_noop() { + let bus = SimBus::new(); + let mut cru = CruSim::new(bus.clone()); + bus.poke32(CRU_WRONG_OFFSET, CRU_GLB_SRST_MAGIC); // the bug + assert_eq!(cru.poll(0), None, "wrong-offset write must NOT reset"); + assert_eq!(cru.reset_count(), 0); + } + + /// Rung 2: enabling the DW watchdog and not petting it past the deadline fires. + #[test] + fn watchdog_backstop_fires_on_timeout() { + let bus = SimBus::new(); + let mut cru = CruSim::new(bus.clone()); + bus.poke32(DW_WDT_TORR, 3); // deadline = now + 8 + bus.poke32(DW_WDT_CR, DW_WDT_EN); + assert_eq!(cru.poll(0), None); // arms at t=0, deadline 8 + assert_eq!(cru.poll(7), None); // not yet + assert_eq!(cru.poll(8), Some(ResetCause::Watchdog)); + } + + /// Petting the watchdog before the deadline prevents the reset. + #[test] + fn watchdog_pet_prevents_reset() { + let bus = SimBus::new(); + let mut cru = CruSim::new(bus.clone()); + bus.poke32(DW_WDT_TORR, 3); // deadline window = 8 ticks + bus.poke32(DW_WDT_CR, DW_WDT_EN); + cru.poll(0); + bus.poke32(DW_WDT_CRR, DW_WDT_PET); // pet at t=5 -> new deadline 13 + assert_eq!(cru.poll(5), None); + assert_eq!(cru.poll(8), None, "petted: original deadline no longer applies"); + assert_eq!(cru.poll(13), Some(ResetCause::Watchdog)); + } + + /// The boot-mode register survives a (warm) reset: set MaskRom, reset via CRU, + /// and the model lands in MaskRom — the on-demand BootROM-download maneuver. + #[test] + fn maskrom_survives_warm_reset() { + let bus = SimBus::new(); + let mut cru = CruSim::new(bus.clone()); + bus.poke32(BOOT_MODE_REG, BOOT_MASKROM); + bus.poke32(CRU_GLB_SRST_FST, CRU_GLB_SRST_MAGIC); + assert_eq!(cru.poll(0), Some(ResetCause::Cru)); + assert_eq!(cru.boot_mode(), BootMode::Maskrom); + } + + /// A power-on reset clears the boot-mode register (unlike a warm reset), so a + /// stale MaskRom request does not strand the device — it boots Normal. + #[test] + fn power_on_reset_clears_maskrom_request() { + let bus = SimBus::new(); + let mut cru = CruSim::new(bus.clone()); + bus.poke32(BOOT_MODE_REG, BOOT_MASKROM); + cru.power_on_reset(); + bus.poke32(CRU_GLB_SRST_FST, CRU_GLB_SRST_MAGIC); + assert_eq!(cru.poll(0), Some(ResetCause::Cru)); + assert_eq!(cru.boot_mode(), BootMode::Normal); + } +} diff --git a/sim/src/lib.rs b/sim/src/lib.rs index a8dc954..9199176 100644 --- a/sim/src/lib.rs +++ b/sim/src/lib.rs @@ -9,8 +9,10 @@ //! is an in-memory word map; on the device, flared's `devmem.rs` implements the same //! trait over `/dev/mem`, so the same code runs against either. See the repo README. +pub mod cru; pub mod hpmcu; pub mod membus; +pub use cru::{BootMode, CruSim, ResetCause}; pub use hpmcu::HpmcuSim; pub use membus::{MemBus, SimBus};