ARTICLE DETAIL

资讯详情

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

U-Boot移植实战指南:从原理到排错的完整索引

U-Boot移植实战指南:从原理到排错的完整索引 做过嵌入式Linux开发的人都知道U-Boot移植是绕不开的一道坎。不管是自己画了块板子还是接手一个不太常见的开发板想把Linux顺利跑起来第一步都是先把U-Boot弄上去让串口能打印、DDR能初始化、内核能被加载。这篇文章我就把它当成一张“索引图”来写把U-Boot移植从整体思路、原理细节、实操流程到排错技巧整个链路梳理清楚给正准备移植或者已经踩进坑里的朋友一条可以照着走的路线。U-Boot不是万能的但摸清它的脾性之后你会发现移植这件事本来就没有想象中那么玄学。我先说个结论U-Boot移植百分之八十的功夫都在“查资料”和“抄作业”上真正需要从零写代码的地方少之又少。它本身就是一个支持了几百种板子的开源引导程序代码里的层级和配置机制已经把“适配新硬件”这件事设计得很好。你要做的就是找到一块和你板子最接近的参考平台然后沿着U-Boot预留的接口把你的硬件信息填进去。下面我按实际移植时会遇到的知识点、操作步骤和坑位挨个说。1. 项目概述与核心思路拆解1.1 为什么需要U-Boot移植先理清一个概念U-Boot是系统上电后最早运行的软件之一它的任务是在Linux内核“起床”之前先把硬件环境收拾好。硬件初始化包括设置CPU时钟、初始化DDR内存控制器、配置串口引脚、识别存储介质、初始化网卡等。这些事如果没有引导程序来做内核本身很难独立完成。你可以把U-Boot想象成酒店前台客人内核入住之前前台得先把房间的电、水、网络都通好再把房卡启动参数交到客人手上。但问题来了每块板子的SoC型号、DDR颗粒、时钟晶振、引脚复用、存储布局都可能不一样。U-Boot虽然自带很多板子的支持却不可能预知你这块板子长什么样。所谓“移植”本质就是把U-Boot里已有的通用框架适配到你的具体硬件上让引导程序认识你这块板的“五脏六腑”。这件事的适用范围很广自己设计的硬件板卡、公司定制的车载设备、基于新SoC做的评估板、老旧开发板的系统升级都会用到U-Boot移植。很多做驱动开发、系统集成、BSP维护的工程师第一份正经工作往往就是改U-Boot让它在新板上跑起来。1.2 整体思路找参考板而不是从零开始U-Boot移植最忌讳一上来就想着“重写”。正确姿势是先找一块与目标硬件最接近的板子作为参考平台。所谓“最接近”包括几个维度SoC型号或同系列、DDR颗粒类型、启动介质SD卡、eMMC、SPI Flash、板载网卡型号。比如你的板子用的是某厂商的Cortex-A7四核SoC那就优先看这个SoC原厂评估板在U-Boot里对应的board目录如果没有现成的再找同芯片厂商、同架构系列的其他板子来参考。为什么这个策略最有效因为U-Boot的代码做了很好的分层CPU相关的初始化在arch/目录下厂商级别的通用代码在mach-目录里真正“板级差异”才放在board目录。这意味着大部分通用的时间、中断、串口驱动你根本不用碰只需要处理板级差异部分。选对了参考板你的工作量可能从“几个星期”直接降到“两三天”。方案上有三条路一是在已支持的板子基础上增量修改只改dts配置和defconfig二是复制一个同系列板子的board目录改头文件适配新板三是完全新建board目录从零搭板级代码。绝大多数场景走前两条路就够。只有当你用的SoC在U-Boot里完全没支持时才需要走第三条路那工作量会大很多要深入照芯片手册配时钟、DDR、GPIO。2. 核心细节解析与实操要点2.1 移植前必须摸清的硬件清单动手之前先把硬件的“家底”搞清楚这一步做扎实了后面能少踩一半的坑。我列一下最关键的几项SoC型号与架构是ARMv7还是ARMv8是Cortex-A7还是Cortex-A53这决定了你用32位还是64位启动流程。DDR颗粒信息和容量型号是DDR3还是DDR4数据位宽是16bit还是32bit容量多大频率跑多少。这些参数要能从原理图和DDR芯片手册里查到。串口信息UART是哪个控制器地址用的是哪个引脚复用GPIO有没有去控制。启动介质系统是从SD卡、eMMC、SPI NOR还是NAND启动启动设备对应的硬件接口是什么。网卡和PHY网卡控制器型号、PHY芯片型号、PHY地址一般通过原理图上地址电阻确认。晶体频率整个系统的时钟源头是24MHz还是25MHz等所有PLL倍频都从它算起。建议画一张硬件信息表对照原理图和芯片手册逐项填。实际操作中很多人连“串口接到了哪个UART控制器”都没查清楚就开始编译结果串口死活没输出回头才发现用的UART1代码里配的是UART2这种低级错误特别浪费时间和心情。2.2 U-Boot的四层配置体系U-Boot这套配置系统说到底是四样东西互相配合defconfig文件、Kconfig菜单、板级头文件、设备树。我分别说一下它们的分工。defconfig文件就是“总开关清单”存在configs/目录下比如configs/myboard_defconfig。它的内容是一堆CONFIG_XXXy这样的宏编译器会通过这些宏决定编译哪些文件、打开哪些功能。make myboard_defconfig就是告诉U-Boot“我在选这款板子的配置”。Kconfig是菜单系统对应make menuconfig可视化界面。它负责把配置选项的依赖关系理清楚防止你选了一个没依赖的选项导致编译失败。一般不直接改Kconfig但你要知道它存在。板级头文件是include/configs/myboard.h里面保存的是“没法用宏来表达”的乱七八糟参数DDR时序寄存器值、内存映射地址、环境变量默认值、启动命令默认值等。虽然现在U-Boot在大力推行“配置尽量往Kconfig和dts里迁移”但你移植过程中大概率还是要动这个头文件。设备树dts负责描述硬件拓扑内存布局、串口地址、GPIO、时钟、网卡都在哪。内核启动时会用到同一个设备树所以这里写错了就算U-Boot能起来内核也会遭殃。这四层的关系可以这样理解defconfig决定“编译哪些模块”头文件决定“模块运行时用哪些参数”dts告诉系统“硬件长什么样”Kconfig管理它们之间的依赖。移植时你基本是在改defconfig和dts偶尔动一下头文件。2.3 启动流程SPL、U-Boot proper与内核U-Boot现在普遍采用“SPL proper”两级架构这个必须搞清楚。所谓SPLSecondary Program Loader是一个迷你版引导程序因为硬件刚上电时DDR还没初始化芯片内部SRAM又太小装不下完整U-Boot所以先把SPL跑起来它只做极少的事初始化最基础的时钟、串口、DDR然后从启动介质里把完整的U-Boot proper加载到DDR里跳转过去执行。这个过程有点像你搬进新家先请一个水电工SPL把水电通上然后才能把家具U-Boot proper搬进屋放好最后再请主人内核入住。SPL阶段是移植时最容易卡住的地方。因为这时候还没有完整驱动出了问题只能靠串口打印来定位。U-Boot的SPL会输出类似“U-Boot SPL 2023.04-rc1”的Logo信息然后进入初始化流程每一步都有debug打印可以打开。如果连这行Logo都看不到说明SPL本身根本没跑起来或者串口配置不对。SPL把U-Boot proper加载到DDR后proper阶段会做完整的板级初始化执行board_init、env_init、relocate等步骤最后跳到命令行。命令行正常出现后你可以敲sd、mmc、ping、fatls这些命令测试硬件功能。再往下才是引导内核由bootcmd命令完成比如从MMC根分区读取内核镜像并跳转执行。3. 实操过程与核心环节实现3.1 搭建开发环境与初始化代码树我用一块虚构的板子来演示它基于某厂商的Cortex-A7四核SoC主频设计为1.2GHz外接两颗DDR3颗粒组成512MB、16bit位宽的存储串口挂在UART2控制器上系统从SD卡启动PHY是常见的RTL8211E千兆网卡。这个配置很典型你对照自己的板子把参数换掉就行。环境方面交叉编译工具链必须和你的SoC架构匹配。以ARM 32位为例我习惯用arm-linux-gnueabihf-gcc 12.x版本如果是64位ARM则用aarch64-linux-gnu-gcc。U-Boot源码从官方git仓库拉取后先切到一个稳定的、和你SoC支持情况匹配的分支不要直接用主线最新代码因为主线每天都有新改动有时会比较激进。我一般选和维护周期较长的板子支持对应的release tag。交叉编译工具链的版本会影响最终生成的镜像是否能在你的板子上正常运行但更关键的是确认你的工具链支持目标CPU架构的浮点运算比如hard-float还是soft-float这要跟U-Boot/内核编译配置保持一致不然启动时会碰到“Kernel panic - not syncing”这类浮点异常问题。3.2 新建板级支持文件这里我以新建board目录为例展示最完整的流程。如果只是改现有板子步骤可以大幅缩减。首先在U-Boot源码根目录下创建board/mycompany/myboard目录mkdir -p board/mycompany/myboard cd board/mycompany/myboard目录里至少需要三个文件Kconfig、Makefile和一个主代码文件通常叫myboard.c。Kconfig用来声明板子名称并接入配置系统if TARGET_MYBOARD config SYS_BOARD default myboard config SYS_VENDOR default mycompany config SYS_SOC default mysoc endifMakefile告诉编译系统要编哪个文件obj-y myboard.omyboard.c里则实现板级初始化函数最关键的是board_init和board_late_init。board_init里要做的事情包括配置引脚比如串口、MMC的引脚复用、初始化GPIO控制信号比如板上的电源使能脚、复位脚、设置系统主频。#include common.h #include asm/arch/gpio.h #include asm/arch/clk.h int board_init(void) { /* 使能串口引脚复用UART2 TX/RX */ writel(0x00000003, 0x01c20800 0x00); /* 示意代码按实际芯片手册填写 */ /* 打开板上3.3V电源使能脚 */ gpio_request(PA15, power_en); gpio_direction_output(PA15, 1); return 0; }实际上每个SoC厂商的寄存器地址和GPIO操作函数都不太一样务必照着参考板的现有代码改不要凭空猜。我当时第一次写这个函数时就直接把参考板的寄存器值抄了过来再对着原理图衍射出来的地址差异逐个确认效果比手工造寄存器值可靠得多。3.3 编写defconfig配置configs/myboard_defconfig文件决定了整个U-Boot的编译范围。这里挑几个关键配置项说一下CONFIG_ARMy CONFIG_ARCH_MYSOCy CONFIG_SYS_BOARDmyboard CONFIG_SYS_VENDORmycompany CONFIG_SYS_CONFIG_NAMEmyboard CONFIG_DEFAULT_DEVICE_TREEmyboard CONFIG_TARGET_MYBOARDy CONFIG_MMCy CONFIG_MMC_SUNXIy CONFIG_NETy CONFIG_RTL8211E_PHYy CONFIG_SYS_CLK_FREQ1200000000 CONFIG_SYS_TEXT_BASE0x4a000000 CONFIG_BOOTDELAY3CONFIG_SYS_TEXT_BASE是U-Boot proper在DDR里的链接地址这个必须要跟你SoC的内存映射对齐。DDR起始地址加偏移就是它偏移量一般留64MB避免覆盖SPL和U-Boot own的早期数据。如果地址填错现象往往是SPL能打印跳转后却一片死寂或者打印几个字符就挂。配置好之后用make命令生成.config文件make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- myboard_defconfig然后开始编译建议用多线程make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j8编译顺利的话会在u-boot.bin、spl/u-boot-spl.bin两个镜像。如果编译报错多半是Kconfig依赖没满足或者文件没创建完整。此时回到源码目录检查board/mycompany/myboard下的三个文件是否齐全以及defconfig里CONFIG_TARGET_MYBOARD是否能在Kconfig体系里找到对应的入口。3.4 设备树把硬件结构告诉系统设备树是移植里的重头戏。在arch/arm/dts/myboard.dts里你需要描述这板子的内存、串口、MMC、以太网等节点。最简单的做法是找到参考板的dts文件复制一份然后修改差异部分。内存节点尤其重要直接关系到DDR容量能不能被系统认到/ { model MyCompany MyBoard based on MySoC; compatible mycompany,myboard, vendor,soc; chosen { stdout-path serial0:115200n8; }; memory80000000 { device_type memory; reg 0x80000000 0x20000000; /* 512MB DDR3 */ }; };这段compatible要跟U-Boot和内核里匹配的driver对应上否则后面驱动加载不到。这里我提个细节串口地址要和board_init里配的引脚一致比如整个SoC的UART2基地址是0x01c28400就在dts的serial节点填这个地址同时把clocks属性和中断号一并填好系统才能正确初始化。设备树写完需要确认它会被编译进最终的dtb文件里。检查arch/arm/dts/Makefile里有没有包含你的dts没有的话要加上dtb-$(CONFIG_TARGET_MYBOARD) myboard.dtb如果你的板子是通过FIT镜像统一打包U-Boot和dtb的还要确认mkimage配置和ITS文件里引用了正确的dtb路径。3.5 参数计算从晶振到DDR频率这里演示一个典型参数推导过程很多新手在这里一头雾水。假设板上的晶振是24MHzSoC要求CPU主频1.2GHz。通常SoC的时钟树是晶振 - PLL - 分频器 - CPU/AXI/AHB。PLL倍频到多少取决于具体芯片的PLL范围和分频系数。核算方法通常是24MHz * 50 1200MHz如果PLL的VCO在600MHz~1.2GHz之间这个50倍频是可行的。AXI总线和AHB总线有最大频率约束比如AXI不能超过400MHz那就需要分频器把1.2GHz / 3 400MHz。串口波特率也依赖PLL输出的UART时钟源比如UART时钟是24MHz才可能精确输出115200波特率。如果配错时钟串口打印会变成乱码。DDR参数就更需要仔细了。以DDR3-1600为例Memory Clock是800MHz数据速率1600MT/s一个时钟周期tCK1.25ns。DDR控制器的时序寄存器里需要填tRCD、tRP、tRAS这些值单位通常是时钟周期换算方式就是“实际时间除以tCK再四舍五入”。虽然现代SoC多数支持DDR PHY自动训练自动从SPD读取时序但你的板子如果是并行DDR没有SPD的离散颗粒裸板上首次启动时大概率还是需要在头文件里预填一组相对保守的时序参数能稳定起来之后再慢慢优化。这块我要多说一句DDR参数错得不离谱时现象往往是U-Boot能打印一部分但一跳到DDR测试或拷数据就死机。如果你看到“Unhandled exception: Data Abort”这类信息或者卡死在“Loading U-Boot ...”优先怀疑DDR时序。3.6 烧录与首次启动验证镜像编译出来后烧录方式取决于启动介质。SD卡启动的话用dd命令把SPL写到SD卡的特定偏移位置比如很多SoC平台要求SPL烧在8KB偏移U-Boot proper烧在32KB或64KB偏移具体数值照文档来。写完后插入板卡上电串口连接115200 8N1观察输出。首次上电期望看到一个渐进式的日志U-Boot SPL 2023.04 Trying to boot from MMC1 U-Boot 2023.04 DRAM: 512 MiB MMC: mmc01c0f000: 0 Net: eth01c30000 Hit any key to stop autoboot: 3看到“DRAM: 512 MiB”基本可以松一口气这说明最难的DDR初始化已经过了。如果卡在SPL阶段先打开SPL的调试开关在defconfig里加CONFIG_SPL_DEBUGy重新编译后串口会打印更多调试信息能精确到卡在哪一步。到了命令行之后敲mmc info验证SD卡识别用dhcp或ping测网络用fatls mmc 0:1 /看看文件系统能否读取。这些命令每通过一个对应的功能模块就移除了一个疑点。不要急着引导内核先把这些基础功能测完再说。4. 常见问题与排查技巧实录4.1 问题速查表移植过程中会遇到的问题看起来很玄但大多数可以归纳为下面几类。我结合自己的实践整理了一个速查表方便你对照排查现象可能原因排查手段串口完全无输出晶振未起振、UART引脚复用错、波特率不匹配、SPL根本没编译进烧录位置示波器抓晶振引脚核对芯片手册UART引脚复用表检查烧录偏移尝试不同波特率卡在“U-Boot SPL”后无进一步输出DDR初始化失败、串口在DDR之后才初始化打开SPL_DEBUG看卡点把DDR时序改成保守参数检查DDR供电电压SPL打印正常跳转proper后无输出CONFIG_SYS_TEXT_BASE与DDR映射不符、镜像被破坏核对链接地址和DDR起始地址检查镜像偏移用JTAG查看PC值DRAM容量显示不对dts内存节点reg的值错了检查reg的起始地址和size确认是十六进制换算正确MMC/SD识别不到引脚复用没配、SD供电没使能、控制器时钟不对检查board_init里的MMC电源脚用mmc list看设备是否挂上核对dts的mmc节点网络ping不通ethaddr没设置、PHY地址不对、网卡驱动没选对命令行先setenv ethaddr用mii info查看PHY寄存器确认PHY芯片的地址引脚环境变量无法保存存储介质分区大小不对、保存位置被其他数据占用检查CONFIG_ENV_OFFSET和CONFIG_ENV_SIZE确保和分区表一致我自己碰到最多的其实是第一类串口没输出。新手往往以为是代码问题结果查了半天发现是烧录偏移写错SPL根本没被放到正确位置。所以我强烈建议第一次烧录前先确认SD卡烧录偏移用hexdump校验一下卡上对应位置的内容是不是u-boot-spl.bin的头部数据。4.2 独家避坑经验再说几个常规文档不会写的实操心得这些是我踩了坑之后总结出来的。第一个心得移植初期不要开太多功能。defconfig里天天堆CONFIG_NET、CONFIG_USB、CONFIG_VIDEO一堆选项看着很爽但每一个开启的功能都可能引入新的初始化流程一旦出错你根本分不清是基础硬件的问题还是某个外设驱动的问题。我习惯先做一个“最小系统”配置只保留串口、DDR、SD卡这三样确认能稳定跑到命令行再逐个功能往上加。每加一个功能就编译烧录验证一次即使出问题你也能立刻锁定是新加的那个功能。第二个心得善用打印定位。U-Boot的debug()和printf()是你的左膀右臂。代码里到处都可以临时加printf打印变量的值。比如怀疑时钟配置不对就在clock_init后打印PLL实际频率怀疑DDR时序有问题就在初始化之后把配置的寄存器组打印出来比对。不要觉得这样low这是嵌入式调试最有效的手段远比对着寄存器发呆强。第三个心得直接看二进制比看源码有时候更直观。U-Boot编译生成的u-boot.map文件能告诉你代码的链接布局u-boot.bin的符号表可以用nm工具查。当你知道某个函数的地址结合JTAG读PC值就能精确定位崩溃现场。可惜很多人连map文件打开都没打开过浪费了这个好东西。第四个心得备份你的每一个“能跑”的版本。我见过太多人改了几行配置后板子起不来了却再也回不到之前能跑的版本。用git在U-Boot目录里管理每个里程碑每次“起得来”就commit一下。别嫌麻烦一次手滑能让你白干两天。4.3 进阶排查工具的方法当你遇到棘手问题时光靠串口日志还不够还要学会用硬件调试手段。逻辑分析仪和示波器是排查时钟和时序问题的利器。比如怀疑I2C读取DDR SPD失败就直接抓I2C总线的波形确认通信是否正常。比如怀疑晶振没起振用示波器一量就知道有无振荡波形比反复编译快得多。JTAG/SWD调试器更是雪中送炭。很多SoC支持通过JTAG直接连接调试器你可以单步执行U-Boot的第一条指令查看CPU的PC寄存器走到哪里。我曾经遇到一个非常诡异的问题同样的代码在参考板上正常在我的板子上总在某个地方死机。后来接上JTAG单步才发现是某一处cache操作指令在另一颗不同版本的CPU核上行为不一致换了一个内存屏障宏解决这种问题靠串口日志几乎不可能定位。如果你手里暂时没有这些硬件工具那就退而求其次在代码里做“二分法”注释掉一半初始化流程看问题是否消失逐步缩小可疑范围。比如怀疑某个GPIO初始化导致系统挂起就把它注释掉多测几次找到临界点。5. 移植完成后的扩展思路板子能正常启动、命令行稳定、环境变量能保存这已经算移植成功了。但实际工程里你往往还要继续做几件后续的事。第一件是配置正确的bootcmd让系统默认从你需要的介质启动比如从SD卡的第一分区读取kernel然后从第二分区挂rootfs。实现方式是调整defconfig里的CONFIG_BOOTCOMMAND或者直接在命令行setenv bootcmd后saveenv。这个要尽早弄好因为后续要在U-Boot里反复重启测试。第二件是引入boot.scr或FIT镜像机制。U-Boot完全支持从FIT镜像里同时打包内核、设备树、ramdisk并校验哈希对量产设备来说是一个可靠的部署方案。它的好处是单镜像烧录且启动时可以做完整性校验。如果只是个人学习可以不急着上但产品化一定要考虑。第三件是整理一份“移植备忘文档”。把你在移植过程中确定的硬件参数、寄存器值、坑位记录写下来。相信我三个月后你自己回头看时也会感激这份文档。芯片手册几百页你不可能全记在脑子里但你的备忘文档刚好是浓缩过的重点。我现在的体会是U-Boot移植这门手艺本身没有太多“妙手偶得”的成分它靠的就是细致。细到每个引脚的电平方向、每个寄存器的位域含义、每个启动日志的含义。只要你愿意逐行逐句地把启动过程读明白U-Boot会像一本打开的书一样告诉你它下一步准备干什么、需要什么。最后再分享一个小技巧每次从板子上拿到一份串口日志都别急着断开把完整日志存成文件标注好当时的代码commit编号和改动内容。这份“日志代码”的对照记录是移植调试过程中最宝贵的资产。希望这篇U-Boot移植索引能帮你少走弯路把板子顺利跑起来。
返回列表