☰
从单片机到u-boot:QEMU ARM64实战入门与启动流程解析
2026/9/29 1:54:06 网站建设 项目流程

1. 从单片机到 u-boot:为什么我劝你尽早跨过这道坎

玩过 51 或者 STM32 的朋友大概都有这种体会:点个灯、跑个 OLED、调个串口,成就感来得很快。但当你真正想往嵌入式 Linux 方向走的时候,会发现前面横着一堵墙——Bootloader。而 u-boot,就是这堵墙上最厚的那块砖。

我自己是从 STC89C52 一路玩上来的,中间经历过 STM32F103 的寄存器开发、HAL 库开发,后来因为一个项目需要跑 Linux 系统,被迫啃 u-boot。说实话,第一次看 u-boot 源码的时候,满屏的宏定义和链接脚本,头都大了。但熬过那两三个月之后回头看,单片机那套东西和 u-boot 比起来,真的只是"玩具级"的复杂度。

这篇内容我想聊的不是"u-boot 是什么"这种教科书问题,而是一个从单片机转过来的开发者,怎么用最短的时间把 u-boot 跑起来、看懂、改得动。核心关键词就几个:u-boot、嵌入式、ARM64、QEMU。我会用 QEMU 模拟 ARM64 环境来演示,因为这样你不需要买任何开发板,一台电脑就能把整个流程走通。适合谁看?有 C 语言基础、玩过单片机、想往嵌入式 Linux 方向转的兄弟。如果你连指针和结构体都还没搞明白,建议先把 C 语言补一补再来。

整个思路是这样的:先用 QEMU 把 ARM64 的 u-boot 编译、运行、调试跑通,建立感性认识;然后拆解 u-boot 的启动流程和关键数据结构;最后动手改一个自己的命令,理解 u-boot 的驱动模型和命令行框架。每一步我都会给出具体的命令和参数,你照着敲就行。

2. 为什么是 u-boot 而不是继续玩单片机

2.1 单片机与 u-boot 的本质差异

很多人觉得单片机玩到 STM32H7 这个级别,主频都 480MHz 了,和嵌入式 Linux 应该差不多吧?差得远。这个差异不在于主频,而在于软件栈的层次。

单片机开发,你写的代码直接跑在裸机上,最多加个 RTOS。中断向量表是你自己填的,堆栈是你自己设的,内存布局是你自己在链接脚本里定的。整个系统就你一个人在管,简单直接。

u-boot 不一样。它本身是一个裸机程序,但它要负责的事情多得多:初始化 DDR 控制器、配置时钟树、加载设备树、从各种介质(eMMC、NAND、TFTP、USB)读取内核镜像、校验镜像完整性、传递启动参数给内核。做完这些之后,它还要把自己"让出去",把 CPU 控制权交给 Linux 内核。这个过程涉及的东西,比单片机的中断处理复杂一个数量级。

我个人的感受是:单片机教会你怎么控制硬件,u-boot 教会你怎么管理一个完整的启动链路。前者是点,后者是线。

2.2 从单片机转 u-boot 需要补哪些课

从我的经验来看,从单片机转到 u-boot,需要补的课主要有这几块:

  • ARM64 架构基础:异常等级(EL0-EL3)、MMU 页表、GIC 中断控制器。这些在单片机上基本不涉及,但 u-boot 在 ARM64 上跑的时候必须处理。
  • 设备树(Device Tree):u-boot 和 Linux 内核都靠设备树来描述硬件。你得会看.dts和.dtsi文件,知道compatible属性怎么匹配驱动。
  • 链接脚本与内存布局:u-boot 的u-boot.lds决定了代码段、数据段、BSS 段放在哪里。这个比单片机的分散加载文件复杂得多。
  • 命令行框架与驱动模型:u-boot 有一套自己的 U_BOOT_CMD 宏和 DM(Driver Model)框架,理解这套东西才能改得动代码。

好消息是,这些概念你不需要一次性全部搞懂。我的建议是先跑起来,再逐步深入。下面我就用 QEMU 带你走一遍。

2.3 QEMU 为什么是学习 u-boot 的最佳搭档

学 u-boot 最大的障碍是什么?是烧录和调试的成本。你要是用真实开发板,每次改代码都要重新编译、烧录、上电、看串口输出,一轮下来少说五分钟。而且一旦 u-boot 跑飞了,串口什么都不输出,你根本不知道死在哪里。

QEMU 解决了这个问题。它可以在你的 x86 电脑上模拟一颗 ARM64 的 CPU,配合 GDB 可以单步调试 u-boot 的每一条指令。编译完直接运行,不需要烧录,不需要硬件。而且 QEMU 支持-d参数导出各种调试信息,配合-S -s可以让你在 u-boot 的第一条指令处就停下来。

我实测下来,用 QEMU 学习 u-boot 的效率至少是真实开发板的三倍。等你把 QEMU 上的流程跑通了,再上手真实开发板,会发现只是换了个加载方式而已。

3. 环境搭建:从零把 ARM64 u-boot 跑起来

3.1 工具链与依赖安装

先说环境。我用的是 Ubuntu 22.04,其他发行版大同小异。需要装的东西如下:

sudo apt update sudo apt install -y git build-essential bison flex \ libssl-dev libncurses-dev python3 python3-pip \ gcc-aarch64-linux-gnu gdb-multiarch \ qemu-system-arm device-tree-compiler

这里解释一下几个关键包的作用:

  • gcc-aarch64-linux-gnu:ARM64 的交叉编译器。你的电脑是 x86_64,要编译出 ARM64 能跑的代码,必须用交叉编译器。
  • qemu-system-arm:QEMU 的 ARM 模拟器。注意包名是qemu-system-arm,但它同时支持 ARM32 和 ARM64。
  • gdb-multiarch:支持多架构的 GDB,用来调试 ARM64 程序。
  • device-tree-compiler:设备树编译器,把.dts编译成.dtb。

注意:不要用gcc-arm-linux-gnueabihf,那是 ARM32 的编译器。ARM64 必须用aarch64前缀的。

3.2 获取 u-boot 源码并配置

源码直接从官方仓库拉:

git clone https://source.denx.de/u-boot/u-boot.git cd u-boot git checkout v2024.01

我选 v2024.01 这个版本,因为它对 QEMU ARM64 的支持比较完善,而且相对稳定。你如果拉最新版也行,但可能会遇到一些新引入的编译问题。

配置 QEMU ARM64 的默认配置:

make qemu_arm64_defconfig

这个defconfig文件在configs/qemu_arm64_defconfig,里面定义了 QEMU 虚拟机的默认配置。你可以打开看看,里面指定了CONFIG_TARGET_QEMU_ARM_64BIT=y、CONFIG_SYS_TEXT_BASE=0x40200000等关键参数。

3.3 编译与产物说明

编译命令:

make CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)

编译完成后,在根目录下会生成几个关键文件:

文件名作用
u-bootELF 格式的 u-boot,带符号信息,用于 GDB 调试
u-boot.bin纯二进制格式,用于烧录到开发板
u-boot.map链接映射文件,可以看到每个函数和变量在内存中的地址
u-boot.srecS-record 格式,某些烧录工具需要
.config当前配置,和内核的.config类似

u-boot.map这个文件非常重要。当你调试的时候想知道某个函数在哪个地址,直接在这个文件里搜就行。我经常用它来确认board_init_f、board_init_r这些关键函数的地址。

3.4 用 QEMU 启动 u-boot

启动命令:

qemu-system-aarch64 \ -machine virt \ -cpu cortex-a57 \ -nographic \ -bios u-boot.bin

参数解释:

  • -machine virt:QEMU 的虚拟开发板,ARM64 上最常用的虚拟平台。
  • -cpu cortex-a57:模拟 Cortex-A57 核心,这是 ARMv8 的 64 位核心。
  • -nographic:不用图形界面,串口输出直接打到终端。
  • -bios u-boot.bin:把 u-boot.bin 当作固件加载。

启动后你应该能看到类似这样的输出:

U-Boot 2024.01 (Jan 15 2024 - 10:30:00 +0800) DRAM: 128 MiB Core: 51 devices, 14 uclasses, devicetree: board Flash: 64 MiB In: serial Out: serial Err: serial Net: No ethernet found. Hit any key to stop autoboot: 0 =>

看到=>提示符,说明 u-boot 已经跑起来了。你可以敲help看看支持哪些命令,敲bdinfo看看板级信息,敲printenv看看环境变量。

提示:如果卡在 "Hit any key to stop autoboot",直接按回车就能进入命令行。

4. 深入 u-boot 启动流程:从第一条指令到命令行

4.1 ARM64 的启动入口与异常等级

u-boot 在 ARM64 上的入口是arch/arm/cpu/armv8/start.S里的_start。这个文件的第一条指令就是整个 u-boot 的起点。

ARM64 有四个异常等级:EL0(用户态)、EL1(内核态)、EL2(虚拟化)、EL3(安全监控)。u-boot 通常运行在 EL2 或 EL1。QEMU 的virt机器默认从 EL2 开始执行,所以 u-boot 启动后会先判断当前异常等级,然后做相应的初始化。

_start里做的事情按顺序是:

  1. 关闭 MMU 和 D-Cache(因为此时还没有建立页表)
  2. 设置异常向量表基地址
  3. 判断当前 EL 等级,如果是 EL3 或 EL2,做相应的降级或初始化
  4. 设置栈指针
  5. 跳转到_main

这个流程和单片机的启动文件(比如 STM32 的startup_stm32f103.s)思路是一样的,只是 ARM64 多了异常等级的处理。

4.2 board_init_f 与 board_init_r 的分工

_main在arch/arm/lib/crt0_64.S里,它最终会调用两个关键函数:board_init_f和board_init_r。

board_init_f运行在重定位之前,此时 u-boot 还在只读内存(QEMU 里是 ROM 或 Flash)中运行。它做的事情包括:

  • 初始化串口(让你能看到输出)
  • 初始化定时器
  • 计算重定位目标地址
  • 填充gd(global data)结构体
  • 调用board_init_f的初始化序列

board_init_r运行在重定位之后,此时 u-boot 已经被拷贝到 DDR 中运行。它做的事情包括:

  • 初始化 DM(Driver Model)框架
  • 扫描设备树,绑定驱动
  • 初始化各种外设(MMC、USB、网络等)
  • 进入主循环,等待用户输入命令

这个"两阶段"设计和单片机的启动流程有本质区别。单片机通常只有一次初始化,而 u-boot 必须分两阶段,因为它在重定位之前不能写全局变量(还在只读内存里)。

4.3 重定位:u-boot 为什么要"搬家"

重定位是 u-boot 里一个非常重要的概念。简单说,u-boot 编译出来的代码链接地址是CONFIG_SYS_TEXT_BASE(QEMU ARM64 上是0x40200000),但实际运行时,它需要把自己拷贝到 DDR 的高地址区域(通常是内存顶端往下若干 MB)。

为什么要搬家?因为 u-boot 最终要加载 Linux 内核,内核需要一块连续的低地址内存。如果 u-boot 一直占着低地址不放,内核就没地方放了。所以 u-boot 在初始化完 DDR 之后,会把自己拷贝到内存高端,把低端内存让出来给内核。

重定位的核心代码在common/board_f.c的relocate_code函数里。它会计算源地址、目标地址、代码长度,然后调用memcpy完成拷贝。拷贝完成后,通过修改gd->relocaddr和调整栈指针,让后续代码在新地址上运行。

我踩过的一个坑是:如果你在board_init_f阶段用了一个全局变量,重定位之后这个变量的值会丢失,因为全局变量在重定位时被拷贝了一份,但指针还指向旧地址。正确的做法是用gd结构体来传递数据,因为gd的地址在重定位时会被更新。

4.4 设备树在 u-boot 中的角色

u-boot 从 v2015.07 开始引入 DM(Driver Model),设备树成为描述硬件的核心机制。在 QEMU ARM64 上,u-boot 会从arch/arm/dts/qemu-arm64.dts编译出设备树,并在启动时解析它。

设备树里每个节点都有一个compatible属性,u-boot 用这个属性来匹配驱动。比如串口节点:

uart0: serial@9000000 { compatible = "arm,pl011"; reg = <0x0 0x9000000 0x0 0x1000>; clocks = <&uartclk>; };

u-boot 启动时会扫描设备树,找到compatible = "arm,pl011"的节点,然后调用对应的 PL011 串口驱动来初始化它。这个机制和 Linux 内核的设备树匹配完全一样。

理解设备树是理解 u-boot 驱动模型的关键。如果你之前只玩过单片机,建议花点时间看看设备树的语法,至少要知道reg、compatible、status这几个属性的含义。

5. 动手改一个 u-boot 命令:从看懂到改得动

5.1 u-boot 命令行框架解析

u-boot 的命令行框架核心是U_BOOT_CMD宏。每个命令都是通过这个宏注册的。以md(memory display)命令为例:

U_BOOT_CMD( md, 5, 1, do_mem_md, "memory display", "[.b, .w, .l, .q] address [# of objects]" );

参数含义:

  • 第一个参数:命令名
  • 第二个参数:最大参数个数
  • 第三个参数:是否可重复(1 表示按回车重复执行)
  • 第四个参数:命令处理函数
  • 第五个参数:简短描述
  • 第六个参数:用法说明

这个宏展开后会生成一个cmd_tbl_t结构体,放在.u_boot_list段里。u-boot 启动时会遍历这个段,把所有命令注册到命令表里。

5.2 编写一个自定义命令

我们来写一个简单的命令myinfo,功能是打印当前 CPU 的异常等级和 u-boot 的重定位地址。

在cmd/目录下新建myinfo.c:

#include <common.h> #include <command.h> #include <asm/system.h> static int do_myinfo(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[]) { unsigned long el; el = current_el(); printf("Current EL: %lu\n", el); printf("U-Boot relocaddr: 0x%lx\n", gd->relocaddr); printf("U-Boot text base: 0x%lx\n", gd->ram_base); return CMD_RET_SUCCESS; } U_BOOT_CMD( myinfo, 1, 1, do_myinfo, "print my custom info", "" );

然后在cmd/Makefile里加上:

obj-$(CONFIG_CMD_MYINFO) += myinfo.o

在cmd/Kconfig里加上配置项:

config CMD_MYINFO bool "myinfo" help Print custom information about the current U-Boot.

最后在configs/qemu_arm64_defconfig里加上CONFIG_CMD_MYINFO=y,重新编译。

5.3 编译、运行与验证

重新编译:

make CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)

启动 QEMU:

qemu-system-aarch64 -machine virt -cpu cortex-a57 -nographic -bios u-boot.bin

在 u-boot 命令行里敲myinfo,你应该能看到:

=> myinfo Current EL: 2 U-Boot relocaddr: 0x7fe00000 U-Boot text base: 0x40000000

这说明你的自定义命令已经成功注册并运行了。Current EL: 2表示 u-boot 运行在 EL2,relocaddr是重定位后的地址,ram_base是 DDR 的起始地址。

提示:如果你敲myinfo提示 "Unknown command",检查一下.config里CONFIG_CMD_MYINFO是否真的被设成了y。有时候defconfig的修改不会自动生效,需要手动跑一次make qemu_arm64_defconfig再编译。

5.4 用 GDB 单步调试 u-boot

QEMU 配合 GDB 调试 u-boot 非常方便。启动 QEMU 时加上-S -s:

qemu-system-aarch64 -machine virt -cpu cortex-a57 -nographic -bios u-boot.bin -S -s

-S表示启动时暂停,-s表示在 1234 端口开启 GDB server。

另开一个终端,启动 GDB:

gdb-multiarch u-boot

在 GDB 里连接 QEMU:

(gdb) target remote :1234 (gdb) b board_init_f (gdb) c

这样就能在board_init_f处停下来,然后单步执行,观察寄存器和内存的变化。我经常用这种方式来确认重定位前后的地址变化,比看代码直观得多。

6. 常见问题与排查技巧实录

6.1 QEMU 启动后没有任何输出

这是最常见的问题。排查思路如下:

现象可能原因解决方法
完全无输出-bios参数路径错误确认u-boot.bin在当前目录
完全无输出编译目标不是 ARM64检查CROSS_COMPILE是否为aarch64-linux-gnu-
输出乱码串口波特率不匹配QEMU 默认 115200,检查CONFIG_BAUDRATE
输出后卡死设备树不匹配确认用的是qemu_arm64_defconfig

我遇到过一次,编译出来的u-boot.bin只有几 KB,明显不对。后来发现是make的时候没加CROSS_COMPILE,用的是本机 x86 编译器,编译出来的东西根本跑不了。所以每次编译前一定要确认交叉编译器前缀。

6.2 重定位后代码跑飞

如果你在board_init_f里加了自定义代码,重定位后跑飞了,大概率是全局变量的问题。前面说过,重定位前全局变量在只读内存里,重定位后会被拷贝到新地址,但如果你在重定位前修改了全局变量,重定位时会被覆盖掉。

正确的做法是用gd结构体。gd是一个指向global_data的指针,重定位时 u-boot 会更新这个指针的值,保证它始终指向正确的位置。

6.3 自定义命令不生效

检查清单:

  1. .config里对应的CONFIG_CMD_XXX是否为y
  2. cmd/Makefile里是否加了obj-$(CONFIG_CMD_XXX) += xxx.o
  3. cmd/Kconfig里是否定义了配置项
  4. 编译时是否有报错被忽略

我踩过的坑是:在Kconfig里定义了配置项,但忘了在defconfig里打开,结果编译出来的 u-boot 里根本没有这个命令。后来养成习惯,每次加新命令都用grep CONFIG_CMD_MYINFO .config确认一下。

6.4 GDB 连接不上 QEMU

如果 GDB 报 "Connection refused",检查:

  • QEMU 是否加了-s参数
  • 1234 端口是否被占用(lsof -i :1234)
  • 防火墙是否拦截了本地回环

还有一个容易忽略的点:gdb-multiarch需要指定架构。如果连接后提示 "Remote 'g' packet reply is too long",在 GDB 里执行set architecture aarch64再连接。

6.5 编译报错 "No rule to make target"

这种错误通常是defconfig文件损坏或者版本不匹配。解决办法:

make distclean make qemu_arm64_defconfig make CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)

make distclean会清除所有生成的文件,包括.config,然后重新配置。这个操作我每次切换 u-boot 版本的时候都会做一遍,能避免很多莫名其妙的编译问题。

7. 从 QEMU 到真实开发板的迁移思路

在 QEMU 上把 u-boot 跑通之后,下一步就是上真实开发板。迁移的核心思路是换配置、换设备树、换加载方式。

换配置:把qemu_arm64_defconfig换成你开发板对应的defconfig,比如orangepi_pc2_defconfig、rock64-rk3328_defconfig等。每个开发板的配置里定义了 DDR 初始化参数、时钟频率、串口引脚等硬件相关的东西。

换设备树:QEMU 用的是qemu-arm64.dts,你的开发板有自己的.dts文件,通常在arch/arm/dts/目录下。设备树里描述了开发板上的所有外设,包括 eMMC、网口、USB、GPIO 等。

换加载方式:QEMU 用-bios直接加载 u-boot.bin,真实开发板通常通过 SD 卡、eMMC 或串口加载。SD 卡启动的话,需要把 u-boot 写到特定的偏移地址(比如 RK3328 是 0x8000),这个偏移量在芯片手册里有说明。

我个人的建议是:先在 QEMU 上把 u-boot 的启动流程和命令框架搞明白,再上开发板。因为开发板上如果 u-boot 跑不起来,你连串口输出都看不到,排查起来非常痛苦。QEMU 至少能保证你有一个可工作的参考环境。

8. 学习路径与资源推荐

从单片机转到 u-boot,我建议按这个顺序来:

  1. 先跑通 QEMU ARM64 的 u-boot,理解启动流程和命令行
  2. 读board_init_f和board_init_r的源码,理解两阶段初始化的设计
  3. 学设备树语法,能看懂.dts文件,知道compatible怎么匹配驱动
  4. 写一个自定义命令,理解U_BOOT_CMD宏和命令注册机制
  5. 用 GDB 单步调试,观察重定位前后的内存变化
  6. 上真实开发板,把 QEMU 上的经验迁移过去

资源方面,u-boot 官方文档(doc/目录下的.rst文件)是最权威的参考资料。另外,u-boot.map和System.map是调试时的好帮手,遇到不认识的函数直接在里面搜地址。

最后分享一个我自己的习惯:每次改 u-boot 代码之前,先用git diff看一下当前修改,确认没有误改其他文件。u-boot 的代码量很大,一个不小心改错了地方,编译能过但运行跑飞,排查起来非常费时间。保持修改的最小化和可追溯性,是玩 u-boot 的基本素养。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询