Table of Contents
RK3588 LPDDR5 overclock via ddrbin_tool.py
Recipe for pushing RK3588's LPDDR5 past the 2400 MHz shipped cap, without touching the training code. All the work happens in Rockchip's own parameter table, via Rockchip's own tool.
Current target hardware: CoolPi CM5 GenBook (“ampere”), 32 GB LPDDR5. Ran to 3200 MHz (6400 MT/s, JEDEC LP5 max) with 4 channels on 2026-04-23.
TL;DR
- Rockchip ships
ddrbin_tool.pyinsiderkbin. It edits the parameter table inside a DDR training blob in place: cold-boot frequency, DFS OPPs, UART config, pstore offsets, ECC/derate flags. - The v1.19 blob's PLL programmer computes dividers from the
lp5_freq=field. Set it to 2736 or 3200 and the blob trains there. - MegabitChip had noted “2736 dropped in v1.16”. That's half-true: Rockchip deleted the 2736 pre-built variants from rkbin, but the code path is still live in v1.19. ddrbin_tool.py exposes it as a knob.
- Community (SBCwiki, SkatterBencher #89) reaches 3200 MHz on Radxa Rock-5B+ / Rock-5T with SK Hynix modules using the same approach.
Results on ampere (2026-04-23)
| < 100% > | ||||
| lp5_freq | MT/s | peak BW | delta vs 2112 | status on ampere |
|---|---|---|---|---|
| 2112 (stock conservative) | 4224 | 67.6 GB/s | baseline | known-good |
| 2400 (stock aggressive) | 4800 | 76.8 GB/s | +13.6 % | stable post-CM5-reseat |
| 2736 | 5472 | 87.5 GB/s | +29.5 % | cold-boot clean, memtester 11/17 patterns clean |
| 3200 (JEDEC LP5 max) | 6400 | 102.4 GB/s | +51.5 % | cold-boot clean warm-ambient, memtester in progress |
Idle thermals unchanged between rates (46–48 °C package). memtester at 2736 ran at 57–59 °C package — 3200 soak expected a few degrees warmer but fine.
Thermal mod helping: 1-cent copper-plated coin laid on each LPDDR5 package with Arctic Silver 5, plus the Copperfield copper shim on the SoC package. Total invested: 2 cents plus a gram of paste.
Recipe
On the u-boot host (boltzmann):
1. Verify baseline (optional)
cd ~/projects/AMPere/rkbin
python3 tools/ddrbin_tool.py rk3588 -g /tmp/extract.txt \
bin/rk35/rk3588_ddr_lp4_2112MHz_lp5_2400MHz_v1.19.bin
grep -E 'lp5_freq|lp5_f[0-9]' /tmp/extract.txt
Reads out lp5_freq=2400, lp5_f1_freq_mhz=534, lp5_f2_freq_mhz=1320, lp5_f3_freq_mhz=1968.
2. Build a param file
Start from the stock template, fill only the fields you want to change:
cp ~/projects/AMPere/rkbin/tools/ddrbin_param.txt /tmp/ddr_param.txt sed -i 's/^lp5_freq=.*/lp5_freq=3200/' /tmp/ddr_param.txt
Everything else blank = “keep the blob's default”.
3. Patch the blob
cp ~/projects/AMPere/rkbin/bin/rk35/rk3588_ddr_lp4_2112MHz_lp5_2400MHz_v1.19.bin \
~/projects/AMPere/rkbin/bin/rk35/marfrit/rk3588_ddr_lp5_3200_v1.19_marfrit.bin
cd ~/projects/AMPere/rkbin
python3 tools/ddrbin_tool.py rk3588 /tmp/ddr_param.txt \
bin/rk35/marfrit/rk3588_ddr_lp5_3200_v1.19_marfrit.bin
Same size (76704 B). Parameter table bytes + typ YY/MM/DD-HH:MM.SS,fwver stamp update. Code untouched.
4. Verify with -g readback
python3 tools/ddrbin_tool.py rk3588 -g /tmp/verify.txt \
bin/rk35/marfrit/rk3588_ddr_lp5_3200_v1.19_marfrit.bin
grep -E 'lp5_freq|lp5_f[0-9]' /tmp/verify.txt
Confirm lp5_freq=3200. Note the updated typ timestamp — useful signature later to confirm the right blob flashed.
5. Build u-boot with the patched blob
cd ~/src/u-boot
make O=build-tfa clean
make O=build-tfa -j$(nproc) \
BL31=/home/mfritsche/projects/AMPere/trusted-firmware-a/build/rk3588/release/bl31/bl31.elf \
ROCKCHIP_TPL=/home/mfritsche/projects/AMPere/rkbin/bin/rk35/marfrit/rk3588_ddr_lp5_3200_v1.19_marfrit.bin
6. Pad to 8 MB
binman produces ~1.66 MB. SPI chip is 8 MB; rest is 0xff:
python3 -c "
raw = open('build-tfa/u-boot-rockchip-spi.bin','rb').read()
open('out-8mb.bin','wb').write(raw + b'\xff' * (8*1024*1024 - len(raw)))
"
7. Pre-flash sanity
xxd -s 0x8000 -l 8→ should readRKNSxxd -s 0x60000 -l 8→ should readd00d feed(FIT)xxd -s 0x7ffff0 -l 16→ should be all0xffstringsshould show the updated DDR stamp and Collabora TF-A banner
binman has silently truncated before (Bin campaign). Always verify structural markers before flashing.
8. Flash from running ampere
# on ampere sudo flashcp --partition /tmp/out-8mb.bin /dev/mtd0 # post-flash verify sudo dd if=/dev/mtd0 bs=4096 2>/dev/null | sha256sum # should match sha256sum of your source image
–partition writes only differing 4 KB blocks. Flash-wear-friendly and fast.
Rollback
Always dump the currently-working SPI as a flashcp target before pushing anything more aggressive:
sudo dd if=/dev/mtd0 of=~/spi-backups/ampere-spi-$(date +%Y%m%d-%H%M).bin bs=4096
If a new image doesn't boot but leaves the shell reachable somehow, flashcp back. If it truly bricks, maskrom via meitner + rkdeveloptool remains the last line of defense. Never push without a verified local backup.
On ampere the rollback chain currently is:
~/spi-backups/ampere-spi-rockhard-tfa-lp5-2736-VERIFIED-20260423-2229.bin— 2736-stable~/spi-backups/ampere-spi-pre-rockhard-tfa-20260423-2138.bin— pre-TFA Bin-era
Performance implication
Peak BW scales linearly with DDR clock (4 × 32-bit channels × MT/s ÷ 8). Real-world gains:
- Streaming / bandwidth-bound (GPU textures, NPU inference on big models, ffmpeg sw encode, kernel-level bulk copies): 20–40 %. The workloads where this actually matters.
- Compile / dev-loop (kernel, Rust, Node, browsers): 10–15 %. Cache does most of the work; RAM gets hit on working-set spills.
- Latency-bound (UI, small random access, IO-bound): ~0 %. LP5 CAS in nanoseconds is roughly flat across freqs.
- Allocator / fs cache refill (under memory pressure, no-swap system): ~30 %.
Costs:
- DDR power: +25–35 % at full read/write load. Minutes off battery under heavy DRAM stress, not hours.
- DDR heat: proportional to power. See thermal mod above.
- Bit-error margin: silicon-lottery shaped. Rockchip capped at 2400 for yield reasons; individual silicon may or may not hold higher rates. Memtester + daily use is the only way to tell.
Why ddrbin_tool.py works
The DDR blob has a 0x12345678 magic header at ~0x11B38 in v1.19 marking the parameter table. Behind that is a versioned schema of field-name + offset + type, including lp5_freq, lp4_freq, lp4x_freq, each DFS OPP (lp5_f1_freq_mhz..lp5_f5_freq_mhz), UART config, etc.
The blob's training code reads these fields at runtime and derives DFI PLL dividers from them. Setting lp5_freq=3200 doesn't need new code — the existing PLL programmer computes fbdiv + refdiv + postdiv to hit 3200 MHz. What Rockchip deleted at v1.16 was the pre-built, field-validated variants in bin/rk35/rk3588_ddr_lp4_*MHz_lp5_2736MHz_v1.03.bin — they stopped promising the rate, not computing it.
ddrbin_tool.py parses the parameter table by chip + version, shows fields as named keys, lets you override, and writes back. -g extracts the current config; no arg = modify mode.
Known gotchas
- The
-goption needs three positional args:rk3588 -g <out.txt> <blob>. Missrk3588and it silently falls back to “others chip” and writes nothing useful. - The tool updates the
typtimestamp on patch. If you're trying to keep byte-identical blobs for checksum reasons (you shouldn't), note the new timestamp. - binman silently drops the DDR blob out of the idbloader if the build's O= dir has a stale config. Always
make O=<dir> cleanbefore a re-patch-rebuild cycle. Verify withstrings out-8mb.bin | grep “DDR .*fwver”— should show today's timestamp. - The community-facing
hbiyik/rkddrtool is a TUI wrapper around the same tables. Pick whichever interface you prefer.
Coverage matrix
From rkbin/tools/ddrbin_tool_user_guide.txt, RK3588 row:
| uart info | ddr freq | ssmod | DDR 2T | sr pd | drv/odt/vref | dis train print | dis CBT | ddr2/3/4 lp2/3 vref |
| V1.00 | V1.00 | X | V1.00 | V1.00 | V1.00 | X | X | X |
“X” = not exposed. “V1.xx” = exposed from that blob version onward.
Same tool handles RK3576, RK3566/3568, RK3528, RK3562.
See also
- RK3588 DDR — MegabitChip (matching-decomp of v1.19 blob)
- Project Bin — u-boot + display + keyboard on GenBook
