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.
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.lp5_freq= field. Set it to 2736 or 3200 and the blob trains there.| < 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.
On the u-boot host (boltzmann):
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.
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”.
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.
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.
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
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)))
"
xxd -s 0x8000 -l 8 → should read RKNSxxd -s 0x60000 -l 8 → should read d00d feed (FIT)xxd -s 0x7ffff0 -l 16 → should be all 0xffstrings should show the updated DDR stamp and Collabora TF-A bannerbinman has silently truncated before (Bin campaign). Always verify structural markers before flashing.
# 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.
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-eraPeak BW scales linearly with DDR clock (4 × 32-bit channels × MT/s ÷ 8). Real-world gains:
Costs:
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.
-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.typ timestamp on patch. If you're trying to keep byte-identical blobs for checksum reasons (you shouldn't), note the new timestamp.make O=<dir> clean before a re-patch-rebuild cycle. Verify with strings out-8mb.bin | grep “DDR .*fwver” — should show today's timestamp.hbiyik/rkddr tool is a TUI wrapper around the same tables. Pick whichever interface you prefer.
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.