第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启动全流程。你看一眼,心里就有谱了。

U-Boot 启动流程全景图 SoC内部ROM Code SPL (Secondary Program Loader) U-Boot proper 加载内核与设备树 启动内核 (bootm/booti) SPL 内部流程 1. 初始化时钟/PLL 2. 初始化DDR控制器 3. 初始化存储接口 4. 加载U-Boot到DDR 5. 跳转到U-Boot入口 U-Boot proper 流程 1. board_init_f() 2. relocate_code() 3. board_init_r() 4. 进入main_loop() 5. 等待用户命令 6. 自动启动脚本

我的经验:调试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 自定义板级配置

如果你想从零开始移植,步骤是这样的:

  1. 在 board/ 下创建你的板子目录,比如 board/myboard/
  2. 编写 Kconfig 和 MAINTAINERS
  3. 编写 board.c,实现 board_init()、dram_init() 等函数
  4. 在 arch/arm/dts/ 下创建设备树文件
  5. 在 include/configs/ 下创建头文件(如果不用设备树)
  6. 在顶层 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上的移植实战,都过了一遍。你跟着做一遍,应该能自己移植到其他平台上了。