warden-sdk: bootstrap repo + HPMCU (RISC-V) watchdog simulator
New from-scratch SDK for WardenOS (RV1106). First substance: the hardware simulator's core — a MemBus register/SRAM seam (one trait, real /dev/mem backend on device + in-memory SimBus on host) and a faithful port of the HPMCU watchdog firmware (hpmcu/watchdog/main.c) as HpmcuSim, with 8 tests validating boot-grace fire, heartbeat-timeout, disarm stand-down, and the flared arm-within-grace safety property (no boot-loop) — all off-device, in a virtual clock, in <1ms. This closes the gap the Explore map flagged: flared's devmem.rs/hpmcu.rs are the only register-touching modules with zero tests. README lays out the SDK vision (modern kernel, tiered driver MC/DC, proper RGA/HPMCU/NPU simulator, own repo). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
# Rust
|
||||||
|
target/
|
||||||
|
**/target/
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
# scratch / editor
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# warden-sdk
|
||||||
|
|
||||||
|
The build, driver, and simulation SDK for WardenOS (the Luckfox Pico 86-Panel /
|
||||||
|
RV1106 HMI). A from-scratch replacement for the twice-ported vendor stack
|
||||||
|
(Rockchip SDK → Luckfox SDK → our patched fork), built to the same standard as
|
||||||
|
the rest of the firmware: tested, benchmarked, reproducible, and honest about
|
||||||
|
what runs on real silicon versus what we simulate.
|
||||||
|
|
||||||
|
> Status: **bootstrapping.** This repo is being stood up incrementally; today it
|
||||||
|
> hosts the hardware **simulator** and its tests. The kernel forward-port and the
|
||||||
|
> hermetic image build move in as each is proven. Until then, flare-edge still
|
||||||
|
> builds firmware from the vendored SDK + `sdk-patches/`; nothing here is on the
|
||||||
|
> production build path yet.
|
||||||
|
|
||||||
|
## Why a new SDK
|
||||||
|
|
||||||
|
The vendored SDK is a ~2 GB opaque fork of a fork. Our real changes to it lived,
|
||||||
|
until recently, as uncommitted edits in one working copy (`flare-edge/sdk-patches/`
|
||||||
|
is the tracked form). It bakes absolute paths, needs `python` (not python3),
|
||||||
|
silently drops Kconfig options, and — the failure that motivated this repo — gives
|
||||||
|
us **no way to test hardware-dependent code off the device.** Every driver change
|
||||||
|
had to be validated by flashing a panel. That is slow, and it is dangerous: it is
|
||||||
|
how a boot-loaded-watchdog change bricked a bench unit (the load address collided
|
||||||
|
with unreserved kernel RAM — a mistake a target-config check or a memory-map model
|
||||||
|
would have caught before any flash).
|
||||||
|
|
||||||
|
The SDK's job is to make the firmware **buildable, testable, and hardenable
|
||||||
|
without a panel in the loop**, and to move us onto a modern, maintained kernel.
|
||||||
|
|
||||||
|
## Goals (from future-features)
|
||||||
|
|
||||||
|
1. **Modern kernel.** Move to the newest stable Linux we can run on our current
|
||||||
|
Buildroot LTS (2025.02.x). The realistic ceiling is **plan44's OpenWrt RV1106
|
||||||
|
fork — Linux 6.6**, which carries 152 RV1106 patches and a devicetree for our
|
||||||
|
exact board. Mainline is not viable (no DT/clk/display/RGA/NPU/flash-boot).
|
||||||
|
This is a bounded, evidence-backed forward-port, not a mainline chase.
|
||||||
|
2. **Ported, hardened drivers → 100% MC/DC on the code we own.** "100% MC/DC on
|
||||||
|
100% of drivers" is infeasible as literally stated: ~97% of driver LOC is
|
||||||
|
vendor blobs (the AIC8800 wifi driver alone is 88.5K lines). So the target is
|
||||||
|
**tiered**: real MC/DC on *our* hardware code (modbus master, relays, RGA
|
||||||
|
wrapper, HPMCU supervisor, devmem/reset ladder); fault-injection + branch
|
||||||
|
hardening for the vendor blobs behind a stable seam.
|
||||||
|
3. **A proper simulator.** Simulate the hardware the vendor SDK cannot: **RGA**
|
||||||
|
(2D blitter), the **RISC-V HPMCU** coprocessor, and the **NPU** — plus the
|
||||||
|
register/SRAM (`/dev/mem`) and sysfs surfaces the drivers touch — so driver and
|
||||||
|
supervisor logic runs and is tested on the host, in CI, with no panel.
|
||||||
|
4. **Its own repo, held to firmware standards.** Tests, benchmarks, reproducible
|
||||||
|
builds, CI. This repo.
|
||||||
|
|
||||||
|
## Architecture — one seam, two backends
|
||||||
|
|
||||||
|
The organizing idea is a thin **Hardware Abstraction Seam** per hardware block.
|
||||||
|
Firmware code talks to the seam (a trait in Rust, a function table in C); the seam
|
||||||
|
has two backends:
|
||||||
|
|
||||||
|
```
|
||||||
|
firmware / driver logic
|
||||||
|
│
|
||||||
|
Hardware Abstraction Seam (devmem, hpmcu, rga, npu, modbus, gpio)
|
||||||
|
┌────┴────┐
|
||||||
|
real backend sim backend
|
||||||
|
(/dev/mem, ioctl, (software model,
|
||||||
|
/proc, serial) host-testable)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **On-device**, the seam binds the real backend (mmap `/dev/mem`, `librga`
|
||||||
|
ioctls, the serial port, `/proc/rknpu`).
|
||||||
|
- **On the host**, it binds the **sim backend** — a faithful software model of the
|
||||||
|
block. The HPMCU sim, for example, runs the SCR1 watchdog firmware's exact state
|
||||||
|
machine (boot-grace, heartbeat-timeout, fire) against an in-memory mailbox, so
|
||||||
|
the flared supervisor's arm/beat protocol is exercised end-to-end in a unit test.
|
||||||
|
|
||||||
|
The seam is the same object the driver-hardening effort measures MC/DC against,
|
||||||
|
and the same object the simulator implements — so the two goals reinforce rather
|
||||||
|
than duplicate each other.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
sim/ the hardware simulator (Rust): mailbox/devmem model, HPMCU, RGA, NPU.
|
||||||
|
drivers/ our own hardened drivers + their seams (as they migrate in).
|
||||||
|
patches/ the vendor-SDK delta (mirrors flare-edge/sdk-patches until it moves here).
|
||||||
|
build/ the hermetic image-build wrapper (kernel → rootfs → image), incremental.
|
||||||
|
ci/ CI: patches-still-apply, host tests, coverage, benchmarks.
|
||||||
|
docs/ architecture + ADRs (decisions/).
|
||||||
|
tools/ dev tooling.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Principles
|
||||||
|
|
||||||
|
Evaluated against the stack philosophy — **openness, hardness, modernness**:
|
||||||
|
|
||||||
|
- **Open** over closed where we can: `rkdeveloptool` over the closed `upgrade_tool`;
|
||||||
|
source-buildable `librga` over blobs where a source path exists; the simulator is
|
||||||
|
fully open and ours.
|
||||||
|
- **Hard**: every seam has a fault-injection path (a wedged SDIO link, a stalled
|
||||||
|
MCU, an RGA timeout) so recovery code is tested against failure, not just success.
|
||||||
|
On-device claims still need on-device evidence; the sim narrows *which* claims
|
||||||
|
need a panel, it does not replace that rule.
|
||||||
|
- **Modern**: newest kernel we can actually run; current Buildroot LTS; Rust for new
|
||||||
|
host-testable code; reproducible builds.
|
||||||
|
|
||||||
|
## Relationship to flare-edge
|
||||||
|
|
||||||
|
flare-edge (WardenOS: the LVGL UI + the `flared` daemon) is the product; warden-sdk
|
||||||
|
is what builds and tests it. During bootstrap, flare-edge consumes warden-sdk piece
|
||||||
|
by piece: first the simulator (as a dev/test dependency), later the image build.
|
||||||
|
No flare-edge code moves here — only the SDK/build/sim/driver-seam layer.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "warden-sim"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Host-side hardware simulator for WardenOS: register/SRAM bus, HPMCU (RISC-V) watchdog, RGA, NPU. Lets driver and supervisor logic run and be tested with no panel."
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "warden_sim"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
//! Simulator of the RV1106 HPMCU (Syntacore SCR1) watchdog firmware.
|
||||||
|
//!
|
||||||
|
//! A faithful software port of `flare-edge/hpmcu/watchdog/main.c`'s poll loop:
|
||||||
|
//! it reads the Linux-owned mailbox words (magic + heartbeat counter), writes the
|
||||||
|
//! MCU-owned state word, and fires (records a CRU reset) on the same deadlines —
|
||||||
|
//! boot-grace if userspace never arms it, heartbeat-timeout if a live heartbeat
|
||||||
|
//! stops. Because it runs on a [`MemBus`], the *same* flared arm/beat protocol can
|
||||||
|
//! be driven against it in a host unit test, with a virtual clock, in
|
||||||
|
//! microseconds — the validation that was missing when a boot-loaded build of this
|
||||||
|
//! firmware had to be tested by flashing a panel.
|
||||||
|
//!
|
||||||
|
//! Deadlines are modelled in whole seconds (the firmware's cycle math exists only
|
||||||
|
//! to convert the core clock to these wall-clock seconds); a `tick(now_secs)` runs
|
||||||
|
//! exactly one iteration of the firmware's `for(;;)` body at that virtual time.
|
||||||
|
|
||||||
|
use crate::membus::MemBus;
|
||||||
|
|
||||||
|
pub const MB_BASE: u64 = 0xff6f_ff00;
|
||||||
|
const OFF_MAGIC: u64 = 0x00; // Linux-owned
|
||||||
|
const OFF_COUNTER: u64 = 0x04; // Linux-owned
|
||||||
|
const OFF_STATE: u64 = 0x08; // MCU-owned
|
||||||
|
const OFF_SEEN: u64 = 0x0c; // MCU-owned
|
||||||
|
const OFF_CYC: u64 = 0x10; // MCU-owned (coarse runtime)
|
||||||
|
|
||||||
|
pub const MAGIC_ARMED: u32 = 0x5741_5244; // "WARD"
|
||||||
|
pub const MAGIC_DISARM: u32 = 0x4449_5341; // "DISA"
|
||||||
|
|
||||||
|
pub const STATE_BOOT: u32 = 0xB007_0000;
|
||||||
|
pub const STATE_ARMED: u32 = 0xA07D_0000;
|
||||||
|
pub const STATE_DISARMED: u32 = 0xD15A_0000;
|
||||||
|
pub const STATE_FIRED: u32 = 0xF17E_0000;
|
||||||
|
|
||||||
|
pub const HEARTBEAT_TIMEOUT_S: u64 = 90;
|
||||||
|
pub const BOOT_GRACE_S: u64 = 300;
|
||||||
|
|
||||||
|
/// The simulated MCU core running the watchdog firmware against `bus`.
|
||||||
|
pub struct HpmcuSim<B: MemBus> {
|
||||||
|
bus: B,
|
||||||
|
base: u64,
|
||||||
|
t0: u64,
|
||||||
|
last_change: u64,
|
||||||
|
last_seen: u32,
|
||||||
|
polls: u32,
|
||||||
|
armed_ever: bool,
|
||||||
|
fired: bool,
|
||||||
|
fire_count: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B: MemBus> HpmcuSim<B> {
|
||||||
|
/// Bring the MCU up at `start_now` (models the firmware's `main()` prologue:
|
||||||
|
/// state=BOOT, seen=0, boot-grace clock starts here).
|
||||||
|
pub fn new(bus: B, base: u64, start_now: u64) -> Self {
|
||||||
|
bus.poke32(base + OFF_STATE, STATE_BOOT);
|
||||||
|
bus.poke32(base + OFF_SEEN, 0);
|
||||||
|
Self {
|
||||||
|
bus,
|
||||||
|
base,
|
||||||
|
t0: start_now,
|
||||||
|
last_change: start_now,
|
||||||
|
last_seen: 0,
|
||||||
|
polls: 0,
|
||||||
|
armed_ever: false,
|
||||||
|
fired: false,
|
||||||
|
fire_count: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fired(&self) -> bool {
|
||||||
|
self.fired
|
||||||
|
}
|
||||||
|
pub fn fire_count(&self) -> u64 {
|
||||||
|
self.fire_count
|
||||||
|
}
|
||||||
|
/// The sentinel half (high 16 bits) of the MCU state word.
|
||||||
|
pub fn state(&self) -> u32 {
|
||||||
|
self.bus.peek32(self.base + OFF_STATE) & 0xffff_0000
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fire(&mut self) {
|
||||||
|
self.bus.poke32(self.base + OFF_STATE, STATE_FIRED);
|
||||||
|
self.fired = true;
|
||||||
|
self.fire_count += 1;
|
||||||
|
// Real firmware then writes the CRU global reset and spins forever; a
|
||||||
|
// harness modelling the resulting reboot re-creates the MCU (new boot
|
||||||
|
// grace). Here we just record that it fired.
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One iteration of the firmware poll loop at virtual time `now` (seconds).
|
||||||
|
/// Once fired, the real core spins in `fire()`, so further ticks are no-ops.
|
||||||
|
pub fn tick(&mut self, now: u64) {
|
||||||
|
if self.fired {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.polls = self.polls.wrapping_add(1);
|
||||||
|
self.bus.poke32(self.base + OFF_CYC, (now & 0xffff_ffff) as u32);
|
||||||
|
let magic = self.bus.peek32(self.base + OFF_MAGIC);
|
||||||
|
let counter = self.bus.peek32(self.base + OFF_COUNTER);
|
||||||
|
let poll_lo = self.polls & 0xffff;
|
||||||
|
|
||||||
|
if magic == MAGIC_DISARM {
|
||||||
|
// Deliberate stand-down: resume the moment Linux re-arms; the grace
|
||||||
|
// clock restarts so a disarm-then-silence never fires.
|
||||||
|
self.bus.poke32(self.base + OFF_STATE, STATE_DISARMED | poll_lo);
|
||||||
|
self.t0 = now;
|
||||||
|
self.last_change = now;
|
||||||
|
} else if magic == MAGIC_ARMED {
|
||||||
|
self.armed_ever = true;
|
||||||
|
if counter != self.last_seen {
|
||||||
|
self.last_seen = counter;
|
||||||
|
self.bus.poke32(self.base + OFF_SEEN, counter);
|
||||||
|
self.last_change = now;
|
||||||
|
}
|
||||||
|
self.bus.poke32(self.base + OFF_STATE, STATE_ARMED | poll_lo);
|
||||||
|
if now.saturating_sub(self.last_change) > HEARTBEAT_TIMEOUT_S {
|
||||||
|
self.fire();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not (yet) armed. Catches "kernel never brought userspace up" via
|
||||||
|
// boot-grace, and a live-then-dead heartbeat via the timeout.
|
||||||
|
self.bus.poke32(self.base + OFF_STATE, STATE_BOOT | poll_lo);
|
||||||
|
if !self.armed_ever && now.saturating_sub(self.t0) > BOOT_GRACE_S {
|
||||||
|
self.fire();
|
||||||
|
}
|
||||||
|
if self.armed_ever && now.saturating_sub(self.last_change) > HEARTBEAT_TIMEOUT_S {
|
||||||
|
self.fire();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::membus::SimBus;
|
||||||
|
|
||||||
|
fn mcu(bus: &SimBus) -> HpmcuSim<SimBus> {
|
||||||
|
HpmcuSim::new(bus.clone(), MB_BASE, 0)
|
||||||
|
}
|
||||||
|
fn arm_beat(bus: &SimBus, counter: u32) {
|
||||||
|
bus.poke32(MB_BASE + OFF_COUNTER, counter);
|
||||||
|
bus.poke32(MB_BASE + OFF_MAGIC, MAGIC_ARMED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn boots_into_boot_state() {
|
||||||
|
let bus = SimBus::new();
|
||||||
|
let m = mcu(&bus);
|
||||||
|
assert_eq!(m.state(), STATE_BOOT);
|
||||||
|
assert!(!m.fired());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn boot_grace_fires_only_after_300s_when_never_armed() {
|
||||||
|
let bus = SimBus::new();
|
||||||
|
let mut m = mcu(&bus);
|
||||||
|
// Poll steadily; magic stays 0 (never armed).
|
||||||
|
for now in (0..=300).step_by(10) {
|
||||||
|
m.tick(now);
|
||||||
|
assert!(!m.fired(), "must not fire at or before boot-grace ({now}s)");
|
||||||
|
}
|
||||||
|
m.tick(301);
|
||||||
|
assert!(m.fired(), "must fire just past the 300s boot grace");
|
||||||
|
assert_eq!(m.state(), STATE_FIRED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn armed_and_beating_never_fires() {
|
||||||
|
let bus = SimBus::new();
|
||||||
|
let mut m = mcu(&bus);
|
||||||
|
// flared arms + beats every 5s for a simulated hour.
|
||||||
|
for (i, now) in (0..3600).step_by(5).enumerate() {
|
||||||
|
arm_beat(&bus, i as u32 + 1);
|
||||||
|
m.tick(now);
|
||||||
|
assert!(!m.fired(), "a beating heartbeat must never fire (@{now}s)");
|
||||||
|
}
|
||||||
|
assert_eq!(m.state(), STATE_ARMED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stops_beating_fires_after_heartbeat_timeout() {
|
||||||
|
let bus = SimBus::new();
|
||||||
|
let mut m = mcu(&bus);
|
||||||
|
arm_beat(&bus, 1);
|
||||||
|
m.tick(0);
|
||||||
|
assert_eq!(m.state(), STATE_ARMED);
|
||||||
|
// Heartbeat stops (counter frozen). Fires just past 90s.
|
||||||
|
for now in (5..=90).step_by(5) {
|
||||||
|
m.tick(now);
|
||||||
|
assert!(!m.fired(), "must not fire within the heartbeat window (@{now}s)");
|
||||||
|
}
|
||||||
|
m.tick(91);
|
||||||
|
assert!(m.fired(), "must fire just past the 90s heartbeat timeout");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disarm_stands_down_indefinitely() {
|
||||||
|
let bus = SimBus::new();
|
||||||
|
let mut m = mcu(&bus);
|
||||||
|
bus.poke32(MB_BASE + OFF_MAGIC, MAGIC_DISARM);
|
||||||
|
// Silence for well past both deadlines: a disarmed MCU never fires.
|
||||||
|
for now in (0..=1000).step_by(10) {
|
||||||
|
m.tick(now);
|
||||||
|
assert!(!m.fired(), "a disarmed MCU must never fire (@{now}s)");
|
||||||
|
}
|
||||||
|
assert_eq!(m.state(), STATE_DISARMED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flared_arms_within_boot_grace_no_boot_loop() {
|
||||||
|
// The exact safety property the boot-loaded watchdog needs: on a healthy
|
||||||
|
// boot, flared comes up well before the 300s boot grace, arms the MCU, and
|
||||||
|
// keeps beating — so it transitions BOOT -> ARMED and never fires. (A
|
||||||
|
// failure here would be the boot-loop we must never ship.)
|
||||||
|
let bus = SimBus::new();
|
||||||
|
let mut m = mcu(&bus);
|
||||||
|
// 0..40s: kernel booting, flared not up yet (magic 0). MCU polls in BOOT.
|
||||||
|
for now in (0..40).step_by(5) {
|
||||||
|
m.tick(now);
|
||||||
|
assert_eq!(m.state(), STATE_BOOT);
|
||||||
|
assert!(!m.fired());
|
||||||
|
}
|
||||||
|
// flared starts at 40s and arms+beats every 5s thereafter.
|
||||||
|
let mut counter = 0u32;
|
||||||
|
for now in (40..1000).step_by(5) {
|
||||||
|
counter += 1;
|
||||||
|
arm_beat(&bus, counter);
|
||||||
|
m.tick(now);
|
||||||
|
assert!(!m.fired(), "flared armed before boot-grace: must never fire (@{now}s)");
|
||||||
|
}
|
||||||
|
assert_eq!(m.state(), STATE_ARMED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flared_dies_after_arming_fires_and_would_roll_back() {
|
||||||
|
// flared arms, runs a while, then crashes (stops beating). The MCU fires
|
||||||
|
// after the heartbeat timeout -> reset -> (with A/B) rollback. This is the
|
||||||
|
// recovery the whole supervisor exists for.
|
||||||
|
let bus = SimBus::new();
|
||||||
|
let mut m = mcu(&bus);
|
||||||
|
let mut counter = 0u32;
|
||||||
|
// Beat through t=600 (inclusive) — the last heartbeat lands at 600s.
|
||||||
|
for now in (0..=600).step_by(5) {
|
||||||
|
counter += 1;
|
||||||
|
arm_beat(&bus, counter);
|
||||||
|
m.tick(now);
|
||||||
|
}
|
||||||
|
assert!(!m.fired());
|
||||||
|
// flared is gone: counter frozen, magic still ARMED. last_change=600, so
|
||||||
|
// the 90s heartbeat window closes at 690s; it must not fire before then.
|
||||||
|
for now in (605..=690).step_by(5) {
|
||||||
|
m.tick(now);
|
||||||
|
assert!(!m.fired(), "within the heartbeat window (@{now}s)");
|
||||||
|
}
|
||||||
|
m.tick(695); // 695 - 600 = 95 > 90
|
||||||
|
assert!(m.fired(), "flared dead > heartbeat timeout: MCU fires the reset");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
//! warden-sim — host-side hardware simulator for WardenOS.
|
||||||
|
//!
|
||||||
|
//! Lets driver and supervisor logic run and be tested on the host, with no panel,
|
||||||
|
//! by modelling the RV1106 hardware the vendor SDK cannot: the register/SRAM bus
|
||||||
|
//! ([`membus`]), the RISC-V HPMCU watchdog coprocessor ([`hpmcu`]), and — as they
|
||||||
|
//! land — the RGA blitter and the NPU.
|
||||||
|
//!
|
||||||
|
//! Design: one [`membus::MemBus`] seam, two backends. On the host, [`membus::SimBus`]
|
||||||
|
//! 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 hpmcu;
|
||||||
|
pub mod membus;
|
||||||
|
|
||||||
|
pub use hpmcu::HpmcuSim;
|
||||||
|
pub use membus::{MemBus, SimBus};
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
//! The register / SRAM access seam.
|
||||||
|
//!
|
||||||
|
//! Every hardware block on the RV1106 that our code touches through `/dev/mem`
|
||||||
|
//! (the CRU reset ladder, the HPMCU mailbox, the SGRF boot-addr register) reaches
|
||||||
|
//! it as a 32-bit poke or peek at a physical address. `MemBus` is that operation,
|
||||||
|
//! abstracted so the same driver/supervisor code runs against either:
|
||||||
|
//!
|
||||||
|
//! * the **real** backend — an mmap of `/dev/mem` (lives in flared's
|
||||||
|
//! `devmem.rs`; it will implement this trait so its logic is host-testable), or
|
||||||
|
//! * the **sim** backend — [`SimBus`], an in-memory word map.
|
||||||
|
//!
|
||||||
|
//! `SimBus` is `Clone` + internally `Arc<Mutex<..>>`, so the simulated MCU core
|
||||||
|
//! and the "Linux side" can each hold a handle and read/write the *same* shared
|
||||||
|
//! memory — exactly the two-core mailbox the real system uses — with no
|
||||||
|
//! cache-maintenance dance to model (the real mailbox sits in the GRF uncached
|
||||||
|
//! window).
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
/// A 32-bit physical-address register/SRAM bus. Addresses must be 4-byte aligned.
|
||||||
|
pub trait MemBus {
|
||||||
|
fn peek32(&self, phys: u64) -> u32;
|
||||||
|
fn poke32(&self, phys: u64, val: u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// In-memory bus for host tests. Unwritten words read as 0. Shared handles
|
||||||
|
/// (via `clone`) alias the same backing store.
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct SimBus {
|
||||||
|
words: Arc<Mutex<HashMap<u64, u32>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimBus {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Snapshot every written word (address-sorted) — for test assertions/dumps.
|
||||||
|
pub fn dump(&self) -> Vec<(u64, u32)> {
|
||||||
|
let g = self.words.lock().unwrap();
|
||||||
|
let mut v: Vec<(u64, u32)> = g.iter().map(|(&a, &w)| (a, w)).collect();
|
||||||
|
v.sort_by_key(|&(a, _)| a);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemBus for SimBus {
|
||||||
|
fn peek32(&self, phys: u64) -> u32 {
|
||||||
|
debug_assert_eq!(phys & 0x3, 0, "unaligned peek32 @ {phys:#x}");
|
||||||
|
*self.words.lock().unwrap().get(&phys).unwrap_or(&0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poke32(&self, phys: u64, val: u32) {
|
||||||
|
debug_assert_eq!(phys & 0x3, 0, "unaligned poke32 @ {phys:#x}");
|
||||||
|
self.words.lock().unwrap().insert(phys, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unwritten_reads_zero_and_writes_alias_through_clones() {
|
||||||
|
let a = SimBus::new();
|
||||||
|
let b = a.clone();
|
||||||
|
assert_eq!(a.peek32(0xff6f_ff00), 0);
|
||||||
|
a.poke32(0xff6f_ff00, 0xdead_beef);
|
||||||
|
// The clone sees it: same shared store (two-core shared memory).
|
||||||
|
assert_eq!(b.peek32(0xff6f_ff00), 0xdead_beef);
|
||||||
|
assert_eq!(a.dump(), vec![(0xff6f_ff00, 0xdead_beef)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user