
1. 为什么我要把 U-Boot 移植单独拎出来做索引搞嵌入式这行的朋友尤其是做 ARM、RISC-V、MIPS 平台 BSP 的U-Boot 移植几乎是一道绕不过去的坎。你拿到一块新板子SoC 是新的DDR 是新的Flash 是新的串口可能都不太一样第一件事就是让 U-Boot 跑起来把串口打印出来把 DDR 初始化好把内核从存储里搬到内存里。这个过程听起来简单但真正做过的人都知道从零开始移植一版 U-Boot短则两三天长则两三周中间踩的坑能写满一个笔记本。我之所以想做一个 U-Boot 移植的索引式整理是因为这些年我在不同平台上反复做类似的事情每次都会遇到一些“上次好像也遇到过”的问题但具体细节又记不清了。比如 DDR 参数怎么配、SPL 和 TPL 什么时候用、设备树怎么改、环境变量存哪里、启动参数怎么传这些问题散落在各种文档、邮件列表、代码注释里没有一个统一的入口。所以我就想干脆按自己的经验把 U-Boot 移植这件事拆成几个核心模块每个模块讲清楚“为什么这么做”和“具体怎么做”形成一个可以随时查阅的索引。这篇文章适合谁看如果你刚接触 U-Boot想搞清楚移植的完整流程那这篇内容可以当作一个路线图。如果你已经做过几次移植但每次都要重新翻代码和文档那这篇内容可以当作一个速查手册。如果你在做 FreeRTOS、LVGL、CANopen 这类上层应用的移植底层 U-Boot 已经跑通了那你可以跳过前面几节直接看设备树和启动参数部分因为那些内容跟你的应用层配置是直接相关的。我个人的习惯是拿到一块新板子之后先不急着改代码而是把 U-Boot 的源码目录结构、配置系统、启动流程过一遍心里有个大概的框架然后再动手。这个习惯帮我省了很多时间因为很多问题不是出在代码本身而是出在对整个构建系统和启动链路理解不到位。下面我就按这个思路从整体设计开始一步步拆解 U-Boot 移植的核心环节。2. U-Boot 移植的整体设计与思路拆解2.1 先搞清楚 U-Boot 的目录结构和构建逻辑U-Boot 的源码目录看起来很多但真正跟移植相关的其实就那么几个。arch/下面放的是跟 CPU 架构相关的代码比如arch/arm/、arch/riscv/里面又按 SoC 厂商分目录比如arch/arm/mach-stm32mp/、arch/arm/mach-imx/。board/下面放的是具体板级的代码通常按厂商和板名分目录。configs/下面放的是 defconfig 文件每个板子一个编译的时候通过make xxx_defconfig来生成.config。drivers/下面是各种外设驱动dts/下面是设备树源文件include/configs/下面是板级配置头文件。我刚开始做移植的时候最困惑的就是“到底该改哪个文件”。后来我总结了一个简单的判断方法如果改动是跟 SoC 内部模块相关的比如时钟、引脚复用、DDR 控制器那就去arch/arm/mach-xxx/下面找如果改动是跟板子外部器件相关的比如 PHY、PMIC、Flash 型号那就去board/xxx/或者configs/下面找如果改动是跟启动参数、环境变量相关的那就去include/configs/下面找。这个分类不是绝对的但能帮你快速定位到大概范围。构建逻辑方面U-Boot 用的是 Kconfig 加 defconfig 的方式。你执行make xxx_defconfig的时候它会把configs/xxx_defconfig里的配置项读进来生成.config然后再根据.config去编译对应的代码。这个过程跟 Linux 内核的构建方式很像如果你之前编过内核上手会快很多。需要注意的是U-Boot 的 defconfig 文件通常只包含最核心的配置项很多选项是通过 Kconfig 的默认值来决定的。所以如果你发现某个功能没编进去不要只盯着 defconfig 看还要去查对应的 Kconfig 文件看看默认值是什么依赖关系是什么。2.2 移植前必须确认的硬件信息清单在动手改代码之前我通常会先整理一份硬件信息清单。这份清单不需要很复杂但必须覆盖以下几个关键点SoC 型号和封装、DDR 型号和容量、启动介质类型SPI Flash、eMMC、SD 卡、NAND、串口编号和波特率、时钟源频率、电源管理芯片型号、网络 PHY 型号。这些信息决定了你后面要改哪些配置、要配哪些参数。举个例子DDR 型号和容量直接决定了 DDR 初始化参数。不同厂商的 DDR 颗粒时序参数差别很大就算容量一样tRCD、tRP、tRAS 这些值也可能不同。如果你拿到的板子上用的是三星的颗粒但参考设计用的是美光的那你就不能直接照搬参考设计的 DDR 参数必须根据三星的 datasheet 重新算一遍。这个计算过程后面我会详细讲。启动介质类型决定了你编译出来的 U-Boot 要放到哪里、怎么放。SPI Flash 通常需要 SPL 加 U-Boot proper 两级启动eMMC 和 SD 卡可以只用一级NAND 又不一样。串口编号和波特率决定了你调试的时候怎么接、怎么配。时钟源频率决定了 PLL 怎么配、分频系数怎么算。电源管理芯片决定了上电时序怎么控制、电压怎么调。网络 PHY 决定了 MDIO 地址、PHY 地址、时钟模式怎么配。这份清单看起来琐碎但如果你不提前整理后面改代码的时候就会反复回来查效率很低。我的做法是拿到板子之后先花半天时间把原理图和 datasheet 过一遍把关键信息记在一个文本文件里后面改代码的时候直接对照着看。2.3 选择参考板的原则和常见误区U-Boot 源码里已经支持了很多板子找一个跟你的硬件最接近的参考板是移植效率最高的方式。但选参考板有几个原则不是随便找一个同 SoC 的就行。首先启动介质要一致。如果你的板子用 SPI Flash 启动那就优先找同样用 SPI Flash 启动的参考板因为启动流程和镜像布局差别很大。其次DDR 类型要一致。DDR3 和 DDR4 的初始化流程完全不同LPDDR4 和 DDR4 也不一样选错了参考板DDR 部分基本要重写。再次外设配置要接近。如果你的板子有千兆网口参考板只有百兆那网络部分的驱动和配置就要大改。常见的误区是很多人喜欢选一个“功能最全”的参考板觉得这样后面加功能方便。但实际上功能越全的参考板配置越复杂很多配置项你根本用不到反而会增加理解和调试的负担。我的建议是选一个“最小可用”的参考板只保留你需要的功能把不需要的配置项全部关掉这样编译出来的镜像小启动快调试也简单。等你把基本功能跑通了再按需添加其他功能。还有一个误区是直接拿 SoC 厂商提供的 U-Boot 源码来改而不是用主线 U-Boot。厂商的源码通常基于某个较老的主线版本加了很多私有补丁代码结构可能跟主线差别很大。如果你后面要升级或者跟社区同步会很痛苦。我的做法是优先用主线 U-Boot如果主线不支持某个关键功能再考虑从厂商源码里把相关补丁移植过来。这样既能保证代码的可持续性又能利用社区的更新。3. 核心细节解析与实操要点3.1 DDR 初始化参数的推导与配置方法DDR 初始化是 U-Boot 移植里最核心也最容易出问题的部分。如果 DDR 没初始化好U-Boot 根本跑不起来串口也不会有任何输出。DDR 初始化的核心是配置控制器的时序参数这些参数必须跟 DDR 颗粒的 datasheet 匹配。不同厂商的 DDR 控制器寄存器定义不一样但基本的参数类型是类似的主要包括 tRCD、tRP、tRAS、tRC、tWR、tRFC、tFAW 这些。以 tRCD 为例它表示从激活命令到读/写命令之间的最小延迟单位是时钟周期。DDR 颗粒的 datasheet 里通常会给出一个纳秒值比如 13.75ns。你需要根据 DDR 控制器的时钟频率把这个纳秒值转换成时钟周期数。假设 DDR 时钟频率是 533MHz一个周期就是 1.875ns那么 tRCD 就是 13.75 / 1.875约等于 7.33向上取整就是 8 个周期。这个计算过程看起来简单但实际配置的时候还要考虑控制器的内部延迟、PCB 走线延迟等因素所以通常会在计算值的基础上加一两个周期的余量。我个人的经验是DDR 参数不要一次配到最优先配一个比较保守的值让 U-Boot 能跑起来串口能打印然后再用mtest命令做内存测试逐步收紧参数。mtest是 U-Boot 自带的内存测试命令可以对指定地址范围进行读写测试如果参数太紧测试会报错这时候就放宽一点直到测试稳定通过。这个过程可能需要反复几次但比一开始就追求最优值要稳妥得多。还有一个容易忽略的点是 DDR 的电源和参考电压。有些 DDR 颗粒对 VREF 的要求比较严格如果板子上的 VREF 分压电阻精度不够或者电源纹波太大就算时序参数配对了DDR 也可能不稳定。这种情况下你可能需要调整 VREF 的电压或者换更高精度的电阻。这个问题在调试阶段不容易发现因为 U-Boot 可能能跑起来但跑一段时间就挂了。我的做法是在 DDR 初始化代码里加一些打印把关键寄存器的值打出来跟 datasheet 对照确认配置无误。3.2 串口调试通道的配置与常见问题串口是 U-Boot 移植过程中最重要的调试手段没有之一。如果串口不通你基本就是瞎子摸象。串口配置主要涉及三个部分引脚复用、时钟使能、波特率设置。引脚复用通常是在 SoC 的 pinmux 驱动里配你需要根据原理图找到串口对应的引脚然后在 pinmux 表里把这两个引脚配成 UART 功能。时钟使能是在时钟驱动里配你需要确保 UART 控制器的时钟源已经打开并且分频系数正确。波特率设置是在 UART 驱动里配通常是基于时钟源频率和期望波特率算出一个分频值。常见的问题是串口能打印但打印出来是乱码。这种情况通常是波特率不对或者时钟源频率不对。你可以先用示波器或者逻辑分析仪量一下 TX 引脚上的波形算一下实际波特率是多少然后跟期望值对比。如果实际波特率是期望值的两倍或者一半那多半是时钟源频率搞错了。如果实际波特率跟期望值差一点点那可能是分频系数的计算方式不对比如有些控制器用的是四舍五入有些用的是向下取整。还有一个问题是串口只能打印一部分然后就卡住了。这种情况通常是 FIFO 配置有问题或者中断没处理好。U-Boot 的串口驱动通常用的是轮询模式不依赖中断所以如果你看到打印到一半卡住可能是 FIFO 满了没有及时清或者发送函数里有个死循环。我的做法是在串口发送函数里加一个超时计数如果超过一定次数还没发送成功就直接返回错误避免死等。另外如果你用的是 USB 转串口模块要注意模块的兼容性。有些便宜的模块在高速波特率下会丢数据或者驱动有问题。我一般会用 FT232 或者 CP2102 这类比较成熟的芯片稳定性好很多。还有串口的 GND 一定要跟板子的 GND 接在一起不然可能会有共模干扰导致通信不稳定。3.3 设备树文件的修改与适配技巧设备树是 U-Boot 里描述硬件配置的主要方式尤其是对于 ARM 和 RISC-V 平台。U-Boot 的设备树跟 Linux 内核的设备树语法基本一样但有一些 U-Boot 特有的节点和属性比如u-boot,dm-pre-reloc、u-boot,dm-spl这些用来标记哪些节点在 SPL 阶段就需要被加载。移植的时候你通常是从参考板的设备树文件复制一份然后根据自己板子的硬件差异进行修改。修改设备树的时候有几个地方要特别注意。首先是内存节点memory节点的reg属性要跟实际的 DDR 容量和地址匹配。如果你的 DDR 是 512MB起始地址是 0x40000000那reg就是0x0 0x40000000 0x0 0x20000000。这个如果配错了U-Boot 可能能跑起来但内核启动的时候会报内存错误。其次是串口节点aliases节点里的serial0要指向正确的 UART 控制器不然stdout-path找不到串口就不会有打印。再次是时钟节点clocks属性要跟实际的时钟树匹配不然驱动拿到的时钟频率不对波特率、外设速率都会有问题。我个人的经验是改设备树的时候不要一次改太多改一点编译一次烧录一次确认没问题再改下一处。因为设备树的错误有时候不会导致编译失败但会导致运行时行为异常如果你一次改了很多地方出了问题很难定位是哪个改动引起的。另外U-Boot 的设备树编译出来是 dtb 文件你可以用fdtdump或者dtc工具把它反编译成 dts跟源码里的 dts 对比确认改动是否生效。还有一个技巧是利用 U-Boot 的fdt命令在运行时查看和修改设备树。比如fdt print /soc/serialxxx可以打印某个节点的属性fdt set /soc/serialxxx status okay可以修改节点状态。这个在调试阶段非常有用可以快速验证某个配置改动是否有效不用每次都重新编译烧录。3.4 环境变量存储方案的选择与配置环境变量是 U-Boot 里用来保存启动参数、IP 地址、bootcmd 等配置的机制。环境变量可以存在很多地方比如 SPI Flash 的某个偏移、eMMC 的某个分区、NAND 的某个块、甚至 FAT 分区里的一个文件。选择哪种存储方案取决于你的板子有什么存储介质以及你对可靠性和灵活性的要求。SPI Flash 是最常见的方案因为大多数嵌入式板子都有 SPI Flash而且 U-Boot 本身通常也放在 SPI Flash 里环境变量放在同一个 Flash 的另一个偏移就行。配置的时候你需要在 defconfig 里设置CONFIG_ENV_IS_IN_SPI_FLASHy然后设置CONFIG_ENV_OFFSET和CONFIG_ENV_SIZE。CONFIG_ENV_OFFSET要避开 U-Boot 镜像本身占用的区域通常放在 U-Boot 镜像后面或者放在 Flash 的最后一个扇区。CONFIG_ENV_SIZE通常是一个扇区的大小比如 4KB 或者 64KB取决于 Flash 的扇区大小。eMMC 方案适合有 eMMC 的板子环境变量可以存在 eMMC 的某个偏移或者某个分区里。配置的时候设置CONFIG_ENV_IS_IN_MMCy然后设置CONFIG_SYS_MMC_ENV_DEV和CONFIG_SYS_MMC_ENV_PART。这个方案的好处是 eMMC 容量大可以存很多环境变量而且读写速度快。但要注意eMMC 的分区表可能会被内核或者其他系统修改如果环境变量所在的分区被覆盖了环境变量就丢了。NAND 方案适合用 NAND Flash 的板子配置的时候设置CONFIG_ENV_IS_IN_NANDy然后设置CONFIG_ENV_OFFSET和CONFIG_ENV_SIZE。NAND 有坏块问题所以环境变量所在的块如果坏了需要有一个备份机制。U-Boot 支持冗余环境变量可以配置两个偏移一个主用一个备份主用坏了自动切到备份。我个人的建议是如果你的板子有 SPI Flash优先用 SPI Flash 方案因为最简单最可靠。如果只有 eMMC那就用 eMMC 方案但要注意分区保护。如果只有 NAND那就用 NAND 方案并且一定要配冗余。另外环境变量的默认值可以在include/configs/xxx.h或者 defconfig 里设置比如CONFIG_BOOTCOMMAND、CONFIG_BOOTARGS、CONFIG_IPADDR这些。这些默认值在第一次启动或者环境变量被擦除之后会生效所以要把常用的配置写进去减少后面手动设置的工作量。4. 实操过程与核心环节实现4.1 从零开始编译一版 U-Boot 的完整流程假设你拿到一块基于某款 ARM Cortex-A 系列 SoC 的板子SoC 厂商提供了参考板主线 U-Boot 也有对应的支持。你的目标是把 U-Boot 编译出来烧到板子上让串口能打印。下面是我通常的操作流程。第一步安装交叉编译工具链。ARM 平台通常用arm-linux-gnueabihf-或者aarch64-linux-gnu-具体用哪个取决于你的 SoC 是 32 位还是 64 位。工具链可以从 Linaro 或者芯片厂商的官网下载也可以用发行版自带的。安装完之后把工具链的bin目录加到PATH里然后执行arm-linux-gnueabihf-gcc -v确认版本。第二步获取 U-Boot 源码。我一般从主线仓库克隆然后切到最新的稳定分支。如果你用的是厂商的源码那就按厂商的说明来。克隆完之后进入源码目录执行make distclean清理一下确保没有残留的配置文件。第三步选择参考板的 defconfig。比如你的 SoC 是 i.MX6ULL参考板是mx6ull_14x14_evk那就执行make mx6ull_14x14_evk_defconfig。执行完之后会在源码目录下生成.config文件。你可以用make menuconfig打开配置界面检查一下关键配置项比如串口、DDR、启动介质、环境变量存储位置。第四步修改配置和代码。根据你的硬件差异修改 defconfig、设备树、板级代码。比如串口编号不一样就改设备树里的aliases和chosen节点DDR 容量不一样就改内存节点和 DDR 初始化参数启动介质不一样就改 SPL 的配置和镜像布局。第五步编译。执行make -j$(nproc)开始编译。如果编译报错根据错误信息定位问题。常见的错误包括缺少头文件、函数未定义、配置项冲突。编译成功之后会在源码目录下生成u-boot.bin、u-boot.img、SPL等文件具体生成哪些文件取决于你的配置。第六步烧录。烧录方式取决于你的启动介质。SPI Flash 通常用sf probe和sf write命令或者用外部烧录器。SD 卡可以直接用dd命令把镜像写到卡里。eMMC 可以用mmc write命令。烧录之前一定要确认烧录地址和偏移写错了可能会把板子变砖。第七步上电测试。接好串口打开串口终端波特率通常设成 1152008N1。上电之后如果一切正常你应该能看到 U-Boot 的启动打印包括版本号、DRAM 大小、启动设备等信息。如果没有任何打印先检查串口连接和波特率再检查 DDR 初始化是否成功最后检查启动介质是否烧录正确。4.2 SPL 和 U-Boot proper 的分工与配置SPL 是 Secondary Program Loader 的缩写它的主要作用是在 DDR 还没初始化的时候从启动介质里加载 U-Boot proper 到 SRAM 里运行然后由 U-Boot proper 初始化 DDR再把内核加载到 DDR 里。SPL 的代码量很小通常只有几十 KB因为它要能塞进 SoC 内部的 SRAM 里。U-Boot proper 是完整的 U-Boot功能齐全但体积大必须运行在 DDR 里。配置 SPL 的时候你需要在 defconfig 里设置CONFIG_SPLy然后设置CONFIG_SPL_TEXT_BASE为 SRAM 的起始地址CONFIG_SPL_MAX_SIZE为 SRAM 的大小。SPL 的入口代码通常在arch/arm/cpu/armv7/start.S或者类似的汇编文件里它会做一些最基本的初始化比如设置栈指针、关闭看门狗、初始化串口然后跳转到 C 代码。SPL 的 C 代码里会调用board_init_f和board_init_r这两个函数里会做 DDR 初始化、加载 U-Boot proper 等操作。我遇到过一个比较典型的问题是SPL 能打印但加载 U-Boot proper 之后没有任何输出。这种情况通常是 U-Boot proper 的加载地址不对或者 DDR 初始化有问题。你可以先在 SPL 里加一些打印确认 U-Boot proper 的镜像确实被加载到了正确的地址然后再检查 DDR 初始化代码确认 DDR 控制器配置正确。还有一个可能是 U-Boot proper 的入口地址跟 SPL 跳转的地址不一致这个要对照链接脚本和配置项仔细检查。另外SPL 和 U-Boot proper 的设备树是分开的。SPL 只需要包含最基本的节点比如串口、时钟、DDR 控制器其他节点都可以去掉以减小 SPL 的体积。你可以在设备树里用u-boot,dm-spl属性标记哪些节点在 SPL 阶段需要被加载。这个属性很重要如果某个节点没有标记SPL 阶段就找不到对应的驱动可能会导致初始化失败。4.3 启动参数的配置与内核加载U-Boot 的最终目的是加载内核并启动。启动参数主要通过bootargs环境变量传递给内核包括根文件系统位置、控制台设备、内存大小等。bootargs的格式是一串用空格分隔的键值对比如consolettyS0,115200 root/dev/mmcblk0p2 rootwait rw。这个字符串会被 U-Boot 写到内核的chosen节点里内核启动的时候会解析这个节点获取启动参数。配置bootargs的时候有几个关键点要注意。console参数要跟实际的串口设备匹配如果内核里的串口驱动跟 U-Boot 里的不一致控制台可能没有输出。root参数要跟实际的根文件系统位置匹配如果根文件系统在 eMMC 的第二个分区那就是/dev/mmcblk0p2如果在 SD 卡上可能是/dev/mmcblk1p2。rootwait表示等待根设备就绪再挂载对于 eMMC 和 SD 卡这种可能需要一定时间初始化的设备加上这个参数比较稳妥。bootcmd是 U-Boot 启动时自动执行的命令序列通常包括加载内核镜像、加载设备树、加载 initrd如果有的话然后执行bootm或者booti启动内核。比如bootcmdmmc dev 0; fatload mmc 0:1 0x80800000 zImage; fatload mmc 0:1 0x83000000 imx6ull-14x14-evk.dtb; bootz 0x80800000 - 0x83000000。这个命令序列的意思是切换到 eMMC 设备 0从第一个 FAT 分区加载内核镜像到 0x80800000加载设备树到 0x83000000然后用bootz启动。我个人的经验是bootcmd不要写得太复杂尽量保持简洁。如果启动流程比较复杂可以写成一个脚本用source命令执行。另外bootcmd里的地址要根据你的内存布局来定不要跟 U-Boot 自身占用的内存区域冲突。通常 U-Boot 会把自己加载到内存的高端把低端留给内核和设备树。具体的地址分配可以看 U-Boot 启动时打印的内存布局信息。4.4 网络和存储外设的驱动适配网络和存储是 U-Boot 里最常用的两个外设因为很多启动方式都依赖它们。网络方面U-Boot 支持tftp、nfs、dhcp等命令可以从网络加载内核和文件系统。存储方面U-Boot 支持mmc、usb、sata、nvme等命令可以从各种存储设备读写数据。网络驱动适配的关键是 PHY 的配置。你需要确认 PHY 的地址、接口模式RGMII、RMII、SGMII、时钟模式PHY 提供时钟还是 SoC 提供时钟。这些信息通常在原理图和 PHY 的 datasheet 里。配置的时候在设备树里找到以太网节点设置phy-mode、phy-handle、clocks等属性。如果 PHY 地址不对mdio命令会找不到 PHY网络就用不了。你可以用mdio list命令扫描 MDIO 总线上的 PHY 地址确认实际地址是多少。存储驱动适配的关键是控制器的初始化和分区表的识别。eMMC 和 SD 卡通常用mmc命令你需要确认控制器编号、总线宽度、时钟频率。如果mmc list能看到设备但mmc dev切换不过去可能是时钟或者电压配置有问题。分区表方面U-Boot 支持 MBR 和 GPT 两种分区表你可以用part list命令查看分区信息。如果分区表识别不了可能是分区表格式不对或者存储设备的前几个扇区被覆盖了。我遇到过一个比较坑的问题是eMMC 的复位引脚没有正确配置导致 eMMC 初始化失败。这个问题的现象是mmc list看不到设备或者看到了但读写报错。排查的时候先确认复位引脚的 GPIO 配置是否正确再确认复位时序是否符合 eMMC 规范。有些 eMMC 需要在上电后保持复位一段时间如果复位时间不够eMMC 可能无法正常初始化。5. 常见问题与排查技巧实录5.1 串口无输出问题的排查思路串口无输出是 U-Boot 移植里最常见的问题可能的原因很多排查的时候要按顺序来。第一步确认硬件连接。串口的 TX、RX、GND 是否接对TX 和 RX 有没有接反波特率是否匹配。我见过很多人把 TX 接到 TX 上结果当然没有输出。第二步确认串口终端配置。波特率、数据位、停止位、校验位是否跟 U-Boot 的配置一致。U-Boot 默认通常是 115200 8N1但有些板子可能用 57600 或者 921600。第三步确认 U-Boot 是否真的在运行。你可以用示波器量一下 TX 引脚看有没有波形。如果有波形但终端没显示那就是终端配置问题如果没有波形那就是 U-Boot 没跑起来。如果 U-Boot 没跑起来下一步就是查 DDR 初始化。DDR 初始化失败是导致 U-Boot 无法运行的最常见原因。你可以先在 SPL 里加一些打印确认 SPL 是否执行到了 DDR 初始化函数。如果 SPL 都没有打印那问题可能在更早的阶段比如时钟初始化、引脚复用、串口初始化。如果 SPL 有打印但 U-Boot proper 没有那问题可能在 DDR 初始化或者 U-Boot proper 的加载地址。还有一个容易被忽略的点是看门狗。有些 SoC 默认开启看门狗如果 U-Boot 在启动过程中没有及时喂狗看门狗会复位系统导致你看到的现象是串口打印了一部分然后重启。这种情况下你需要在 SPL 或者 U-Boot 的早期代码里关闭看门狗或者定期喂狗。关闭看门狗的方法通常是写某个寄存器具体寄存器地址和值要看 SoC 的参考手册。5.2 DDR 初始化失败的典型现象与解决方法DDR 初始化失败的表现形式有很多种。最直接的是串口完全没有输出因为 U-Boot 还没跑到串口初始化就挂了。稍微好一点的是 SPL 有输出但 U-Boot proper 没有输出因为 SPL 在 SRAM 里跑不依赖 DDR而 U-Boot proper 需要 DDR。还有一种情况是 U-Boot 能跑起来但跑一段时间就死机或者mtest报错这通常是 DDR 参数不够稳定。排查 DDR 问题的时候我通常会先确认 DDR 的电源和参考电压是否正常。用万用表量一下 DDR 的 VDD、VDDQ、VREF确认电压在 datasheet 规定的范围内。如果电压不对先解决电源问题再调时序参数。然后确认 DDR 控制器的时钟频率是否正确。有些 SoC 的 DDR 时钟是从 PLL 分频出来的如果 PLL 配置不对DDR 时钟频率就会偏差很大导致初始化失败。时序参数方面我一般会先用一个非常保守的配置比如把所有时序参数都设成 datasheet 里的最大值然后逐步收紧。收紧的过程中用mtest做压力测试比如mtest 0x40000000 0x42000000测试 32MB 的内存区域。如果测试通过就继续收紧如果测试报错就回退到上一个稳定的值。这个过程可能需要反复很多次但能帮你找到最稳定的参数组合。还有一个技巧是利用 U-Boot 的ddr命令或者 SoC 厂商提供的 DDR 测试工具在 U-Boot 命令行里动态调整 DDR 参数不用每次重新编译烧录。比如有些 SoC 支持ddr param命令可以实时修改时序参数并重新初始化 DDR然后立即用mtest验证。这个方式效率很高适合在调试阶段快速迭代。5.3 环境变量丢失或损坏的处理方法环境变量丢失是另一个常见问题尤其是在 SPI Flash 或者 NAND 上。现象是每次重启之后之前设置的bootargs、bootcmd、ipaddr都恢复成默认值了。这种情况通常是环境变量存储区域被覆盖了或者环境变量的 CRC 校验失败了。排查的时候先用env info命令查看环境变量的存储位置和大小确认跟配置一致。然后用sf probe和sf read命令把环境变量所在区域读出来看看数据是否正常。如果数据全是 0xFF说明环境变量区域被擦除了可能是烧录的时候不小心擦到了或者 U-Boot 镜像太大覆盖了环境变量区域。如果数据不是 0xFF 但 CRC 校验失败可能是环境变量在写入过程中被打断了比如写的时候断电了。解决方法是重新设置环境变量并保存。用env default -a恢复默认值然后用env set设置需要的变量最后用env save保存。如果保存之后重启还是丢失那就要检查环境变量存储区域的配置是否正确比如CONFIG_ENV_OFFSET是否跟 U-Boot 镜像的结束地址冲突CONFIG_ENV_SIZE是否跟 Flash 的扇区大小匹配。我个人的经验是在调试阶段可以先用CONFIG_ENV_IS_NOWHEREy把环境变量放在内存里不保存到存储介质。这样虽然重启之后会丢失但调试的时候不用担心环境变量区域被覆盖等基本功能都调通了再改成持久化存储。另外如果环境变量经常丢失可以考虑用冗余存储配置两个偏移一个主用一个备份U-Boot 会自动处理主用损坏的情况。5.4 启动卡在某个阶段的定位方法U-Boot 启动卡住是另一个让人头疼的问题。现象是串口打印到某一行就不动了或者打印了某个错误信息之后就不动了。定位这种问题关键是找到卡住的位置。U-Boot 的启动流程通常分为几个阶段SPL 阶段、U-Boot proper 阶段、内核加载阶段。每个阶段都有一些标志性的打印信息你可以根据最后一行打印来判断卡在哪个阶段。如果卡在 SPL 阶段通常是 DDR 初始化、时钟初始化、串口初始化的问题。你可以在 SPL 的代码里加一些打印比如在board_init_f的每个步骤前后加打印确认卡在哪个函数里。如果卡在 U-Boot proper 阶段通常是驱动初始化、设备树解析、环境变量加载的问题。你可以用debug命令打开调试输出或者用log命令查看日志。如果卡在内核加载阶段通常是镜像格式不对、加载地址不对、设备树不匹配的问题。你可以用iminfo命令检查镜像信息用fdt命令检查设备树。还有一个技巧是利用 U-Boot 的panic和hang机制。如果 U-Boot 检测到严重错误会调用panic函数打印错误信息并挂起。你可以根据panic打印的信息定位问题。如果 U-Boot 没有打印任何错误就挂起了那可能是死循环或者硬件异常。这种情况下你可以用 JTAG 调试器连接板子查看 CPU 的 PC 指针确认卡在哪个地址然后对照反汇编代码定位问题。我遇到过一个比较隐蔽的问题是U-Boot 卡在board_init_r里没有任何打印。后来用 JTAG 查了一下发现是某个驱动的probe函数里有个死循环因为设备树里的某个属性配错了驱动一直在等待一个永远不会发生的条件。这种问题只能靠调试器定位所以如果你的板子支持 JTAG建议在调试阶段把 JTAG 接上关键时刻能省很多时间。5.5 常见问题速查表问题现象可能原因排查方法解决方法串口无输出串口连接错误、波特率不匹配、DDR 初始化失败检查硬件连接、确认波特率、示波器量 TX 波形修正连接、调整波特率、检查 DDR 参数串口乱码时钟源频率不对、分频系数计算错误量实际波特率、对照时钟树修正时钟配置、重新计算分频系数SPL 有输出但 U-Boot proper 无输出DDR 初始化失败、加载地址错误检查 DDR 参数、确认加载地址调整 DDR 时序、修正加载地址环境变量丢失存储区域被覆盖、CRC 校验失败env info查看位置、sf read读数据重新配置偏移、启用冗余存储启动卡在某个阶段驱动初始化失败、设备树配置错误加打印、用 JTAG 查 PC 指针修正驱动配置、修改设备树网络不通PHY 地址错误、时钟模式不对mdio list扫描 PHY、检查设备树修正 PHY 地址、调整时钟模式eMMC 识别不到复位引脚配置错误、时钟频率不对检查 GPIO 配置、量时钟频率修正复位时序、调整时钟配置这个表格是我这些年遇到过的比较典型的问题实际调试的时候可能还会遇到更奇怪的现象。我的建议是遇到问题先不要慌按照“硬件连接 - 时钟 - 电源 - 配置 - 代码”的顺序一步步排查大部分问题都能定位到。另外养成记录的习惯每次解决的问题和解决方法都记下来下次遇到类似问题就能快速找到思路。6. 移植后的验证与优化建议6.1 基本功能验证清单U-Boot 移植完成之后不要急着上内核先把 U-Boot 自身的基本功能验证一遍。我通常会按以下清单逐项检查串口打印是否正常、bdinfo是否能正确显示板级信息、printenv是否能正确显示环境变量、mtest是否能通过内存测试、mmc list是否能识别存储设备、sf probe是否能识别 SPI Flash、mdio list是否能识别网络 PHY、ping是否能通、tftp是否能下载文件、bootm是否能加载内核镜像。这个清单看起来很长但每项检查都很快加起来也就十几分钟。如果某一项不通过就针对性地排查。比如mmc list不通过就检查 eMMC 的时钟、复位、电压配置ping不通过就检查 PHY 地址、网络时钟、MAC 地址配置。把这些基本功能都验证通过之后再上内核出问题的概率会小很多。我个人的习惯是在 U-Boot 里把常用的命令都试一遍确认没有异常。比如mmc read和mmc write做一次读写测试确认存储设备真的能读写sf read和sf write做一次 Flash 读写测试确认 Flash 没有坏块tftp下载一个文件到内存确认网络稳定。这些测试虽然简单但能帮你提前发现很多潜在问题。6.2 启动速度优化的几个实用技巧U-Boot 的启动速度直接影响产品的用户体验尤其是消费类电子产品。优化启动速度的方法有很多我常用的有以下几个。第一关闭不需要的功能。在 defconfig 里把用不到的命令、驱动、文件系统都关掉编译出来的 U-Boot 体积小加载快初始化也快。第二减少 SPL 的工作量。SPL 只做最必要的初始化DDR 初始化、串口初始化、加载 U-Boot proper其他事情都交给 U-Boot proper 做。第三使用压缩镜像。U-Boot proper 可以用 gzip 或者 lzma 压缩SPL 加载的时候解压虽然解压需要一点时间但加载时间缩短了总体可能更快。第四优化 DDR 初始化。DDR 初始化通常是最耗时的步骤如果 SoC 支持 DDR 自刷新或者快速启动模式可以开启跳过部分初始化步骤。还有一个技巧是把内核和设备树也放到 SPL 能直接访问的存储介质里让 SPL 直接加载内核跳过 U-Boot proper。这种方式叫 Falcon 模式或者 SPL 直接启动可以大幅缩短启动时间。但缺点是失去了 U-Boot 的命令行功能调试不方便。所以通常在产品量产阶段用这种方式开发阶段还是用完整的 U-Boot。我实测下来关闭不必要的功能和驱动启动时间能从两三秒缩短到一秒以内。如果再开启 Falcon 模式可以缩短到几百毫秒。但具体能优化到什么程度取决于 SoC 的性能和存储介质的速度。我的建议是先保证功能完整再逐步优化不要为了追求启动速度牺牲调试能力。6.3 代码整洁与后续维护的建议U-Boot 移植完成之后代码的整洁度和可维护性很重要尤其是如果你后面还要升级 U-Boot 版本或者移植到其他板子。我的建议是尽量把板级相关的改动集中到board/和configs/目录下不要散落在arch/和drivers/里。如果确实需要改arch/或者drivers/里的代码尽量用条件编译或者弱函数的方式避免影响其他板子。设备树文件也要保持整洁只保留跟你的板子相关的节点和属性不要从参考板复制一大堆用不到的节点。如果某个节点跟参考板一样可以用#include的方式引用参考板的设备树只覆盖差异部分。这样后面参考板更新了你的设备树也能跟着更新。另外建议把移植过程中的关键改动记录下来比如改了哪些文件、为什么改、改成了什么值。这个记录不需要很正式一个文本文件就行但后面升级或者排查问题的时候非常有用。我自己的习惯是每做一个改动就在 Git 里提交一次提交信息写清楚改了什么、为什么改这样后面用git log就能看到完整的改动历史。最后如果条件允许尽量把改动反馈给 U-Boot 社区。社区接受你的补丁之后后面升级 U-Boot 版本的时候你的改动就自动包含在里面了不用每次手动合并。而且社区的 review 过程也能帮你发现一些潜在问题提升代码质量。我个人的经验是第一次提交补丁可能会比较麻烦要符合社区的代码风格和提交规范但一旦提交成功后面的维护成本会低很多。