Preparing the repo to go public (ADR-0007): - CI: kernel-build moves from the self-hosted runner to ubuntu-latest (installs its own cross toolchain + qemu, caches the pristine tarball). On a public repo a registered self-hosted runner is reachable from approved fork-PR workflows — i.e. arbitrary code on private infrastructure — and the build never actually needed the SDK host. ADR-0004 marked superseded-in-part; docs/ci-cd.md rewritten (site specifics now live only in the private deployment log). - Licensing: LICENSE gains the GPL-2.0 carve-out for patches/ and the kernel source excerpts (Linux derivatives; per-driver provenance was already tracked in PROVENANCE.md); patches/README.md states it too. - Scrubbed from the tip: bench-unit dev credentials and its gadget IP (m2-boot notes), the site AP SSID+BSSID and a neighboring AP's BSSID and the device WLAN MAC (wifi bring-up evidence — BSSIDs are geolocatable), the runner mesh IP. NOTE: these remain in git history; decision on a pre-publication history rewrite is separate. - Emoji cleanup across 21 tracked files (kernel port docs, review report, enforce-mcdc.sh) per repo text conventions: status marks became [x]/[wip]/[ ]/OK plain text. - "[maintainer]-gated" process phrasing normalized to "maintainer-gated" (attributions in dated evidence docs kept). Verified: zero emojis tracked; scrub grep clean; patches carry no internal references; ci.yml parses; shellcheck unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HUayid7W5w7jBdb9Rrj1K
13 KiB
warden-sdk architecture
How the SDK makes WardenOS buildable, testable, and hardenable without a panel in the loop. Grounded in a full survey of the current flare-edge firmware (the seam inventory below is from that survey, not aspiration).
1. The problem the seams solve
The firmware touches RV1106 hardware through a grab-bag of mechanisms, each tested (or not) differently. Today:
| Block | Where | Access | Test seam today | Fails on host by |
|---|---|---|---|---|
| Registers / SRAM (CRU reset, HPMCU mailbox) | flared/src/devmem.rs, hpmcu.rs |
/dev/mem mmap peek/poke32 |
none — zero tests | (would fault; not exercised) |
| HPMCU / RISC-V coproc | flared/src/hpmcu.rs |
via devmem + firmware blob load | WARDEN_HPMCU_FW redirects the blob path only |
env gate disables it |
| NPU load | ui-src/.../sysmon.c |
read /proc/rknpu/load |
none — literal path | file absent → "NPU absent" |
| RGA (2D blit) | ui-src/.../warden_rga.c |
librga improcess + dma-heap ioctl |
compile-time #if WARDEN_USE_RGA |
#if off → LVGL software path |
| RS485 daemon | warden-modbus/modbus_engine.c |
open("/dev/ttyS4") |
recompile -DRS485_PORT=<pty> |
(recompiled for a pty) |
| RS485 panel client | ui-src/.../modbus.c |
AF_UNIX socket |
WARDEN_MODBUS_SOCK env override |
socket absent → "unavailable" |
| Relays / GPIO | ui-src/.../relays.c |
/sys/class/gpio sysfs |
none — literal paths | path absent → "unavailable" |
| Slot metadata | flared/src/slotctl.rs |
misc partition + /proc/cmdline |
WARDEN_MISC_DEV, WARDEN_CMDLINE_FILE env overrides |
(redirected to scratch files) |
Three patterns coexist: compile-time #if (RGA), env-override (modbus
socket, misc dev, cmdline, hpmcu fw), and fails-soft-because-the-path-is-absent
(NPU, relays, devmem-would-fault). The last is not a test seam — you cannot inject
"relay 1 is ON" or "NPU at 80%", only "absent". The SDK's job is to turn all of
these into one deliberate seam per block with a real backend and a sim backend.
2. The seam taxonomy
Two seam kinds cover everything above:
- Register/SRAM seam → a trait.
MemBus(sim/src/membus.rs):peek32/poke32at a physical address. Real backend = flareddevmem.rsmmap; sim backend =SimBus(in-memory word map,Cloneso two "cores" alias shared memory). The HPMCU watchdog and the CRU reset ladder both ride this. Built. - Resource-path seam → env-override + injection. For file/socket/sysfs paths
(
/proc/rknpu/load,/sys/class/gpio/*,/dev/ttyS4,misc), generalize the existingWARDEN_MISC_DEV/WARDEN_MODBUS_SOCKpattern into one rule: every device/proc/sys path a driver opens is resolved through a single indirection (warden_hw_path("npu.load")in C, an env-overridable const in Rust), so a test points it at a fake file/fifo the sim writes. No LD_PRELOAD, no fake mounts.
RGA stays compile-time — its #if WARDEN_USE_RGA already cleanly isolates the
librga/dma-heap calls behind the always-compiled LVGL draw-unit glue; the sim
backend is "a fake improcess that records the blits it was asked to do", swapped
behind the same #if, so the offload dispatch logic gets tested even though the
blit itself is modelled.
3. The simulator (sim/)
A host Rust library modelling the hardware the vendor SDK cannot, so driver and supervisor logic runs in CI with no panel.
membus— register/SRAM bus. Done.MemBustrait +SimBus.hpmcu— the RISC-V watchdog coprocessor. Done. Faithful port ofhpmcu/watchdog/main.c's state machine (boot-grace, heartbeat-timeout, disarm, fire) against aSimBusmailbox, virtual clock, 7 tests including the arm-within-grace no-boot-loop safety property. This is the model that would have 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).cru— reset ladder. Done.CruSimonMemBus(soflared::devmem::hard_reset's ladder is host-tested against the known glb_srst_fst / DW-watchdog registers), plus the boot-mode register's survives-warm-reset / cleared-by-POR behaviour (the MaskRom recovery maneuver). The matching firmware-sideBusseam on flared'sdevmem— so the shipped ladder can be asserted to poke the confirmed offset, never the wrong-SoC one — lands when flare-edge consumes warden-sdk (§7 item 3, maintainer-gated), not yet on flare-edgemain.modbus— RS-485 device end. Done.ModbusSlave: a byte-in/byte-out RTU slave (CRC16 byte-identical to the master, FC 0x01–0x06/0x0F/0x10/0x11, exception replies, and fault injection — silent-drop and forced-NAK) sowarden-modbus's master can be hardened to MC/DC against realistic device behaviour with no serial hardware. MEI (0x2B/0x0E) identification is the documented follow-up. The same slave also serves as the QEMU device sim's field bus:qemu/rs485-bridge/feeds it from a serial chardev so the guest's real master polls it over what it believes is /dev/ttyS4 (§7).npu— NPU load model. Done.NpuSimmodels/proc/rknpu/load(the exact "NPU load: N%" text the sysmon reads) behind the path seam, so the load-readout UI is host-testable. NPU compute is explicitly out of scope — no inference runs here.rga— 2D blitter offload. Done.RgaSim, a recordingimprocessfake with a programmableIM_STATUS, so the RGA offload-dispatch and CPU-fallback logic is exercised behind the#if WARDEN_USE_RGAseam without librga; wired into therga_improcessbenchmark.- Next: MEI (0x2B/0x0E) Modbus identification; the Tier-2 driver sources
(
modbus_engine.c,warden_rga.c) migrate in with the flare-edge unification (ADR-0005) — their hardware ends are already modelled and tested above.
Integration with flare-edge: flared implements MemBus for /dev/mem and gains
#[cfg(test)] tests driving its real arm/beat logic against HpmcuSim. This needs
warden-sdk reachable as a Cargo dependency in CI — i.e. a remote for this repo,
which is a maintainer go-ahead item (credential/remote creation). Until then the
firmware-side seam and a local test double land in flare-edge, unified with sim/
once the dependency exists. No duplication of logic — only the tiny trait.
4. Driver hardening (the "port + harden to MC/DC" goal)
"100% MC/DC on 100% of drivers" is infeasible literally: ~97% of driver LOC is vendor blobs (AIC8800 wifi = 88.5K lines). Tiered target:
- Tier 1 — our own hardware code → real MC/DC. Method: the proven
tests/uboot-abpattern — extract the unit behind a small injectable seam, mock its world, build-fcondition-coverage, enforce with the shareddrivers/enforce-mcdc.sh(gcc-14gcov --conditions) in the CImcdcjob. Done here now:relays.c(40/40 conditions) andfreshness.c(66/66), both at 100% MC/DC and CI-enforced. Migrate in next: the modbus master (modbus_engine.c) and the RGA wrapper's dispatch — their hardware ends are already modelled and tested insim/(modbus,rga); the driver sources move in with the flare-edge unification (ADR-0005). The HPMCU supervisor and devmem reset ladder are covered Rust-side (sim/hpmcu,sim/cru). - Tier 2 — near-mainline small drivers → branch coverage + fault injection.
- Tier 3 — vendor blobs (AIC8800, MPP/ISP/RGA libs) → fault-injection hardening
behind the seam, not MC/DC. The AIC8800 SDIO-wedge Tier-1 fix + the designed
reset-on-ETIMEDOUT recovery are this tier: test the recovery path against an
injected wedge on the
MemBus/SDIO seam, since the blob itself is untestable.
Every seam gets a fault-injection mode (a wedged SDIO link, a stalled MCU heartbeat, an RGA timeout, a GPIO write EIO) so recovery code is tested against failure, not just the happy path.
5. Target-config checks (a class the sim cannot cover)
The brick was a memory-map fault: the boot-loaded MCU's load address (0x40000)
is a reserved carve-out on Thunder-Boot boards but plain kernel RAM on ours. No
behavioural sim catches that — it needs a static check against the target DT:
"every address the MCU/coprocessor code loads to is inside a reserved-memory
node." warden-sdk owns these config-lint checks (idblock loader .ini vs DT
reservations, partition table vs image sizes, vermagic vs kernel) as CI gates, so a
mistake is caught before a flash rather than on the bench.
Built: tools/config-lint implements the first and most important of these —
the MCU-load-vs-reserved-memory gate. It parses the rkbin loader .ini for
every LOADERn=Hpmcu firmware and its [LOADERn_PARAM] LOAD_ADDR, parses the
target devicetree (.dts, or dtc -I dtb output in CI) for reserved-memory
ranges, and fails if any MCU load lands outside a reservation. Its test suite
encodes the c8a3 brick itself: the real Thunder-Boot .ini (Hpmcu @ 0x40000)
fails against a DT with no rtos@40000 node and passes once the reservation is
added. Next target-config checks: partition-table-vs-image-size and
vermagic-vs-kernel.
6. Kernel forward-port (done — see ADR-0001)
A self-built Linux 6.18.46, forward-ported directly from the vendor 5.10.160 tree
onto our Buildroot LTS/uClibc base — not the plan44/OpenWrt 6.6 fork this section
originally reached for. ADR-0001 records why that was superseded: plan44 drops
Buildroot for OpenWrt/musl and ships no AIC8800 kmod, so it was a swap-out, not a
forward-port. Mainline alone was not viable either (no DT/clk/display/RGA/NPU/
flash-boot upstream for RV1106); the port reuses the already-in-mainline rv1126
register data where it matches and carries our deltas as the reviewable patches/
series. This is done and hardware-verified on warden-c8a3 — clk, pinctrl, eMMC,
GMAC, TRNG, OTP, SARADC/TSADC, RTC, USB host, PWM/backlight, VOP display, GT911 touch,
AIC8800 wifi, RGA, I2S audio, HPMCU mailbox, the open NPU driver, and PVTM all boot.
The dominant risk was the struct-ABI break (the VLAN saga), mitigated by shipping the
kernel move as one matched boot+oem image, never a partial reflash.
build/build-kernel.sh (the hermetic build) and the patches-apply CI gate keep the
series honest against pristine 6.18.46; provenance is in patches/README.md and
kernel/rv1106-enablement/.
7. Device emulation (qemu/) — see ADR-0006
The third simulator, deliberately not named "sim": a QEMU VM (-M virt,highmem=off,
one Cortex-A7, 256M — the RV1106G3's shape) that boots the real forward-ported
kernel and real userspace, entering at -kernel zImage because everything below
(BootROM, idblock/DDR-init, U-Boot, the BCB A/B machinery) is closed blobs plus
mask ROM. The canonical RV1106 zImage boots virt unmodified; an additive kconfig
fragment (qemu/configs/virt.fragment via WARDEN_KCONFIG_FRAGMENT) adds the
scenario devices (PCI serial for RS485, i6300esb watchdog, WireGuard,
virtio-gpu/input for the 720x720 UI). The virtio disk carries the device's exact
12-partition blkdevparts= A/B layout and the /dev/block/by-name/ contract.
Where the seams meet: the guest runs the real binaries (static musl flared,
the LVGL fbdev UI); the RS485 bridge (qemu/rs485-bridge/) connects a QEMU
serial chardev to sim/'s ModbusSlave, so the register-level models serve as
the VM's field bus — behavior lives in one place, sim/, and the VM consumes
it. Scenario tests: qemu/tests/boot-smoke.sh (CI, in kernel-build),
portal-scenario.sh (check-in + OTA offer download against flare-edge's mock
portal), ui-shot.sh (QMP screendump + touch injection). Division of labour
with the other sims: NPU/RGA/HPMCU behavior stays sim/; UI rendering
development stays lvglsim; the VM is where processes, the kernel, and the
network meet. §5 still applies — no behavioural sim, this one included, catches
memory-map faults; and "boots under emulation" is never on-silicon evidence.
8. Order of work
- Simulator core —
membus,hpmcu, thecrureset ladder,modbus, plus therga/npumodels. Done. - C-driver MC/DC harnesses —
relays.candfreshness.cat 100% MC/DC, CI-gated via the shareddrivers/enforce-mcdc.sh. Done (the first C coverage gate). - flared devmem/hpmcu seam + tests — firmware-side trait, unified with
sim/once flare-edge consumes warden-sdk (a separate, maintainer-gated step). Pending. - Config-lint CI gates (§5) — the brick-class of bug. Done.
- Hermetic kernel build (
build/build-kernel.sh+ thepatches-applygate). Done. - Kernel 5.10→6.18.46 forward-port (§6, ADR-0001). Done (hardware-verified).
- QEMU device sim (§7, ADR-0006) — boot smoke, A/B disk harness, RS485 bridge, portal/watchdog/clock scenarios, display+touch. Done (emulation-verified; booting the real flare-edge rootfs+oem image pair is a documented later milestone).