
想让你手里那块板子跑起 LinuxU-Boot 这一关绕不过去。我前些年帮人调过不少板子十次里有七八次卡在同一个节点上电后屏幕没反应、串口不输出任何字符折腾半天才发现是引导程序根本没跑起来。今天聊一聊 U-Boot 移植最基础的东西不是教你几分钟生成一个“神级配置”而是把从拿到一块陌生板子到 U-Boot 稳定给出命令行这条流程走通。适合手里正攥着开发板、准备做系统移植的工程师也适合那些已经编译过 U-Boot 却搞不清 board 目录在设计什么的人。1. 先把“移植”拆开U-Boot 上电后的一整套接力赛1.1 三段式启动不是 U-Boot 第一个跑刚入行时我也以为U-Boot 是板子通电后第一个执行的程序。实际上不是。SoC 里固化了一段 BootROM它才是真正意义上的“第一棒”。这段代码不用你改你也改不动它烧死在硅片里。BootROM 做的事很单一根据启动引脚、拨码开关或者熔丝状态决定从哪个介质取代码——SD 卡、eMMC、NAND、SPI NOR 都可能是候选——然后读出前几十 KB 放到片内 SRAM跳进去执行。问题就出在 SRAM 通常太挤。老一点的 SoC 只有 8KB、32KB后来普遍做到 192KB、256KB但完整 U-Boot 动不动几百 KBSRAM 根本塞不下。主流做法是把 U-Boot 拆成两级第一级叫 SPLSecondary Program Loader尽量压缩到巴掌大只干两件事——初始化时钟、初始化 DDR 控制器然后从启动介质里把第二级也就是完整 U-Boot 搬运到 DDR 内存中再跳转过去执行。很多开发板的“启动卡”“烧写脚本”里会出现 u-boot-spl.bin、u-boot.img、u-boot.bin 这些文件就是这套结构的产物。用生活里的类比BootROM 是小区门口保安他只会告诉快递员电梯在哪SPL 是那个把货搬上电梯的人完整 U-Boot 才是真正开车送货、把货物送到具体楼层的司机。你要移植的主要是后两棒尤其是 SPL 里跟 DDR 初始化、时钟初始化相关的部分——这两块直接决定“能不能跑到下一步”。1.2 板级代码和 SoC 代码分工比你想的清楚打开 U-Boot 源码树层次其实是严格分好的。arch/ 下面是针对具体 CPU 体系结构和 SoC 的代码比如 arch/arm/mach-sunxi、arch/arm/mach-exynos。board/ 下面是具体开发板或者厂商的代码比如 board/samsung/smdk4412、board/sunxi/。这两类代码的职责不一样SoC 层管“这颗芯片的共性问题”比如中断控制器、定时器、串口硬件、DDR 控制器寄存器映射板级代码管“这块板子独有的问题”比如用了哪个串口当控制台、GPIO 哪个引脚接了 LED、环境变量保存在哪个存储偏移、电源管理芯片怎么上电。这个分工想明白后移植的边界就出来了。如果你换的只是板子、芯片没换那么 arch 层基本不动主要改 board 层和 defconfig、设备树如果芯片也换了那你其实是在做“给一个新 SoC 做板级支持”那就要动 arch 层工作量和难度完全不是一个量级。刚开始做移植我强烈建议先挑一颗已经被 mainline 支持得很好的 SoC比如全志 H3、H5、树莓派的 BCM283x 或者 STM32MP1 这类你只需要在已有框架里加一块板子风险可控得多。1.3 移植的边界适配不是重写见过不少新人一上来就想“推倒重写”觉得板级代码不好用就自己写一套。我的建议是除非你是要为某个完全不存在的 SoC 做 bringup否则不要重写。U-Boot 这套代码经过大量板子验证里面的启动流程、驱动模型、环境变量机制都已经很成熟。你做移植本质上是“在通用框架里描述你这块板子的特殊情况”告诉它 CPU 主频多少、DDR 怎么配置、控制台在哪个串口、启动介质在哪个控制器上、设备树里描述哪些外设。描述得越准确U-Boot 跑得越稳。重写意味着你还要重新处理一堆跟芯片强相关的细节等于把别人验证过的东西推翻重来不值得。2. 动手前的硬件功课哪些参数只能查、不能猜2.1 从原理图和手册里要抄下这四类参数动手改代码之前先把这些信息抄到笔记里。我见过最大的问题是不看原理图凭“别人说”“印象里”去填参数结果一个 DDR 时序不对光排查就耗掉好几天。第一类是时钟。主晶振频率是多少24MHz 还是 25MHz或者 32.768kHz 的 RTC 晶振SoC 的 PLL 配置可以支持到多少UART 模块的时钟源是哪一个。这些数值直接决定串口波特率是否准确。很多时候你会看到一个现象U-Boot 有输出但全是乱码那八成不是代码逻辑问题而是 UART 时钟频率给错了导致波特率算偏了。这部分要从原理图里找到晶振型号再到 SoC 数据手册里查 PLL 和 UART 的时钟树。第二类是 DDR。容量多大、位宽是 16bit 还是 32bit、有几个 rank、采用的是 DDR3 还是 DDR4/LPDDR3、颗粒型号里的时序参数CL-tRCD-tRP 等。这些最终要换算成 DDR 控制器寄存器里的具体数值。第三类是启动介质。板子上焊的是 eMMC、NAND、NOR 还是只留了 SD 卡座对应的控制器是哪个SD 卡启动的话BootROM 从卡上什么偏移读 SPL这些决定你在烧写时要写到什么位置。第四类是控制台。默认的控制台串口是哪个 UARTUART0 还是 UART2引脚复用到了哪个 GPIO 组电平是 3.3V 还是 1.8VTTL 串口的定义我在后面单独说因为这里踩坑的人特别多。2.2 找对参考板移植的第一捷径移植最有效的思路不是从零开始而是找一块“跟你板子最像”的参考板。所谓“最像”按优先级排序是这样同一个 SoC 或者同一个 SoC 系列这是最关键的其次是同样的启动介质比如都是 SD 卡启动这样烧写方式可以照抄再次是 DDR 尽量同型号或者同系列这样时序参数能很大程度上复用最后是板级外设差异比如 LED 引脚、串口编号不同这些都是小改。举个例子你要给一块全志 H3 的核心板做移植直接参考板就可以选 Orange Pi PC 或者 NanoPi NEO它们都是 H3 方案SD 卡启动DDR3 参数在厂商源码里已经调好。你要做的通常只是改设备树里 GPIO 定义、改环境变量保存位置然后把 defconfig 复制一份。反过来如果你拿着 H3 的板子去参考 H6 的配置那差距就太大了DDR 协议不同时钟树不同反而更痛苦。我经常举的另一个例子是 iTOP-4412 这类 Exynos 4412 板子很多人在网上找“移植 U-Boot 2017”其实正确的起点是三星官方那套 smdk4412 配置或者其他已经适配过的 4412 板级文件。先把参考板的配置编译通过、跑起来再往你的板子上平移差异这个顺序才是对的。2.3 TTL 串口给自己留一条能看见输出的门缝做 U-Boot 移植串口是最重要的“眼睛”。U-Boot 不像 Linux 有各种日志系统它早期启动阶段连中断都没配置好基本只有串口输出这一个调试手段。所以动手前先确认板子上有没有引出 UART 的测试点或排针。很多路由器、电视盒子、工控板的串口并不是 DB9而是 3.3V TTL 电平的排针需要用 USB 转 TTL 模块接。接线规则很简单四根线就够了开发板的 TX 接模块的 RX开发板的 RX 接模块的 TXGND 必须共地然后根据电平选择 3.3V 或者 1.8V千万不要贪方便去接模块上的 VCC 输出。我就见过不止一个人把 TTL 模块的 5V 接到开发板串口排针上结果直接把 SoC 的串口引脚烧了。接好之后用 minicom、picocom、PuTTY 或者 macOS 下的 screen 打开串口波特率先按 115200 试不对再试 57600、38400、1500000 等常见数值。开机瞬间如果能看到 BootROM、SPL 刷出来的字符说明调试链路已经打通。有些平台还要在开机时快速按回车或者某个特定按键才能停进 U-Boot 命令行比如部分海思方案这个“快捷键”其实就是在等一个打断标志具体键值要看代码实现但只要有串口输出一般都能找到提示。3. 源码树的“边界感”知道改哪里更要知道不该改哪里3.1 从 defconfig 到 dts 再到 board 目录的关系我开始带新人时第一件事就是让他先画一遍源码树里跟一块板子有关的文件关系。以主流 ARM 平台的 U-Boot 为例一块板子的“身份文件”有三类defconfig 文件、设备树源文件dts、board 目录下的板级 C 代码。这三类文件通过一系列配置项串起来。比如你在 configs/ 下新建一个 myboard_defconfig里面有这样几行CONFIG_SYS_CONFIG_NAMEmyboard CONFIG_DEFAULT_DEVICE_TREEmyboard CONFIG_SYS_EXTRA_OPTIONS...CONFIG_SYS_CONFIG_NAME 指向 include/configs/myboard.h这是老式配置体系里定义板级宏的地方CONFIG_DEFAULT_DEVICE_TREE 指向 arch/arm/dts/myboard.dts用来描述板上有哪些外设、引脚怎么复用。而 board/mycompany/myboard/ 下面是真正的板级初始化函数比如 board_init()、board_late_init()、dram_init() 这些。简单说defconfig 决定“编译哪些代码”dts 描述“硬件长什么样”board 目录决定“上电后要做哪些板级动作”。三者是一个闭环哪一环对不上编译出来就不是你想要的产物。下面这个表格可以帮你快速建立对应关系路径或命名作用最容易犯的错configs/board_defconfig决定编译配置项与默认设备树只复制不修改板名对不上arch/arm/dts/board.dts描述外设、引脚复用漏改串口/以太网节点board/vendor/board/板级初始化函数直接照抄厂商配置不看差异include/configs/board.h老式配置头文件在驱动模型时代还手动改宏3.2 添加一块新板的最小改动路径如果你已经有了一块参考板添加新板的最省事路径是这样在 configs/ 目录下复制参考板的 defconfig改成自己的板名在 arch/arm/dts/ 下复制参考板的 dts如果使用设备树改成自己的 dts并把设备树里跟实际硬件不符的内容改掉比如 LED 引脚、串口编号、以太网 PHY 地址在 board/ 下复制参考板整个目录改 Makefile、Kconfig、MAINTAINERS再把源文件里的板名、引脚定义、DDR 参数按自己的原理图改一遍。这样三步做完基本就能进入编译环节。很多新手会漏掉 Kconfig 的修改因为 defconfig 里选的 BOARD 名字在 Kconfig 里没有对应项menuconfig 会报错或者生成一个旧配置。所以新建板子时需要同时处理configs/myboard_defconfigboard/mycompany/myboard/Kconfigboard/mycompany/myboard/Makefilearch/arm/dts/myboard.dtsinclude/configs/myboard.h如果用老式配置这几处名字必须一致U-Boot 的构建系统才会把你的板子识别成完整目标。3.3 为什么不建议动 arch 层经常有人问“我的板子芯片很偏门arch 层要不要加代码”确实有些芯片在 mainline 里没有支持但 arch 层改动需要非常谨慎。arch 层会被同一系列大量板子共用你为了自己这块板子改一个公共文件很可能影响其他板子。而且 arch 层的东西很多涉及芯片底层寄存器定义、启动汇编代码改错之后连编译都不一定报错但运行起来就是死机。正确的做法是先把板子差异留在 board 层和 dts 层。U-Boot 提供了大量 weak 函数机制默认实现只是一个空壳你可以在板级文件里重新实现。比如 board_init()、dram_init() 这些都是弱符号你在 board 目录里写一个同名函数就会覆盖默认版本。如果确实需要新增 arch 层支持也尽量用 CONFIG_MACH_xxx 这类 Kconfig 开关隔离起来保证默认流程不被破坏。刚开始移植动手范围越保守排查问题越容易。4. 一次最小可行移植的完整日志编译、烧录、进命令行4.1 交叉工具链的选择与验证U-Boot 的编译依赖交叉编译器。ARM 32 位平台一般用 arm-linux-gnueabihf- 或 arm-none-eabi-ARM 64 位平台用 aarch64-linux-gnu-。这里有个容易踩的坑不要为了图新就上最新版本工具链。U-Boot 对老工具链的兼容性反而更好有些 SoC 厂商的 BSP 还指定了具体版本。我建议先看参考板文档里写的工具链版本保持接近再逐个试。验证工具链是否可用的命令很简单arm-linux-gnueabihf-gcc -v能正常打印版本号之后接着验证它能不能编译最基础的 hello再开始编 U-Boot。很多人直接跳过这步结果编到一半报“找不到 stdint.h”之类其实不是 U-Boot 的问题是工具链本身缺库。4.2 配置、编译和三种产物的区别拿到源码后第一件事是生成针对你板子的配置。假如参考板是 Orange Pi PCdefconfig 叫 orangepi_pc_defconfig那么make orangepi_pc_defconfig make menuconfig # 可选微调配置 make -j$(nproc)编译完成后你在当前目录会看到一堆产物。这里重点看三类u-boot.bin 是完整的 U-Boot 二进制u-boot.img 是带有 U-Boot 头部信息的镜像通常用于从文件系统或网络加载u-boot-spl.bin 是 SPL它通常和 u-boot.bin 打包成一个带 SPL 的镜像比如全志平台的 u-boot-sunxi-with-spl.bin。不同平台产物名字有差异但逻辑是一致的SPL 负责初始化 DDR 和加载第二阶段完整 U-Boot 负责初始化外设和环境变量最后引导内核。还有一点值得注意如果看到编译产物缺失多半是你在 menuconfig 里关掉了不该关的驱动比如串口驱动、MMC 驱动。做最小移植时我建议先保留参考板的默认配置只改你确定要改的选项不要为了瘦身把没用的驱动全砍了——那些“没用”的驱动很多时候正是排查需要的。4.3 把 U-Boot 放进启动介质烧写方式完全取决于启动介质。SD 卡启动最直观很多开发板的做法是把编译出来的整个镜像写到 SD 卡的某个偏移位置比如全志平台的典型命令sudo dd ifu-boot-sunxi-with-spl.bin of/dev/sdX bs1024 seek8 convfsync这里的 seek8 是关键它代表从 SD 卡第 8KB 处开始写这是由 SoC 的 BootROM 定义好的偏移。不同平台这个值完全不同比如一些平台可能是 seek1从 1KB 处所以不能照抄别人的命令必须去查对应平台的文档或者源码头文件。eMMC 和 NAND 的烧写就更复杂一些通常要先进入一个能用的 U-Boot通过 SD 卡或串口下载再通过 mmc write、nand write 这类命令把新镜像写进对应分区。我建议第一次做移植时优先选择 SD 卡启动它不需要额外的烧录器改起来也方便而且就算写坏了一张卡换一张就行不会把板子弄死。4.4 第一次上电没输出的排查顺序第一次上电最怕的不是看到错误信息而是串口屏幕上什么都没有。什么都看不到你连从哪一行查起都不知道。我自己的排查顺序是固定的。先确认电源。看板子上有没有电源指示灯如果没有用万用表量关键点电压。这一步能排除“板子根本没起来”和“U-Boot 死在极早期”两种情况。确认电源没问题后再用示波器或者万用表量启动介质的时钟线、片选线有没有动作。如果介质有访问动作说明 BootROM 至少跑了。然后检查串口接线确认没有接反 TX/RX、地线已经共地、软件里的串口号和波特率和板级配置一致。最后才是怀疑代码问题。一个实用技巧如果代码里有早期打印比如在 SPL 开头加一句 puts()或者使用 U-Boot 的 debug_uart 机制在 DDR 初始化之前就能输出字符。这样即使 DDR 配置完全错误导致后续死机你也至少知道代码跑到了哪一步。这个“早期打印”手段能帮你把排查范围大大缩小。5. 最容易翻车的两处板级参数DDR 与启动介质5.1 DDR 初始化别当配置填要当时序理解DDR 是移植中翻车率最高的地方没有之一。很多人以为 DDR 初始化就是把寄存器填几个数其实那串数值背后是一整套时序协议。DDR 颗粒的 tCL、tRCD、tRP、tRAS 这些时序参数都写在颗粒数据手册里控制器要把这些值换算成以时钟周期为单位的寄存器数值。频率不同、CL 不同、地址映射不同最终配置完全不一样。如果你把参考板的 DDR 配置原封不动搬到一个颗粒型号不同的板子上最典型的后果是上电后完全无输出或者初始化偶尔成功但运行一段时间随机死机。前者还容易想到是 DDR后者往往被误判成内核问题。所以我建议除非你能确认两颗 DDR 颗粒型号完全一致否则一定要去查颗粒手册把时序参数一项项核过。有些 SoC 厂商会提供 DDR 配置工具比如全志的 DDR 初始化代码是封装好的通常只要改 DRAM 型号、频率、位宽这几个宏观参数控制器细节不用碰这对新手是很大的友好度提升。反过来如果你用的是一片没有任何参考支持的 DDR 颗粒那就别指望一次成功。先对照参考代码里的注释和数据手册把频率降到比标称值低一些跑通基本流程再逐步拉到正常频率做内存压力测试——比如在 U-Boot 命令行跑 mtest或者进 Linux 后用 memtester 长时间压测。这一步省不掉我见过太多人内存配置“看起来能开机”就跳过验证最后在产品阶段出现神秘的内存错误。5.2 启动介质和“刷不坏”的保底策略启动介质这块的坑更多是操作层面的。常见问题是把 U-Boot 写到 SD 卡的起始扇区第 0 扇区结果把分区表覆盖了或者从 eMMC 的 user 分区写结果把 bootloader 写到了 GPT 分区表区域。归根到底是你没有弄清楚 SoC 的 BootROM 究竟从哪个偏移地址读取 SPL。这个问题必须回到平台文档、启动流程说明或者全志/瑞芯微等厂商的烧录脚本里去查而不是类比其他平台。这里我想强调“保底策略”。做 U-Boot 移植板子变砖是几乎必然要经历的事区别只是你能不能快速救回来。SD 卡启动的优势在这时就体现出来了你可以把一张正常的 SD 卡做成“救砖卡”插上去就能从卡启动进入 U-Boot 或 Linux然后再从 U-Boot 里把 eMMC/NAND 上写坏的 bootloader 修回来。如果板子只从 eMMC 启动那你至少要留一个 JTAG 接口或者串口下载模式作为后手。不少板子还支持在 BootROM 阶段按特定按键进入 USB 下载模式类似全志的 FEL 模式这种模式能绕过介质直接加载镜像到内存是真正的救命通道。我建议拿到新板子头一天第一件事不是写代码而是把“救砖路径”验证一遍——确认一旦写坏你有一条路能把它刷回来。6. 给刚开始移植的人几条经验都是我踩过的6.1 每次只动一个变量移植调试最大的敌人是自己。我早期移植时喜欢一次改好几处换个 defconfig、改下 DDR、再调串口结果跑不起来后根本不知道是哪一处导致的。后来养成一个习惯在移植过程中建立 Git 仓库每个小阶段单独提交。比如先保证“能得到参考板默认的编译产物”提交一次再把自己的板名和 dts 加上去提交一次然后再逐步改 DDR、串口、LED。每次只动一个变量一旦出问题git diff 能告诉你这次到底改了什么git bisect 能帮你快速定位坏的提交。这条规则看起来慢实际是整体最快的方式。6.2 先用厂商源码跑通再追 mainline很多老平台在 mainline U-Boot 里并没有现成支持比如当年的 4412、一些海思方案。这时候不要一上来就追最新 mainline。我更推荐先把 SoC 厂商或者开发板厂商提供的 BSP 源码跑通哪怕那个版本老一点、代码风格丑一点。BSP 的价值在于它的 DDR 参数、时钟配置、启动脚本都是针对实际硬件验证过的这些信息远比代码本身难得。等你在 BSP 上把流程走通、理解了板子的特性之后再去对照 mainline 看哪些支持已经合入哪些需要自己移植过来方向就会清晰很多。直接拿最新 U-Boot 硬啃一个没有维护的 SoC大概率会消耗你大量时间最后发现问题是某一段受专利或者保密协议限制的闭源 bl33。6.3 环境变量出问题值得单独提一下U-Boot 跑起来之后还有一个让新手苦恼的问题环境变量保存失败。你在命令行里 setenv bootargs...然后 saveenv重启后设置丢了或者干脆提示 CRC 错误载入默认值。这通常不是环境变量代码的逻辑问题而是你指定给环境变量的存储介质和偏移不对比如板子上根本没有 NOR你却配置了 CONFIG_ENV_IS_IN_SPI_FLASH或者 eMMC 上指定的环境变量分区被别的内容占用了。排查方法也比想象中简单先不保存环境变量用默认值跑通系统确认启动本身没问题再一步步去验证 saveenv 和 loadenv 对应的设备和偏移。环境变量一般只占一小块固定区域它不值得让你熬夜但值得你在移植清单里提前给它留一个位置。最后再补充一个实际操作中的小习惯每次调完 U-Boot我都会保存一份“能开机”的镜像副本命名里带上日期和当时的配置摘要。这个东西看起来朴素真到某一天板子被我折腾到没输出、又急着手头没有原始版本时它比什么调试技巧都管用。U-Boot 移植基础说到底就是把“未知”一项项变成“已知”剩下的事情就是你手里那块板子的启动日志会越来越长直到某一天你看到那行熟悉的 U-Boot 版本号自己都知道下一句应该是什么了。