1. 从一张索引表说起:U-Boot移植到底在移什么
很多人第一次接触U-Boot移植,脑子里冒出来的画面是"把一堆源码改一改,编译烧进去能跑就行"。我当年也是这么想的,结果在一块自制的板子上折腾了整整两周,串口连个字符都不吐。后来才明白,U-Boot移植的本质不是"改代码",而是让一个通用的引导程序认识你这块板子的硬件——它得知道内存从哪开始、串口挂在哪个地址、时钟怎么配、启动介质是什么。这些信息在U-Boot里通过一套叫"板级配置"的机制来描述,移植工作就是把这套描述从"别人的板子"改成"你的板子"。
所谓"索引",我理解成两层意思。第一层是知识索引:U-Boot移植涉及的知识点非常散,从目录结构、配置文件、设备树、驱动模型到编译系统,每一块单独拎出来都能写一篇长文,需要有一个清晰的索引帮你定位"我现在卡在哪一环"。第二层是操作索引:移植过程中你会反复回到几个关键文件——include/configs/xxx.h、arch/arm/dts/xxx.dts、board/xxx/、configs/xxx_defconfig,这些文件构成了移植的"操作地图",记住它们的位置和职责,效率能翻好几倍。
这篇文章面向的是已经会写点C、用过Linux命令行、手上有块开发板(或者准备自己画板)的嵌入式开发者。如果你连交叉编译工具链都没配过,建议先补一下基础再来。全文我会围绕"移植的完整链路"展开,从拿到源码到串口出字、从网络不通到能加载内核,把每一步的意图、坑点和验证方法都讲透。热词里提到的那些移植话题——FreeRTOS移植、LVGL移植、EasyLogger移植——本质上和U-Boot移植是同一类问题:让一个软件栈适配一套新硬件,思路是相通的,看完这篇你再去搞那些会轻松很多。
2. 动手之前:源码、工具链与目录结构的全局认知
2.1 选对源码版本比什么都重要
U-Boot的版本迭代很快,不同版本之间的API、配置方式、设备树支持程度差异巨大。我的建议是:优先选和你芯片厂商BSP匹配的版本。比如你用的是某国产RISC-V芯片,厂商SDK里往往带了一个已经调通的U-Boot版本,直接用它作为起点,比你去官网下最新版从零适配要省事得多。原因很简单,厂商已经把时钟、DDR初始化、引脚复用这些最恶心的部分调好了,你只需要在此基础上改板级差异。
如果你非要用主线版本,那就选一个近一年内的稳定tag,别用master分支。master分支经常有破坏性改动,你今天编译通过,明天pull一下可能就挂了。我一般会看芯片的dts文件在主线里是否已经存在,如果存在,说明社区已经有人适配过,移植难度会低很多。
工具链方面,ARM平台常用的是arm-linux-gnueabihf-或者aarch64-linux-gnu-,RISC-V用riscv64-linux-gnu-。这里有个坑:U-Boot是裸机程序,理论上用-none-eabi工具链更纯粹,但实际项目中用Linux工具链也没问题,只要注意不要链接到glibc就行。我遇到过用某版本工具链编译出来的U-Boot在启动时卡死,换一个版本就好了,所以工具链版本也要记录清楚,别到时候复现不了。
2.2 目录结构:把源码当成一张地图
U-Boot源码目录乍看很乱,但移植时你真正需要关心的就那么几个:
| 目录/文件 | 职责 | 移植时关注度 |
|---|---|---|
arch/arm/或arch/riscv/ | CPU架构相关代码、dts | 高 |
board/厂商/板名/ | 板级初始化代码 | 高 |
configs/xxx_defconfig | 默认配置 | 高 |
include/configs/xxx.h | 板级头文件配置 | 高 |
drivers/ | 各类外设驱动 | 中 |
common/ | 通用启动流程 | 低 |
cmd/ | 命令行命令 | 低 |
移植的核心动作,说白了就是新建一个board/目录、新建一个configs/配置、新建或修改一个dts、可能再改一个include/configs/头文件。其他目录基本不用动,除非你要加新驱动。
2.3 从"抄"开始:找一个最接近的参考板
这是我最想强调的经验:不要从零写,找一个和你板子最像的参考板,复制它的配置然后改。什么叫"最像"?同芯片系列、同架构、同启动方式(比如都是从SPI Flash启动)。比如你用的是某款Cortex-A7芯片,那就找同系列其他芯片的板子配置,把board/目录、configs/文件、dts都复制一份,重命名成你自己的板子名,然后逐个字段改。
这样做的好处是,你有一个"已知能工作"的基线,改坏了可以对比。我见过太多人一上来就自己新建文件,结果编译报错几十个,根本不知道从哪查起。复制参考板之后,先编译一遍确认能过,再开始改硬件相关的部分,这样每次只引入一个变量,出问题好定位。
3. 板级配置的三驾马车:defconfig、头文件与设备树
3.1 defconfig:编译系统的入口
configs/xxx_defconfig是编译时make xxx_defconfig读取的文件,它决定了哪些功能被编进U-Boot。这个文件里通常包含几类配置:
- 架构和CPU:
CONFIG_ARM=y、CONFIG_TARGET_XXX=y、CONFIG_SYS_ARCH等 - 启动介质:
CONFIG_SPL、CONFIG_SPI_BOOT等 - 外设开关:
CONFIG_DM_SERIAL、CONFIG_DM_MMC等 - 环境变量存储位置:
CONFIG_ENV_IS_IN_MMC等
移植时最容易出错的是CONFIG_TARGET_XXX,这个宏必须和board/目录下的Kconfig里定义的一致,否则编译系统找不到你的板子。我踩过一次坑:改了板子名但忘了改board/厂商/板名/Kconfig里的config TARGET_XXX,结果make xxx_defconfig直接报"target not found",查了半天才发现是这里。
另一个经验是:defconfig里的配置项不要手动全写,用make menuconfig生成。你先复制参考板的defconfig,然后make xxx_defconfig,再make menuconfig进去按需调整,保存后U-Boot会自动生成一个精简的defconfig。手动写容易漏掉依赖项,导致编译出来的U-Boot缺功能。
3.2 板级头文件:那些"魔法数字"的归宿
include/configs/xxx.h是传统U-Boot配置的核心,虽然现在很多配置迁移到了defconfig和dts,但这个头文件里仍然保留了大量板级参数,比如:
#define CONFIG_SYS_SDRAM_BASE 0x80000000 #define CONFIG_SYS_INIT_SP_ADDR 0x80200000 #define CONFIG_SYS_LOAD_ADDR 0x80800000 #define CONFIG_SYS_MALLOC_LEN (4 * 1024 * 1024) #define CONFIG_SYS_BOOTM_LEN (16 * 1024 * 1024)这些地址不是随便写的。CONFIG_SYS_SDRAM_BASE是DDR的物理起始地址,必须和你的硬件设计一致;CONFIG_SYS_INIT_SP_ADDR是初始化阶段的栈指针,通常放在DDR靠前的位置,要保证不会覆盖U-Boot自身;CONFIG_SYS_LOAD_ADDR是加载内核的默认地址,要避开U-Boot运行区域。
注意:这些地址如果配错,典型症状是U-Boot启动到某一步突然重启或者卡死,串口没有任何输出。排查方法是先用JTAG或者调试器看PC指针停在哪,再反推是哪个地址出了问题。
我的习惯是,在头文件里给每个地址都加注释,写清楚为什么是这个值。比如CONFIG_SYS_INIT_SP_ADDR我会注明"DDR起始+32MB,避开前32MB的保留区"。这样过几个月回来看,或者交接给同事,都能快速理解。
3.3 设备树:现代U-Boot的硬件描述中心
从2015年左右开始,U-Boot全面拥抱设备树(Device Tree)。现在移植一块新板子,大部分硬件信息都写在dts里,而不是头文件里。dts描述了串口、I2C、SPI、MMC、网口等外设的寄存器地址、中断号、时钟、引脚复用等信息。
移植时,你通常从芯片厂商的arch/arm/dts/芯片名.dtsi开始,这个文件描述了芯片内部所有外设,然后你的板级dts通过#include引入它,再覆盖或补充板级差异。比如:
#include "mychip.dtsi" / { model = "My Custom Board"; compatible = "myvendor,myboard", "myvendor,mychip"; chosen { stdout-path = &uart0; }; }; &uart0 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&uart0_pins>; };chosen节点里的stdout-path决定了U-Boot的调试串口是哪个,这个配错了串口就没输出。compatible字段要和U-Boot代码里的匹配表对应,否则板级初始化代码不会被调用。
设备树最大的坑是引脚复用(pinctrl)。很多芯片的引脚可以复用成多种功能,dts里必须正确配置pinctrl节点,否则外设虽然寄存器配对了,但引脚没切到对应功能,照样不通。我调一块板子的网口时,寄存器读出来都对,就是ping不通,最后发现是pinctrl里RMII的引脚配成了GPIO模式。
4. 编译、烧录与串口第一次出字的完整链路
4.1 编译:从make xxx_defconfig到u-boot.bin
编译U-Boot的标准流程是:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- xxx_defconfig make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j8第一条命令生成.config,第二条开始编译。编译产物里你需要关注几个:
u-boot.bin:纯二进制,可以直接烧到启动介质u-boot:ELF格式,带符号,调试用u-boot.map:链接映射表,查符号地址用spl/u-boot-spl.bin:如果用了SPL,这是第一阶段引导
编译报错时,先看第一个错误,后面的往往是连锁反应。常见错误包括:找不到头文件(路径没配对)、未定义符号(配置项没开)、dts语法错误(少分号或括号)。dts错误尤其烦人,报错信息经常指向一个莫名其妙的位置,这时候用dtc单独编译dts能拿到更准确的错误行号。
4.2 烧录:不同启动介质的差异
烧录方式取决于你的启动介质:
- SD卡:直接
dd到卡里,注意偏移量。很多芯片要求U-Boot写在SD卡的特定扇区(比如8KB偏移),不是从0开始。 - SPI Flash:用烧录器或者通过U-Boot自己的
sf命令写。 - NAND:需要先擦除,注意坏块管理。
- eMMC:类似SD卡,但要注意boot partition的配置。
我遇到最多的问题是烧录偏移量搞错。比如某芯片要求SPL写在0x0,U-Boot proper写在0x20000,你如果两个都从0写,肯定起不来。这个偏移量在芯片手册的"Boot ROM"章节里有,一定要查清楚。
4.3 串口出字:移植成功的第一个里程碑
当你烧录完,接上串口线,打开串口终端(波特率通常是115200,8N1),看到类似下面的输出,恭喜你,移植成功了一大半:
U-Boot 2023.07 (Jan 01 2024 - 00:00:00 +0000) CPU: MyChip ARMv7 Processor rev 1 (v7l) Model: My Custom Board DRAM: 512 MiB MMC: mmc@12340000: 0 In: serial Out: serial Err: serial Net: eth0: ethernet@12350000 Hit any key to stop autoboot: 0 =>如果串口没输出,按这个顺序排查:
- 波特率对不对:有些芯片默认波特率不是115200,查手册确认。
- 串口引脚对不对:用示波器看TX脚有没有波形,没波形说明引脚复用没配对。
- 时钟对不对:串口时钟源配错,波特率就会偏,输出会是乱码。
- DDR有没有初始化成功:如果DDR没起来,U-Boot代码根本跑不到串口初始化。这时候需要看SPL的输出,或者用调试器看PC。
我调一块新板子时,串口乱码折腾了一下午,最后发现是晶振频率填错了,导致串口时钟计算偏差。所以晶振频率这个参数一定要和硬件确认,别想当然。
5. 让U-Boot真正能用:网络、存储与内核加载
5.1 网络不通的排查思路
串口通了之后,下一个要打通的是网络,因为你要用TFTP从服务器加载内核。网络不通的排查链路是这样的:
- PHY有没有被识别:U-Boot启动时会打印PHY地址和ID,如果打印的是
No ethernet found,说明MDIO总线没通。 - MDIO通不通:MDIO是管理接口,负责读写PHY寄存器。如果MDIO不通,先查MDIO的时钟和引脚。
- PHY和MAC之间的接口模式:RMII还是RGMII?时钟是MAC提供还是PHY提供?这个配错,PHY识别了但数据不通。
- ping测试:
ping 192.168.1.1,如果不通,用mii info看PHY状态,用mii dump看寄存器值。
我遇到过一个经典问题:PHY识别正常,但ping不通,最后发现是RGMII的延时(delay)没配。RGMII接口对时序敏感,TX和RX需要加内部延时,这个在dts的PHY节点里配。
5.2 存储设备:MMC、SPI Flash与USB
U-Boot支持多种存储设备,移植时要确保你用的那个被正确初始化。以MMC为例,mmc list能看到设备,mmc dev 0能切换,mmc info能看容量。如果mmc list是空的,检查:
- dts里MMC节点
status是不是okay - 时钟和引脚复用对不对
- 卡检测引脚(CD)有没有配
SPI Flash用sf probe探测,sf read/write/erase操作。注意SPI Flash的地址映射,有些芯片支持memory-mapped模式,可以直接像内存一样读,速度更快。
5.3 加载内核:bootm、booti与bootz
U-Boot加载内核有三种主要方式:
bootm:加载uImage(老式,带U-Boot头)bootz:加载zImage(ARM 32位常用)booti:加载Image(ARM 64位常用)
典型流程是:
tftp 0x80800000 zImage tftp 0x82000000 myboard.dtb bootz 0x80800000 - 0x82000000这里的关键是地址不能重叠。内核、设备树、ramdisk各占一块内存,如果重叠了,加载完内核把设备树覆盖了,启动就会失败。我一般会在头文件里定义好这些地址,并在文档里画一张内存布局图。
设备树传给内核时,U-Boot会做一些修改(比如填/chosen节点的bootargs),所以bootargs可以在U-Boot里通过setenv bootargs设置,也可以写在dts里。我习惯在U-Boot里设,方便调试时改。
6. 移植过程中那些让人抓狂的坑与排查实录
6.1 坑一:DDR初始化失败导致"无输出"
这是最让人崩溃的情况:烧录完,串口一点输出都没有。原因通常是DDR没初始化成功,U-Boot代码跑不起来。DDR初始化涉及一堆寄存器配置,时序参数、驱动强度、ODT等,任何一个错了都可能导致DDR不稳。
排查方法:先用厂商提供的DDR初始化工具生成参数。很多芯片厂商有专门的工具,你输入DDR型号和板级参数,它生成一段初始化代码或者寄存器配置表。把这个表填到SPL里,成功率比手调高得多。如果还是不行,用示波器看DDR的时钟和片选信号,确认硬件本身没问题。
6.2 坑二:环境变量保存失败
U-Boot的环境变量(bootargs、bootcmd等)需要保存在非易失存储里。如果你配了CONFIG_ENV_IS_IN_MMC但MMC没通,保存环境变量时会报错,而且每次重启都恢复默认值。
我遇到过一次:环境变量能读能写,但重启后丢失。查了半天发现是环境变量存储的偏移量和分区表冲突,写进去被其他数据覆盖了。解决办法是确认环境变量存储区域没有被其他分区占用,或者改用独立的SPI Flash区域存环境变量。
6.3 坑三:设备树与内核不匹配
U-Boot用的设备树和内核用的设备树可以是同一个,也可以是不同的。有些项目里,U-Boot用一份精简的dts(只描述启动必需的外设),内核用一份完整的dts。如果你把U-Boot的dts直接传给内核,内核可能因为缺少某些节点而启动失败。
我的做法是:U-Boot和内核共用一份dts,这样维护简单。但要注意,U-Boot对dts的支持不如内核完整,某些节点U-Boot会忽略,这没关系,只要不影响启动就行。
6.4 坑四:编译通过但运行卡死
有时候编译一切正常,烧录后串口输出几行就卡死。这种问题最难查,因为没有任何报错信息。我的排查套路是:
- 加打印:在可疑的初始化函数前后加
printf,看卡在哪一步。 - 看门狗:有些芯片默认看门狗是开的,U-Boot如果没及时喂狗,就会被复位。检查是否需要关闭看门狗。
- 时钟:某些外设的时钟没使能,访问它的寄存器就会卡死(总线挂起)。
- 内存:如果卡在内存相关操作,可能是DDR不稳,跑个内存测试。
我印象最深的一次,U-Boot卡在MMC初始化,最后发现是MMC控制器的时钟源选错了,导致访问寄存器时总线一直等待。改了一个时钟选择位就好了。
7. 从能跑到好用:性能优化与可维护性
7.1 裁剪U-Boot体积
U-Boot默认编译出来可能有好几百KB,如果你的启动介质很小(比如SPI Flash只有512KB),就需要裁剪。裁剪手段包括:
- 关掉不用的命令:
CONFIG_CMD_XXX - 关掉不用的驱动:
CONFIG_DM_XXX - 用SPL:把第一阶段精简到几十KB
- 开启LTO(链接时优化):
CONFIG_LTO
裁剪时要注意别把启动必需的驱动裁掉了。比如你把MMC驱动裁了,但启动要从MMC加载内核,那就起不来了。我的做法是先用make menuconfig把能关的都关了,编译烧录测试,确认能启动后再继续关,逐步逼近最小体积。
7.2 加快启动速度
U-Boot启动慢通常是因为:
- 串口打印太多:关掉
CONFIG_DEBUG_UART或者降低日志级别 - 延时太长:
CONFIG_BOOTDELAY设小一点 - 探测外设太慢:比如USB探测、网络DHCP,如果不需要可以关掉
- 内核加载慢:用压缩内核或者加快存储读取速度
我做过一个项目,要求U-Boot在1秒内启动完,最后是把BOOTDELAY设为0,关掉所有非必要打印,用SPL直接加载内核,做到了800ms。
7.3 版本管理与可维护性
U-Boot移植的代码是要长期维护的,所以版本管理很重要。我的做法是:
- 用git管理,基于官方tag建分支
- 板级改动单独提交,commit message写清楚改了什么、为什么
- 维护一个
README,记录板子信息、编译命令、烧录方法、已知问题 - 定期rebase到新的官方tag,但不要盲目追新
这样即使过了一年,你或者同事回来改,也能快速上手。
8. 移植完成之后:验证清单与后续扩展
移植完U-Boot,别急着宣布成功,跑一遍验证清单:
| 验证项 | 方法 | 通过标准 |
|---|---|---|
| 串口输出 | 上电看终端 | 正常打印版本和硬件信息 |
| DDR容量 | bdinfo | 显示的DRAM大小和实际一致 |
| 存储设备 | mmc list/sf probe | 能识别到设备 |
| 网络 | ping | 能ping通网关 |
| 环境变量 | saveenv后重启 | 变量保持 |
| 内核加载 | bootz | 内核能启动到命令行 |
| 复位 | reset | 能正常重启 |
全部通过,才算真正移植完成。
后续如果要扩展,比如加USB启动、加LCD显示、加Fastboot支持,都是在现有基础上加驱动和配置。这时候你对U-Boot的目录结构和配置机制已经熟悉了,加功能就是查文档、改dts、开配置项的事。
热词里提到的那些移植——FreeRTOS、LVGL、EasyLogger——虽然目标不同,但方法论是一样的:找参考、改配置、调硬件、验证。U-Boot移植因为涉及启动流程和硬件初始化,算是其中比较硬核的一类,把这块啃下来,其他移植你会有种"降维打击"的感觉。
我个人在实际操作中的体会是,U-Boot移植最耗时间的不是写代码,而是定位问题。串口没输出、网络不通、内核起不来,每一个现象背后可能有好几种原因。建立一套系统的排查方法——从电源、时钟、复位这些最基础的信号查起,再到寄存器、引脚复用、软件配置——比记住某个具体问题的答案重要得多。这套方法一旦建立起来,你移植任何新板子都会快很多。