这块板卡拿回来之后,我第一件事就是把 Rockchip 官方 SDK 里的 U-Boot 编出来烧进 eMMC,结果串口没有任何输出。后来才发现问题不在编译,而在启动链路里一个很容易被忽略的角色——miniloader。标题里写的“RK3399 Uboot编译移植-boot from eMMC for with miniloader”,本质上就是要把“从eMMC启动”和“带miniloader的镜像方案”这两件事彻底搞清楚。这篇文章我就按我实际踩过的路径,从启动原理、编译环境、镜像生成,到 eMMC 烧录和串口调试,完整走一遍。适合正在做 RK3399 类板卡系统移植、量产烧录、或者被“U-Boot 编译出来但上电起不来”困扰的嵌入式工程师。
1. 先理清启动链路:BootROM、miniloader、U-Boot 是怎么接力跑的
很多人一上来就 make,烧进去发现起不来,就开始怀疑编译器版本、怀疑源码分支,其实大多数问题都出在没搞懂 RK3399 的启动流程上。这个平台的启动不是简单的一条线“BootROM→U-Boot→kernel”,中间至少插了两次接力,而第一棒就必须由 miniloader 接住。
1.1 BootROM 到底会去读什么地方
RK3399 的片内 BootROM 出厂就写死了,不可修改。上电后它先做时钟和基础 IO 初始化,然后根据启动介质选择引脚(或者芯片内部烧写的 fuse)决定从 eMMC、SD、SPI NOR/NAND 还是 USB 下载模式启动。
这里最关键的一点是:BootROM 不会去遍历文件系统,也不会去识别 U-Boot 的格式,它只认一个固定位置、固定格式的“初始数据块”。对于 eMMC 启动,BootROM 会去读用户数据区起始偏移 32KB(也就是 0x40 扇区)处的数据。这个 32KB 偏移是芯片设计时硬编码的,不是我们想放哪就放哪。
那这 32KB 处放的是什么?就是 idbloader.img,或者说官方 SDK 路径下的 MiniLoaderAll.bin。它由两部分拼接:TPL 和 SPL。在官方语义里,这个二合一的启动镜像就是“miniloader”。
1.2 miniloader 为什么不可跳过
BootROM 自己太小,能干的事极其有限,它甚至没有能力初始化 DDR。而 U-Boot 本体都是要跑在 DDR 里的,几十 KB 的 BootROM 不可能直接把 U-Boot 搬进去,更不可能引导 Linux。于是 BootROM 只能先加载一个小巧的 loader,让这个 loader 去完成最脏最累的活:
- 初始化 DDR 控制器,做 DDR 颗粒的训练和稳定化;
- 配置 PLL 和整棵时钟树,让 CPU、总线、外设跑到目标频率;
- 关闭或打开对应电源域,初始化 console 串口;
- 把自身搬运到 DDR 里继续执行,避免一直吃亏在 SRAM 的容量限制里;
- 再根据烧写时的启动介质信息,把真正的 U-Boot 镜像从 eMMC/SD 的指定偏移处读进 DDR,并跳转过去。
也就是说,没有 miniloader,eMMC 里的 U-Boot 就算编得再正确,也永远不会被执行。它就像一整栋大厦的门禁系统:大楼大门(BootROM)只认门禁卡,而你手里拿的是办公室钥匙(U-Boot)。没有门禁员(miniloader)帮你开门,钥匙再对也没用。
1.3 主线 U-Boot 与 SDK 方案的 loader 差异
这里要区分两条路径。
Rockchip 官方 SDK 里的 U-Boot 是 2017.09 左右的分支,配合的工具链和目录结构都比较老。它用的 miniloader 是 rkbin 仓库里预编译好的 MiniLoaderAll.bin,里面包含闭源的 DDR 初始化代码和 miniloader 主程序。这套方案的好处是 DDR 驱动是原厂调过的,颗粒兼容性有保障,缺点是闭源,出了问题只能找原厂。
主线 U-Boot 则是另一套玩法。它的 TPL 里直接放进了开源版的 DDR 初始化驱动(drivers/ram/rockchip/sdram_rk3399.c),TPL 和 SPL 拼接后生成 idbloader.img,在启动链路上完全替代了官方 MiniLoaderAll.bin 的角色。也就是说,主线 U-Boot 里的“miniloader 功能”被 TPL+SPL 这个组合承接了。
理解了这条链路,后面所有编译和烧写操作才有意义。
2. 编译环境选择:交叉工具链、U-Boot 分支与 ATF 版本怎么搭配不踩坑
RK3399 是 64 位六核 SoC,U-Boot 和 ATF 都必须用 aarch64 交叉编译器。环境这一步看起来简单,但版本配不对会浪费大量时间。
2.1 交叉编译器选择
我实测下来,Linaro 的 aarch64-linux-gnu- 工具链 7.5 和 9.3 都能正常编译主线 U-Boot 和 ATF。不用追求特别新的 GCC 12/13,反而有些新版本会和 U-Boot 里较老的 Makefile 语法产生兼容性问题。
安装方式按你自己的发行版来,Debian/Ubuntu 系可以直接:
sudo apt install gcc-aarch64-linux-gnu device-tree-compiler \ bison flex swig python3-dev libssl-dev注意libssl-dev不能漏。新版 U-Boot 在生成一些镜像和 hash 工具时需要 OpenSSL 头文件,缺了会在编译中段报奇怪的 header 缺失错误。python3-dev则是给 dtoc 和 binman 用的,主线 U-Boot 的 binman 工具链依赖 Python 的 libfdt 绑定,编译报找不到 Python 头文件的,基本都是少了这个包。
2.2 选择哪条 U-Boot 源码树枝
我的建议是:如果是产品量产、追求稳定,优先用官方 SDK 的 U-Boot 分支,因为它的 loader、DDR 参数、parameter 分区表和烧写工具是完整配套的;如果是要长期维护、跟进新特性、做安全审计,或者想完全掌控启动代码,就用主线 U-Boot。
主线源码获取:
git clone https://source.denx.de/u-boot/u-boot.git cd u-boot git checkout v2020.10 # 或者你习惯的稳定版本RK3399 对应的配置是rockchip/rk3399_defconfig,更早的版本里叫rk3399_defconfig,make menuconfig 里也可以搜到。编译之前先确认 defconfig 存在,这一步能避免后面白编。
2.3 ATF 与 BL31 的位置
主线 U-Boot 需要和 ARM Trusted Firmware 配合,RK3399 的 PSCI 功能(CPU 启停、系统休眠)都在 BL31 里。U-Boot 只是在 EL2 层跑,内核启动时要通过 SMC 调用进入 EL3 让 BL31 接管。
ATF 编译路径:
git clone https://github.com/ARM-software/arm-trusted-firmware.git cd arm-trusted-firmware make CROSS_COMPILE=aarch64-linux-gnu- PLAT=rk3399 bl31编译完成后输出bl31/bl31.elf,这个 elf 后面要喂给 U-Boot 打包。如果忘了编 BL31,U-Boot 也能编出来,但生成不了u-boot.itb,烧到板子上后启动会在 U-Boot 徽标后卡死,内核根本醒不过来。这是移植新手最容易撞的墙之一。
3. 主线 U-Boot 编译实操:从 defconfig 到 idbloader.img 和 u-boot.itb
环境准备就绪后,编译过程本身其实很机械,但每个产物是什么、干嘛用的,一定要门清。
3.1 完整编译命令序列
export ARCH=arm64 export CROSS_COMPILE=aarch64-linux-gnu- make rockchip/rk3399_defconfig make -j8编译完成后,会在源码目录下看到一大堆产物。这里挑关键的说:
tpl/u-boot-tpl.bin:TPL 镜像,BootROM 直接加载的初始代码,体积很小,里面包含 DDR 初始化逻辑;spl/u-boot-spl.bin:SPL 镜像,相当于官方体系里 miniloader 的主体,负责加载 u-boot.itb;idbloader.img:TPL 和 SPL 的拼接体,这就是最终烧到 eMMC 偏移 32KB 处的 miniloader 镜像;u-boot.bin:纯 U-Boot 本体,没有 ATF;u-boot.itb:打包了 U-Boot proper、BL31、以及可选 BL32 的 FIT 镜像,这个才烧到 8MB 偏移处。
如果你编完发现没有 idbloader.img,多半是 TPL 没编进去。检查 defconfig 里有没有CONFIG_TPL=y,以及CONFIG_ROCKCHIP_RK3399=y。旧代码有些默认配置不开放 TPL,需要在 menuconfig 里手动打开。
3.2 把 BL31 加进来
只编译到上面那一步是不够的。想要生成u-boot.itb,必须把 BL31 的路径传给 U-Boot:
make CROSS_COMPILE=aarch64-linux-gnu- BL31=../arm-trusted-firmware/bl31/bl31.elf -j8此时再检查产物,u-boot.itb应该出现了。可以用下面命令看它的内容:
fit_check_sign -f u-boot.itb # 或者直接看日志:binman 会打印 FIT 结构一个常见的误操作是:先单独编了一次 U-Boot,然后把 BL31 路径加进去又 make,结果因为增量编译缓存导致 itb 没重新生成。遇到这种情况直接make clean再来一遍,或者删掉u-boot.itb和u-boot.dtb再编。
3.3 官方 SDK 路径的编译产物
如果你走官方 SDK 路线,编译命令不太一样:
make ARCH=arm64 rk3399_defconfig make ARCH=arm64 -j8SDK 的编译脚本会额外调用打包工具,最终在out/目录下生成MiniLoaderAll.bin、uboot.img、trust.img、boot.img、rootfs.img等。这套里面MiniLoaderAll.bin就是我们要的 miniloader,uboot.img是 U-Boot 本体,trust.img则是 ATF BL31。
注意官方 SDK 的 U-Boot 是 32 位和 64 位混编逻辑,如果你发现编译报 ASM 语法错误,先看编译器是不是被设成了 aarch64-linux-gnu-,有些 SDK 老分支需要先用 32 位编译器编一个 build 工具再切 64 位。这个坑是 SDK 独有的,主线 U-Boot 没有这个问题。
4. 移植适配环节:DDR 参数、IO 域配置与串口确认
标题里写着“移植”,所以不能只讲编译,还要说清楚拿到一块新板子时,U-Boot 层面到底要改哪些东西。
4.1 DDR 颗粒与 TPL 参数
同一颗 RK3399,不同厂商的二三线板卡可能用完全不同的 DDR 颗粒,有 DDR3、LPDDR3、LPDDR4 之分,运行频率和电压也不一样。主线 U-Boot 的 TPL 里带的 DDR 参数表是通用型,它能初始化大多数颗粒,但兼容性远不如官方调校过的闭源 DDR bin。
我在实际操作中的做法是:先用主线 TPL 验证最小系统,如果能跑到 DDR 初始化完成、U-Boot 正常起来,那就继续用主线方案;如果 DDR 初始化卡死,日志停在 DDR training 的阶段,就果断切官方 SDK 方案,使用 rkbin 里对应频率的 ddr 初始化 bin,编译时通过配置把 TPL 的 DDR 驱动替换成官方闭源 bin,这样能少掉一大半头发。
主线 U-Boot 里和 DDR 相关的配置主要在:
CONFIG_RAM_ROCKCHIP和CONFIG_RAM_RK3399CONFIG_ROCKCHIP_DDR4、CONFIG_ROCKCHIP_DDR3、CONFIG_ROCKCHIP_LPDDR4这些决定支持哪种颗粒- 设备树里的
rockchip,ddr-frequency决定 DDR 工作频率
如果颗粒类别没选对,DDR 初始化基本必挂。这点在 menuconfig 里核对一下再编。
4.2 IO 域(IO Domain)电压匹配
RK3399 的 IO 电压是个隐蔽的坑。它的 eMMC、SDIO、GPIO 等模块分属不同 IO 域,对应 VCCIO 电压有 1.8V 和 3.3V 两档。如果你板子的 VCCIO3V3 实际供电是 1.8V,而设备树里 IO 域配置成 3.3V,eMMC 就会出现“能识别但读写不稳定、或者根本无法识别”的症状。
这个问题的排查在 U-Boot 阶段就能看到:启动日志可能出现Card did not respond to voltage select或mmc_init failed。遇到这种情况,优先检查设备树里 io-domain 节点的配置,对照板子原理图确保 VCCIO 对应正确。
4.3 串口调试口别选错
RK3399 的调试串口默认是 UART2,也就是/dev/ttyS2,在设备树里对应serial@ff1a0000。如果你的板子设计时把调试口引到了别的 UART,哪怕只差一个引脚,串口都看不见任何输出。移植时记得全文搜索自己板子的 dts 里chosen节点的stdout-path,确保它指向实际引出的串口。
我试过一个项目,板子把调试串口改到了 UART4,U-Boot 和内核的数字都正常,但所有日志都从 UART2 出去,接上去一片死寂。最后查原理图才发现问题,改一行 dts 重新编译就好。
5. eMMC 烧录与分区:偏移地址、parameter 表和两种实操方案
镜像编出来之后,烧录才是最不能出错的一步。eMMC 启动对偏移极其敏感,偏一个扇区都 boot 不起来。
5.1 主線 U-Boot 镜像在 eMMC 上的标准偏移
先给出一张我实际验证过的映射表(按 512 字节扇区计算):
| 镜像文件 | 起始扇区偏移 | 字节偏移 | 说明 |
|---|---|---|---|
| idbloader.img | 64(0x40) | 32KB | BootROM 固定读取位置 |
| u-boot.itb | 16384(0x4000) | 8MB | U-Boot proper + BL31 |
注意这里说的是 eMMC 用户数据区的偏移。BootROM 读的 32KB 正是idbloader.img所在位置,而 U-Boot 本体放在 8MB 处,是 Rockchip 原厂定义的分区布局规范,SPL 会从这个位置读取 u-boot.itb。
5.2 官方 SDK 路线的 eMMC 分区表
如果走官方 SDK,镜像更碎,分区布局如下:
| 镜像文件 | 起始扇区偏移 | 字节偏移 |
|---|---|---|
| MiniLoaderAll.bin | 64(0x40) | 32KB |
| uboot.img | 16384(0x4000) | 8MB |
| trust.img | 32768(0x8000) | 16MB |
| boot.img(kernel) | 取决于 parameter.txt | 16MB 之后 |
这里就引出parameter.txt的作用了。它本质是分区表描述文件,U-Boot 和内核的 mtdparts 命令行都靠它来识别分区。如果你只烧了 MiniLoaderAll 和 uboot.img,却忘了烧 parameter 分区,U-Boot 起来后会因为没有正确分区表而找不到 boot 分区里的 kernel。
一个典型的 parameter.txt 片段长这样:
CMDLINE: mtdparts=rk29xxnand:0x00002000@0x00004000(misc),-@0x0000c000(userdata)如果你手动改过迁移启动介质,一定要让镜像烧写位置和 parameter.txt 里的偏移保持一致,否则会出现“U-Boot 起来了,但加载内核时分区找不到”的情况。
5.3 方案一:Linux 下用 dd 直接烧写
最直白的方式是把 eMMC 挂到一台 Linux 机器上,或者让板子先从 SD 卡启动一个 Linux 系统,然后在板内把 eMMC 当块设备直接写。
# 先确认设备名 lsblk # 以 /dev/mmcblk0 为例,烧 idbloader sudo dd if=idbloader.img of=/dev/mmcblk0 bs=512 seek=64 conv=fsync # 烧 u-boot.itb sudo dd if=u-boot.itb of=/dev/mmcblk0 bs=512 seek=16384 conv=fsync # 如果是官方 SDK 方案,还需烧 trust.img sudo dd if=trust.img of=/dev/mmcblk0 bs=512 seek=32768 conv=fsync这里有个细节:conv=fsync是让 dd 在返回前把数据真正落盘,而不是停留在 page cache,避免你刚断电就掉数据。SD 卡烧录时同样适用,只是把设备名换成 SD 卡对应的/dev/sdX或/dev/mmcblk1。
5.4 方案二:upgrade_tool 与 Maskrom 模式
如果 eMMC 焊死在板子上,且板子上又没有 SD 卡槽,那就必须用 Rockchip 的烧录工具走 USB 下载通道。操作流程是:
- 按住板上的 recovery 键(或短接 Maskrom 触点),然后 USB 连接电脑并上电;
- 板卡进入 Maskrom 模式后,先用工具加载 miniloader 到 DDR:
这个sudo upgrade_tool db MiniLoaderAll.bindb动作就是把 miniloader 下载到片内 SRAM 并运行,让工具后续能通过它访问 eMMC; - 然后整体升级或分区升级:
sudo upgrade_tool uf update.img # 或者分开烧: sudo upgrade_tool di -b MiniLoaderAll.bin sudo upgrade_tool di -u uboot.img sudo upgrade_tool di -t trust.img
注意db这一步其实就是“for with miniloader”在烧录场景下的具体体现:烧录工具自己也需要先加载 miniloader,才能操作后续的分区写入。很多人在这一步卡住是因为板子根本没进 Maskrom 模式,串口随便动,工具一直报“找不到设备”。确认方法很简单:插入 USB 后执行lsusb,会出现一个厂商 ID 为 2207 的 USB 设备,那就是 Maskrom 状态。
6. 串口启动日志分析与高频问题清单
最后来看“上电没起来”怎么排查。启动类问题绕不开串口日志,而 RK3399 的调试串口有一个所有人都容易忽略的细节。
6.1 波特率不是 115200,是 1500000
RK3399 默认的调试串口波特率是 1500000,也就是 1.5Mbps,不是常见的 115200。如果你的 USB 转串口模块不支持 1.5M 波特率,或者终端软件没有正确配置,你会看到一堆乱码,或者干脆什么都没有。
支持 1.5M 的常用模块有 CP2102、FT232 这类;部分 CH340 也能跑到,但稳定性看人品。我在实际中推荐 FT232 加杜邦线,接线时注意 TX/RX 交叉,GND 必须共地。用 minicom 或 screen 时波特率直接设1500000:
sudo minicom -D /dev/ttyUSB0 -b 15000006.2 正常启动日志的几个关键标记
一份健康的主线 U-Boot 启动日志,从 eMMC 启动时大概长这样:
U-Boot TPL 2020.10-... Trying to boot from MMC DDR Version 1.00 20181022 ... U-Boot SPL 2020.10-... Trying to boot from MMC NOTICE: BL31: v2.5(release): U-Boot 2020.10-... ... switch to partitions #0 MMC: mmc@fe320000: 0 Loading fitimage... Starting kernel ...这里有几个阶段性标志:TPL 打印出现,说明 BootROM 成功从 eMMC 偏移 32KB 处读到了 idbloader;SPL 打印出现,说明 TPL 的 DDR 初始化成功并且跳转正常;BL31 的 NOTICE 打印出现,说明 u-boot.itb 里的 ATF 被正确加载;最后Starting kernel出现,说明 U-Boot 已经完成引导任务。
6.3 异常日志与对应问题
我整理了一张高频问题对照表,基本覆盖了移植初期 90% 的故障:
| 现象 | 可能原因 | 检查方向 |
|---|---|---|
| 串口完全无输出 | 启动介质配置不对、波特率错、TX/RX 接反、未进 eMMC 启动模式 | 先看 MiniLoader 有无烧到 32KB,换终端波特率 |
| 打印停止在 “DDR Version” 后 | DDR 初始化失败 | DDR 颗粒类型、频率参数、供电电压 |
| SPL 打印后有 U-Boot 徽标但卡死 | u-boot.itb 缺少 BL31 | 确认编译时是否传入 BL31 路径 |
| 反复重启,日志每次都完整跑一遍 | eMMC 读取 U-Boot 后校验失败或分区表不对 | 检查 8MB 偏移是否正确,parameter 分区是否烧录 |
| mmc 初始化时报 “Card did not respond” | eMMC 供电或 IO 域电压不匹配 | 核对 VCCIO 电压和设备树 io-domain 配置 |
| 能进 U-Boot 但找不到 boot.img | partition 分区表与镜像实际位置不一致 | 核对 parameter.txt 的 mtdparts 和烧录偏移 |
面对这些情况,我的习惯是先把启动介质简化:如果板子支持 SD 卡启动,先在 SD 卡上验证同一份 idbloader.img 和 u-boot.itb 能否正常启动,能启动就说明编译没问题,问题基本锁定在 eMMC 烧录偏移或硬件电压上。不要一上来就在 eMMC 上反复烧写,浪费时间和寿命。
6.4 一条有用的经验:让 U-Boot 打印更多调试信息
实在查不出来时,可以临时打开 U-Boot 的调试打印。在 defconfig 里加上:
CONFIG_DEBUG_UART=y CONFIG_DEBUG_UART_BASE=0xff1a0000 CONFIG_DEBUG_UART_CLOCK=24000000然后重新编译跑一遍,TPL 和 SPL 会输出更多底层信息,比如 DDR 训练过程中的寄存器状态,这些对定位硬件问题很有用。不过在量产固件里记得关掉,DEBUG_UART 会拖慢启动速度,而且会泄露一些地址信息。
最后说一个我自己的习惯:每次移植一块新板子,我都会先用官方 SDK 的 MiniLoaderAll.bin 配合 SDK 的 uboot.img 验证硬件,确认 DDR 和 eMMC 通路都没问题,再切换到主线 U-Boot。这个顺序能快速区分“芯片/板卡硬件问题”和“软件代码问题”,能省很多查日志的时间。