
1. 从单片机到u-boot为什么我劝你尽早跨过这道分水岭如果你现在还在用51单片机点灯、用STM32跑裸机循环或者刚把DHT11和LCD1602调通觉得自己已经摸到了嵌入式的门槛那我先说一句可能不太中听的话你离真正的嵌入式开发还有相当长的一段路。这不是贬低单片机恰恰相反单片机是每个嵌入式工程师的必经之路它帮你建立了对寄存器、时序、中断、外设这些底层概念的直觉。但问题在于单片机的世界和现代嵌入式Linux系统的世界几乎是两套不同的游戏规则。单片机开发的核心逻辑是“我直接控制一切”时钟树我自己配中断向量表我自己填内存布局我自己规划甚至连启动代码都是我自己写的。你写一个while(1)CPU就老老实实待在那个循环里。这种模式在资源受限、功能单一的场合非常高效比如一个电饭煲、一个电子秤、一个遥控器。但当你面对的是需要跑文件系统、网络协议栈、多任务调度、图形界面的场景时裸机开发就会变得极其痛苦——你要自己实现内存管理、自己写任务调度器、自己移植TCP/IP协议栈这几乎是不可能完成的任务。这时候u-boot就登场了。u-boot的全称是Universal Boot Loader它是嵌入式Linux系统中最常用的引导加载程序。你可以把它理解成PC上的BIOS加GRUB的合体上电后第一段真正干活的代码就是它它负责初始化DDR内存、配置时钟、加载内核镜像和设备树到内存中、然后把控制权交给Linux内核。没有u-boot你的ARM64开发板就是一块砖头内核镜像躺在存储介质里根本跑不起来。那为什么我要专门写一篇关于u-boot的内容因为我在带新人的过程中发现一个非常普遍的现象很多人学完了单片机想往嵌入式Linux方向转结果卡在u-boot这一关就再也上不去了。他们能看懂C语言能理解寄存器操作但面对u-boot那套SPL、TPL、设备树、驱动模型、环境变量、启动脚本的组合拳完全不知道从哪里下手。更麻烦的是u-boot的官方文档虽然全但偏向于代码结构和API说明缺少那种“一个过来人告诉你实际怎么玩”的视角。这篇文章就是来解决这个问题的。我会从单片机开发者的认知基础出发把u-boot的核心概念、启动流程、实操方法、常见坑点全部拆开讲清楚。适合的读者是有C语言和单片机基础、想往嵌入式Linux方向进阶、但被u-boot挡在门外的开发者。如果你完全没接触过单片机直接看u-boot会非常吃力建议先补一补裸机开发的基础。如果你已经在做嵌入式Linux应用层开发但对底层启动流程一知半解这篇文章也能帮你把知识链条补完整。另外说一句学习u-boot不需要你手上有昂贵的开发板。QEMU可以模拟ARM64平台配合u-boot和Linux内核你可以在自己的电脑上完整地跑通整个启动流程。这对于初学者来说是最低成本的入门方式后面我会详细讲怎么搭这个环境。2. u-boot到底在干什么从硬件上电到内核接管的完整链路2.1 上电后的第一段代码为什么需要SPL很多人第一次看u-boot的源码结构时会懵怎么有两个u-boot一个叫u-boot-spl一个叫u-boot。这就要从芯片的启动特性说起了。以常见的ARM64 SoC为例比如i.MX8、RK3399、全志H6这些芯片上电后CPU会从芯片内部固化的BootROM开始执行。BootROM会按照预设的顺序去查找可启动介质先是SPI Flash然后是SD卡然后是eMMC。找到之后它会把介质上特定偏移处的代码加载到芯片内部的SRAM中执行。注意是SRAM不是DDR。因为此时DDR内存控制器还没有初始化DDR颗粒根本不能用。问题来了SRAM的容量通常只有几十KB到几百KB而完整的u-boot编译出来动辄几百KB甚至上MB根本塞不进SRAM。所以就需要一个“先遣队”——SPLSecondary Program Loader。SPL是一段精简到极致的代码它的唯一使命就是初始化DDR控制器、把完整的u-boot从存储介质搬到DDR里、然后跳过去执行。SPL的体积通常控制在几十KB以内刚好能塞进SRAM。这个设计思路和单片机的BootloaderAPP分区方案非常像。你在STM32上做OTA升级时是不是也写过一个小的Bootloader负责把APP从Flash的备份区搬到运行区SPL和u-boot的关系就是类似的SPL是那个小Bootloaderu-boot是那个APP。区别在于SPL要初始化的东西复杂得多尤其是DDR控制器的初始化涉及到一堆时序参数的配置这些参数通常由芯片厂商提供以头文件或二进制片段的形式集成在u-boot源码中。注意有些芯片方案会把SPL和u-boot合并成一个镜像或者用TPLTertiary Program Loader做三级加载。具体用哪种方式取决于SoC的设计不是固定的。你在拿到一块新板子时第一件事就是确认它的启动链路是SPL→u-boot还是SPL→TPL→u-boot。2.2 u-boot的三个阶段重定位之前和之后完整的u-boot启动过程可以粗略分成三个阶段SPL阶段、u-boot重定位前阶段、u-boot重定位后阶段。SPL阶段刚才讲过了核心就是初始化DDR。当SPL把u-boot搬到DDR并跳转过去后u-boot开始执行。但此时u-boot的代码可能被链接在Flash的地址上而实际运行在DDR的地址上这就涉及到重定位。重定位的本质是把代码段、数据段、BSS段从加载地址搬到运行地址并修正所有绝对地址引用。这个过程在单片机上很少遇到因为单片机的代码通常直接在Flash里执行XIP不需要搬来搬去。但在嵌入式Linux系统中内核和u-boot都需要重定位因为DDR的访问速度远快于Flash而且DDR的容量也大得多。重定位完成之后u-boot进入板级初始化阶段串口初始化让你能看到打印信息、MMC/NAND/SPI驱动初始化让你能读取存储介质、网络驱动初始化让你能用tftp加载文件、USB驱动初始化让你能用fastboot烧录。这些初始化都是通过u-boot的驱动模型Driver Model来管理的这也是u-boot从2014年开始引入的一套新架构目的是让驱动代码更加模块化和可复用。2.3 设备树u-boot和内核之间的“硬件说明书”如果你之前只玩过单片机设备树Device Tree这个概念可能会让你觉得多此一举。在单片机世界里硬件信息是直接写死在C代码里的哪个引脚接LED、哪个寄存器控制串口、时钟频率是多少都是硬编码。但嵌入式Linux系统要支持成百上千种不同的板子如果每个板子都编译一个专属的内核那维护成本就爆炸了。设备树就是来解决这个问题的。它用一种叫DTS的文本格式描述硬件信息编译成DTB二进制文件后由u-boot传递给Linux内核。内核在启动时解析DTB根据里面的描述去加载对应的驱动、分配资源、注册设备。u-boot自己也会用到设备树尤其是在驱动模型中设备的探测和绑定都依赖于设备树节点的描述。对于初学者来说你不需要一上来就手写DTS文件但你必须能看懂它。至少要能看懂/memory节点描述DDR的起始地址和大小、/chosen节点描述启动参数和initrd位置、/soc下面的串口和MMC控制器节点。这些信息在你调试启动失败时非常关键。2.4 启动脚本和环境变量u-boot的“批处理”u-boot有一个非常实用的功能环境变量和启动脚本。你可以把环境变量理解成u-boot的“配置项”比如bootargs存放内核启动参数bootcmd存放默认的启动命令序列ipaddr和serverip存放网络配置。这些变量保存在存储介质的一个固定分区里掉电不丢失。启动脚本则是把一系列u-boot命令写成一个文本文件编译成boot.scr或boot.scr.uimgu-boot在启动时会自动执行这个脚本。这相当于把“加载内核→加载设备树→设置启动参数→跳转内核”这一整套动作自动化了。你可以把它类比成单片机上电后的初始化流程只不过u-boot的脚本更灵活可以在不重新编译u-boot的情况下修改启动行为。实操心得在开发阶段我强烈建议把bootcmd设置成进入u-boot命令行而不是直接启动内核。这样每次上电你都有机会检查环境变量、手动加载镜像、排查启动问题。等调试稳定了再改回自动启动。很多新手把bootcmd设成自动启动结果内核起不来串口又没有任何输出就完全不知道从哪里查起。3. 用QEMU搭建ARM64 u-boot实验环境零成本入门方案3.1 为什么选QEMU而不是买开发板我经常被问到“学u-boot要不要买开发板”。我的回答是初期不需要QEMU足够了。原因有三点。第一成本。一块能跑ARM64 Linux的开发板加上电源、串口线、SD卡、网线少说也要几百块。而QEMU是免费的装在电脑上就能用。第二可重复性。QEMU模拟的环境是确定的你编译出来的镜像每次运行结果都一样不会因为硬件差异导致玄学问题。第三调试方便。QEMU支持GDB调试你可以像调试单片机程序一样单步跟踪u-boot的执行这在真实开发板上反而不好做。当然QEMU也有局限。它不能模拟真实的硬件时序问题也不能让你练习外设驱动开发。但作为理解u-boot启动流程的工具它是最合适的。等你把QEMU上的流程跑通了再买开发板做实战学习曲线会平滑很多。3.2 工具链准备交叉编译器的选择在x86电脑上编译ARM64的代码需要交叉编译器。推荐使用Linaro或ARM官方提供的GNU工具链包名通常是gcc-aarch64-linux-gnu。在Ubuntu上可以直接用apt安装sudo apt install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu安装完成后用aarch64-linux-gnu-gcc --version确认版本。我建议用较新的版本9.0以上因为老版本对ARM64的支持不够完善编译u-boot时可能会遇到奇怪的链接错误。QEMU本身也需要安装Ubuntu下直接sudo apt install qemu-system-arm即可。注意要装的是qemu-system-aarch64不是qemu-system-arm。前者模拟64位ARM后者模拟32位ARM。你可以用qemu-system-aarch64 --version确认。3.3 编译u-boot从源码到可执行镜像u-boot的源码可以从官方仓库获取。我建议用稳定版本比如v2023.10或v2024.01不要用master分支因为master随时可能引入不稳定的改动。git clone https://source.denx.de/u-boot/u-boot.git cd u-boot git checkout v2024.01QEMU的ARM64虚拟平台在u-boot中对应的配置是qemu_arm64_defconfig。编译步骤如下export CROSS_COMPILEaarch64-linux-gnu- make qemu_arm64_defconfig make -j$(nproc)编译完成后在根目录下会生成u-boot.bin。这个就是我们可以直接喂给QEMU的镜像。如果你看到编译过程中有警告一般可以忽略只要最终生成了u-boot.bin就行。注意有些版本的u-boot在QEMU上需要额外的配置才能正常输出串口信息。如果你启动后看不到任何打印检查一下CONFIG_DEBUG_UART和CONFIG_DEBUG_UART_BASE是否配置正确。在qemu_arm64_defconfig中这些通常是默认打开的。3.4 启动QEMU一条命令跑通u-boot编译完成后用下面这条命令启动QEMUqemu-system-aarch64 \ -machine virt \ -cpu cortex-a57 \ -nographic \ -bios u-boot.bin参数解释-machine virt指定虚拟平台-cpu cortex-a57指定CPU型号-nographic表示不使用图形界面、串口输出直接打到终端-bios u-boot.bin指定启动镜像。如果一切正常你会在终端看到u-boot的启动日志最后停在一个提示符前。这就是u-boot的命令行界面。你可以输入help查看所有支持的命令输入bdinfo查看板级信息输入printenv查看环境变量。这个环境虽然简单但已经足够你理解u-boot的基本操作了。你可以尝试用md命令查看内存内容用mw命令修改内存用mmc命令操作虚拟SD卡。这些命令在真实开发板上也是一样的用法。3.5 加载Linux内核让QEMU跑起完整系统光有u-boot还不够我们最终目标是让它加载Linux内核。你需要准备一个ARM64的内核镜像Image和一个设备树文件qemu-arm64.dtb。可以从Linux内核源码编译也可以直接下载发行版提供的内核包。在u-boot命令行中用tftp或load命令把内核和设备树加载到内存然后设置bootargs最后用booti命令启动setenv bootargs consolettyAMA0 root/dev/vda rw booti 0x40000000 - 0x42000000这里的地址需要根据你的QEMU配置和设备树来调整。booti是ARM64专用的启动命令它会按照ARM64 Linux的启动协议把控制权交给内核。如果一切顺利你会看到内核启动日志最终进入Linux shell。这个过程听起来简单但实际操作中很容易卡住。最常见的问题是内核地址和设备树地址搞错或者bootargs中的console参数和实际串口不匹配。我的建议是先用bdinfo确认DDR的起始地址然后用printenv看看默认的bootargs是什么在此基础上修改。4. 从单片机思维切换到u-boot思维几个关键认知转变4.1 内存管理从静态分配到动态分配单片机开发中内存通常是静态分配的全局变量放在.data或.bss段局部变量放在栈上堆用得很少。但在u-boot中动态内存分配是常态。u-boot自己实现了一套简单的malloc叫malloc_simple在重定位之前使用重定位之后会切换到完整的堆实现。这个转变意味着你不能假设某个地址的内存是“安全”的。在u-boot中DDR的布局是动态确定的u-boot自身占用一段堆占用一段栈占用一段剩下的才是可用内存。如果你在u-boot中写代码时随意访问一个固定地址很可能会踩到u-boot自己的数据。实操心得在u-boot中调试内存问题时bdinfo命令是你的好朋友。它会打印出u-boot的起始地址、结束地址、堆的起始地址、栈指针位置等关键信息。每次遇到数据异常先跑一遍bdinfo确认你的操作没有越界。4.2 驱动模型从直接操作寄存器到分层抽象单片机开发中你通常是直接操作寄存器GPIOA-ODR | (1 5)。这种方式直接、高效但不可移植。u-boot的驱动模型DM引入了一套分层抽象UCLASS设备类别、UDEVICE设备实例、DRIVER驱动。每个设备都属于某个UCLASS比如GPIO、MMC、NET。驱动通过probe函数初始化硬件通过ops结构体暴露操作接口。这套机制的好处是上层代码不需要知道底层硬件的具体实现。比如你要操作GPIO只需要调用gpio_set_value不需要关心它是STM32的GPIO还是i.MX的GPIO。坏处是学习曲线变陡了。你需要理解设备树节点如何绑定到UDEVICE驱动如何通过of_match表匹配设备probe函数在什么时候被调用。我的建议是不要一上来就试图理解整个DM框架。先找一个简单的驱动比如GPIO或串口从设备树节点开始跟踪它的probe函数看看它是怎么被调用的。把这个流程走通一遍再去看其他驱动就轻松多了。4.3 启动流程从复位向量到内核入口单片机的启动流程很直接上电→复位向量→启动代码→main函数。u-boot的启动流程要复杂得多但核心逻辑是一样的上电→BootROM→SPL→u-boot→内核。每一级都负责初始化一部分硬件然后把控制权交给下一级。理解这个链路的关键是搞清楚每一级运行在哪个内存区域。BootROM运行在芯片内部ROMSPL运行在SRAMu-boot运行在DDR内核也运行在DDR。每一级的内存空间是独立的不能混用。这也是为什么SPL必须先把DDR初始化好否则u-boot根本没法运行。4.4 调试手段从点灯到串口再到GDB单片机调试的终极手段是点灯程序跑飞了在可疑的地方翻转一个GPIO用示波器看波形。u-boot调试的终极手段是串口打印printf是你的主要工具。u-boot的printf实现比较完善支持格式化输出可以在启动的各个阶段打印信息。但串口打印有个问题如果u-boot在串口初始化之前就挂了你什么都看不到。这时候就需要早期调试手段。u-boot支持CONFIG_DEBUG_UART可以在串口驱动还没初始化的时候就用一个极简的串口输出函数打印字符。另外QEMU支持GDB调试你可以用-s -S参数启动QEMU然后用aarch64-linux-gnu-gdb连接上去单步跟踪u-boot的执行。这是排查启动问题的终极武器。5. 常见问题与排查技巧实录5.1 u-boot启动后没有任何输出这是新手最常遇到的问题。可能的原因有几种串口配置错误、DDR初始化失败、镜像加载地址错误。排查顺序如下。先确认QEMU命令是否正确。-nographic参数必须加上否则串口输出不会打到终端。然后确认u-boot.bin是否真的被加载了。可以用-d参数让QEMU输出调试日志看看它从哪个地址开始执行。如果QEMU命令没问题那可能是u-boot的串口配置和QEMU虚拟平台不匹配。QEMU的virt平台使用PL011串口地址是0x9000000。检查u-boot的设备树中/soc/serial9000000节点是否存在stdout-path是否指向这个节点。5.2 内核启动后卡在“Starting kernel...”这个问题的原因通常是内核镜像格式不对或者加载地址不对。ARM64 Linux内核要求使用Image格式未压缩的原始镜像不能用zImage或uImage。加载地址通常是DDR起始地址加0x80000但具体取决于内核配置。另外设备树必须和内核匹配。如果你用的设备树是给别的板子编译的内核可能找不到串口或存储控制器导致启动卡住。用QEMU自带的qemu-arm64.dtb通常是最稳妥的。5.3 环境变量保存失败u-boot中修改环境变量后需要用saveenv命令保存。如果保存失败通常是存储介质没有正确初始化或者环境变量分区不存在。在QEMU中默认没有持久化存储所以saveenv会失败。你可以通过添加-drive参数给QEMU挂一个虚拟SD卡或虚拟Flash然后在u-boot中配置对应的MMC或SPI驱动。5.4 常见问题速查表问题现象可能原因排查方法无串口输出串口配置错误检查设备树stdout-path卡在SPLDDR初始化失败检查DDR时序参数卡在Starting kernel内核格式或地址错误确认使用Image格式saveenv失败存储介质未初始化检查MMC/SPI驱动网络不通网卡驱动未加载检查设备树网络节点加载镜像超时tftp服务器未启动确认服务器IP和文件路径6. 从u-boot出发嵌入式Linux学习的下一步把u-boot跑通之后你其实已经站在嵌入式Linux的大门口了。接下来可以往几个方向深入。第一个方向是内核移植。尝试给QEMU编译一个自己的Linux内核修改设备树添加一个简单的字符设备驱动。这会让你理解内核是如何初始化硬件、如何加载驱动的。第二个方向是根文件系统。u-boot启动内核后内核需要挂载根文件系统。你可以用BusyBox构建一个最小的根文件系统或者用Buildroot自动生成。这会让你理解Linux系统的完整启动链路。第三个方向是应用层开发。当系统跑起来后你就可以在上面写应用程序了。这时候你会发现之前学的单片机C语言功底在这里完全用得上只是运行环境从裸机变成了Linux。第四个方向是驱动开发。如果你对底层感兴趣可以尝试写一个简单的GPIO驱动或I2C驱动。u-boot的驱动模型和Linux的驱动模型有很多相似之处学过u-boot的DM框架后再看Linux的platform driver会容易很多。我个人在实际操作中的体会是u-boot是嵌入式Linux学习路上的一道坎但也是一道分水岭。跨过这道坎之后你会发现之前学的单片机知识并没有白费它们是你理解底层硬件的基石。而u-boot教给你的是如何在一个更复杂的系统中组织代码、管理资源、协调各个模块。这种思维方式的变化比具体的技术细节更重要。最后分享一个小技巧如果你在QEMU中调试u-boot时遇到问题不妨把-nographic换成-serial mon:stdio这样你可以用CtrlA然后按C切换到QEMU的monitor界面查看虚拟机的状态。这个技巧在排查启动问题时非常有用。