Skip to content

Board Provisioning Rock 5B Plus

Michael Fischer edited this page Oct 10, 2026 · 5 revisions

Board Provisioning — Radxa ROCK 5B+ (10.3.0.235)

How the second RK3588 board was brought up to the project's benchmark standard (performance governors, DDR and A76 at their rated clocks). Two things were missing out of the box and neither is a kernel problem — one lives in the bootloader, one in the device tree.

Sibling page: Kernel Modifications, which covers the other board (10.3.0.236) and its NPU-specific kernel patches.

.236 (ROCK 5B, reference) .235 (ROCK 5B+) before .235 after
DDR type LPDDR4X LPDDR5 LPDDR5
DDR clock 2112 MHz no dmc device at all 2400 MHz
A76 max 2400 MHz 2304 MHz 2400 MHz
BL31 rkbin upstream TF-A rkbin v1.56
OS / root Armbian, custom kernel Armbian 6.1.115-vendor-rk35xx, NVMe Armbian, micro SD (see below)

.235 is therefore +13.6% DDR clock over .236 and matched on A76. The two boards are not interchangeable for measurement — see "Comparability" at the bottom.


Part 1 — DDR DVFS (the dmc devfreq device was absent)

Symptom

/sys/class/devfreq/ contained only fb000000.gpu and fdab0000.npu. No dmc, so no way to pin the DDR governor — step 3 of the benchmark standard was simply unperformable. Boot log:

rockchip-dmc dmc: current ATF version 0x0
rockchip-dmc dmc: trusted firmware unsupported, please update

Root cause

DDR DVFS on RK3588 is not done by the kernel. The rockchip-dmc driver asks BL31 to retrain the DRAM through a Rockchip-private SIP interface. Armbian's rock-5b-plus U-Boot package is built from mainline U-Boot v2026.04 with upstream TF-A as BL31, and upstream TF-A does not implement that interface. So the driver's version probe returns 0x0 and it disables itself.

This was confirmed by symbol presence, not inferred. Comparing the flashed SPI against a rebuild:

symbol Armbian's stock SPI rebuild with rkbin BL31
rockchip_plat_sip_handler 1 1
ddr_plat_smc_handler 0 1
ddr_round_rate 0 1
dfs DDR fsp_params 0 1
DDR_MCU init status error 0 1

Upstream TF-A has the generic Rockchip SIP dispatcher but none of the DDR services behind it — exactly the shape of failure the 0x0 version probe reports.

Do not use the FIT node name as the discriminator. U-Boot's binman labels the BL31 slot tfa-bl31 regardless of which file you feed it, so that string is present in both images. The blobs are told apart by the rkbin version string (bl31-v1.56) and by the DDR SIP symbols above.

Fix — rebuild U-Boot with rkbin BL31

Armbian's rockchip64_common.inc already names the right DDR blob; the defect is that UBOOT_TARGET_MAP passes a bare BL31=bl31.elf, i.e. Armbian's own upstream-TF-A build. Rebuilding the same U-Boot tag from Armbian's own .config, substituting rkbin's BL31, is enough.

Build natively on the board — no cross toolchain needed (gcc 14.2, dtc 1.7.2, ~47 s on 8 cores):

sudo apt-get install -y gcc make bison flex libssl-dev device-tree-compiler \
                        python3-dev python3-setuptools swig python3-pyelftools
mkdir -p ~/ubuild && cd ~/ubuild
git clone --depth 1 -b v2026.04 https://github.com/u-boot/u-boot.git
git clone --depth 1 https://github.com/rockchip-linux/rkbin.git

cd u-boot
# Armbian's exact config, straight from the installed package — do not guess a defconfig
cp /usr/lib/linux-u-boot-vendor-rock-5b-plus/u-boot-config-target-1 .config
make olddefconfig

R=$HOME/ubuild/rkbin
make -j8 BL31=$R/bin/rk35/rk3588_bl31_v1.56.elf \
         ROCKCHIP_TPL=$R/bin/rk35/rk3588_ddr_lp4_2112MHz_lp5_2400MHz_v1.24.bin
# -> u-boot-rockchip-spi.bin   (2,185,216 B; RKNS idbloader magic at 0x8000)

Notes:

  • No Armbian board patches exist for rock-5b-plus (only generic rk3588 ones), so a vanilla v2026.04 build from Armbian's .config is faithful to what the package would have produced.
  • Armbian pins BL31 v1.48 / ddrbin v1.20; rkbin HEAD (3e288fe, 2026-06-26) no longer carries those, so the newer vendor-matched pair v1.56 / v1.24 was used. rkbin's own compatibility string (current trust bl31 need match with loader ddr bin V1.13 or newer) is satisfied. The stock SPI image self-reports DDR … fwver: v1.20; the rebuild reports v1.24 and bl31-v1.56.
  • The DDR blob auto-detects the memory type. rk3588_ddr_lp4_2112MHz_lp5_2400MHz means LPDDR4 → 2112, LPDDR5 → 2400; one blob serves both. This is why the 5B+ reaches 2400 and the 5B does not — the blob is the same, the DRAM is not. Do not read a DDR frequency off the DT OPP table: /proc/device-tree/dmc-opp-table is SoC-generic and lists frequencies the board will never run (reading 2750 MHz there led to a wrong LPDDR5-speed guess early in this work).
  • tee-os (OP-TEE) is reported missing by binman — ignore it. Neither board runs OP-TEE (no /dev/tee*, no /sys/bus/tee), and .236's DDR DVFS works regardless. BL31 logs BL31: BL32 initialization failed and continues.

Flashing

# BACK UP FIRST, and copy the backup OFF the board — if it won't boot you can't fetch it later
mkdir -p ~/spi-backup
sudo dd if=/dev/mtd0 of=~/spi-backup/mtd0-original-$(date +%Y%m%d).img bs=1M
# ... scp it to a workstation now ...

sudo apt-get install -y mtd-utils        # binaries land in /usr/sbin, not on the user PATH
sudo /usr/sbin/flashcp -v u-boot-rockchip-spi.bin /dev/mtd0
sudo dd if=/dev/mtd0 bs=1 count=$(stat -c%s u-boot-rockchip-spi.bin) of=/tmp/readback.bin
sha256sum /tmp/readback.bin u-boot-rockchip-spi.bin    # must match
sudo reboot

mtd0 is 16 MB with a 4 KB erase size. The new image (2,185,216 B) fully covers the old image's data extent (2,119,679 B), so no stale tail is left behind and a whole-chip erase is unnecessary. flashcp -v verifies as it writes; the independent dd + sha256sum readback is a second check worth doing anyway.

Verification

rockchip-dmc dmc: current ATF version 0x100      <- was 0x0
/sys/class/devfreq/dmc/available_frequencies: 534000000 1320000000 1968000000 2400000000
/sys/class/devfreq/dmc/cur_freq:              2400000000

Recovery

  • SPI backup: ~/spi-backup/mtd0-original-20261009.img on the board and ~/spi-backup-235/ on the Mac (sha256 f13c22d2…). Restore with the same flashcp.
  • If it does not boot at all: maskrom + rkdeveloptool with an rk3588_spl_loader from rkbin. This needs physical access — there is no remote recovery from a format-valid bootloader that hangs, because the BootROM only falls through to SD/USB when the SPI image is invalid. Weigh that before flashing a board you cannot reach.

Part 2 — A76 capped at 2304 MHz (PVTM speed-bin gating)

Mechanism

The OPP frequencies are identical on both boards; the gate is the second cell of opp-supported-hw, which is a pvtm-volt-sel mask. At boot the kernel measures the die's PVTM ring oscillator, derives pvtm-volt-sel, and enables an OPP only if (1 << pvtm-volt-sel) matches.

Stock values — identical in Rockchip's vendor rk3588s.dtsi and in Armbian's shipped rk3588-rock-5b.dtb and rk3588-rock-5b-plus.dtb:

opp-2256000000   opp-supported-hw = <0xf9 0x13>;   /* volt-sel 0,1,4 */
opp-2304000000   opp-supported-hw = <0xf9 0x24>;   /* volt-sel 2,5   */
opp-2352000000   opp-supported-hw = <0xf9 0x48>;   /* volt-sel 3,6   */
opp-2400000000   opp-supported-hw = <0xf9 0x80>;   /* volt-sel 7     */

.235 reports pvtm-volt-sel=2 on both A76 clusters (cpu4, cpu6; bin=0, leakage=7), so only 0x24 matches and cpufreq tops out at 2304 MHz. That is Rockchip's binning working as designed, not a misconfiguration.

.236's live device tree has <0xf9 0xffff> on all four — the gate removed. That is a modification carried by .236's custom kernel build, not a board difference: the vendor DTSI and both stock Armbian DTBs ship the gating. So every "A76 @ 2.4 GHz" figure in this wiki was taken on a board with the speed bin lifted.

That change is already written up at Kernel Modifications § A76 clusters unlocked to 2400 MHz (2026-09-01) — read it, because it argues the case better than "override" does: all of 2304/2352/2400 specify the same 1.00 V, the rail already sits at 1.00 V against a 1.05 V ceiling, and RK3588's A76 is specified at 2.4 GHz, so the part was factory-conservative rather than incapable. The residual risk is timing closure on the individual die, which is what the sustained-load run tests. It also records that .236's two clusters binned differently from each other (2304 and 2352) off identical DT tables — a clean demonstration that the gate is a live per-cluster PVTM measurement.

The difference here is the mechanism, not the change. .236 carries it as an edit to rk3588s.dtsi, i.e. compiled into the kernel image — which that page itself flags as the wrong packaging ("the correct form for an optional feature is a device tree overlay … opt-in, reversible by removing one line, and it survives kernel package upgrades"). .235 uses the overlay form.

Fix — a device-tree overlay (reversible, survives kernel updates)

Patching the DTB in place gets overwritten by the next kernel package; an overlay does not.

/boot/overlay-user/rk3588-a76-2400.dts:

/dts-v1/;
/plugin/;

&{/cluster1-opp-table/opp-2256000000} { opp-supported-hw = <0xf9 0xffff>; };
&{/cluster1-opp-table/opp-2304000000} { opp-supported-hw = <0xf9 0xffff>; };
&{/cluster1-opp-table/opp-2352000000} { opp-supported-hw = <0xf9 0xffff>; };
&{/cluster1-opp-table/opp-2400000000} { opp-supported-hw = <0xf9 0xffff>; };
&{/cluster2-opp-table/opp-2256000000} { opp-supported-hw = <0xf9 0xffff>; };
&{/cluster2-opp-table/opp-2304000000} { opp-supported-hw = <0xf9 0xffff>; };
&{/cluster2-opp-table/opp-2352000000} { opp-supported-hw = <0xf9 0xffff>; };
&{/cluster2-opp-table/opp-2400000000} { opp-supported-hw = <0xf9 0xffff>; };
dtc -@ -I dts -O dtb -o rk3588-a76-2400.dtbo rk3588-a76-2400.dts

# dry-run the merge and diff it BEFORE touching boot — should be exactly 8 property changes
cp /boot/dtb/rockchip/rk3588-rock-5b-plus.dtb /tmp/base.dtb
fdtoverlay -i /tmp/base.dtb -o /tmp/merged.dtb rk3588-a76-2400.dtbo
dtc -I dtb -O dts -o /tmp/base.dts /tmp/base.dtb
dtc -I dtb -O dts -o /tmp/merged.dts /tmp/merged.dtb
diff /tmp/base.dts /tmp/merged.dts

sudo mkdir -p /boot/overlay-user
sudo cp rk3588-a76-2400.dtbo /boot/overlay-user/
echo "user_overlays=rk3588-a76-2400" | sudo tee -a /boot/armbianEnv.txt
sudo reboot

Armbian's /boot/boot.cmd applies user_overlays and, on fdt apply failure, reloads the original DT — so a malformed overlay degrades to the stock boot rather than bricking it.

Result and stability

scaling_available_frequencies: ... 2208000 2256000 2304000 2352000 2400000
cpuinfo_max_freq:              2400000

opp-microvolt is 1000000 for all four of these OPPs, so the override does not raise voltage — it asks a lower-binned die for 2400 MHz at the same 1.0 V. That must be load-tested, and the failure mode to look for is a wrong answer, not a hang:

taskset -c 4-7 stress-ng --cpu 4 --cpu-method all --verify --metrics-brief --timeout 300

Result on .235: 4 passed / 0 failed, 256,338 bogo-ops, held 2400 MHz for the full 300 s, peak 62 °C (throttle is 85 °C), no throttling/MCE/oops in dmesg. Treat that as sufficient for provisioning, not as a proof of long-run stability at this bin.

To revert: remove the user_overlays= line from /boot/armbianEnv.txt and reboot.


Governor persistence

Governors reset on every reboot, which has silently invalidated measurements before. .235 now pins them at boot via /etc/systemd/system/ork-governors.service:

[Unit]
Description=Pin RK3588 DDR + A76 governors to performance (ork-driver benchmark standard)
After=multi-user.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh -c "echo performance > /sys/class/devfreq/dmc/governor"
ExecStart=/bin/sh -c "for c in 4 5 6 7; do echo performance > /sys/devices/system/cpu/cpu$c/cpufreq/scaling_governor; done"
[Install]
WantedBy=multi-user.target

.236 has no equivalent — governors there are still set by hand.


Comparability

Results from .235 and .236 are not directly comparable. They differ in DDR type (LPDDR5 2400 vs LPDDR4X 2112), kernel (stock Armbian vendor vs the patched tree in Kernel Modifications — NPU SRAM enabler + rknpu IOMMU refcount fix), and bootloader. Record uname -r, the DDR clock and the board IP with any number that matters.

A STREAM-style triad does not distinguish the two memory systems — it saturates at ~22 GB/s on both because it is CPU-bound, well under the ~30 GB/s DRAM ceiling measured in Exp-2026-09-05. Any claim that .235 has more usable bandwidth needs a test that actually reaches the ceiling (concurrent CPU+NPU), not a single-engine triad.

The M.2 slot stopped linking — and that is probably the DRIVE, latched

.235 now boots from micro SD, not the NVMe, because partway through this work the M.2 slot stopped training:

rk-pcie fe150000.pcie: PCIe Linking... LTSSM is 0x1
rk-pcie fe150000.pcie: PCIe Link Fail, LTSSM is 0x0, hw_retries=0

LTSSM 0x1 is Detect.Active falling back to 0x0 = no receiver detected at the far end.

NOTHING BELOW IS CONFIRMED OR ELIMINATED. The cause is open. What follows is what was observed and, for each, what the observation does not establish — recorded this way because an earlier revision of this page wrongly presented these as "ruled out by measurement".

observed what it does NOT show
/sys/class/regulator/…/state for vcc3v3_pcie30 reads enabled for the ~2 s training window across a rk-pcie rebind, released on failure that 3.3 V actually reached the M.2 connector pins. This is the regulator framework's software state; the enable GPIO can be asserted while the rail is loaded down or the regulator is faulty. No voltage was measured.
The booted DTB is byte-identical (sha256) to a locally built one, status = "okay", num-lanes = <0x02>, reset-gpios and vpcie3v3-supply present that the DTB is correct for this hardware. The comparison was against our own build, not against a DTB captured from a boot in which the NVMe actually enumerated — so "known-good" was not earned.
The NVMe linked twice after the SPI reflash; the failure persisted after restoring the original SPI byte-for-byte that the bootloader was uninvolved in the original failure. The restore test ran after the link had already failed, so it only shows stock SPI does not revive an already-dead link.
The drive fails identically in the other M-key slot (fe160000) that both slots are bad. One latched drive carried between two slots produces exactly this, which is why the observation misleads toward a "two dead slots" reading.

Open lead (one anecdote, untested here): an NVMe that lost power abruptly — this one took a 10 s power-button hold while running — may latch into a state where it will not answer link training until it has been initialised in another machine. Reported from earlier Rock 5B experimentation. Plausible and cheap to test; not evidence yet.

Two tests would partition the space, and neither has been run:

  1. A known-good M.2 device in .235's slot — discriminates slot vs drive in one step.
  2. This drive in another host — discriminates drive-latched vs drive-dead.

Worth resolving rather than routing around: these M.2 controllers are the critical path for the intended two-node RDMA PCIe fabric, so neither a "dead slot" nor a "dead drive" verdict should be accepted without one of the two tests above.

Still outstanding on .235

  • NPU driver patches. .235 runs the stock Armbian vendor kernel (rknpu is built in, not a module, so this needs a full kernel build — RKNPU driver reports v0.9.8 and NPU SRAM is empty). The target is the clean upstreamable series, oRKLLM/rk3588-kernel branch rknpu-iommu-rework (PR #390) — not .236's #59 diagnostic tree, which carries ~49 numbered working-tree hunks that exist in no branch. See Kernel Modifications § Where the kernel work lives.
  • SRAM enabler: probably skip. It is explicitly optional, it was A/B'd on 2026-08-26 with no measurable effect for our workload, and it costs the video decoders their SRAM. If it is ever wanted on .235, do it as a .dtbo overlay (the mechanism is already set up here) rather than a dtsi patch — which is what that page recommends and what .236 does not do.
  • .236's own pvtm-volt-sel is unread (no persistent journal, dmesg rolled). Worth capturing at its next natural reboot: if it is also 2, that is direct evidence the 2400 override is safe on sibling silicon rather than a lucky bin.

2026-10-09 — .235 moved onto the .236 board tree, with SRAM

Per a direction call: one kernel for both nodes, feature parity except where the hardware differs. .235 no longer runs a bespoke "PR #390 bed" build. It is built from the same source state .236 runs — oRKLLM/rk3588-kernel branch board-snapshot-2026-10-09 (8fac2134ab53, a snapshot of .236's working tree, armbian rk-6.1-rkr5.1 + the ork commits + its uncommitted hunks) — with .235's own stock armbian .config, CONFIG_ROCKCHIP_RKNPU_SRAM=y, and the domain-reference leak fix. LOCALVERSION stays -vendor-rk35xx-npu, so /boot/RESTORE_STOCK_KERNEL.sh still reverts to stock in three symlink flips. Config delta vs .235 stock is the SRAM line plus the out-of-tree armbian wifi drivers this tree does not carry.

Verified after install: uname -r 6.1.115-vendor-rk35xx-npu; DDR performance @ 2400 (bootloader, unaffected); NPU SRAM live — /proc/device-tree/npu@fdab0000/rockchip,sram present and RKNPU: sram region: [0x00000000ff001000, 0x00000000ff0f0000), sram size: 0xef000 (956 KB), where before it reported freeSRAM=0KiB.

Correction: the A76 overlay is still REQUIRED

This page says above that .236's DT carries <0xf9 0xffff> on all four top OPPs and that the gate is "a modification carried by .236's custom kernel build". That is not true of board-snapshot-2026-10-09. Its rk3588s.dtsi cluster1-opp-table ungates only up to opp-2208000000; opp-2256/2304/2352/2400 keep their stock PVTM masks, and the built rk3588-rock-5b-plus.dtb shows opp-2304000000 = <0xf9 0x24>, opp-2400000000 = <0xf9 0x80>. Booted without user_overlays=rk3588-a76-2400, .235 came up at cpu4 2304 / cpu6 2352. With the overlay restored it is 2400/2400 on both clusters. So keep the overlay; do not remove it on the assumption the DTS covers it, and re-check that assumption for .236 at its next reboot.

No remote power control for .235 — plan around it

Home Assistant exposes exactly one usable outlet, Rock 5B Plug (Hallway), and that is .236. The Office outlet Radxa Rock 5B Blue - 32GB reads unavailable. A hard wedge on .235 therefore needs someone physically present. One happened on the very first NPU workload after this kernel change (details in the leak write-up): ./i4 drove a soft-reset storm and the machine went silent at 99.9 s uptime with no panic, no oops and no netconsole output. Until a switchable outlet is wired for .235, treat every NPU run on it as something you may not be able to recover remotely.

Clone this wiki locally