第3章 Bootloader原理与移植:U-Boot启动流程分析
Bootloader,说白了就是上电后第一个跑的程序。它负责把硬件初始化好,再把内核加载进内存。在ARM Linux系统里,U-Boot几乎是事实标准。我这些年做过的项目,从Cortex-A7到A72,基本都用它。
这一章,我们就把U-Boot的启动流程、源码结构、板级配置讲透。最后用QEMU virt平台做一次完整的移植实战。嗯,跟着走一遍,你就能自己动手移植了。
3.1 U-Boot启动流程全景
先看整体流程。U-Boot启动分两个阶段:SPL(Secondary Program Loader)和U-Boot proper。SPL是精简版,负责初始化DDR、时钟等基础外设,然后加载完整的U-Boot。
核心要点:SPL是U-Boot 2014年以后引入的机制。早期U-Boot只有一个blob,但很多SoC内部SRAM太小(通常只有64KB~256KB),装不下完整U-Boot。SPL就是为解决这个问题诞生的。
下面这张图,是我整理的U-Boot启动全流程。你看一眼,心里就有谱了。
我的经验:调试SPL阶段的问题最头疼。因为SPL运行在SRAM里,没有完整串口驱动,打印信息有限。我习惯在SPL里加GPIO翻转,用逻辑分析仪看波形,比看串口日志靠谱得多。
3.2 U-Boot源码结构
U-Boot源码目录看着大,其实核心就几个。我列个表,你对照着看。
| 目录/文件 | 作用 | 我关注的点 |
|---|---|---|
arch/arm/ |
ARM架构相关代码,含CPU初始化、异常向量 | 启动时第一个跳转入口在这里 |
board/ |
板级代码,每个板子一个子目录 | 移植时主要改这里 |
include/configs/ |
板级配置头文件 | 裁剪时删宏定义就行 |
drivers/ |
外设驱动,串口、网卡、存储等 | 加新硬件时加驱动 |
dts/ |
设备树源文件 | 现在U-Boot也用自己的设备树 |
common/ |
通用代码,如命令行、环境变量 | main_loop()在这里 |
tools/ |
mkimage等工具 | 打包内核镜像用 |
启动入口在 arch/arm/lib/vectors.S。上电后CPU从异常向量表跳转到 _start,然后执行 cpu_init_crit、lowlevel_init,最后进入 board_init_f。这段汇编代码,我建议你至少读一遍。虽然看着枯燥,但能帮你理解ARM上电后的硬件状态。
3.3 板级配置与设备树
U-Boot的板级配置,传统方式是用头文件宏定义。比如 include/configs/qemu-arm.h 里定义内存大小、串口地址、环境变量位置等。但新版本U-Boot已经全面转向设备树(DTB)了。
注意:U-Boot的设备树和内核的设备树是两套。虽然语法一样,但U-Boot的设备树只描述U-Boot自己需要的外设。比如U-Boot不需要GPU驱动,设备树里就不写GPU节点。我见过有人直接把内核设备树扔给U-Boot,结果U-Boot解析失败,卡死在初始化阶段。
板级配置的核心文件:
- Kconfig:在
board/目录下,定义板级配置选项 - MAINTAINERS:维护者信息
- board.c:板级初始化函数,如
board_init()、dram_init() - 设备树:在
arch/arm/dts/下,以.dts文件存在
举个例子,QEMU virt平台的设备树节点长这样:
/dts-v1/;
/ {
model = "QEMU virt ARM64";
compatible = "qemu,virt";
memory@40000000 {
device_type = "memory";
reg = <0x0 0x40000000 0x0 0x40000000>;
};
uart@9000000 {
compatible = "ns16550a";
reg = <0x0 0x9000000 0x0 0x1000>;
clock-frequency = <3686400>;
};
};
看到没?就三个节点:根节点、内存节点、串口节点。U-Boot启动时,会解析这个设备树,获取内存大小和串口地址。不需要的节点一概不写。
3.4 U-Boot移植实战:以QEMU virt平台为例
好,理论讲完了。我们动手做一次移植。目标平台是QEMU virt,ARM64架构。这个平台没有真实硬件,但用来学移植流程再合适不过。
3.4.1 环境准备
你需要:
- 交叉编译工具链:aarch64-linux-gnu-
- U-Boot源码:从
https://source.denx.de/u-boot/u-boot.git克隆 - QEMU:安装qemu-system-aarch64
我个人习惯用Linaro的工具链,稳定。安装命令:
sudo apt install gcc-aarch64-linux-gnu qemu-system-arm
3.4.2 配置U-Boot
U-Boot已经内置了QEMU virt平台的配置。直接使用:
make qemu_arm64_defconfig
make CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)
编译完成后,生成 u-boot.bin。就这么简单?嗯,因为U-Boot主线已经支持这个平台了。但我要带你看看配置是怎么生效的。
执行 make menuconfig,你会看到:
- ARM architecture → Select target architecture → ARM64
- Board type → QEMU virt
- Device Tree Control → 已经选中
qemu-arm64.dts
这些配置项,最终会生成 .config 文件,控制哪些代码被编译进去。
3.4.3 启动测试
编译完成后,用QEMU启动:
qemu-system-aarch64 -machine virt -cpu cortex-a57 \
-nographic -m 1G \
-bios u-boot.bin
你会看到U-Boot的启动日志,最后进入命令行。输入 printenv 看看环境变量,输入 bdinfo 查看板级信息。
避坑指南:我曾经在QEMU上调试时,发现U-Boot启动后串口没输出。查了半天,发现是QEMU的串口地址和U-Boot设备树里写的不一致。QEMU virt平台的串口地址是0x9000000,但有些文档写的是0x1000000。嗯,一定要以QEMU源码里的 hw/arm/virt.c 为准。
3.4.4 自定义板级配置
如果你想从零开始移植,步骤是这样的:
- 在
board/下创建你的板子目录,比如board/myboard/ - 编写
Kconfig和MAINTAINERS - 编写
board.c,实现board_init()、dram_init()等函数 - 在
arch/arm/dts/下创建设备树文件 - 在
include/configs/下创建头文件(如果不用设备树) - 在顶层
arch/arm/Kconfig中添加你的板子选项
以QEMU virt为例,它的板级代码在 board/emulation/qemu-arm/ 下。核心函数就几个:
int board_init(void)
{
/* 初始化串口、定时器等 */
return 0;
}
int dram_init(void)
{
/* 从设备树读取内存大小 */
gd->ram_size = fdtdec_get_config_int(gd->fdt_blob,
"memory", "reg", 0);
return 0;
}
你看,代码量很少。U-Boot的框架已经把大部分工作做了,你只需要告诉它内存多大、串口在哪。
3.5 启动流程关键函数调用链
最后,我整理一下U-Boot proper阶段的函数调用链。这个链搞清楚了,调试时就知道该在哪加打印。
| 函数 | 位置 | 作用 |
|---|---|---|
_start |
arch/arm/lib/vectors.S | 异常向量表入口 |
reset |
arch/arm/cpu/armv8/start.S | 复位处理,设置栈指针 |
board_init_f |
common/board_f.c | 第一阶段初始化,初始化串口、定时器、DDR |
relocate_code |
arch/arm/lib/relocate.S | 将U-Boot自身从Flash拷贝到DDR |
board_init_r |
common/board_r.c | 第二阶段初始化,初始化完整外设、环境变量 |
main_loop |
common/main.c | 进入命令行循环,等待用户输入 |
调试技巧:如果U-Boot在 board_init_f 阶段就卡死了,多半是DDR初始化失败。如果卡在 relocate_code 之后,可能是重定位地址不对。我习惯在 board_init_f 开头加一个串口输出,确认代码执行到了哪一步。
好了,这一章的内容就这些。U-Boot的启动流程、源码结构、板级配置、设备树,还有QEMU上的移植实战,都过了一遍。你跟着做一遍,应该能自己移植到其他平台上了。