Repository navigation
Board Provisioning Rock 5B Plus
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.
/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
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-bl31regardless 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.
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.configis 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-reportsDDR … fwver: v1.20; the rebuild reportsv1.24andbl31-v1.56. -
The DDR blob auto-detects the memory type.
rk3588_ddr_lp4_2112MHz_lp5_2400MHzmeans 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-tableis 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 logsBL31: BL32 initialization failedand continues.
# 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 rebootmtd0 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.
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
- SPI backup:
~/spi-backup/mtd0-original-20261009.imgon the board and~/spi-backup-235/on the Mac (sha256f13c22d2…). Restore with the sameflashcp. - If it does not boot at all: maskrom +
rkdeveloptoolwith anrk3588_spl_loaderfrom 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.
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.
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 rebootArmbian'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.
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 300Result 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.
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.
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.
.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:
- A known-good M.2 device in
.235's slot — discriminates slot vs drive in one step. - 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.
-
NPU driver patches.
.235runs the stock Armbian vendor kernel (rknpuis 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-kernelbranchrknpu-iommu-rework(PR #390) — not.236's#59diagnostic 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.dtbooverlay (the mechanism is already set up here) rather than a dtsi patch — which is what that page recommends and what.236does not do. -
.236's ownpvtm-volt-selis unread (no persistent journal, dmesg rolled). Worth capturing at its next natural reboot: if it is also2, that is direct evidence the 2400 override is safe on sibling silicon rather than a lucky bin.
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.
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.
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.