
1. 从单片机到 u-boot为什么我劝你尽早跨过这道坎如果你现在还在用 51 单片机点灯、用 STM32 跑裸机循环觉得嵌入式也就那么回事那我接下来要说的话可能会让你有点不舒服你目前接触的只是嵌入式世界里最表层的那一小块。真正让嵌入式工程师拉开差距的不是你会不会写GPIO_SetBits而是你能不能把一颗 ARM64 芯片从加电那一刻开始一步步引导到操作系统跑起来。这中间的核心角色就是u-boot。我大概带了不下二十个从单片机转过来的兄弟几乎所有人第一次面对 u-boot 源码时的反应都是一样的目录一大堆、配置文件看不懂、编译报错、烧进去串口没输出。但一旦你啃下第一块板子后面的事情就会变得非常顺。u-boot 不只是一个 bootloader它是你理解嵌入式 Linux 启动链路、ARM64 体系结构、设备树、内存布局、驱动模型的最佳入口。你搞懂了 u-boot回头再看内核源码会发现很多设计思路是一脉相承的。这篇文章我打算把从单片机思维切换到 u-boot 开发的完整路径讲清楚。包括为什么要学、学之前要补哪些课、怎么用QEMU 模拟 ARM64环境零成本上手、u-boot 的启动流程到底分几个阶段、编译配置怎么改、常见问题怎么排查。适合有 C 语言基础、玩过单片机、想往嵌入式 Linux 方向走的同学。不需要你手上有开发板一台能跑 QEMU 的电脑就够了。2. 单片机思维和 u-boot 思维差在哪里2.1 单片机开发到底在做什么先把你熟悉的东西捋一遍。51 单片机也好STM32 也好本质上你做的事情是上电后 CPU 从固定地址取第一条指令这条指令通常在你的启动文件里然后跳到main函数你在main里初始化时钟、配置外设、写业务逻辑。整个系统只有你一个程序在跑内存是你直接操作的物理地址没有虚拟内存没有进程调度没有文件系统。这种模式的好处是确定性极强。你写P1 0xFEP1 端口的电平立刻变化中间没有任何抽象层。但问题也很明显代码规模一大就难以维护想跑网络协议栈、想接文件系统、想同时处理多个任务裸机就会变得非常吃力。这就是为什么复杂嵌入式系统最终都要上 Linux。2.2 u-boot 在系统里扮演什么角色u-boot 全称 Universal Boot Loader它做的事情可以类比成 PC 上的 BIOS 加上 GRUB 的组合。芯片上电后最先运行的是一小段固化在芯片内部的 ROM 代码BootROM它负责从启动介质eMMC、SD 卡、SPI Flash、网络等加载一小段代码到片内 SRAM 执行。这段代码就是 u-boot 的SPLSecondary Program Loader阶段。SPL 的任务很单一初始化 DDR 控制器把完整的 u-boot 从存储介质搬到 DDR 里然后跳过去执行。完整的 u-boot 跑起来之后它会初始化串口、网口、存储、USB 等外设提供命令行交互界面最后根据环境变量加载 Linux 内核和设备树把控制权交给内核。所以 u-boot 的核心职责就三件事初始化硬件、加载内核、传递参数。这里有个关键点很多人一开始不理解为什么不能直接上电就跑 Linux 内核因为内核镜像通常好几 MB而芯片内部的 SRAM 只有几十到几百 KB根本放不下。所以必须有一个中间人先把 DDR 初始化好再把内核搬到大内存里。这个中间人就是 u-boot。2.3 从单片机转过来需要补的三块知识第一块是ARM64 体系结构基础。你不需要把 ARM Architecture Reference Manual 几千页全看完但至少要搞清楚异常级别 EL0 到 EL3 的区别、AArch64 的寄存器布局、MMU 页表的基本概念。这些在 u-boot 的启动汇编代码里到处都会出现。第二块是设备树Device Tree。单片机时代你是在代码里硬编码硬件信息ARM Linux 体系下硬件描述被抽离成了设备树文件。u-boot 自己也用设备树来描述板级硬件编译时会用到.dts文件。你得能看懂一个简单的 dts 文件知道compatible、reg、status这些属性是什么意思。第三块是交叉编译工具链。单片机你可能用 Keil 或者 IAR 一键编译u-boot 是在 Linux 环境下用aarch64-linux-gnu-gcc这类交叉编译器构建的。你得熟悉 Makefile 的基本规则、环境变量的设置、链接脚本的作用。提示不要试图先把这三块知识全部学完再动手。正确的做法是边做边学先用 QEMU 把 u-boot 跑起来遇到不懂的概念再回头查。这样学习效率比啃书高十倍。3. 用 QEMU 搭建 ARM64 的 u-boot 实验环境3.1 为什么选 QEMU 而不是买开发板我见过太多人一腔热血买了块 ARM64 开发板结果卡在串口驱动装不上、烧录工具不兼容、板子根本起不来热情三天就耗光了。QEMU 的好处是零成本、可重复、随时快照。你编译出来的 u-boot 二进制直接丢给 QEMU 就能跑串口输出直接打印在终端里不需要任何硬件连线。而且 QEMU 支持-d参数导出 CPU 执行日志支持 GDB 远程调试这些在真实硬件上要么做不到要么需要昂贵的仿真器。对于学习 u-boot 启动流程来说QEMU 是最合适的工具没有之一。3.2 环境准备的具体步骤我用的环境是 Ubuntu 22.04其他 Linux 发行版操作类似。先装必要的工具sudo apt update sudo apt install -y git build-essential flex bison \ libssl-dev libncurses-dev device-tree-compiler \ gcc-aarch64-linux-gnu qemu-system-arm gdb-multiarch这里每个包都有用flex和bison是 u-boot 构建系统需要的词法语法分析工具libssl-dev用于签名相关功能device-tree-compiler提供dtc命令来编译设备树gcc-aarch64-linux-gnu是 ARM64 交叉编译器qemu-system-arm是模拟器本体gdb-multiarch用于后续调试。装完之后验证一下交叉编译器aarch64-linux-gnu-gcc --version能打印出版本号就说明工具链没问题。接下来拉 u-boot 源码git clone https://source.denx.de/u-boot/u-boot.git cd u-boot git checkout v2024.01我建议 checkout 一个稳定的 tag不要直接用 master因为 master 上的代码随时在变可能引入你排查不了的编译问题。v2024.01 是我实测比较稳定的版本。3.3 QEMU 的 ARM64 虚拟平台选哪个QEMU 支持好几种 ARM64 虚拟板子最常用的是qemu_arm64_defconfig对应的virt机器。这个虚拟平台模拟了一颗通用的 ARM64 CPU、一段内存、一个 PL011 串口、一个 GIC 中断控制器。u-boot 官方对它有完整的支持编译出来直接能跑。配置和编译命令如下make qemu_arm64_defconfig make CROSS_COMPILEaarch64-linux-gnu- -j$(nproc)编译完成后在源码根目录下会生成u-boot.bin这就是我们要交给 QEMU 的文件。整个编译过程大概两三分钟取决于你的机器性能。启动命令qemu-system-aarch64 -machine virt -cpu cortex-a57 \ -nographic -bios u-boot.bin-nographic把串口重定向到当前终端-bios指定固件文件。执行后你应该能看到 u-boot 的启动日志最后停在一个提示符前面。恭喜你你已经成功让 u-boot 在 ARM64 上跑起来了。4. u-boot 启动流程的深度拆解4.1 从第一条指令到 board_init_ru-boot 的启动流程可以分成两个大阶段SPL 阶段和完整 u-boot 阶段。在 QEMU 环境下因为不需要真正初始化 DDRSPL 阶段被简化了但流程逻辑是一样的。完整 u-boot 的入口在arch/arm/lib/vectors.S里的_start标号。第一条指令是跳转到reset向量然后依次执行设置 CPU 到 EL2 或 EL1、关闭 MMU 和 Cache、设置栈指针、清零 BSS 段、调用board_init_f。board_init_f是在 DDR 可用之前运行的它只能使用全局数据区gd不能使用全局变量。这个函数的主要工作是初始化串口让你能看到输出、计算内存布局、填充 gd 结构体。做完这些之后它会调用relocate_code把 u-boot 自身重定位到 DDR 的高端地址然后跳转到board_init_r。board_init_r运行在 DDR 里可以使用完整的 C 运行环境。它负责初始化所有外设驱动、扫描存储设备、设置环境变量、最后进入主循环等待用户输入。你在串口里看到的提示符就是主循环的产物。4.2 关键数据结构 gd 和 bdu-boot 里有两个非常重要的全局数据结构gdglobal data和bdboard data。gd存放的是 u-boot 运行时的全局状态比如串口设备指针、环境变量地址、重定位偏移量等。bd存放的是板级信息比如内存起始地址、内存大小、CPU 频率等。这两个结构体在board_init_f阶段被填充在board_init_r阶段被大量使用。你如果要在 u-boot 里加自己的调试代码打印gd-ram_size或者gd-relocaddr是最常用的手段。4.3 环境变量的加载与保存u-boot 的环境变量默认保存在存储介质的一个固定偏移处格式是一个 CRC32 校验头加上一堆keyvalue字符串。启动时 u-boot 会尝试从CONFIG_ENV_OFFSET指定的位置读取环境变量如果 CRC 校验失败就加载编译时内置的默认环境变量。在 QEMU 环境下默认环境变量是编译进二进制的你修改后用saveenv保存会提示找不到存储设备。这是正常的因为虚拟平台没有配置持久化存储。如果你想练习环境变量操作可以给 QEMU 挂一个虚拟 SD 卡或者用-drive参数挂一个文件作为存储后端。5. 实操从零编译到串口交互的完整过程5.1 编译配置的定制化修改默认的qemu_arm64_defconfig已经够用了但如果你想体验真实的开发流程可以试着改一些配置。比如打开CONFIG_CMD_BOOTI支持直接启动 Linux 内核镜像打开CONFIG_CMD_FDT支持设备树操作。修改配置有两种方式直接编辑configs/qemu_arm64_defconfig文件或者用make menuconfig图形界面。我推荐后者因为菜单界面能看到每个选项的依赖关系和帮助说明对理解配置项很有帮助。make CROSS_COMPILEaarch64-linux-gnu- menuconfig进去之后你可以看到 u-boot 的配置系统有多庞大从 CPU 架构到具体驱动从命令支持到调试选项几千个配置项。不要被吓到你只需要关注Command line interface和Device Drivers这两大类就够了。5.2 启动日志的逐行解读当你执行 QEMU 启动命令后串口会打印出一大段日志。我挑几个关键行解释一下U-Boot 2024.01 (Jan 15 2024 - 10:30:00 0800)这是版本和编译时间。如果你改了代码重新编译时间会更新可以用来确认你烧进去的是不是最新版本。DRAM: 128 MiB这是 QEMU virt 平台默认的内存大小。这个值来自设备树里的 memory 节点u-boot 解析设备树后填充到 gd 结构体里。Core: 12 devices, 8 uclasses, devicetree: separate这行说明 u-boot 的驱动模型扫描到了 12 个设备归入 8 个 uclass。uclass 是 u-boot 驱动模型的核心概念类似 Linux 的设备类把功能相似的驱动归在一起统一管理。Loading Environment from nowhere... OK前面说过QEMU 环境没有持久化存储所以环境变量是从内置的默认值加载的。In: serial Out: serial Err: serial标准输入输出都绑定到了串口这就是为什么你能在终端里和 u-boot 交互。5.3 常用命令的实操演示进入提示符后你可以试试这些命令 bdinfo打印板级信息包括内存起始地址、大小、重定位后的 u-boot 地址、环境变量地址等。这个命令是排查启动问题的第一选择。 printenv打印所有环境变量。你会看到baudrate、bootcmd、bootargs等关键变量。bootcmd是自动启动时执行的命令序列bootargs是传给 Linux 内核的启动参数。 md 0x40000000 0x10从内存地址 0x40000000 开始显示 16 个字。md是 memory display 的缩写调试时用来查看内存内容非常方便。 mm 0x40000000交互式修改内存。输入地址后会提示你输入新值回车确认输入q退出。这个命令在验证寄存器写入时很有用。 help列出所有可用命令。不同配置下命令数量不同默认的 qemu_arm64 配置大概有七八十条命令。6. 常见问题排查与避坑经验6.1 编译报错的高频原因报错一aarch64-linux-gnu-gcc: command not found原因是你没装交叉编译器或者装了但没在 PATH 里。用which aarch64-linux-gnu-gcc确认一下。如果确实装了但找不到检查你的~/.bashrc里有没有把工具链路径加进去。报错二fatal error: openssl/ssl.h: No such file or directory缺少libssl-dev。u-boot 的签名和加密功能依赖 OpenSSL 头文件。装上就好。报错三ERROR: You must install dtc缺少设备树编译器。sudo apt install device-tree-compiler解决。报错四编译到一半提示multiple definition of xxx这种通常是你的编译环境里残留了上次编译的目标文件而源码结构发生了变化。执行make distclean彻底清理后重新配置编译。6.2 QEMU 启动无输出的排查思路这是新手最常遇到的问题命令敲下去终端一片空白没有任何输出。排查顺序如下第一步确认u-boot.bin文件存在且大小合理。正常的 ARM64 u-boot 二进制大概几百 KB 到 1 MB 左右。如果只有几 KB说明编译没成功。第二步确认 QEMU 命令参数正确。-machine virt和-cpu cortex-a57是必须的-nographic也是必须的否则串口输出不会重定向到终端。第三步加-d in_asm参数看 QEMU 是否在执行指令。如果日志里全是 0 地址的指令说明 u-boot 的入口地址和 QEMU 期望的不一致。第四步用 GDB 连上去看 PC 指针停在哪里。启动 QEMU 时加-s -S参数然后用gdb-multiarch连接localhost:1234执行info registers看 CPU 状态。6.3 环境变量保存失败的解决在 QEMU 里执行saveenv大概率会报错因为默认没有配置可写的存储后端。解决办法是给 QEMU 挂一个虚拟磁盘qemu-system-aarch64 -machine virt -cpu cortex-a57 \ -nographic -bios u-boot.bin \ -drive fileenv.img,formatraw,ifnone,idenvdisk \ -device virtio-blk-device,driveenvdisk然后需要在 u-boot 配置里打开CONFIG_ENV_IS_IN_MMC或CONFIG_ENV_IS_IN_VIRTIO并设置正确的偏移量。这个操作稍微复杂一点但走通一次之后你就理解了环境变量持久化的完整链路。6.4 常见问题速查表问题现象可能原因排查方法编译报错找不到编译器工具链未安装或 PATH 未配置which aarch64-linux-gnu-gcc编译报错缺头文件依赖库未安装根据报错信息安装对应 dev 包QEMU 启动无输出参数错误或二进制损坏检查文件大小加-d in_asm串口输出乱码波特率不匹配QEMU 默认 115200检查配置saveenv 失败无持久化存储挂载虚拟磁盘并配置环境变量存储GDB 连不上未加-s -S参数启动命令加上调试参数注意每次修改 u-boot 配置后一定要先make distclean再重新make xxx_defconfig否则旧的配置缓存会导致各种诡异问题。这个坑我踩过不止一次。7. 从 u-boot 出发下一步该往哪走7.1 用 u-boot 加载 Linux 内核u-boot 跑通之后最自然的下一步是让它加载一个真实的 Linux 内核。你需要准备三个东西内核镜像Image、设备树文件qemu-arm64.dtb、以及一个根文件系统。QEMU 环境下可以用-kernel参数直接加载内核但那样就绕过了 u-boot。正确的做法是在 u-boot 命令行里用booti命令手动加载。具体操作是先用tftp或者load命令把内核镜像和设备树加载到内存然后执行 booti 0x40080000 - 0x44000000第一个参数是内核镜像地址第二个参数是 initrd 地址没有就写-第三个参数是设备树地址。执行后 u-boot 会把控制权交给内核你会看到内核启动日志刷屏。7.2 阅读 u-boot 源码的正确姿势u-boot 源码有几十万行不要试图从头读到尾。正确的做法是跟着启动流程读。从_start开始一路跟到board_init_r把这条主线上调用的每个关键函数都看一遍。遇到不认识的函数就跳进去看实现看完再回来继续。我建议重点读这几个文件arch/arm/lib/vectors.S入口汇编、common/board_f.cboard_init_f 实现、common/board_r.cboard_init_r 实现、drivers/serial/serial-uclass.c串口驱动模型。把这四个文件吃透u-boot 的骨架你就掌握了。7.3 驱动模型和 Linux 的异同u-boot 的驱动模型DM和 Linux 的设备模型在设计理念上很像都是通过设备树匹配驱动、通过 uclass/class 统一管理同类设备。但 u-boot 的 DM 更轻量没有电源管理、没有热插拔、没有复杂的引用计数。如果你以后要写 Linux 驱动先在 u-boot 里写一个简单的 DM 驱动是很好的练手。u-boot 的驱动代码量小、逻辑清晰调试也方便适合用来理解设备树匹配、probe 流程、uclass 操作这些核心概念。7.4 进阶方向安全启动与固件更新u-boot 还支持安全启动Verified Boot和固件更新DFU、fastboot。安全启动的核心是对内核镜像做签名校验防止被篡改。固件更新则是通过 USB 或者网络把新固件写入存储介质。这两个方向在实际产品中非常重要但学习曲线也比较陡建议先把基础启动流程搞扎实再碰。我个人在实际操作中的体会是u-boot 这个东西入门确实有门槛但它的知识密度极高。你花两周时间啃下来的东西可能比你在单片机上折腾两个月学到的还多。而且这些知识是通用的换任何一块 ARM64 板子启动流程、驱动模型、环境变量机制都是一样的。最后再分享一个小技巧把 QEMU 的启动命令写成一个 shell 脚本每次改完代码一键编译加启动能省下大量重复劳动的时间。