Same Chip, Two Walls: TrustZone-M vs RISC-V PMP on the RP2350

TL;DR

The RP2350 has both an Arm Cortex-M33 pair (TrustZone-M) and a RISC-V Hazard3 pair (PMP) on one die. I built the same minimal isolation demo on both — a guarded secret, a call-in service, and a deliberate illegal read — and verified both on the same Pico 2 over SWD. Findings, all measured rather than read off a spec:

  • Same result, different shape. Both block the illegal read with the secret intact. TrustZone partitions the address map and the toolchain builds the gate for you; PMP filters by privilege and you write the gate yourself.
  • Arm tells you where it broke, Hazard3 doesn’t. SFSR = 0x48 sets SFARVALID, so the faulting address is there for the taking. Hazard3 leaves mtval = 0 on a PMP access fault. mcause = 5 is all you get.
  • The same silicon only fought one of them. The Arm Non-Secure side hit two RP2350 walls that forced a bare-metal, RAM-resident NS image. The RISC-V U-mode side just ran from flash.
  • Hazard3 has a trick TrustZone structurally can’t do: Xh3pmpm lets M-mode sandbox itself, reversibly.
  • Neither of them covers the DMA. That’s a separate MPU, and it’s the RP2350 detail most likely to quietly ruin your day.

Code: rp2350-tz-teeminimal-tz (Arm) and minimal-pmp (RISC-V).

Read more

rknn-toolkit2 2.3.2 Conversion Runs Natively on arm64 — and the Two Gotchas Nobody Documents

TL;DR

If you’ve read that Rockchip’s rknn-toolkit2 model converter “only runs on x86,” that was true once and isn’t anymore. On version 2.3.2 the official aarch64 wheel installs, imports, and converts an ONNX model to a .rknn end-to-end — I did it on a plain arm64 Ubuntu 24.04 host (Python 3.12), no x86 anywhere, no cross-machine dance.

The conversion itself is boring, which is the point. What isn’t boring is the two things that stop you before you get there — both undocumented, both a fast dead end if you don’t know them:

  1. pip install 'setuptools<81'. rknn-toolkit2 2.3.2 imports pkg_resources at load; modern setuptools deleted it, so from rknn.api import RKNN blows up before you write a line of your own code.
  2. Your output .rknn won’t match anyone else’s md5 — and that’s normal. rknn files aren’t byte-reproducible. If you “verify” a fresh conversion by diffing checksums against a reference, you’ll scare yourself for no reason. The real check is elsewhere.

The rest is the walk-through and the reasoning.

Read more

RK3576 UEFI: Teaching EDK2 to Drive HDMI, USB, and eMMC

TL;DR

  • EDK2 boots a real UEFI environment on the RK3576 — swapping out U-Boot proper for EDK2 after BL31, verified on two boards (Radxa ROCK 4D over SPI, ArmSoM CM5-IO over SD).
  • HDMI’s worst bug was the debugging itself: reading overlay registers back after enable — read-only, no writes — silently perturbed VOP2 and killed the picture; the one real fix underneath all the phantom bugs was a register-write-ordering issue.
  • USB needed two DWC3 xHCI workarounds: skip a structurally-redundant EvaluateContext on SuperSpeed, and mark the hub slot-context flag before the interrupt endpoint arms, or the onboard hub never enumerates.
  • eMMC was simply held in hardware resetEMMC_RST_N defaults to 0, and RK3588 boards never notice because SPL boots from eMMC and clears it in passing; RK3576 booting SD/SPI never touches that bit at all.
  • Ethernet’s PHY part number was copy-pasted wrong everywhere — four independent sources call it an RTL8211F; the schematic says MotorComm YT8531C. It works either way (both are generic Clause-22 PHYs), until you need PHY-specific tuning.
  • Two firmware/OS handoff bugs lived below any single peripheral: a coincidental MZ byte pair in unrelated memory crashed the OS-loader scan, and an unpopulated RAM bank register read as all-zero decoded into a phantom 256 MB.
Read more

Bringing Up the RK3576 NPU on Mainline Linux

TL;DR

Goal: run MobileNet on the RK3576’s NPU through the open rocket driver + Mesa Teflon — the same stack is byte-perfect on the RK3588, so the bug is RK3576-specific. A month of all-zero output later:

  • The int8 convolution is byte-correct now. The “all-grey” wall was a fixed-point bug: the rescale multiplier went out at Q14 where the chip wants Q4 — 2¹⁰ too hot, so every pixel saturated. Fix that (plus a pad value and a bias term) and a single conv matches the CPU reference byte-for-byte.
  • MobileNet end-to-end still returns zero, behind what looked like two walls but turned out to be one, living below the registers: the command stream I send is byte-identical to the vendor’s and the chip still behaves differently. The wall is multi-task dispatch — the compute units won’t re-arm themselves for each task the way the vendor’s do — and the depthwise, which I’d taken for a second separate wall, is just the first layer wide enough to be forced through it (I confirmed that by instrumenting the vendor’s own driver and watching its tiled depthwise run aligned and correct). Ordinary single-task convolutions compute fine on the same path.
  • So the open driver is exonerated — every byte I hand the chip matches the vendor’s; the gap is in silicon state, below what software can observe on either side. I even booted the driver on top of a mainline OP-TEE to rule out the firmware — same failure. The way around (not through): the wall only bites multi-task jobs, so send each row-tile as its own single-task job, which the hardware runs. That’s the next build.

The rest is the long version — mostly me being wrong, in order.

Read more

Porting OP-TEE to the RK3576

TL;DR

  • It came up secure end-to-end: memory map + DDR firewall, a real hardware TRNG (RK3576’s RKRNG, not RK3588’s TRNG_V1), and an OTP-derived Hardware Unique Key — xtest passes 113/114 on hardware, and the one miss was a build config, not the platform.
  • The silent console was a forced-override bug. TF-A hands RK3576’s BL32 no non-secure DT pointer, so the normal console probe never runs; needs CFG_EARLY_CONSOLE forced on (not just defaulted) and TF-A/OP-TEE agreeing on UART0.
  • The OTP key is a one-way door, and it’s treated like one: ephemeral HUK by default, persistent fuse-burning gated behind an explicit off-by-default flag until the OTP index is confirmed — burn the wrong row and it’s permanent.
  • Base platform support (#7821) is merged into mainline OP-TEE; the OTP key-derivation half (#7841) is still in review, split out deliberately so the irreversible part gets its own scrutiny.
Read more

A One-Line TF-A Fix, and the Review That Came With It

TL;DR

  • The patch deletes one line — a leftover GICV2_G0_FOR_EL3 := 1 override in plat/rockchip/rk3576/platform.mk that was both redundant (the tree-wide default is already 0) and wrong (RK3576 has no reason to route GICv2 Group 0 to EL3).
  • It’s still the first thing I sent to Trusted Firmware-A, and it got read closely by engineers from ST, Google, and Rockchip before it landed.
  • The point isn’t the diff size — it’s what upstream review actually asks of you even when the change is nearly nothing: is this safe for every configuration, not just yours?
Read more