ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

U-Boot移植全流程索引:从SPL、设备树到调试实战

U-Boot移植全流程索引:从SPL、设备树到调试实战 很多人问我“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.0Flash是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_COMPILEarm-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 ifspl/boot.bin of/dev/sdb seek1 convfsync sudo dd ifu-boot.itb of/dev/sdb seek64 convfsync这里的偏移值不是随意来的而是芯片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移植是一个看似复杂、实际上很成体系的工作。只要把本文开头的那些思维框架装进脑子里再按照后面的步骤认真走一遍你一定可以把一块陌生的板子快速跑起来。等你有了一两个成功案例再回看这块工作你会觉得它的难度更多来自于信息分散而不是技术门槛。我希望能把这些分散的信息用索引的方式帮你收拢起来让你能少走一些冤枉路。
返回列表