很多人问我“U-Boot移植”到底怎么上手,面对一份主控芯片资料和一箩筐的参考代码,总觉得思路被厚厚的寄存器描述塞满,无从下手。我做了几年固件和系统引导相关的工作,前前后后折腾过不少厂商的开发板、定制板卡,包括给RISC-V系列、Cortex-A系列写过板级支持,也顺手做过一些类似CherryDAP这类调试器的跨平台移植。这篇文章其实是给自己做的一个“索引”,把U-Boot移植过程中最关键的几步、最容易踩的坑、以及排查套路都梳理成一条清晰的线索,也希望能帮刚接触这块的朋友少走我当年走过的弯路。索引不是完整教程,而是把一道道工序拆开,告诉你每一处该重点关注什么。
1. U-Boot移植到底在干什么
1.1 一个操作系统启动前的小故事
我们都熟悉操作系统,但很少有人关心操作系统被引导起来之前,世界是什么样子。芯片上电后,内部的BootROM会先执行一段固化代码,它负责从一个约定的介质(比如SD卡、SPI Flash、USB或者网络)读取下一段引导程序,然后跳转过去。U-Boot在这一幕里扮演的,就是从BootROM手里接管硬件、初始化内存、时钟、串口等基础外设,再加载真正的操作系统内核(比如Linux或某个RTOS)入场。U-Boot移植,说白了就是把U-Boot这套代码在“你的这板子”上跑起来,让它能找到内核、能启动内核、还留给你一个可用的调试环境。
很多刚入门的朋友会把U-Boot移植和写Linux内核驱动搞混,实际上两者的区别挺大:U-Boot是在内核跑起来之前的临时世界,它面对的硬件状态极不完整,很多外设连初始化都没有做,而且它的运行环境非常受限,可能连内存都还没有完全配置好,所以它对工程上有更高的“裸奔”要求。你会经常发现U-Boot驱动比内核驱动的代码要“直接”得多,它很少依赖复杂框架,很多时候就是直接操作寄存器。原因很简单:U-Boot的使命是尽快让系统到达一个可操作的状态,而不是追求极致的可扩展性。
1.2 U-Boot移植和普通软件开发的不同之处
我见过不少从裸机项目转过来的朋友,他们写STM32程序很熟练,觉得U-Boot移植也不过是配置几个寄存器,烧进去跑就完了。等真正上手才知道,U-Boot移植更像是在搭一个“中间层”,它既要适配具体的芯片型号,还要兼顾一套完整的分区表、启动参数、多媒体支持(比如显示、存储)等顶级设计。
举个很贴切的类比:把一台显示器接入一台电脑,显示器不需要关心电脑里装的是Windows还是Ubuntu,它只需要兼容好标准的视频接口协议就行。U-Boot就是那个“显示器”,它必须兼容上游通用的启动协议(比如ARM64下常见的FIT Image、extlinux风格配置),同时又能对下搞定不同的内存、存储、网络等芯片。所以U-Boot编译时,首先要选择“当前板级的配置”,这个配置并不是单指某个头文件,而是经过Kconfig和设备树叠加后的综合结果。
1.3 移植前必须搞清楚几个基础概念
开始之前,我建议你务必把以下几个概念按顺序弄明白,因为这决定了后面看代码是否顺畅:第一,SPL和TPL,它们是非常精简的初级引导程序。大一点的设计里,SPL负责初始化最基础的内存,然后加载完整的U-Boot主体。如果芯片内部SRAM太小,还会引入TPL做二次最小初始化。第二,设备树(Device Tree),它用于描述硬件资源——地址、中断、时钟关系、引脚复用。U-Boot从2015年之后逐渐把传统“配置文件+宏定义”的硬件描述方式迁移到设备树,现在新加入的板子基本都是纯设备树方式了。第三,DEFCONFIG,这是一种精简的配置方案,U-Boot源码下通常有个configs/目录,里面保存着一堆xxx_defconfig,每次用make xxx_defconfig就可以直接生成一份具体的构建配置,这可比逐个打开menuconfig手工点选要高效得多。
这三个概念串成一条线:defconfig决定U-Boot代码怎么编译,设备树决定U-Boot看到的硬件长什么样,而U-Boot本身决定系统怎么把内核带起来。真正动手移植时,很大一部分工作就是在改这三个层面上的内容,剩下的时间里,你基本都在串口调试、反复烧写、抓启动日志。
2. 移植前的准备工作:环境和硬件分析
2.1 拿到一块新板卡,先别急着敲代码
我见过最急躁的同事,芯片手册还没看两页,就直接从某宝淘来的参考板配置开始编译。结果开起来串口一片空白,然后三天都在那儿换晶振、调电阻,特别冤枉。这里我特别想讲一句:移植U-Boot前,首要任务不是打开代码,而是把硬件“画”出來。你需要了解CPU型号、主芯片的家族(比如全志V3s、瑞芯微RV1126、意法半导体STM32MP1,或者带RISC-V核的CH32V305这类小众芯片)、DDR颗粒型号、Flash器件型号、串口对应引脚、启动拨码开关顺序等。
一份好的硬件原理图比任何教程都有效。我自己习惯用表格把关键信息整理好,比如:哪个UART口做调试口,速率是多少;DDR容量和位宽是多少,是DDR3还是DDR4还是LPDDR4;复位信号是上拉还是下拉;SD卡走的是SDIO 2.0还是3.0;Flash是SPI接口还是eMMC,访问模式是什么。这些信息在配置U-Boot时,每一个都会形成一个具体的参数,比如内存选择、时钟频率、Mux配置,出错就可能导致系统跑飞。
2.2 我的交叉编译环境搭建清单
环境搭建这块,不夸张地说,很多人卡在了连编译都过不去。U-Boot的编译系统依赖gcc-arm-工具链,但我更加常用gcc-arm-linux-gnueabihf或者aarch64-linux-gnu-gcc,具体选哪个要看目标架构。以我常用的方式为例,安装交叉编译器后,建议再装device-tree-compiler和make、bison、flex、python3等基础工具。
一个容易忽视的细节是,U-Boot对编译器版本比较在意,太新的GCC也偶尔会有问题,比如某些老版本U-Boot使用GCC 12编译时报“隐式函数”警告升级为错误。所以我的经验是:先从某个官方发布版本(比如U-Boot 2024.04)开始,配合比较稳定的GCC 11或GCC 10,这样可以把“编译不过”这类环境问题和板级代码问题分开,不然混合在一块会让人想摔键盘。
构建环境还要特别注意一点:尽量使用Linux环境编译,不要在Windows上硬刚。虽然WSL和一些人可能会说“我在Windows下也能编”,但U-Boot涉及符号链接、shell脚本、分区路径等,原生Linux环境绝对省心得多。我经常看到用WSL编译到一半路径错误、权限问题,真的不如直接装个Ubuntu虚拟机或者在一台Linux宿主机上干。
2.3 硬件手册里的关键信息怎么提取
对着一本八百页的芯片手册,很多人半天摸不到重点。我这里给一套比较实用的抓重点顺序。
第一步查“Memory Map”。看DDR控制器地址范围是多少,各外设的基地址分别在哪儿。U-Boot设备树里几乎每个节点都要用到地址和中断,地址写错一个数,整个设备节点就无法工作。第二步查“Clock tree”。U-Boot启动早期需要初始化系统时钟,大多数SoC默认跑在慢速内部振荡器,你需要在代码里配置PLL,提高CPU频率和总线频率,同时保证串口波特率计算准确。第三步查“Pin Mux”。例如串口TXD/RXD,主控经常支持多个引脚复用,你需要找到默认启动介质对应的引脚组合,否则BootROM能启动,U-Boot却没法通过串口交互,很多奇怪的“假死”都是引脚配置不对造成的。
做这一步时,其实很多SoC厂商都会提供一个官方评估板的设备树或者板级头文件,你要做的就是从他们设备树里找出与你设计相同或相近的部分,拿过来改。这比从零写要快得多,而且兼容性更有保障。
3. U-Boot移植的核心流程分步拆解
3.1 第一步:找到最接近的参考板
U-Boot源码的board/目录下按厂商做了分类,比如board/rockchip、board/sunxi、board/st。移植的第一步永远是“找亲戚”,而不是从零开始。我通常会比较SoC内部资源,比如同样是A35核心的芯片,可能就可以直接参考同系列其它芯片的板级配置,再修改内存和时钟部分。如果是完全不同的芯片厂商,那么至少可以参考同为Cortex-A系列或者RISC-V系列的启动流程,搞清楚SPL阶段的入口函数在哪里,然后逐步替换为当前芯片的实现。
这是一个很实用的建议:先跑通“最小系统”,再添加功能。最小系统包括:串口输出、内存初始化、从启动介质加载下一阶段镜像。你可以在参考板配置基础上,把板级文件复制一份并改名,然后删除所有与存储、网络、显示相关的功能,让U-Boot可以编译通过,并在串口看到“U-Boot SPL”字样。这一步成功了,你就从“移植小白”升级成为“有基础板的移植者”。
3.2 第二步:板级目录与Kconfig配置
每个板级目录下都会有一个Kconfig,用来描述在这个板子上可选的功能选项。新建一块板子,你需要在arch/<arch>/mach-xxx/Kconfig或者board/<vendor>/<board>/Kconfig中添加新的配置项,比如TARGET_MY_BOARD。然后还要在configs/my_board_defconfig中指定默认配置。这两个文件配合相当于告诉U-Boot构建系统:“这款硬件存在,并且默认只开启这些功能”。
实际操作中,我习惯把参考板的defconfig复制过来,然后用make menuconfig逐项查看差异项。比如参考板使用了DDR3,我这边是DDR4,就需要去CONFIG_SYS_SDRAM_SIZE、时序参数相关项中修改。还有CONFIG_SYS_TEXT_BASE,这是U-Boot代码的链接地址,必须和内存映射中预留的区域一致,否则跳转会跑飞。这些配置虽然看起来繁琐,但它们之间是互相约束的,改一处不查其他地方,很容易在后续调试中造成困惑。
3.3 第三步:设备树文件的修改
设备树是U-Boot移植中“占比最大”的细节工作。当前绝大多数U-Boot都采用与Linux内核相同风格的设备树,也就是说,你把U-Boot的设备树写得越完整,后面内核移植时也越轻松,两者往往可以共用大部分dts。
首先在arch/<arch>/dts/下创建一个my_board.dts文件,然后#includeSoC共用的.dtsi描述(里面就是内存控制器、中断控制器、时钟、UART等基础节点)。再在.dts中覆盖/添加板级外设:比如调试串口节点,需要设置compatible = "ns16550a"或厂商自定义的属性,以及reg = <0x 0x10010000>这样的地址,还要设置clock-frequency属性,这是很多串口初始化班机的关键,频率不对甚至会导致打印乱码。chosen节点中的stdout-path也要指向你的串口,U-Boot才会把控制台输出导向这个设备。
此外,网卡、SD卡、USB、LCD控制器都在设备树中对应节点。你的板子如果用了某款PHY芯片,就需要覆盖phy-handle属性,甚至添加新的GPIO复位控制。我见过不少“U-Boot能起来,但网卡ping不通”的案例,最后基本都是设备树中MAC节点地址、PHY地址或者reset-gpio配置与硬件不符。
3.4 第四步:驱动适配(串口、网卡、存储)
设备树只解决“硬件长什么样”的问题,底层的驱动代码才是真正操作寄存器的地方。好在U-Boot本身自带了很多驱动,尤其是相对通用的串口、网卡、MMC、USB设备。大部分时候,你要做的不是重写驱动,而是让驱动匹配你的设备树节点和板级初始化函数。
以调试串口为例:早期SPL阶段,因为没有设备树或还没有完整解析设备树,你可能需要直接调用board_debug_uart_init()这样的板级函数,在这里面通过寄存器直接配置UART的引脚复用、时钟使能和波特率。一旦U-Boot主体跑起来,它才会切换到设备树中的串口驱动。这一步的调试比较伤脑筋,经常出现SPL阶段串口有打印,主体U-Boot阶段串口没打印,此时要重点检查设备树节点里u-boot,dm-pre-reloc标志有没有加上,这个标志决定该设备是否在U-Boot重定位前就被初始化。
网卡驱动方面,比较常见的是设计中使用千兆以太网,但U-Boot驱动里只有百兆稳定或者需要额外初始化PHY芯片。这种情况下,你可能需要先用裸机GPIO操作让PHY进出自定义复位流程,再用mii命令查看PHY状态。我的习惯是,先把芯片官方自带的中断式驱动丢到一遍,直接用mdio命令扫描PHY地址,确认物理层通了后,才继续调试MAC收发。这件事急不得,越急越乱。
3.5 第五步:编译、烧写、启动
这一步骤虽然简单,但很讲究顺序。编译U-Boot命令通常是:
make my_board_defconfig make CROSS_COMPILE=arm-linux-gnueabihf- -j16如果芯片是64位,将arm-linux-gnueabihf-换成aarch64-linux-gnu-。如果板子上没有SPL,则还会生成整个U-Boot的二进制镜像,常见的是u-boot.bin。如果有SPL,那么SPL和U-Boot主体会分开生成,还需要用mkimage工具去封装成一个引导镜像(比如spl/sunxi-spl.bin),然后打包成适合SD卡或者Flash烧写的格式。
烧写这个环节非常考验细心程度。对于SD卡,通常用dd直接写入偏移地址,例如SD卡前1M区域存放SPL,比如:
sudo dd if=spl/boot.bin of=/dev/sdb seek=1 conv=fsync sudo dd if=u-boot.itb of=/dev/sdb seek=64 conv=fsync这里的偏移值不是随意来的,而是芯片BootROM约定好的。对于SPI Flash,就要用flashcp或sf probe配合sf write写入。必须提醒的是,烧写前要看清楚你的板子的启动配置参数(拨码开关、电阻选择),很多时候不是软件写错,而是启动源选错——U-Boot压根没被拉到内存里执行。
4. 调试U-Boot时的那些坑和排查技巧
4.1 串口没有输出怎么办
“串口没有输出”绝对是U-Boot移植路上遇到的第一个顽固障碍。为什么我敢说第一?因为你所有的调试手段几乎都依赖这一条通道,没有打印,基本变成盲操作。
遇到这种情况,先不要急着怀疑代码,按顺序检查硬件链路:测量调试串口的TXD引脚电平,确认有没有数据跳变。如果是电平一直在低或者一直高,则确认调试串口有没有选错引脚,或者是波特率不匹配。很多开发板的调试串口板载了一个USB转串口芯片,如果驱动没安装或者接线交叉了,也会导致没输出。当确定硬件链路没问题后,再检查U-Boot源码里关于早期输出功能有没有打开:GCC编译时是否定义了CONFIG_DEBUG_UART,以及板级初始化函数debug_uart_init是否被调用。SPL阶段比较常见的问题是,board_init_f函数里SCB、DDR初始化还没完成,串口就已经输出,此时如果时钟不稳定,打印就会是乱码或丢失。所以在SPL阶段,我会把串口延时放到clk_init之后,保证波特率计算稳定。
一个实用的排查顺序是:用示波器看TXD脚有没有初始化为复用功能(不是GPIO)、量电压是否正常、再检查BootROM阶段有没有默认把串口打开(有些芯片用拨码开关控制是否从串口下载模式启动)。这块坑多,但排查熟练了,基本十分钟就能定位。
4.2 内存映射错误导致的异常
DDR是U-Boot运行的地基。代码能编译、能下载,但一执行就“死”,十有八九是DDR配置不对。这里的异常通常分几类:跳转地址非法、内存带宽不对导致数据错乱、内存训练失败导致随机崩溃。很多新板子会拿参考板时序参数直接硬套,这很常见,但DDR颗粒不同,走线长度不同,都需要重新调整控制器参数。U-Boot里有些SoC通过dram_init函数读取或者计算内存大小,也可能需要你在板级文件里写死gd->ram_size。
我的建议是,直接用芯片厂商提供的DDR初始化或者培训工具,先把DDR跑稳了,再进入U-Boot移植的正题。这里又回到前面“找准参考板”的步骤上,如果参考板用的DDR型号和你的不一样,差一个bit位宽或者频率等级,都需要仔细核对芯片的DDR控制器寄存器。如果不确定,宁可先跑低频率,等稳定后再逐步提高。经验之谈:高性能不是第一步,稳定输出才是。
4.3 存储设备访问失败的排查
U-Boot起来之后,最重要的功能之一就是从存储介质读取内核,比如SD卡、eMMC、Flash。存储设备访问失败的报错往往五花八门:Card did not respond to voltage select、MMC init failed、** Unrecognized filesystem type **,等等。
如果遇到SD卡无法识别,先检查设备树节点中SDIO/MMC的电源和GPIO复位引脚;再检查是否有信号线反了或者RDLY/CLK信号质量不好。简单的方法是先用mmc list查看有几个MMC设备,再用mmc dev和mmc info看设备细节;如果设备能看到,但仍无法读取分区,那可能是分区表位置烧写有问题。这里有个经常被忽略的细节:某些主控的SD卡检测脚(CD)默认是输入上拉,如果你硬件上没有接卡检测,需要在设备树里把broken-cd属性加上,告诉驱动“卡一直在线”,否则驱动会因为偶尔检测不到卡而拒绝访问。
Loop测试法比较实用:先格式化一张FAT32的SD卡,放入一个已知的文本文件,再用fatls mmc 0:0命令,看能不能列出文件。如果列出失败,可以试着换一张卡,有时候卡兼容性问题也会导致特定卡不能工作,这不是U-Boot代码的问题。
4.4 常见问题速查表
| 症状 | 可能原因 | 排查方向 |
|---|---|---|
| 串口完全无输出 | BootROM跳转失败、串口未初始化 | 检查启动介质、引脚复用、波特率 |
| 串口输出乱码 | 时钟频率不对、波特率配置错误 | 核对UART时钟、分频系数 |
| U-Boot启动后自动重启 | 看门狗未关闭、DDR不稳定 | 禁用看门狗,降低DDR频率 |
| SD卡无法读取 | 电压选择、检测脚、电平转换 | 检查SD供电、CD引脚、broken-cd |
| 网络ping不通 | PHY复位、设备树地址错误 | 先用mdioscan确认PHY地址 |
| 烧写后启动失败 | 偏移地址错误、启动引脚配置 | 核对数据手册中的镜像偏移 |
| 编译时库函数冲突 | 工具链版本过新 | 使用GCC 10/11版本 |
这张表我建议你截图或者记录下来,它基本能覆盖八成的新手问题。我自己调试时,会在串口日志没头绪时,先回到这个表里对照一遍,往往能快速缩小范围。
5. 移植之后的延伸工作与参考索引
5.1 从U-Boot到内核再到文件系统
U-Boot只负责把内核“扶上马”,之后的路还要内核自己走。正因为这样,U-Boot移植完成的标准不只是能启动到U-Boot命令行,而是能通过bootm或booti命令成功加载内核并跳转。跳转后内核会出现新的问题,例如内核完全起不来、屏幕分辨率不对、串口找不到,这些又得回到设备树上去查。
多数团队在项目里把U-Boot和内核的设备树放在同一个仓库中,每次物理改板后,不仅U-Boot要重新编译,内核设备树也会同步修改。日常工作中,我强烈建议把U-Boot移植与内核移植放在同一个工单下做,它们有太多重叠的工作量,比如时钟、GPIO、存储控制器,两边的登记信息完全一致时,系统启动链路才是健康的。
5.2 U-Boot与其他轻量级系统的“协同移植”参考
说实话,国内可穿戴设备、工业HMI产品的研发中,越来越多的团队在同一个主控上同时考虑Linux和RTOS两种启动路径。比如你在STM32MP1、全志V3s或者一些RISC-V芯片上做产品,底层既可能跑U-Boot,也可能直接在SRAM中跑一个精简的裸机程序或FreeRTOS。我见过很多把U-Boot移植思路与FreeRTOS、LVGL移植关联起来的开发人员——这部分经验完全可以互相借鉴:都是先建最小工程,再分层理解和适配BSP。比如做CherryDAP这类HID调试器的移植,虽然目标不是启动Linux,但它的USB协议栈移植过程跟U-Boot中的USB Host驱动移植很类似,都要面对端点描述符、控制传输的超时问题。再比如用EasyLogger做日志系统,你会有意识地先搭好串口通道、保证日志输出不阻塞主流程,这跟U-Boot调试阶段要提前打通串口是完全一样的道理。
从纵向看,还有更多小型中间件可以借鉴:比如在裸机RTOS中实现Modbus协议栈(很多工业项目叫nanomodbus移植),它要求对MCU的时钟和串口外设非常熟悉,这和U-Boot里配置波特率寄存器没有本质区别。不妨这样思考:U-Boot移植不是孤立的,它本质上是“板级支持包(BSP)开发”的一个复杂样例。一旦搞通了U-Boot,再回过头去移植FreeRTOS、LVGL、甚至把Android Studio里的老项目跨到新框架下,你会发现很多套路都是相通的——搜索代码、移植配置、硬件验证。
5.3 我私藏的参考资源索引
虽然U-Boot自带的doc/目录非常齐全,但我还是要单独列几个实用索引路径:doc/README.fec可以看网络驱动实现细节,doc/README.imx8、doc/README.sunxi等对应不同平台,开发新板卡前一定要先扫一遍这些文档。然后是源码里的configs/目录,它相当于一个巨大的“硬件兼容性列表”,每看到一个近似的板子名,我都会比较它和我的板子有什么差异。再然后是Linux内核里的设备树和驱动代码,因为U-Boot和内核的设备树大部分能共用,Linux的驱动代码往往写得更规范,理解得更清晰后,你反而不是总去查芯片手册,而是去对照Linux驱动中的寄存器操作。
在线资源里,U-Boot邮件列表是一个几乎能找到所有“掉坑”经验的地方,但直接搜索稳定版本源码的git log也是个好主意。尤其是在git log --oneline -- <文件路径>里查某个驱动文件的历史提交时,你能看到很多“fix: 兼容XXX板卡”的消息,这往往就是你的板卡资料。多去翻这类日志,比盲目百度要好太多。
6. 最后再补充点我在实际操作中的体会
真要说心得体会,我会反复强调一个词:剪枝。一开始移植U-Boot,很多人恨不得把所有功能都打开,网络、显示、USB、文件系统全都要,结果编译又大又慢,一个问题嵌套另一个问题,很难定位。正确的做法是先砍到只剩调试串口和从启动介质读取内核这两条路,跑通之后再一截一截加回功能,每加一个功能,就做一次完整的启动测试和压力测试。
另一个经验是善用串口控制台的多个层级:U-Boot的命令行界面本身就是一个极好的调试器。不要只把串口当打印口,你可以通过md、mm命令直接改写某个地址的内存,通过sf命令读写SPI Flash,通过tftp从网络下载镜像,还能通过setenv修改环境变量。很多硬件问题在U-Boot阶段就能顺手排查掉,根本不用等到Linux起来后在Linux驱动里瞎猜。
嗯,U-Boot移植是一个看似复杂、实际上很成体系的工作。只要把本文开头的那些思维框架装进脑子里,再按照后面的步骤认真走一遍,你一定可以把一块陌生的板子快速跑起来。等你有了一两个成功案例,再回看这块工作,你会觉得它的难度更多来自于信息分散,而不是技术门槛。我希望能把这些分散的信息用索引的方式帮你收拢起来,让你能少走一些冤枉路。