User Tools

Site Tools


rk3588_lp5_oc

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.py inside rkbin. 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 read RKNS
  • xxd -s 0x60000 -l 8 → should read d00d feed (FIT)
  • xxd -s 0x7ffff0 -l 16 → should be all 0xff
  • strings should 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 -g option needs three positional args: rk3588 -g <out.txt> <blob>. Miss rk3588 and it silently falls back to “others chip” and writes nothing useful.
  • The tool updates the typ timestamp 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> clean before a re-patch-rebuild cycle. Verify with strings out-8mb.bin | grep “DDR .*fwver” — should show today's timestamp.
  • The community-facing hbiyik/rkddr tool 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_lp5_oc.txt · Last modified: by 127.0.0.1