ARTICLE DETAIL

资讯详情

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

U-Boot移植实战笔记:从最小系统点亮到内核引导

U-Boot移植实战笔记:从最小系统点亮到内核引导 搞嵌入式的谁没被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 → SPLSecondary 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 ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- make myboard_defconfig make -j8这里有一个细节ARCHarm指的是U-Boot的架构目录不是Linux内核那种区分arm和arm64的方式。U-Boot用ARCH统一指代CPU架构而用CONFIG_ARM64来区分是32位还是64位。所以你在64位平台上也经常看到ARCHarm不要觉得奇怪。工具链和源码版本还有个隐性关联新版本U-Boot引入了binman、加密签名等构建工具这些工具依赖Python版本和系统库。如果编译到某个阶段报module找不到优先考虑是不是系统Python太老而不是源码有问题。3. 实际动手跑起来的关键几步3.1 新建板级目录与defconfig照着抄但要抄明白整个移植动作里最安全的操作就是复制但复制不等于无脑粘贴。你复制参考板的文件之后必须把里面任何与原有板子绑定的信息替换掉否则会出现“明明改的是自己的板子行为却还是参考板的”这种灵异事件。我的操作顺序是这样的先在configs/目录下把参考板的defconfig复制一份改成自己的板名比如myboard_defconfig。打开这个文件找到CONFIG_DEFAULT_DEVICE_TREEmcimx6ull-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节点长这样memory80000000 { device_type memory; reg 0x80000000 0x20000000; };reg里的第二个数字是内存大小0x20000000就是512MB。很多同学板子上实际焊了256MB内存结果忘了改这里U-Boot跑起来以为有512MB内核启动时访问到不存在的地址直接崩溃或随机死机。这类问题非常隐蔽。第二件必须确认的是串口节点。包括clocks、pinctrl、status三个属性。你可以参考原厂dts里同系列芯片的串口写法把引脚复用改成自己板子的实际连接。比如i.MX6ULL的uart1在dts里一般叫uart1pinctrl节点引用pinctrl_uart1。改完之后编译设备树可以单独执行make myboard.dtb它会生成编译好的设备树文件配合反编译工具dtc -I dtb -O dts就能确认内容是否真的改对了。第三件才是网卡、SD卡、USB这些。我的建议是先把串口跑通再回来加这些不要一次性堆太多。网卡改的时候重点看PHY地址很多板子PHY地址是0x1或0x2设备树里写错了会导致miiphy_register之后一直link不上。3.3 编译烧录与启动日志的首次点亮串口和设备树改好之后就可以完整编译一次了make clean make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- myboard_defconfig make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j8编译完去u-boot.bin、u-boot.dtb、SPL这三个主要产物。不同平台的烧录方式不同以从SD卡启动为例经常能看到这样的命令sudo dd ifSPL of/dev/sdb bs1k seek1 convfsync sudo dd ifu-boot.bin of/dev/sdb bs1k seek69 convfsync这里的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 TestRockchip的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、复位GPIOmd命令读PHY寄存器SD卡无法识别设备树MMC节点、供电GPIOmmc 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_LOGy之后在命令行执行log level 7可以看到驱动初始化过程的详细日志。外设初始化失败时这条命令通常比你自己猜半天强得多。5. 移植方法论的可迁移性不止是U-Boot5.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提交为一条独立commitcommit信息里写明改动原因和硬件版本号。下次拿到一个新板子git log一翻就能知道这块板子走了多少弯路。另外强烈建议在你自己的板级目录里写一个README把烧录命令、串口参数、内存配置来源、phy地址这些只有“当时人才知道”的信息记下来。U-Boot移植这个东西三个月不看你再回来看自己写的代码都会陌生更别说同事接手了。我在实际项目里还养成了一个习惯凡是遇到并解决了的问题就在源码旁边用注释记一行“为什么”。这个注释在三个月后救过我很多次因为那时候你早忘了当初为什么要把某个PHY配置写成那样。最后U-Boot移植真正值钱的不是最后跑起来那一下而是在这个过程中被逼出来的调试能力和对硬件的理解。我现在拿到一块新板子还是会先把参考板的README和原厂dts读一遍然后才动手改。这个过程没有捷径但也不是莽撞地试错。希望这篇索引式的分享能帮你在接下来的移植任务里少一点半夜盯着串口工具发呆的时间。
返回列表