u-boot入门:从单片机到嵌入式Linux的启动链路与实战
2025年了嵌入式圈子里的入门姿势比前几年丰富太多但我发现一个特别有意思的现象大量从51单片机、STM32一路走过来的朋友接触嵌入式很久了一听u-boot还是发怵。有人觉得那都是芯片原厂工程师才需要碰的东西有人试着看了几天源码被start.S、device tree、Kconfig一堆概念劝退又默默回去点流水灯、调DHT11了。我当年也是从430、51、STM32这么摸过来的后来因为工作原因碰了嵌入式Linux才发现u-boot这东西真没你想的那么高不可攀。恰恰相反它就是从单片机走向真正嵌入式最合适的一座桥。今天这篇就当是跟当初的自己聊天把u-boot是什么、为什么绕不开、怎么上手、踩坑怎么排查老老实实讲清楚。1. 从单片机跨到嵌入式为什么第一站是u-boot先回答一个很多人没想明白的问题我单片机玩得挺熟定时器、中断、I2C、SPI都耍得转直接学Linux内核不行吗为什么非要先学u-boot这么问的朋友多半还没意识到单片机跟嵌入式Linux之间差着一整套启动生态。你在单片机上写程序代码从Flash里执行或者拷贝到RAM里执行一颗芯片、一个IDE、一根下载线就齐活了。按键一按程序就跑起来所谓的启动不过就是芯片内部的复位逻辑把PC指针指到0x08000000或者0x00000000这类固定地址。嵌入式Linux完全不是这个路数。Linux内核是个体量很大的程序它需要运行在DDR内存里需要外部存储SD卡、EMMC、NAND提供镜像需要网络、串口、显示这些外设驱动去交互。问题是芯片上电那一刻DDR根本没初始化外设时钟也没打开连最基本的栈都建立不起来——你连个C语言函数都跑不了更别说Linux内核了。这时候就需要一个领路人在系统最荒芜的时候做三件事把最基本的硬件抠干净时钟、电源、存储控制器把内存打通再把内核从介质里捞出来扔进内存。这个领路人就是u-boot。u-boot全称Universal Boot Loader它继承了早期版本的理念把引导加载这件事做成标准化的通用框架。你完全可以把它理解成一个大号的单片机裸机工程——里面全是寄存器操作、时钟树配置、DDR时序参数、GPIO复用设置跟在STM32上做板级初始化没有本质区别只是它服务的对象从你的业务逻辑变成了下一阶段的Linux内核。我想表达的核心观点是学u-boot不是从零学起而是把你已经会的单片机基本功放到一个更复杂的运行环境里重新整合。寄存器还是那些寄存器外设还是那些外设只是多了一层如何让硬件准备到可以运行操作系统的思维。1.1 别用点灯思维理解u-boot很多单片机朋友初次接触u-boot上来就问我能不能用Keil把u-boot打开然后像点灯一样打断点调试这个思路不算错但会非常痛苦。u-boot的开发和单片机裸机开发有个显著的差别它面向的不是单一的编译-下载-运行闭环而是一套配置-编译-部署的工业化流程。你需要用Kconfig去裁剪功能用设备树描述板级硬件用交叉编译工具链在PC上生成ARM代码最后还要通过烧写或网络把u-boot放到目标设备的启动介质里。这个流程的复杂度和灵活性单片机的IDE开发环境给不了。你得习惯命令行、习惯Makefile、习惯去看芯片手册里的启动模式配置。我把它叫从点灯思维到系统思维的转变。点灯思维关心的是单片机上电后我怎么让一个引脚拉高系统思维关心的是整个链条上每一环节怎么衔接才能在目标设备上把内核跑起来。1.2 为什么恰恰是u-boot而不是其他BootloaderBootLoader其实有不少选择比如老一点的、针对ARM的bootv、armboot还有现在树莓派那套固件以及内核自带的kexec这种应用态引导。但行业里最终u-boot成了事实标准几乎大小芯片原厂、大小开发板厂商都在用它。原因也很直白它支持SoC厂商和板级厂商的定制文化。u-boot看得很清楚天下没有两块完全一样的板子所以它把通用代码和板级代码做了明确分层。SOC厂商只需要维护arch目录下他那颗芯片的底层初始化板厂在他的基础上改board目录里的配置和设备树最终一份u-boot源码可以让几百种不同板子各自编译出各自的镜像。这种分层思想也是单片机世界很少见的。你写STM32工程不同的板子顶多改一下.H文件里的引脚宏很少有人会去设计一套平台层板级层的代码框架。u-boot把这个框架摆在你面前让你第一次直观感受工业级嵌入式软件怎么管理硬件千差万别但软件尽量复用的矛盾。就冲这一点它也值得认真学一遍。2. u-boot的启动链路为什么是多级接力而不是一步到位掌握了u-boot是什么之后下一步就是看它怎么跑起来。我不建议一上来就扎进arch/arm/lib/crt0.S一行行读汇编那样跟拿牛津词典背单词一样容易放弃。先建立整体链路图再按图索骥去源码里找对应环节效率高得多。2.1 芯片启动的多级接力逻辑现代带MMU、带DDR控制器的应用型处理器上电启动几乎都是多级接力的。第一级是芯片内部固化的Boot ROM。这玩意出厂就焊死在芯片里用户改不了。CPU复位后第一条指令就是从Boot ROM里取的。Boot ROM代码量很小它唯一的工作是读芯片的启动引脚电平也就是常见的BOOT_MODE拨码开关判断要从SD卡、EMMC、NAND还是USB/串口启动然后把外设最开始的一小段数据读进芯片内置的SRAM。第二级就是SPL全称Secondary Program Loader。为什么需要它因为芯片内部SRAM一般只有几十KB到几百KB根本放不下完整的u-boot主镜像。而DDR虽然容量大但它是个需要初始化时序才能解锁的控制器——得像宿舍管理员一样先把参数配置好才能让住户代码和数据进去住。所以芯片先运行Boot ROMBoot ROM把位于启动介质开头的SPL读入SRAMSPL在SRAM里完成DDR初始化然后才把完整的u-boot主镜像拷到DDR里。第三级才是完整的u-boot。它运行在DDR里空间充裕、性能充裕这时才谈得上开串口、开网卡、支持命令交互、支持文件系统读取。它最后会读取环境变量里的bootcmd去加载内核镜像和设备树。这个逻辑链和单片机很不一样。单片机常常是上电即跑你不需要考虑内存控制器初始化、不需要考虑启动介质里分段存放引导程序。但恰恰是这种分段接力解决了大程序装不进小SRAM的矛盾。理解了这一点你再看u-boot的编译产物就能对上一一对应的关系启动阶段代码载体烧录位置主要任务Boot ROM芯片固化不可改读启动介质加载SPL到SRAMSPL编译产物u-boot-spl.bin启动介质开头初始化DDR和时钟加载主镜像U-Boot主镜像编译产物u-boot.bin启动介质SPL之后驱动外设、交互命令、加载内核2.2 在代码里找到接力棒交接的三关有了这个分层模型再去源码里找关键节点就清晰了。u-boot源码树的目录很有规律我从角度给它划成三层。第一层是架构层arch/arm/、arch/riscv/这类目录里放的是CPU复位向量、底层初始化逻辑。你在arch/arm/cpu/armv7/start.S里能看到经典的异常向量表u-boot也是从那个向量表开始的。但注意现代u-boot在编译SPL和编译主镜像时同样的start.S会被编译两次用不同的宏控制走不同的初始化分支这就是SPL和主镜像共用源码的方式。第二层是板级层board/厂商/板型/目录里通常是厂商工程师写的板级初始化代码。像board_init_f、board_init_r这两个函数名字直白得不能再直白f就是early前期初始化r就是后面后期初始化。前者主要搞定DDR、时钟这些生存必需品后者利用已经可用的环境把网络、存储、显示这些高级外设打开。第三层是驱动层drivers/目录下有串口、网卡、MMC、USB等子目录。如果你学过单片机驱动外设的套路看这些驱动不会有太大障碍无非是多封装了几层抽象把寄存器操作藏在了struct udevice之类的结构体背后。2.3 为什么u-boot要搞设备树这么繁琐的东西很多从单片机过来的朋友第一次看设备树Device TreeDTS/DTSI文件都会懵为什么不能用#define一把梭定义引脚为什么非要搞一套树形结构的文本描述这个板子的串口在哪个地址、用的哪个中断、挂了几路时钟我当年也这么困惑过直到我意识到一个现实u-boot和Linux内核是同一个开源生态里的两个项目组他们约定用设备树这种数据与代码分离的方式来描述硬件。好处是显而易见的。同一个芯片可以做出几十种板卡每块板子的引脚、外设连接方式都不同。如果硬件信息写死在代码里那每出一个板卡都要重新编译内核和u-boot还要维护无数个几乎相同但细节不同的驱动文件。现在通过设备树驱动代码只关心我在操作一个GPIO控制器这种通用抽象至于这个GPIO控制器的寄存器基址是多少、中断号是多少、引脚要复用成什么功能全由设备树源文件描述。板子有差异只改设备树编译出来的同一个内核镜像能适配不同板卡。对刚学u-boot的人我对设备树的建议是你不需要一上来就能从零写一整套设备树只需要会看、会改。能看到uart1 { status okay; };和uart1 { status disabled; };的区别能把网卡的phy-mode、phy-handle对应到原理图这就已经通过了u-boot阶段对设备树的基本要求。剩下更深的设备树知识等学内核驱动时再精进不迟。3. 实操从零编译一个能启动的u-boot理论聊完必须上手。这节我用一套经典开发组合来演示x86电脑上装Ubuntu虚拟机或WSL目标板是NXP i.MX6ULL开发板。为什么选它因为i.MX6ULL资料多、价格友好、启动方式简单是教学场景里被玩得最透的一块板子很多伙伴手里的正点原子板子就是这个。3.1 环境准备工具链和源码第一步是搭建交叉编译环境。u-boot是跑在目标板上的程序但编译这步是在你的x86电脑上完成的所以我们需要能交叉编译ARM代码的GCC工具链。在Ubuntu上安装工具链很简单打开终端执行sudo apt update sudo apt install gcc-arm-linux-gnueabihf安装完之后验证一下arm-linux-gnueabihf-gcc -v能打印版本信息就说明工具链OK了。然后获取u-boot源码官方仓库是独立的托管平台也可以从国内镜像拉取git clone https://github.com/u-boot/u-boot.git如果网络条件不理想也可以用厂商提供的bsp包里的u-boot源码但官方主线仓库的好处是更新频繁、社区活跃有问题容易搜到相关讨论。3.2 配置万事开头难defconfig是万能钥匙拿到源码后不要直接make。u-boot极强它把编译哪块板卡这件事抽象成了一套配置系统每个板子对应一个defconfig文件。你可以先看支持的板卡列表ls configs/ | grep mx6ull大概率会看到这样一个文件名imx6ull_14x14_evk_defconfig其中14x14是芯片封装尺寸evk是NXP的评估板套件。我们手头的国产开发板基本都是仿照这个评估板做的直接拿它的配置打底最省事。执行export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- make imx6ull_14x14_evk_defconfig注意两个环境变量ARCH指定目标架构CROSS_COMPILE指定交叉编译工具链前缀。每次编译前最好都先设置它们或者写进当前shell的配置里。执行完配置后会生成一个.config文件。你可以用make menuconfig进去看看图形化配置界面TUI风格的菜单和Linux内核的menuconfig同源。之前担心Kconfig看不懂这里就能体会到它就是个功能开关管理器按下/还能搜索某个配置项比如搜索SPL相关选项能清楚看到SPL功能由哪些开关控制。3.3 编译看产物是如何一层层长出来的配置完成后直接make -j$(nproc)然后看输出的关键字需要了解几个核心文件名它们搞明白了整个编译产出就理解了第一个是u-boot.bin这是完整的主镜像。启动时它会被SPL从存储介质中读到DDR里。第二个是u-boot-spl.bin这是SPL镜像。它会被烧录到启动介质的最开始区域由芯片Boot ROM加载。第三个是u-boot.imx这是NXP芯片特有的打包产物。因为i.MX芯片的Boot ROM要求镜像前加上特定的头部信息包括DCDDevice Configuration Data用于告诉Boot ROM怎么初始化DDR等关键参数所以u-boot构建系统会把SPL和主镜像按照NXP的要求封装成这个文件。它才是最终烧到SD卡/EMMC的交付物。第四个是u-boot.dtb这是编译生成的设备树二进制文件。需要注意的是.dts源文件里的内容会经过编译转换成.dtb最终u-boot主镜像在启动时也会把自己打包进去一段设备树用CONFIG_OF_CONTROL配置项控制。编译完成之后在根目录执行ls -l u-boot*你会看到这几个文件安安静静躺在那里。有一个小技巧要分享u-boot.map文件也非常有用它是链接映射文件把每个符号的地址都标得清清楚楚。日后你想调试某个函数执行到哪里卡住了对照map文件看程序计数器PC指向的地址就能定位到具体函数比瞎猜高效得多。3.4 烧录配置把u-boot写进SD卡的正确姿势现在要把编译出的镜像烧到SD卡里。用读卡器把SD卡插到电脑上先确认设备名lsblk假设SD卡识别为/dev/sdb一定要确认好别把系统盘给刷了别问我是怎么知道的先卸载它的分区sudo umount /dev/sdb*然后烧写i.MX6ULL平台烧U-Boot的命令特别容易记因为它在SD卡里有一张固定的地图从1KB偏移处开始写sudo dd ifu-boot.imx of/dev/sdb bs1k seek1 convfsyncbs1k表示每次写1KB块大小seek1表示跳过开头的1KB扇区。为什么是这样因为i.MX6ULL的Boot ROM固定会从SD卡的1KB偏移处读取代码这是芯片手册里写明的启动约定每个SoC厂商的偏移都可能不同。烧完之后把SD卡插到开发板拨到SD卡启动模式接好串口线上电。如果你的配置和电脑设置都没问题串口终端里会跳出一段uboot日志。首次看到的日志通常类似U-Boot SPL 2023.04-rc4-00001-gabcdef (Mar 05 2025 - 10:00:00) Trying to boot from MMC1 U-Boot 2023.04-rc4-00001-gabcdef (Mar 05 2025 - 10:00:00) CPU: i.MX6ULL 9x9 DRAM: 256 MiB MMC: FSL_SDHC: 0 In: serial Out: serial Err: serial Net: FEC1看到U-Boot SPL那两行说明第一级接力已经跑通了看到DRAM: 256 MiB说明DDR初始化成功看到Net: FEC1说明网卡驱动起来了。到这一步u-boot移植的地基就算打好了。3.5 移植不是全盘照抄至少要改这几处有很多人问我我用的是白菜价国产板子跟原厂EVK板不完全一样那我是不是得从零移植多数情况真不用。国产板子大都是照着EVK设计的你只需要微调。第一步确认CPU型号和内存颗粒。例如你板子上用的是DDR3颗粒容量256MB时序参数和原厂EVK的512MB颗粒不同那你就得去board/freescale/mx6ullevk/imx6ullevk.c里找DDR初始化相关的结构体对照你的颗粒手册去修改density、rowaddr、coladdr这些参数。改错一个参数表现就是u-boot启动卡在DRAM初始化之后或者读取内存时偶尔死机极其隐蔽。第二步核对网卡PHY。如果我的板子PHY芯片型号和EVK不一样网络可能不工作。最常见处理是在设备树里修改#include imx6ul-14x14-evk.dts这个文件找到fec1节点把phy-handle指向的PHY地址改掉。如果PHY芯片完全不同可能还得去对应的PHY驱动代码里看是否支持或配置。第三步改环境变量默认值。EVK板子的默认环境变量里可能有它自己的MAC地址、bootargs默认参数。你可以在include/configs目录下的头文件里改CONFIG_BOOTARGS之类也可以通过u-boot启动后setenv bootargs consolettymxc0,115200 root/dev/mmcblk1p2 rootwait这种方式不改代码直接调。4. 启动过程中的常见问题与排查技巧u-boot移植和调试说白了就是黑灯瞎火猜原因但是串口日志能给你指路。下面是几个我实际工作中被问得最多、自己踩过的坑。4.1 一点输出都没有不要先怀疑u-boot这是头号问题尤其是新手。板子插上电源串口毫无动静第一反应往往是u-boot没编译对吧代码有问题吧实际上大部分无输出问题的根源在硬件基础配置而不是u-boot代码本身。排查顺序我建议这样走确认开发板供电电压。有些板子存在跳线或独立供电模块USB口供电往往电流不足会导致SoC唤醒异常。确认串口线是正确的引脚组合。大部分串口线是TTL电平别拿RS232的线直接怼确认TX、RX有没有交叉接反。确认BOOT模式引脚拨得对不对。i.MX6ULL要启动SD卡BOOT_MODE0和BOOT_MODE1都应该在特定电平组合上拨错了芯片老老实实去烧写模式等你下载你怎么上电它都不出来。确认你的串口软件参数。波特率115200、8N1是u-boot的默认设置但有些板子在SPL阶段用的波特率和主u-boot阶段不同日志前面一段可能会以邦邦乱码出现。如果以上全没问题还是无输出再回头查u-boot编译配置里串口引脚有没有复用成普通GPIO。别笑真有人把UART引脚在设备树里改成了GPIO用途然后串口忽然永久沉默了。我自己的经验是用示波器或者逻辑分析仪量一下串口TX引脚有没有电平翻转。只要它在疯狂翻转说明代码在跑只是你的接线或终端软件的问题如果纹丝不动才去怀疑代码层面。4.2 有输出但全是乱码重点查时钟串口能输出了但满屏都是类似锟斤拷或者???这种乱码这个现象我建议直接锁定时钟配置。u-boot串口输出的频率是依赖UART外设的时钟频率来产生波特率的。如果uart使用的root clock配置不对实际产生的波特率和终端软件设置的115200不一致就会出现乱码。常见原因是SPL阶段和主u-boot阶段之间PLL频率被重新配置了但UART外设时钟没跟上。排查方法是先确认你的芯片外部晶振频率是多少。i.MX6ULL最常见的两种外部晶振是24MHz和32.768kHz的32K晶振RTC用24MHz主晶振。如果你的板子上用的是别的频率晶振u-boot里所有PLL参数都是按24MHz算出来的分频完自然不对。检查board目录中imx6ul相关板级文件里MXC_CCM_PLL1_SYS_PLL等宏定义是否符合预期。另外还要检查串口终端软件本身的时钟精度某些山寨USB转串口芯片跑115200这种常用速率还好在1.5Mbps甚至更高时极其容易出问题这个时候换个好一点的转接线问题就消失了。4.3 卡在Trying to boot from MMC没有后续这个现象我在用nfs和mmc启动的时候经常交替出现。已经看到SPL正常打印了然后它就卡在Trying to boot from MMC1这行不往下走。这行是SPL尝试从MMC介质加载主u-boot镜像时打出来的。它卡住意味着SPL读MMC的操作没有成功返回。常见原因有三类第一类是MMC设备识别不到。SPL阶段MMC驱动可能有多种卡槽供电、设备树的mmc1节点状态是否正确都可能导致MMC初始化失败。我用过一块板子MMC的时钟通过一个开关芯片控制默认状态是关闭的结果SPL一直尝试超时重试。第二类是镜像放在了SD卡上错误的分区或者错误的偏移。还记得上面提到的seek1吗如果你用dd写SPL的同时把u-boot主镜像也一同写在起始区域很多偏移之外或者主镜像没有放在SPL预期读取的位置它就会一直盲读找不到头。解决思路也很朴素把启动介质重新分区主镜像要么放在指定分区用fatload方式加载要么确认编译时配置的CONFIG_SYS_MMCSD_RAW_MODE_U_BOOT_SECTOR在源码里的默认值就是主镜像在SD卡中的扇区偏移。查一查这个宏再对照你自己的烧写命令多半能发现问题。4.4 tftp网络下载老是超时先ping通再谈启动内核说完启动流程再说编译阶段的网络问题。u-boot烧进板子后我们经常用它来调试内核最常见的方式就是网口tftp下载内核镜像到内存。现实是很多朋友直接在u-boot里tftp 0x80800000 zImage接着就报Link is down或者超时挂起。这步能不能通跟你电脑上的tftp服务器关系小跟两头网络的物理旁路关系极大。我排查的顺序是先在u-boot环境变量中设置好IPsetenv ipaddr 192.168.1.10 setenv serverip 192.168.1.100 saveenv然后在u-boot里用ping 192.168.1.100测试。这个命令不能在u-boot里关闭ping功能不同版本可能有开关但如果ping不通多半是网络协商问题或开发板和电脑直接连接时少了交叉线/路由器。有趣的是我的工作经验里有几次居然是PHY芯片的复位引脚没拉对时间网卡驱动虽然能初始化但是PHY一直没完成自协商导致网络链路始终建立不起来。这种情况厂商SDK里通常有特别说明换个复位时序或者检查硬件复位引脚连接位置即可。4.5 常见问题速查表现象排查思路完全无输出供电、串口接线、BOOT引脚、波特率、示波器测TX输出乱码晶振频率、UART时钟分频、串口线质量差卡在SPL跳主镜像MMC设备驱动、镜像读取位置、烧录偏移DRAM size识别错误DDR颗粒参数、DCD配置、总线宽度网卡初始化失败PHY驱动匹配、复位引脚时序、设备树phy-handle保存环境变量重启丢失saveenv是否执行、保存介质是否可写编译报错undefined reference工具链版本、是否混用了arm-linux-gnueabihf和aarch64工具链卡在内核启动先确认bootargs中console参数与串口一致再用bootm/bootz前手动load镜像排查5. 学了u-boot之后嵌入式学习这条路怎么继续走把u-boot玩明白之后你不是学完了一个孤立的组件而是推开了一扇门。我最后聊聊u-boot在整条嵌入式Linux学习路线里的定位以及接下来的路怎么铺。5.1 用u-boot打通硬件驱动思维和软件框架思维如果你认真走完u-boot的启动流程你会发现它同时训练了两种思维程序员视角的软件框架思维以及电子工程师视角的硬件时序思维。u-boot源码并非简单堆砌它有清晰的框架层命令系统、驱动模型、环境变量存储抽象、Kconfig配置。这些设计思想在后续学习Linux内核时几乎都能复用。内核里的platform_driver、device_node、module_init在u-boot里都有或繁或简的对应物。内核那堆设备树配置在你已经完成u-boot设备树入门之后阅读难度会断崖式下降。所以说u-boot是内核学习预科班毫不夸张。很多朋友说内核代码看不下去直接硬啃drivers/tty/serial/下面的串口驱动看了三小时不知道它在干嘛但如果你从u-boot串口驱动读起它代码量小、依赖少、打印直观很快就建立起驱动到底是怎么和硬件寄存器交互的心智模型。5.2 从u-boot到内核回头认识芯片手册的优先级我还想强调一点学习u-boot的额外收获是它逼着你大量查阅芯片手册。看参考手册的哪个章节、关注哪些寄存器的哪些位。我在玩STM32时也会看手册但大部分时间只是查寄存器基址和位定义。u-boot让你从启动时序最高视角去看参考手册包括时钟树、启动模式、DDR控制器、设备树binding文档。尤其是DDR初始化那部分几乎不写代码光查手册和颗粒数据手册就能花掉一周。这个过程非常像做硬件工程师的活当你某天拿着示波器量DDR的读写信号配合手册说明验证自己配置的时序对不对那种感觉比单纯拧寄存器有成就感得多。对我个人而言这段经验在之后调试MIPI DSI/LVDS显示屏接口、调试网卡PHY时都发挥了巨大作用。因为所有外围器件芯片手册都会用类似的时序图参数表格形式呈现你已经很熟悉这种阅读习惯了。5.3 给自己规划一个u-boot 内核 根文件系统的小目标学习切忌漫无目的。我的建议是不要只玩u-boot就停下来给自己设定一个完整的目标在一块板子上用u-boot引导内核然后挂载根文件系统最终跑起一个Qt应用。当你看到串口里依次滚出u-boot日志、内核日志最后进入根文件系统shell那一刻你对嵌入式Linux系统的理解就真的建立起来了。这时候再回头看单片机那些工程你会发现自己已经站在一个完全不同高度的视角上类似于CRUD程序员第一次理解操作系统调度器时的震动。那些刚开始玩DHT11、LCD1602、STM32的朋友如果你正犹豫下一步该往哪走我想对你说单片机很好它是电子人最初的快乐但u-boot是往更深处扎根的那把锄头多花三个月敲进去回报绝对值得。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →