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

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

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:

Performance implication

Peak BW scales linearly with DDR clock (4 × 32-bit channels × MT/s ÷ 8). Real-world gains:

Costs:

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

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