4. Bootloader移植:U-Boot源码结构、板级配置与设备树、QEMU virt平台适配、U-Boot编译与启动

Bootloader 是嵌入式系统的第一段软件。它负责初始化硬件、加载内核,然后跳转过去。在 QEMU 仿真环境里,我们通常用 U-Boot 来做这件事。

我个人习惯把 U-Boot 看作一个「微型操作系统」。它有自己的驱动框架、命令行、文件系统支持。你想想看,一个能在开发板上跑起来的 U-Boot,其实已经完成了整个嵌入式系统移植的一半工作。

4.1 U-Boot 源码结构

U-Boot 的源码目录乍一看很吓人,但核心部分其实就几个。我刚开始接触时也被绕晕过,后来总结了一张表,按功能划分就清晰多了。

目录/文件 作用 我关注的点
arch/ CPU 架构相关代码(arm、riscv、x86 等) 启动流程的汇编入口在这里
board/ 具体板级初始化、外设配置 移植时改得最多的目录
configs/ 板级默认配置文件(xxx_defconfig) 编译前先看这里
include/configs/ 传统板级头文件配置(旧方式) 新板子建议用设备树替代
dts/ 设备树源文件(.dts / .dtsi) QEMU virt 的 dts 就在这里
drivers/ 各类驱动(串口、网卡、存储等) QEMU 仿真主要用 virtio 驱动
cmd/ U-Boot 命令行命令实现 调试时经常加自定义命令
common/ 通用核心逻辑(启动、环境变量等) 启动流程的主循环在这里

嗯,这里要注意:board/ 和 configs/ 是移植工作的主战场。其他目录一般不动,除非你要加新驱动。

4.2 板级配置与设备树

U-Boot 的板级配置有两种方式:传统头文件方式和设备树方式。QEMU virt 平台推荐用设备树,这也是现代 U-Boot 的趋势。

4.2.1 传统方式 vs 设备树方式

对比项 传统头文件方式 设备树方式
配置位置 include/configs/xxx.h dts/xxx.dts
硬件描述 C 宏定义 + 寄存器地址 树形结构 + 属性
可维护性 差,改一处要重新编译 好,内核和 U-Boot 可共用
QEMU 支持 不推荐 官方推荐

我在项目中遇到过用传统方式维护的老板子,每次换 Flash 型号都要改一堆宏定义,特别痛苦。后来新项目全部切到设备树,清爽多了。

4.2.2 QEMU virt 平台的设备树

QEMU 的 virt 平台是一个通用虚拟平台。它没有固定的物理硬件,所有外设都是虚拟的。U-Boot 源码里已经包含了 dts/qemu-virt.dts,我们只需要确认它是否匹配当前 QEMU 版本。

一个典型的 virt 设备树片段长这样:

/dts-v1/;

/ {
    model = "QEMU virt";
    compatible = "qemu,virt";
    #address-cells = <2>;
    #size-cells = <2>;

    memory@40000000 {
        device_type = "memory";
        reg = <0x0 0x40000000 0x0 0x10000000>;
    };

    uart@9000000 {
        compatible = "ns16550a";
        reg = <0x0 0x9000000 0x0 0x1000>;
        interrupts = <0x1>;
    };
};

这里定义了内存起始地址 0x40000000,大小 256MB。UART 在 0x9000000,中断号 1。这些地址要和 QEMU 命令行参数对应上。

避坑指南: 我曾经因为设备树里的内存大小和 QEMU 的 -m 参数不一致,导致 U-Boot 启动后只认到部分内存。调试了半天才发现是设备树写死了 256MB,但 QEMU 只给了 128MB。记住:设备树是「期望值」,QEMU 参数是「实际值」,两者必须匹配。

4.3 QEMU virt 平台适配

适配 QEMU virt 平台,说白了就是让 U-Boot 知道「我在一个虚拟的 ARM 机器上跑」。具体要做三件事:

  1. 选择正确的 CPU 架构:ARMv8 64位用 arm64,ARMv7 32位用 arm。
  2. 配置串口驱动:virt 平台默认用 ns16550 UART,U-Boot 自带驱动。
  3. 设置启动设备:QEMU 通常用 virtio-blk 或 virtio-net,U-Boot 需要使能对应的 virtio 驱动。

我个人习惯先跑一遍 QEMU 自带的 qemu-system-aarch64 -M virt -machine dumpdtb=virt.dtb,把 QEMU 生成的设备树 dump 出来。然后和 U-Boot 里的 dts 对比,确保寄存器地址、中断号完全一致。

小技巧: 用 dtc -I dtb -O dts virt.dtb 可以把二进制设备树反编译成可读的 dts 文件。这样就能直接看到 QEMU 到底给虚拟机配了哪些外设。

4.4 U-Boot 编译与启动

编译 U-Boot 的流程其实很固定。我每次移植新板子都按这个步骤来,基本没出过问题。

4.4.1 编译步骤

# 1. 清理旧配置
make distclean

# 2. 加载 QEMU virt 的默认配置
make qemu_arm64_defconfig

# 3. 进入菜单配置(可选,一般不用改)
make menuconfig

# 4. 编译
make -j$(nproc)

# 5. 编译完成后,生成 u-boot.bin
ls -lh u-boot.bin

这里要注意:qemu_arm64_defconfig 是 U-Boot 官方为 QEMU virt 平台准备的默认配置。如果你用的是 32 位 ARM,就用 qemu_arm_defconfig。

4.4.2 启动 U-Boot

编译完成后,用 QEMU 启动 U-Boot:

qemu-system-aarch64 \
    -M virt \
    -cpu cortex-a57 \
    -m 256M \
    -nographic \
    -bios u-boot.bin

启动后你会看到 U-Boot 的串口输出,最后停在 => 提示符下。输入 help 可以查看所有命令。

关键输出检查点:

  • 看到 U-Boot 2024.xx 版本号 → 启动成功
  • 看到 DRAM: 256 MiB → 内存识别正确
  • 看到 Model: QEMU virt → 设备树匹配
  • 看到 => 提示符 → 命令行就绪

如果卡在某个地方没反应,多半是设备树或串口配置有问题。我曾经遇到过 U-Boot 启动后只打印一个 U 就卡住了,后来发现是串口时钟频率配置错了。QEMU virt 的 ns16550 UART 默认时钟是 24MHz,但 U-Boot 里写成了 48MHz,导致波特率对不上。

4.5 本章知识体系

下面这张图总结了 U-Boot 移植到 QEMU virt 平台的核心流程。你可以把它当作一个检查清单。

U-Boot 移植到 QEMU virt 平台核心流程 U-Boot 移植到 QEMU virt 平台核心流程 U-Boot 源码结构 arch/ board/ configs/ dts/ 板级配置 传统头文件 vs 设备树 设备树适配 dts/qemu-virt.dts QEMU virt 适配 CPU/串口/启动设备 编译 U-Boot make qemu_arm64_defconfig 启动验证 QEMU -bios u-boot.bin 💡 移植检查清单 1. 确认 CPU 架构(arm64/arm) 2. 确认设备树与 QEMU 参数一致 3. 确认串口驱动使能且时钟正确

这张图从左到右展示了移植的依赖关系。源码结构是基础,板级配置和设备树是核心,QEMU 适配是桥梁,编译和启动是最终验证。每一步都环环相扣。

我的经验: 第一次移植时,不要追求一步到位。先让 U-Boot 在 QEMU 上跑起来,哪怕只打印一个 U 也算成功。然后逐步添加功能:串口输出、内存识别、网络支持。每加一个功能就编译测试一次,这样出了问题能快速定位。