ARTICLE DETAIL

资讯详情

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

U-Boot移植实战:从板级配置到串口出字的完整链路

U-Boot移植实战:从板级配置到串口出字的完整链路 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。这个文件里通常包含几类配置架构和CPUCONFIG_ARMy、CONFIG_TARGET_XXXy、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 ARCHarm CROSS_COMPILEarm-linux-gnueabihf- xxx_defconfig make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j8第一条命令生成.config第二条开始编译。编译产物里你需要关注几个u-boot.bin纯二进制可以直接烧到启动介质u-bootELF格式带符号调试用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写在0x0U-Boot proper写在0x20000你如果两个都从0写肯定起不来。这个偏移量在芯片手册的Boot ROM章节里有一定要查清楚。4.3 串口出字移植成功的第一个里程碑当你烧录完接上串口线打开串口终端波特率通常是1152008N1看到类似下面的输出恭喜你移植成功了一大半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: mmc12340000: 0 In: serial Out: serial Err: serial Net: eth0: ethernet12350000 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与USBU-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与bootzU-Boot加载内核有三种主要方式bootm加载uImage老式带U-Boot头bootz加载zImageARM 32位常用booti加载ImageARM 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移植最耗时间的不是写代码而是定位问题。串口没输出、网络不通、内核起不来每一个现象背后可能有好几种原因。建立一套系统的排查方法——从电源、时钟、复位这些最基础的信号查起再到寄存器、引脚复用、软件配置——比记住某个具体问题的答案重要得多。这套方法一旦建立起来你移植任何新板子都会快很多。
返回列表