搞嵌入式的,谁没被U-Boot劝退过一次呢?我说的不是那满屏的寄存器配置,也不是看起来永远对不上的内存地址,而是明明照着参考板抄了一遍,上电后串口却死活不吐字的那种挫败感。U-Boot移植不是照着手册敲几条命令就能完事,它更像是在一块还没点亮的新板子上做一场“最小系统唤醒手术”:先让CPU跑起来,再让串口说话,最后把内核稳稳当当地交给操作系统。这篇文章就是我这几年的U-Boot移植索引式笔记,也是给准备把一颗新SoC或者一块新板卡跑通的兄弟的一份参考地图。不管你是学生、刚入行的工程师,还是被项目逼着从裸机转Boot的选手,这篇东西应该都能帮你少走几个夜路。
下面我会按照自己的实操顺序来写:先讲明白移植到底移的是什么,再讲参考板选型和工具链怎么配,然后是具体怎么把板子点亮、怎么排查高频问题,最后聊一聊移植方法论在其他组件上的复用。整篇没有华丽的架构图,都是实打实的命令、文件和绕过坑的思路。
1. U-Boot移植这件事,到底在移什么?
1.1 吃透启动流程,移植才不会瞎忙
很多新手拿到一块板子就开始翻U-Boot源码,翻了两天什么也没看懂,问题在于他根本不知道U-Boot在这块硬件上是怎么被“拉起来”的。不同SoC的启动流程虽然有差异,但骨架是一样的:芯片内部的ROM代码在出厂时固化了第一段启动逻辑,它会根据启动引脚的电平状态,从SD卡、eMMC、NOR Flash或SPI Flash里读取下一段代码。
以比较常见的i.MX系和RK系为例,实际跑起来通常是这个链条:ROM code → SPL(Secondary Program Loader)→ ATF(可选)→ U-Boot proper → kernel。SPL是一个精简版U-Boot,它负责最基础的时钟、DDR初始化和存储设备驱动,目的就是把完整版U-Boot从外部存储加载到内存里。这个阶段很小,很多平台编译出来就是几十KB。完整版U-Boot则负责初始化网卡、USB、显示等外设,然后按bootcmd环境变量去加载内核。
所以移植的第一件事不是看代码,而是查你手里的SoC芯片手册,搞清楚它支持哪些启动设备、启动头文件格式是什么、SPL应该被链接到哪个地址。不同厂家的要求千差万别:NXP的i.MX系列要求生成带IVT头的镜像,Rockchip有自己的一套miniloader传递逻辑,全志的sunxi平台又有单独的spl格式。对接不上的话,U-Boot根本不会被ROM code识别,表现就是上电后一片死寂。
1.2 移植的核心任务:一份可执行的清单
把术语去掉,U-Boot移植落到代码层面其实就是四块任务:板级目录、defconfig、设备树、驱动裁剪。
板级目录是board/<厂商>/<板名>/下面的一堆文件,里面是这块板的特定初始化函数,比如board_init、board_late_init、电源时序控制等。defconfig是configs/目录下以板名命名的配置文件,它决定了编译哪些功能、默认设备树是哪棵、环境变量分区怎么定。设备树就是arch/arm/dts/下的.dts文件,描述了内存大小、串口、网卡、MMC这些硬件的连接关系。驱动裁剪则决定了SPL和U-Boot里到底编入哪些驱动,少了跑不起来,多了编译慢还占空间。
我在imx6ull那批板子上第一次移植时,第一版其实就干了三件事:基于mcimx6ull-evk复制了一套板级目录、把defconfig里默认设备树改成自己的、然后把设备树里内存节点改成实际容量。就这么几下,串口就出东西了。
这里我建议所有刚接触移植的朋友先给自己定义一个“最小可启动”的目标:不跑网络、不跑显示、不要USB,只要能从SD卡启动、串口能打印、能执行bootm命令把内核拉起来。这个目标一旦达成,后面的工作就是增量了。
2. 移植前准备:参考板选型与工具链搭建
2.1 参考板怎么选:选错了真能坑到怀疑人生
U-Boot移植有一条近路,就是找一块官方已经支持的“参考板”,然后照着它改。这个参考板选得好不好,直接决定你是几天收工还是几周加班。
我的经验是三个优先:优先选同厂家的同系列SoC,优先选官方评估板,优先选arch/arm/dts/目录下已有现成.dts文件的板子。比如你用的是i.MX6ULL,那mcimx6ull-evk就是最好的参照物;用的是RK3308,那就找Rockchip的EVB。为什么必须是同系列?因为内存控制器、时钟树、启动ROM的逻辑都是同一套,你只需要改引脚复用和板级外设差异,而不用去猜DDR控制器的寄存器含义。反过来,如果你拿一块完全不同的SoC开发板做参考,那整个时钟和内存初始化都要推倒重来,相当于做了一次BSP级别的移植,工作量完全不是一个量级。
另外有个小提示:优先选择主线Linux内核里已经支持比较好的板子。因为U-Boot的设备树和内核设备树很多节点是对应着的,内核有支撑的板子,意味着外设的地址、中断、时钟描述大概率是正确的,你抄的时候不容易抄错。
2.2 工具链与源码版本:对不上号就是折腾自己
工具链是很多萌新踩的第一个坑。ARM 32位平台用arm-linux-gnueabihf-,ARM 64位平台用aarch64-linux-gnu-,这个基本不会混。真正混的是版本:U-Boot更新很快,新版本U-Boot对GCC的版本敏感,比如2023年之后的版本用GCC 12编译没有问题,但用特别老的GCC 4.9就有可能在编译时遇到奇怪的错误,因为链接脚本、内联汇编语法都已经变了。
我的建议是不要自己从零编工具链,直接去ARM官网下预编译的gcc-arm-*-x86_64-arm-none-eabi或aarch64-none-linux-gnu。拿到工具链先验证一下:aarch64-linux-gnu-gcc -v,能正常打印版本就行。然后U-Boot源码建议直接去官方git仓库下载稳定分支,不要下那种网盘里传了三四手的压缩包,里边有什么坑你根本不知道。
调用编译的方式很统一,几乎每个平台都一样:
export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf- make myboard_defconfig make -j8这里有一个细节:ARCH=arm指的是U-Boot的架构目录,不是Linux内核那种区分arm和arm64的方式。U-Boot用ARCH统一指代CPU架构,而用CONFIG_ARM64来区分是32位还是64位。所以你在64位平台上也经常看到ARCH=arm,不要觉得奇怪。
工具链和源码版本还有个隐性关联:新版本U-Boot引入了binman、加密签名等构建工具,这些工具依赖Python版本和系统库。如果编译到某个阶段报module找不到,优先考虑是不是系统Python太老,而不是源码有问题。
3. 实际动手:跑起来的关键几步
3.1 新建板级目录与defconfig:照着抄,但要抄明白
整个移植动作里最安全的操作就是复制,但复制不等于无脑粘贴。你复制参考板的文件之后,必须把里面任何与原有板子绑定的信息替换掉,否则会出现“明明改的是自己的板子,行为却还是参考板的”这种灵异事件。
我的操作顺序是这样的:
先在configs/目录下把参考板的defconfig复制一份,改成自己的板名,比如myboard_defconfig。打开这个文件,找到CONFIG_DEFAULT_DEVICE_TREE="mcimx6ull-evk"这行,改成你自己设备树源文件的名字(注意不带.dts后缀)。然后看CONFIG_SYS_EXTRA_OPTIONS或CONFIG_TARGET_xxx这类选项,它们指向arch/arm/mach-*/Kconfig里的板级选择。这个不仔细改的话,你make的时候会编译参考板的board/目录而不是你自己的。
接着处理board/目录。把参考板整个目录复制成board/mycompany/myboard/,然后改里面的.c文件和Kconfig、MAINTAINERS。Kconfig里最关键的是config TARGET_MYBOARD这个选择项,它要与arch/arm/mach-*下的Kconfig联动起来。联动方式就是在对应mach目录的Kconfig里加一行source "board/mycompany/myboard/Kconfig"。
这一步做完可以先跑一次:
make myboard_defconfig make menuconfigmenuconfig能打开图形配置界面,此时你应该能在Target select里看到自己的板子。如果看到,说明Kconfig链路已经通了。看不到就返回去检查source路径和配置项字符串是否一致。
3.2 设备树修改:内存、串口、网卡一个都不能少
设备树是移植时最容易“抄漏”的环节。U-Boot在启动过程中会读取设备树来初始化外设,你漏掉一个status = "okay",那个外设就静悄悄地不干活。
第一件必须确认的是内存节点。多数板子的/memory节点长这样:
memory@80000000 { device_type = "memory"; reg = <0x80000000 0x20000000>; };reg里的第二个数字是内存大小,0x20000000就是512MB。很多同学板子上实际焊了256MB内存,结果忘了改这里,U-Boot跑起来以为有512MB,内核启动时访问到不存在的地址,直接崩溃或随机死机。这类问题非常隐蔽。
第二件必须确认的是串口节点。包括clocks、pinctrl、status三个属性。你可以参考原厂dts里同系列芯片的串口写法,把引脚复用改成自己板子的实际连接。比如i.MX6ULL的uart1在dts里一般叫uart1,pinctrl节点引用pinctrl_uart1。改完之后编译设备树可以单独执行:
make myboard.dtb它会生成编译好的设备树文件,配合反编译工具dtc -I dtb -O dts就能确认内容是否真的改对了。
第三件才是网卡、SD卡、USB这些。我的建议是先把串口跑通,再回来加这些,不要一次性堆太多。网卡改的时候重点看PHY地址,很多板子PHY地址是0x1或0x2,设备树里写错了会导致miiphy_register之后一直link不上。
3.3 编译烧录与启动日志的首次点亮
串口和设备树改好之后,就可以完整编译一次了:
make clean make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- myboard_defconfig make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j8编译完去u-boot.bin、u-boot.dtb、SPL这三个主要产物。不同平台的烧录方式不同,以从SD卡启动为例,经常能看到这样的命令:
sudo dd if=SPL of=/dev/sdb bs=1k seek=1 conv=fsync sudo dd if=u-boot.bin of=/dev/sdb bs=1k seek=69 conv=fsync这里的seek偏移不是随便写的,它来自SoC的启动ROM规范,比如i.MX6ULL要求SPL写到SD卡的第1KB偏移处,U-Boot本体写到第69KB处。你要是随便写,ROM找不到镜像,板子还是起不来。
烧完上电,最让人开心的画面就是串口工具里出现U-Boot SPL ...和U-Boot ...这两行字。如果没出来,别慌,下一节就是专门讲怎么排查的。第一次点亮之后,我建议立刻saveenv保存一下环境变量,然后跑一遍help命令确认基本命令都在,再试bootm或bootz能不能引导你的内核镜像。
4. 移植过程中的高频问题与排查技巧
4.1 串口无输出:先别急着怀疑硬件
串口无输出是移植第一天遇到概率最高的问题,而大多数情况下,板子是好的,是你的配置没对。
先做一件事:确认U-Boot到底有没有跑起来。最简单的方法是看GPIO。在board_init里临时加一段把某个LED点亮或GPIO拉高的代码,编译烧录后看那个脚有没有电平变化。如果没有变化,说明代码根本没执行到那里;如果有变化而串口无输出,那问题就缩小到调试串口驱动这一侧了。
调试串口驱动侧的排查顺序:第一查DEBUG_UART相关配置,U-Boot在SPL阶段支持早期串口打印,需要定义CONFIG_DEBUG_UART、CONFIG_DEBUG_UART_BASE、CONFIG_DEBUG_UART_CLOCK。第二查pinctrl,确认引脚复用成UART功能。第三查波特率,如果CONFIG_BAUDRATE设置的和终端不一致,你会看到满屏乱码。第四查时钟,串口的时钟源频率错了,波特率就会有偏差。
我自己遇到最多的问题是时钟。很多SoC的UART时钟默认来自24M晶振,但实际板子上用的26M或32.768K,这个时候DEBUG_UART_CLOCK还写的24M,出来的波特率就是错的。用示波器看TX引脚的话,能发现波形宽度不对,这时候就不是“无输出”,而是“乱码输出”的变种。
4.2 内存初始化失败:最常见也最挫败的一关
如果说串口问题只是开胃菜,那DDR初始化失败就是主菜了。SPL阶段最重要的事情之一就是把内存跑通,否则后面的U-Boot本体和内核根本无处安放。不同SoC的DDR初始化差异极大:有些平台直接在SPL里用寄存器配置,有些平台需要加载DDR training固件。
最典型的失败现场是:串口打印了U-Boot SPL之后直接卡住,什么都不往下走了,或者往U-Boot 2023.04那行字之后随机死机。不要急着怀疑内存颗粒质量,大多数情况是配置参数不对。DDR的频率、行列地址宽度、时序参数、片上终结电阻这些参数,错一个都可能跑不动。
我自己的经验就一句话:不要自己算参数,先用原厂默认配置。现在的SoC厂商基本都会提供DDR压力测试工具,比如NXP的DDR Stress Test,Rockchip的DDR工具,它们会根据你的颗粒型号和容量生成一组可用参数。把这组参数填到设备树或板级头文件里,比你自己对着手册算要可靠得多。另外SPL阶段的DDR驱动一般不建议裁剪,省那点空间不值得。
4.3 外设驱动异常的通用排查思路
网卡不通、SD卡识别不到、USB枚举失败,这三类问题占了移植后期八成工作量。它们的排查思路高度统一,核心是记住:在U-Boot的驱动模型(DM)下,外设能否工作取决于三件事——设备树节点是否正确、驱动代码是否被编译进去、硬件电源和复位是否正常。
网卡排查先确认CONFIG_CMD_NET和CONFIG_DM_ETH都打开了,然后在U-Boot命令行执行dhcp或ping。如果提示Could not get PHY for ...,多半是设备树里phy-handle或phy-mode写错了。用md命令直接读PHY寄存器的值,能帮你判断PHY是否在复位后正确响应。
SD卡排查则先看MMC设备有没有被枚举出来,执行mmc list,如果没有任何设备,检查设备树MMC节点的bus-width、non-removable、status,以及板级代码里的SD卡供电GPIO。比如就有很多板子SD卡供电脚默认是关闭的,需要在board_init里把它拉高,这种问题设备树里看不出任何异常,不看原理图永远找不到。
USB和显示也是类似。排查原则是先确认硬件时序,再看驱动匹配,最后才怀疑代码bug。绝大多数情况下,问题出在设备树的描述与硬件实际连接不一致,而驱动代码本身是好的。
| 现象 | 优先排查方向 | 常用手段 |
|---|---|---|
| SPL后无任何输出 | DEBUG_UART配置、时钟、引脚复用 | GPIO点灯确认程序是否运行 |
| 乱码输出 | 波特率、UART时钟源 | 示波器看TX波形周期 |
| DDR初始化卡死 | DDR时序参数、频率、training固件 | 原厂DDR工具生成参数 |
| 网卡不通 | PHY地址、phy-mode、复位GPIO | md命令读PHY寄存器 |
| SD卡无法识别 | 设备树MMC节点、供电GPIO | mmc list、原理图核对 |
| USB枚举失败 | VBUS供电、DP/DM引脚复用 | 万用表量5V输出 |
4.4 另一条独家经验:善用log和反汇编
遇到U-Boot卡死但又没到串口打印阶段,除了点灯法,还有个小技巧是看反汇编。编译完的u-boot是ELF文件,你可以看System.map或直接用aarch64-linux-gnu-objdump -d u-boot查看某个函数的汇编码,对照PC地址定位卡在哪个函数。这个方法虽然比加调试打印麻烦,但在SPL阶段尤其有效,因为SPL往往不够空间放太多printf。
再补一条,U-Boot的log系统很强大,很多新手根本不知道。CONFIG_LOG=y之后,在命令行执行log level 7,可以看到驱动初始化过程的详细日志。外设初始化失败时,这条命令通常比你自己猜半天强得多。
5. 移植方法论的可迁移性:不止是U-Boot
5.1 从U-Boot到FreeRTOS/LVGL:一样的适配套路
“移植”这两个字,在嵌入式领域出现的频率极高。看看现在搜索热词里的freertos移植lvgl、lvgl移植stm32、cherrydap移植ch32v305,你会发现大家都在做同一件事:让一段原本为某平台编写的代码,跑在另一个硬件环境上。U-Boot移植的经验之所以值钱,是因为它把“适配层”这个概念练得滚瓜烂熟。
U-Boot里的适配层是板级目录、设备树加驱动模型,FreeRTOS里的适配层是port目录下的移植文件,负责上下文切换、时钟节拍和中断处理,LVGL里的适配层则是lv_drv接口,你把显示缓冲、触摸读取函数填进去就行。套路是完全一样的:先看对方需要什么接口,再在自家硬件上实现这些接口,最后一步步验证。
拿LVGL移植来说,很多人上来就问怎么把demo跑起来,我建议先把它当成一块“显示器外设”。你在U-Boot里点亮LCD的经验完全可以复用:先解决像素时钟和时序,再解决DMA缓冲,然后解决背光控制,最后才是LVGL的抽象层。底层不通,上层画得再漂亮也是花屏。
5.2 轻量组件移植的通用规律:EasyLogger、NanoModbus这类
很多人觉得移植一个像EasyLogger、NanoModbus这样的轻量组件很简单,直接源码加进去编译就行。现实是编译确实简单,跑起来才是考验。EasyLogger需要你提供一个输出函数、一个锁函数(如果需要线程安全)和一个时间戳函数;NanoModbus需要你提供串口收发接口、CRC计算和RTU帧间隔定时器。
这类组件移植的通用规律我总结成四句话:读头文件找接口依赖矩阵,写一个平台适配文件单独放,不在业务代码里塞平台宏,先跑demo再接入正式业务。我在U-Boot里摸爬滚打锻炼出来的习惯就是,凡是跨平台的代码,绝对不直接改第三方源码,而是写一个platform_xxx.c做适配,以后换板子只需要改这个文件。这个方法在U-Boot移植里让你少改几十个文件,在EasyLogger移植里一样有效。
5.3 长期维护与版本管理的实践建议
移植不是一次性的,项目后续的硬件改版、SoC原厂BSP更新、U-Boot上游版本升级,都会把你的移植工程卷入维护流程。这时候没有版本管理,就是灾难的开始。
我自己的做法是维护一个长期分支,基于上游某个稳定版本打底,所有板级修改都通过patch形式管理。每次硬件改版,先更新设备树和defconfig,提交为一条独立commit,commit信息里写明改动原因和硬件版本号。下次拿到一个新板子,git log一翻就能知道这块板子走了多少弯路。
另外强烈建议在你自己的板级目录里写一个README,把烧录命令、串口参数、内存配置来源、phy地址这些只有“当时人才知道”的信息记下来。U-Boot移植这个东西,三个月不看,你再回来看自己写的代码都会陌生,更别说同事接手了。
我在实际项目里还养成了一个习惯:凡是遇到并解决了的问题,就在源码旁边用注释记一行“为什么”。这个注释在三个月后救过我很多次,因为那时候你早忘了当初为什么要把某个PHY配置写成那样。
最后
U-Boot移植真正值钱的不是最后跑起来那一下,而是在这个过程中被逼出来的调试能力和对硬件的理解。我现在拿到一块新板子,还是会先把参考板的README和原厂dts读一遍,然后才动手改。这个过程没有捷径,但也不是莽撞地试错。希望这篇索引式的分享能帮你在接下来的移植任务里,少一点半夜盯着串口工具发呆的时间。