说实话,嵌入式开发这个行当里,能把 uboot 移植跑通的不少,但能把“移植”这件事真正讲清楚的教程不多。很多人一拿到新板子就急着把源码放进去编译,结果要么卡在启动介质引导头,要么 DDR 初始化过不去,要么串口根本没输出,折腾一个礼拜还不知道问题出在哪。这篇文章不打算按“第几步做什么”来流水账式讲解,而是把 uboot 移植前必须建立的硬件认知、配置系统逻辑、DTS 适配重点、调试三板斧和常见报错定位串起来讲一遍。适合刚入门的 Linux 驱动开发、BSP 工程师,以及手里正好握着一块新板子需要快速 bring-up 的朋友。读完之后你至少能回答一个问题:uboot 移植,到底是在移什么。
1. 拿到板子先看三样东西:BootROM、DDR、串口
1.1 上电那一刻,BootROM 比 U-Boot 先跑
很多人理解 uboot 移植,以为就是从 uboot 源码开始。实际上 SoC 上电后,芯片内部固化的 BootROM 会先执行,它根据熔丝位、启动引脚或 OTP 配置,决定从 SD、eMMC、NAND、NOR 还是 UART 去读取引导代码。BootROM 这一段代码是不可改的,它只负责“把第一段引导代码搬进 SRAM,然后跳过去执行”。
于是问题就来了:SRAM 容量非常有限,几十 KB 到几百 KB 不等,U-Boot 主镜像动辄几百 KB 甚至上 MB,根本塞不进去。所以现代 SoC 普遍采用两级引导设计——SPL(Secondary Program Loader)先被 BootROM 加载进 SRAM,SPL 负责完成最基本的初始化,特别是 DDR 初始化,然后把完整的 U-Boot 从存储介质搬到 DDR 里,再跳转执行。有些平台甚至在 SPL 之前还有一级 TPL,像瑞芯微的 RK3288/RK3399 就是 TPL + SPL 的典型。
移植者最常犯的错误,就是只盯着 U-Boot 主镜像改,忽略了 SPL 的编译产物、启动头格式和大小限制。比如很多 Cortex-A7/A9 平台的 SRAM 只有几十 KB,SPL 一旦编译出来超过这个容量,BootROM 只会读一半,现象就是板子“死了”,串口没有任何输出。这时候你调三天 uboot 配置都没用,先把 SPL 的体积压下来再说。我在实际项目里遇到过 SPL 超了 4KB,启动时随机失败,最后就是把一些无关驱动从 SPL 配置里摘掉才稳定。
1.2 DDR 参数:几乎所有移植难点都在这张表上
要说 uboot 移植里最磨人的部分,DDR 初始化排第二没人敢排第一。DDR 控制器的时序参数包括 tRCD、tRP、tRFC、tFAW、tRAS、tWR 等等,这些值由 DDR 颗粒的规格、工作频率和 PCB 走线长度共同决定。厂商的参考设计里通常会给出一个能跑通的 DDR3/DDR4 初始化序列,有的是 DCD 表(i.MX 系列),有的是专门的 ddrbin(Rockchip),有的干脆就是一套固定的寄存器操作代码。
移植时最稳的做法,是先照抄原厂参考设计的初始化参数,烧进去能起来以后,再通过 DDR 压力测试工具逐步收紧时序换性能。不要一上来就追求高频,那是给自己挖坑。举个例子,我之前在某 Cortex-A7 平台上调 DDR,频率从 400MT/s 改成 533MT/s 后,启动卡在 U-Boot log 的第二行,怎么查都查不出原因。后来用仿真器去看 DDR 控制器的 Vref 校准寄存器,才发现是校准参数没跟着频率一起调,训练出来的参考电压完全偏了。这类问题,靠肉眼 log 是看不出来的,只能靠工具一层层扒寄存器。
还有一点容易被忽略:DDR 电源的上电时序。SoC 的 DDR 控制器手册里通常会有一张上下电时序图,要求 VDD1、VDD2、VDDQ 按顺序上电,间隔多少毫秒都有规定。如果板子的电源管理芯片配置不对,DDR 颗粒的初始化训练就会间歇性失败。很多板子“昨天还能起来,今天就不行了”,大概率不是软件问题,而是电源时序在临界状态。
1.3 串口不通,一切免谈
U-Boot 移植调试的第一依赖不是网口,不是 JTAG,而是串口。BootROM 阶段通常不需要串口,但 SPL 一旦跑起来,第一行 log 就是从那一个 UART 出来的。串口不通,你面对的就是一块“哑巴板子”,所有调试手段都要打折。
拿到新板子做 uboot 移植,第一个目标就是让串口输出“U-Boot SPL ...”。这时候要确认三件事:UART 引脚有没有被其他外设占用,对应 pinmux 寄存器有没有在早期代码里配好,串口时钟源频率和波特率分频是否匹配。很多板子“不启动”的真相只是串口号绑错了,或者复用配置被人改了。我见过一个案子,硬件工程师把 UART3 调试口和 UART2 的流控引脚做了 swap,结果在设备树里查了半天,最后拿万用表量引脚电平才发现的。所以说,移植 U-Boot 之前,先把原理图上的调试串口引脚、主控 pinmux 表那一页翻熟,能不踩的坑先别踩。
2. 配置系统不是玄学:board 目录、defconfig 与 Kconfig 的三层关系
2.1 新建板级目录的正确姿势,而不是盲目复制改名
U-Boot 的代码组织不是按“一块板子一个分支”来管理的,而是按 vendor / board 两层结构。比如你要给一块新的 Cortex-A7 平台做支持,会在 board 目录下新建一个厂商名目录,再在其中建一个板子名目录。里面通常有几个固定文件:
- Kconfig:定义
TARGET_BOARDNAME这个 Kconfig 符号,作为整个编译系统的入口 - MAINTAINERS:写维护者信息,提交上游时必须有
- Makefile:组织这个板子目录下的目标文件,比如 board.o
- board.c:实现
board_init、board_late_init、board_mmc_init等板级回调
一股脑复制别的板子目录然后全局改名的做法,我不是很推荐。因为 Kconfig 符号依赖、头文件 include 路径、defconfig 里CONFIG_SYS_CONFIG_NAME的指向都是联动的,改漏一处编译直接报错,或者编译过了但链接出来的镜像根本不是给这个板子用的。更隐蔽的问题是,复制过来的 board.c 里可能有一堆原板卡特有的初始化代码,比如某路 GPIO 的电平设置、某个 PMIC 的 I2C 配置,这些在你的板子上根本不存在,遗留下来就是隐患。
正确做法是先建一个最小目录,只放 Kconfig、MAINTAINERS、Makefile 和空的 board.c,等编译通路完全打通之后,再对照原厂 BSP 逐步往 board.c 里补初始化逻辑。
2.2 defconfig 里最该盯住的几行
U-Boot 的 defconfig 文件在 configs/ 目录下,命名一般是厂商_板子_defconfig。文件内容看起来是一堆CONFIG_XXX=y,但真正决定平台走向的,是这么几行:
CONFIG_SYS_SOC="armv7" CONFIG_TARGET_XXXX=y CONFIG_DEFAULT_DEVICE_TREE="厂商-板子"CONFIG_SYS_SOC决定了编译哪个 arch 目录下的 CPU 相关代码,CONFIG_TARGET_XXXX对应 board 目录里的 Kconfig 符号,CONFIG_DEFAULT_DEVICE_TREE则绑定了 arch/arm/dts 下的同名 dts 文件。对于 U-Boot 2018 之后的版本,这个机制尤其重要,因为设备树已经是 U-Boot 自身驱动模型的一部分,不再只是“给内核用的附件”。
我在帮朋友把 itop4412 这类较老平台从 U-Boot 2015 升级到 2017+ 版本时,最大的工作量不是改 board.c,而是重新拼接 defconfig 和处理 DTS。老版本里很多平台配置靠configs/xxx.h里的宏定义,新版里逐步迁移到了 Kconfig 和 defconfig。你如果拿着一份老代码直接 make,编译能过,但生成的行为会和原来完全不一样。
2.3 menuconfig 改配置的边界,以及 savedefconfig 的使用习惯
很多新手喜欢在 make menuconfig 里勾选各种配置,其实 menuconfig 只是把 Kconfig 的选择逻辑图形化,真正决定“哪些源码文件被编译进去”的是 defconfig 与各级 Kconfig 合并后生成的.config文件。menuconfig 里改完配置,直接 diff.config,你会发现增量远比你预期的大,因为make defconfig会根据默认值填充所有未显式指定的选项。
我自己的习惯是:先在 menuconfig 里做实验性改动,验证通过后,执行 make savedefconfig,让它生成一份最小化的 defconfig,再替换 configs/ 目录下的原文件。这样提交到版本管理里的配置是干净可读的,同事接手时也知道你到底动了哪些开关。永远不要把编译机里的.config直接提交,那个文件里有大量从默认值继承来的冗余项,review 的时候根本看不出来差别。
还有一点需要特别提醒:SPL 和 U-Boot 主镜像共用一套源码,但配置可能是分离的。很多平台通过CONFIG_SPL_xxx前缀的配置项来单独控制 SPL 里包含哪些代码。调试 DDR 阶段,SPL 里一定要开足调试信息的输出,比如CONFIG_SPL_BANNER、CONFIG_SPL_PRINTF,否则真的会瞎猜。
3. 设备树、时钟与串口:移植期最容易翻车的三个节点
3.1 U-Boot 里的 DTS 到底管什么
从 U-Boot 2018 开始,DTS 在 U-Boot 里的角色越来越重。U-Boot 的驱动模型(DM)通过解析板级 DTB 来匹配设备节点、绑定驱动、填充资源。简单说,U-Boot 自己是带着一张“硬件地图”在跑,不再是早年那种全靠board.c里写死寄存器的做法。
所以在移植 uboot 时,arch/arm/dts/目录下的 dts 和 dtsi 文件,几乎是必改的。常见需要关注的节点包括:
- 串口节点
uart@...:要确认 reg 地址范围、clocks 属性、pinctrl 绑定 - 内存节点
memory@...:告诉 U-Boot DDR 的物理地址和大小,很多板卡的 DDR 信息甚至要在 dts 里显式指定 - GPIO 控制器节点:U-Boot 里的 GPIO 操作依赖它
- chosen 节点:常用来覆盖 bootargs,设置 stdout-path
一个常见的误区是:只把 DTS 当作“给内核的文件”,随便放了个厂商默认 dts 就不管了。结果 U-Boot 自己在初始化串口时找不到匹配节点,或者 GPIO 控制错位,编译没问题但行为完全不对。从 U-Boot 2018 之后的移植项目,我会建议优先把 dts 作为第一层排查重点,而不是一头扎进 board.c。
3.2 时钟树:先全盘照抄,再逐项验证
时钟树配置错误的表现非常隐蔽。串口波特率不对、DDR 频率异常、网卡 PHY 时钟起不来、SD 卡识别超时,背后往往是同一个根因:某个 PLL 的分频参数写错了。
我的建议是,在移植起步阶段,时钟节点里先给一个“最保守”的配置:所有外设分频设到最低,所有 PLL 用芯片参考手册上最通用的推荐值,确认整板能起来之后再逐步往上调。因为时钟树一旦改错,U-Boot 可能在第一条 log 都打不出来之前就死掉了,你根本不知道是时钟的锅还是串口的锅。
还有一个小细节:串口波特率是否准确,取决于外设时钟频率和 UART 分频器。用 115200 波特率做调试,如果时钟源差个百分之几,短 log 看不太出来,但一旦有大量数据交互,就会出现乱码或丢字节。遇到“偶尔有输出、偶尔没输出”的情况,先用逻辑分析仪或者频率计测一下 TX 引脚的实际波特率,有时候比翻代码快得多。
3.3 chosen 节点与 bootargs 的关系
U-Boot 的 dts 里,chosen 节点是一个特殊的“运行时”节点,它常用来覆盖内核启动参数。移植中常见的“U-Boot 起来了,内核也有 log,但 console 完全不工作”的问题,很多都和stdout-path、bootargs没有配对有关。
比如内核早期输出依赖chosen/stdout-path指向的串口别名,如果 dts 里aliases节点没有定义serial0,或者chosen里写的是stdout-path = "serial0:115200n8"但 serial0 实际对应到别的串口,那内核的早期 log 就会打到别处去。调试这种问题,不要在 U-Boot 阶段改一堆代码,先把printenv bootargs和 dts 里的 stdout-path 对齐,通常能省半天时间。
4. 调试三板斧:串口、网络、仿真器,轮着上
4.1 如何用串口中断和快捷键稳定停在 U-Boot
U-Boot 默认的bootdelay时间内,如果你在串口终端按任意键,启动流程会被打断,进入命令行。这个机制听着简单,但在某些平台上,特别是那类“TTL 串口调试口”的板子,实际操作是有讲究的。比如海思 hi3798m100 那类机顶盒方案,板子上电后 BootROM 留给串口中断的窗口非常短,需要在掉电重启瞬间掐准时间连按快捷键,才能稳定停在 U-Boot。很多人用 TTL 转 USB 小板连上去,按回车没反应,不是线的问题,是时机没掐准。
我的做法是先把串口终端软件的流控全部关闭,波特率设成和 U-Boot 一致,然后在脚本里做一个自动循环发送 0x0d 0x0a(回车换行),再反复给板子上电。板子只要能跑,十次里八次能停在命令行。停在 U-Boot 之后,做的第一件事永远是printenv,把环境变量完整存一份到本地文件。这一步是后续一切调试的“基准点”。
4.2 TFTP 下载与 bootm / booti 的正确配合
开发调试阶段,把内核和 dtb 通过 TFTP 下载到 DDR 再启动,是效率最高的方式。具体链路是:板子通过网线连到开发机,U-Boot 里配置好ipaddr、serverip,然后执行:
setenv ipaddr 192.168.1.100 setenv serverip 192.168.1.10 ping 192.168.1.10 tftpboot 0x42000000 zImage tftpboot 0x43000000 board.dtb bootz 0x42000000 - 0x43000000这里有几个细节容易踩坑。第一,TFTP 下载地址要避开 U-Boot 镜像本身所在的内存区间,否则下载过程会把自己覆盖掉。第二,32 位内核用bootz或bootm,64 位内核用booti,用错了会报Wrong Image Format。第三,bootm后面三个参数分别是内核地址、ramdisk 地址、fdt 地址,中间那个参数不需要时可以填-,很多人不知道这个占位符的用法,导致 fdt 传不进去。
还有一个经验:tftpboot 下载时如果反复超时,先别急着查网络驱动,ping一下开发机,再mii看一下 PHY 的 link 状态。很多时候是网线或交换机问题,不是 uboot 的问题。
4.3 用 md / mm 直接读寄存器判断 DDR 是否起来
DDR 初始化是否成功,最直接的验证方式不是看 log(因为 log 本身就依赖 DDR),而是对内存地址做写读回。在 U-Boot 命令行下执行:
mw 0x40000000 0xa5a5a5a5 16 md 0x40000000 16如果读出来的值不是 0xa5a5a5a5,说明 DDR 控制器的训练或时序参数有问题。如果直接报错或死机,那基本可以断定 DDR 初始化根本没有完成。这个方法在 SPL 阶段也可以用,只是需要提前在代码里留一个调试入口。我调试 DDR 参数时,几乎每改一组参数就跑一次这个测试,确认稳定了才会做下一步。
4.4 仿真器/JTAG 作为最后的底牌
串口和网络都不太管用的时候,比如 DDR 压根没初始化、SPL 卡死在开头,就需要上仿真器了。JTAG/SWD 调试器可以直接接管 CPU,查看寄存器、内存、PC 指针。通过仿真器看 PC 停在哪个地址,基本能判断是卡在时钟配置、DDR 训练还是 pinmux 设置。这个方法适合有一定硬件调试基础的工程师,初期学会看几个关键寄存器就够了,不需要精通。
5. 从 “Bad CRC” 到 “No working FDT” 的常见报错复盘
5.1 Bad CRC of Environment:环境变量区被破坏
U-Boot 启动时打印Bad CRC of Environment是特别常见的问题,含义是环境变量存储区里保存的数据校验失败。出现这个报错,板子不会死,U-Boot 会使用编译时内置的默认环境变量继续跑。但它通常说明一件事:CONFIG_ENV_OFFSET和CONFIG_ENV_SIZE这两个配置跟你板子上实际的 Flash 布局对不上。
比如你的 SPI NOR Flash 里,U-Boot 镜像占了 0 ~ 0x100000,环境变量区原本应放在 0x100000 之后,但配置里写的是 0x200000,恰好那里存的是 kernel 镜像的开头。U-Boot 启动时把 0x200000 处的数据读出来当环境变量解析,发现校验不对,于是报 Bad CRC。这种问题在移植阶段几乎人人都会遇到一次,解决办法不是简单执行 saveenv,而是先把环境变量区的偏移和大小正确设置,再 saveenv 写入合法的数据。
5.2 No working FDT:设备树没跟上内核
启动内核时报No working FDT,意思是 U-Boot 需要把设备树地址传给内核,但这个地址上解析不到合法的设备树结构。常见原因有三个:一是 tftpboot 加载 dtb 时下载失败,二是指定了fdt_addr但那个地址上根本没有数据,三是 bootm/booti 命令的第三个参数没写对。
排查套路是先 tftpboot 加载 dtb 到固定地址,然后用fdt addr检查该地址上的设备树是否可解析:
tftpboot 0x43000000 board.dtb fdt addr 0x43000000如果这里不报错,再执行 bootm/booti 并显式指定 fdt 地址。建议把fdt_file、fdt_addr环境变量在移植阶段就配好,别依赖自动探测,减少变量。还有就是确认 dtb 是用同一份 dts 编出来的,版本不匹配也容易出奇怪问题。
5.3 小容量 Flash 平台的体积焦虑:wr703n 刷 uboot 带来的启示
WR703N 这类小路由器,Flash 容量就 4MB 左右,刷第三方 uboot 是社区里玩得很热的项目。它带出的普适问题是:整个 U-Boot 镜像必须控制在 64KB 以内,才能在一个小的 SPI NOR 分区里放下。为了容量,很多人会裁剪掉一批用不到的命令、板级支持、文件系统支持。这个思路对任何 NOR Flash 启动的板子都有参考价值:
- 移除不需要的网络协议栈驱动
- 裁剪掉不用的命令集,比如把 bootm、bootz 之外的命令全部禁用
- 压缩 SPL,关掉 SPL 里不必要的驱动
- 手动指定
CONFIG_SYS_MAXARGS、CONFIG_SYS_CBSIZE等缓冲区参数,腾出少量可执行镜像空间
实际裁剪时要注意,U-Boot 的代码之间有不少隐藏依赖,减过头会导致链接失败或者功能残缺。我的建议是每次裁剪后都实际编译、烧录、验证一遍串口和网络,不要批量裁完一起测,否则出了错根本定位不了是哪一项配置引起的。
5.4 卡死在“无串口输出”阶段时的排查顺序
凡是遇到板子一点输出都没有的情况,按照下面的顺序排查,通常能省下大量时间:
- 先用万用表确认串口 TX 引脚有电平变化,排除线序和电平问题
- 检查板子的电源轨是否全部正常,特别是 DDR、SoC 内核电压
- 确认 BootROM 的启动介质选择引脚有没有被拉错
- 用逻辑分析仪看 BootROM 有没有在 CS 信号上尝试读取启动介质
- 最后才怀疑 U-Boot 代码本身,先烧一份已知能跑的原厂固件做对照
我见过太多人一碰到“板子没输出”就冲进源码里改代码,其实前四步里就能解决八成问题。硬件上的问题靠逻辑分析仪,软件上的问题靠串口和仿真器,这个顺序千万别颠倒。
移植 U-Boot 从来不是一锤子买卖。我的个人习惯是:先让串口有输出,再让 DDR 稳定,然后让网络通,最后才去碰 Flash 和 saveenv。每一步之间都留一个可回退点,把原厂固件完整备份、把 printenv 输出存档。做到这些,哪怕中途翻车,也能几分钟内回到之前的进度。U-Boot 跑通之后,下一个环节就是 kernel 和根文件系统,Zynq 这类平台上顺手还要把 busybox 的移植一起纳入计划,又是一片新的调试天地,不过那是后话了,先把这块板子的 uboot 稳稳跑起来再说。