
1. 从单片机到u-boot为什么我劝你尽早跨过这道分水岭如果你现在还在用51单片机点灯、用STM32跑裸机程序或者刚把DHT11温湿度数据和LCD1602显示调通觉得自己已经“入门嵌入式”了那我得说句可能不太中听的话你离真正的嵌入式开发还有相当长的一段路。单片机那套东西——寄存器配置、中断向量表、主循环轮询——本质上是在一个没有操作系统、资源极其受限的环境里做开发。它能让你理解硬件怎么被软件操控但它不会教你一个现代嵌入式系统是怎么被组织起来的。我见过太多人卡在这个阶段会写单片机程序但一听到“u-boot”“内核启动”“设备树”“根文件系统”就发怵觉得那是另一个世界的东西。其实不是。从单片机到u-boot中间隔的不是天赋而是一层认知的窗户纸。u-bootUniversal Boot Loader是嵌入式Linux系统里负责“把系统从存储介质里拉起来”的那段代码它做的是单片机做不了的事初始化DDR、加载内核镜像、传递启动参数、挂载根文件系统最后把控制权交给Linux内核。你玩单片机的时候程序入口就是你的main函数而在u-boot的世界里你的代码要在没有操作系统支持的情况下自己把内存、时钟、串口、存储全部准备好然后才能谈“启动”。这篇文章适合谁看如果你已经玩过至少一款单片机51、STM32、GD32都算能看懂C语言和基本的汇编知道什么是寄存器、什么是中断但还没真正碰过u-boot或者只停留在“编译过、烧录过、能启动”的层面那这篇内容就是为你写的。我会从“为什么需要u-boot”讲起把启动流程、内存布局、移植要点、QEMU模拟验证、常见问题排查全部拆开结合ARM64和QEMU这两个热词给你一条能直接上手复现的路径。全程不堆砌术语该给命令给命令该给参数给参数该说坑的地方绝不含糊。2. 先搞清楚u-boot到底在干什么从加电到内核接管的完整链路2.1 单片机的启动逻辑为什么在u-boot面前不够用单片机加电之后硬件直接从固定地址取第一条指令通常是复位向量然后跳到启动代码初始化堆栈、时钟、外设最后进main。整个过程没有“阶段”的概念代码想怎么跑就怎么跑因为整个系统的资源都是你的没人跟你抢。但到了应用处理器比如ARM64架构的SoC上情况完全变了DDR内存没有初始化之前根本不能用而初始化DDR的代码又必须放在SRAM里执行存储介质可能是eMMC、SD卡、NAND或者SPI Flash每种介质的读取方式都不一样内核镜像可能被压缩过需要先解压再跳转设备树二进制文件要放在内存的特定位置传给内核。这些事情如果全部塞进内核里做内核会变得无比臃肿而且不同板子的初始化差异会让内核维护变成灾难。所以业界把“启动前期准备”这件事独立出来交给一个专门的引导程序这就是u-boot存在的根本理由。你可以把u-boot理解成一个“硬件保姆加系统搬运工”它先把自己从存储介质里搬到内存里运行然后把内存、时钟、串口、存储控制器全部初始化好接着从存储介质里读出内核镜像和设备树放到内存的指定位置最后设置好启动参数跳转到内核入口。内核启动之后u-boot的使命就结束了内存会被内核接管。这个过程听起来简单但每一步都有讲究尤其是内存布局和启动参数传递搞错了就是各种“Starting kernel ...”之后死机。2.2 u-boot的两阶段启动SPL和Proper的配合逻辑现代u-boot普遍采用两阶段启动架构这是被硬件限制逼出来的设计。第一阶段叫SPLSecondary Program Loader它非常小通常只有几十KB运行在SoC内部的SRAM里。SPL的任务很纯粹初始化DDR控制器、配置时钟、把完整的u-boot镜像从存储介质搬到DDR里然后跳过去执行。第二阶段就是完整的u-boot也叫Proper或者U-Boot Proper它运行在DDR里功能齐全有命令行、有驱动模型、有文件系统支持能加载内核、能通过网络启动、能读写各种存储介质。为什么要分两阶段因为DDR初始化代码必须在DDR可用之前执行而这段代码又不可能太大SRAM装不下完整的u-boot。所以SPL只做最关键的硬件初始化把大块的u-boot搬到DDR里后面的事情就交给Proper去做。这个设计在ARM64平台上尤其常见因为ARM64 SoC的SRAM通常只有128KB到256KB而完整u-boot编译出来动辄几百KB甚至上MB。你在移植u-boot的时候如果发现SPL跑完就卡住了大概率是DDR初始化参数不对或者SPL搬运的地址和长度配置有误。2.3 启动介质与镜像格式为什么你的u-boot烧进去不跑u-boot支持的启动介质非常多SD卡、eMMC、NAND Flash、NOR Flash、SPI Flash、甚至通过网络TFTP加载。不同介质的镜像格式不一样烧录方式也不一样。比如在SD卡上u-boot通常被写到特定的偏移地址前面会留出分区表或者头部信息在SPI Flash上可能需要把SPL和Proper分别写到不同的偏移在eMMC上还要考虑boot partition和user partition的区别。很多人第一次移植u-boot失败不是代码问题而是镜像烧错了位置。我个人的经验是先用QEMU模拟跑通确认编译和启动流程没问题再上真实硬件。QEMU可以模拟ARM64的virt机器u-boot对它有现成的支持你不需要任何硬件就能把启动流程走一遍这对理解u-boot的行为非常有帮助。3. 动手之前先把环境搭好ARM64交叉编译与QEMU模拟平台3.1 交叉编译工具链的选择与安装在x86主机上编译ARM64的u-boot你需要一套aarch64的交叉编译工具链。常见的选择有Linaro的GCC、ARM官方的GNU Toolchain或者发行版自带的gcc-aarch64-linux-gnu。我一般用发行版自带的安装简单版本也够新。以Ubuntu为例直接执行sudo apt update sudo apt install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu安装完之后验证一下aarch64-linux-gnu-gcc --version能输出版本号就说明工具链就绪。这里有个细节u-boot的编译系统会通过CROSS_COMPILE变量来指定工具链前缀所以你在编译的时候要设置CROSS_COMPILEaarch64-linux-gnu-。如果你用的是其他前缀比如aarch64-none-linux-gnu-就相应替换。不要小看这个前缀我见过有人编译报错“aarch64-linux-gnu-gcc: not found”折腾半天发现是工具链没装或者PATH没配好。3.2 QEMU的安装与ARM64 virt机器简介QEMU是一个开源的模拟器可以模拟多种CPU架构和硬件平台。我们要用的是它的ARM64 virt机器这个机器模拟了一个通用的ARM64虚拟平台u-boot官方对它有针对性的配置文件。安装QEMUsudo apt install qemu-system-arm安装完成后你可以用qemu-system-aarch64 -M virt -cpu cortex-a57 -nographic -kernel u-boot.bin这样的命令来启动u-boot。这里的参数含义-M virt指定虚拟机器类型-cpu cortex-a57指定CPU型号-nographic表示不使用图形界面、串口输出到终端-kernel指定要加载的镜像。QEMU的virt机器支持多种启动方式可以直接加载u-boot.bin也可以模拟SD卡启动。对于初学者来说直接-kernel加载是最简单的验证方式。3.3 获取u-boot源码与配置编译u-boot的源码可以从官方仓库获取也可以从SoC厂商的仓库获取。对于QEMU virt机器官方u-boot已经支持得很好。克隆源码git clone https://source.denx.de/u-boot/u-boot.git cd u-boot然后配置QEMU ARM64的目标make qemu_arm64_defconfig这个defconfig会启用QEMU virt机器需要的所有驱动和配置。接着编译make CROSS_COMPILEaarch64-linux-gnu- -j$(nproc)编译完成后你会得到u-boot.bin和u-bootELF格式等文件。u-boot.bin是纯二进制镜像可以直接被QEMU加载。如果你想调试可以用u-boot这个ELF文件配合GDB。编译过程中如果报错最常见的原因是工具链没装好或者环境变量没设置对。另外u-boot的编译对Python版本也有要求一般需要Python 3.6以上如果报Python相关的错误检查一下你的Python环境。4. 把u-boot跑起来QEMU模拟启动与串口交互实操4.1 第一次启动从加电到命令行提示符编译出u-boot.bin之后用下面的命令启动qemu-system-aarch64 -M virt -cpu cortex-a57 -nographic -bios u-boot.bin注意这里用的是-bios而不是-kernel。对于QEMU的virt机器-bios会把镜像放到固件加载地址更接近真实硬件的启动方式。启动之后你应该能在终端看到u-boot的启动日志包括DRAM初始化、串口初始化、设备树加载等信息最后出现提示符。这个提示符就是u-boot的命令行你可以在这里输入命令比如version查看版本bdinfo查看板级信息printenv查看环境变量。如果你看到启动日志卡在某一行不动了比如卡在“DRAM:”或者“MMC:”那说明对应的驱动初始化有问题。在QEMU环境下最常见的问题是设备树不匹配或者驱动没有编译进去。你可以用make menuconfig检查一下相关驱动是否启用。另外QEMU的virt机器默认没有真实的存储设备如果你想测试从虚拟SD卡启动需要用-drive参数挂载一个镜像文件。4.2 常用命令实操环境变量、内存操作、加载内核u-boot的命令行功能很丰富我挑几个最常用的说一下。printenv打印所有环境变量setenv设置变量saveenv保存到存储介质。比如你想设置启动参数setenv bootargs consolettyAMA0 root/dev/vda rw saveenvmd命令用来显示内存内容mw用来写内存mm用来修改内存。这些命令在调试DDR或者验证内存映射的时候非常有用。比如你想看0x40000000地址开始的64个字md 0x40000000 64加载内核一般用fatload、ext4load或者tftpboot。在QEMU环境下你可以用-drive挂载一个包含内核镜像的虚拟磁盘然后用ext4load或者fatload把内核读到内存再用booti启动。booti是ARM64专用的启动命令它会按照Linux内核的要求设置寄存器并跳转。整个流程走一遍你对u-boot的理解会从“知道”变成“会做”。4.3 用GDB调试u-boot定位启动卡死的第一手手段QEMU支持GDB调试你可以在启动QEMU的时候加上-s -S参数-s表示在1234端口开启GDB服务-S表示启动时暂停CPU等待GDB连接。然后另开一个终端用交叉编译工具链里的GDB连接aarch64-linux-gnu-gdb u-boot (gdb) target remote :1234 (gdb) break board_init_f (gdb) continue这样你就能在u-boot的早期初始化函数上下断点单步跟踪执行流程。我调试DDR初始化问题的时候就是靠GDB一步步跟发现某个寄存器的值不对最后定位到设备树里的内存节点配置有误。GDB配合QEMU是学习u-boot最强大的工具组合没有之一。你不需要真实硬件不需要担心烧录失败所有实验都可以在模拟环境里反复做。5. 深入u-boot核心机制内存布局、设备树与启动参数传递5.1 内存布局u-boot把自己放在哪里内核又放在哪里u-boot启动过程中内存布局是有严格规划的。以ARM64为例u-boot通常被加载到DDR的高端地址或者低端地址具体取决于配置。SPL阶段u-boot被搬到DDR的某个基地址然后Proper在这个地址上运行。内核镜像通常被加载到DDR的另一个区域设备树放在内核附近启动参数放在内核能访问到的地方。这些地址不是随便定的它们由链接脚本和配置文件决定。你在移植u-boot的时候如果内存布局和内核的预期不一致就会出现“内核解压后跑飞”或者“设备树找不到”的问题。我一般会先看u-boot编译出来的u-boot.map文件确认各个段的链接地址然后对照SoC手册里的内存映射表检查DDR的可用范围。QEMU virt机器的DDR起始地址是0x40000000大小可以通过-m参数指定。u-boot的QEMU配置文件里已经定义好了这些地址你不需要改但你要知道它们在哪里这样出问题的时候才知道去哪里找。5.2 设备树u-boot怎么知道自己跑在什么硬件上设备树Device Tree是嵌入式Linux里描述硬件拓扑的数据结构u-boot也用它来识别硬件。在QEMU环境下u-boot可以通过内嵌的设备树或者QEMU传递的设备树来获取硬件信息。设备树里描述了CPU、内存、串口、中断控制器、存储控制器等所有硬件资源。u-boot的驱动模型Driver Model会根据设备树里的compatible属性来匹配对应的驱动。如果设备树里没有某个节点或者compatible属性写错了对应的驱动就不会被绑定硬件就不会被初始化。移植u-boot到新板子的时候设备树是必须改的。你需要根据原理图和SoC手册把DDR大小、串口基地址、时钟频率、存储控制器配置全部写进设备树。这个过程很繁琐但一旦写对u-boot就能自动识别硬件。我个人的经验是先用厂商提供的设备树作为基础只改必要的部分不要从头写。QEMU virt机器的设备树在u-boot源码的arch/arm/dts/目录下你可以参考它的写法。5.3 启动参数bootargs和bootcmd的配合逻辑u-boot通过环境变量bootargs把启动参数传给内核通过bootcmd定义自动启动的命令序列。bootargs里常见的内容包括控制台设备、根文件系统位置、内存大小限制等。比如setenv bootargs consolettyAMA0,115200 root/dev/vda rw rootwaitbootcmd则是一串u-boot命令比如setenv bootcmd ext4load virtio 0:1 0x40000000 /boot/Image; ext4load virtio 0:1 0x50000000 /boot/qemu.dtb; booti 0x40000000 - 0x50000000这段命令的意思是从virtio磁盘的第一个分区加载内核镜像到0x40000000加载设备树到0x50000000然后用booti启动。booti的第二个参数是initrd地址这里用-表示没有initrd。这些地址和命令需要根据你的实际环境调整。我见过很多人卡在“Starting kernel ...”之后没有任何输出最后发现是bootargs里的控制台设备写错了内核的输出没有打到串口上。6. 移植u-boot到真实硬件从QEMU到板子的关键跨越6.1 板级配置文件与defconfig的定制从QEMU转到真实硬件第一步是创建自己的板级配置。u-boot的配置体系分两层defconfig是默认配置保存在configs/目录下板级头文件在include/configs/目录下。你需要复制一个相近的defconfig改成自己的名字然后修改里面的关键配置比如DDR大小、串口基地址、启动介质类型。比如cp configs/qemu_arm64_defconfig configs/myboard_defconfig然后编辑myboard_defconfig把CONFIG_TARGET_QEMU_ARM64改成你自己的目标名并添加或修改相关配置。同时你需要在board/目录下创建自己的板级目录实现必要的板级初始化函数。这些函数包括board_init_f、board_init_r、dram_init等。dram_init尤其重要它告诉u-boot有多少内存可用。6.2 DDR初始化移植中最容易翻车的一环DDR初始化是u-boot移植里最硬核的部分。不同SoC的DDR控制器寄存器不一样时序参数也不一样。通常SoC厂商会提供一份DDR初始化代码或者配置表你需要把它移植到u-boot的SPL里。在QEMU环境下DDR是模拟的不需要初始化所以你在QEMU上跑通不代表在真实硬件上能跑通。真实硬件上DDR初始化失败的表现通常是SPL跑完没有任何输出或者输出乱码。排查方法是先用厂商提供的工具或者示例代码验证DDR硬件本身没问题然后检查u-boot里的DDR配置参数是否和硬件匹配。我个人的经验是DDR参数不要自己算直接用厂商提供的。如果你拿不到厂商的配置可以用示波器测量DDR时钟和信号反推时序参数。这个过程很痛苦但一旦搞定后面就顺了。另外有些SoC支持从SPI Flash启动SPL可以直接从Flash里读DDR配置这样你就不需要把DDR参数硬编码在SPL里。6.3 串口与存储驱动让u-boot能说话能读写串口是u-boot调试的生命线。如果串口没配好你什么都看不到。串口驱动需要配置波特率、数据位、停止位、校验位以及寄存器的基地址。在设备树里串口节点通常长这样uart0: serial10000000 { compatible ns16550a; reg 0x0 0x10000000 0x0 0x100; interrupts 0 32 4; clock-frequency 100000000; };你需要根据SoC手册修改reg和clock-frequency。存储驱动同样重要u-boot需要从存储介质里读内核。eMMC、SD卡、NAND的驱动在u-boot里都有现成的你只需要在defconfig里启用对应的配置并在设备树里描述存储控制器。我建议先把串口调通确保能看到输出再调存储。串口通了后面所有问题都能通过打印信息定位。7. 常见问题与排查技巧实录7.1 启动卡死问题速查表现象可能原因排查方法SPL无输出DDR初始化失败、串口未初始化检查DDR参数、串口基地址和时钟SPL输出乱码串口波特率或时钟频率不对核对SoC手册里的串口时钟Proper启动卡在DRAMDDR大小配置错误检查设备树memory节点和dram_init卡在MMC或存储初始化存储控制器驱动未启用或设备树错误检查defconfig和设备树节点Starting kernel后无输出bootargs控制台参数错误检查console参数和串口设备名内核panic找不到根文件系统root参数错误或存储驱动未编译进内核检查内核配置和bootargs这张表是我自己踩坑总结出来的基本上覆盖了80%的启动问题。遇到问题的时候先对照这张表排查能省很多时间。7.2 环境变量丢失与保存失败的处理u-boot的环境变量默认保存在存储介质的某个偏移地址。如果你发现saveenv之后重启环境变量又变回默认值了说明环境变量的存储位置配置有问题。常见原因有存储介质没有初始化、环境变量偏移地址和实际存储布局冲突、存储驱动写保护。排查方法是先用mmc info或者sf probe确认存储介质能被识别然后用md读取环境变量偏移地址的内容看看是不是真的写进去了。如果写进去了但重启后读不出来可能是读取的偏移地址和写入的不一致。7.3 网络启动与TFTP加载的配置要点网络启动是u-boot调试的利器尤其是在没有SD卡或者烧录不方便的时候。你需要配置网络环境变量setenv ipaddr 192.168.1.100 setenv serverip 192.168.1.10 setenv gatewayip 192.168.1.1 setenv netmask 255.255.255.0然后用ping命令测试网络连通性用tftpboot加载文件。网络不通的常见原因网卡驱动没启用、PHY地址不对、网络参数配置错误。我一般先用mii info查看PHY状态确认链路是否up然后再ping。TFTP服务器这边确保文件路径和权限正确防火墙不要挡住69端口。8. 从u-boot出发嵌入式Linux学习路线的下一步把u-boot跑通之后你对嵌入式系统的理解会上一个台阶。你会明白内核不是凭空启动的它需要引导程序把硬件准备好、把参数传对。接下来你可以做几件事一是尝试自己编译内核用u-boot加载自己编译的内核和设备树走完整个启动流程二是尝试用Buildroot或者Yocto构建一个完整的根文件系统让系统真正跑起来三是尝试在u-boot里添加自己的命令理解u-boot的命令注册机制和驱动模型。这三件事做完你对嵌入式Linux的启动链路就有了完整的认知。我个人在实际操作中的体会是u-boot的移植和调试最难的不是写代码而是理解硬件和软件的边界在哪里。哪些事情必须由u-boot做哪些事情可以留给内核这个边界搞清楚了移植就成功了一半。另外QEMU是个被低估的工具很多人觉得模拟环境不真实但实际上QEMU能帮你快速验证启动流程和配置把硬件相关的问题隔离出来。先用QEMU跑通再上真实硬件这个顺序能帮你省下大量烧录和调试的时间。最后再分享一个小技巧u-boot的bdinfo命令会打印板级信息包括内存起始地址、大小、环境变量偏移等遇到内存相关的问题先跑一下bdinfo很多答案就在里面。