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 命令行参数对应上。
-m 参数不一致,导致 U-Boot 启动后只认到部分内存。调试了半天才发现是设备树写死了 256MB,但 QEMU 只给了 128MB。记住:设备树是「期望值」,QEMU 参数是「实际值」,两者必须匹配。
4.3 QEMU virt 平台适配
适配 QEMU virt 平台,说白了就是让 U-Boot 知道「我在一个虚拟的 ARM 机器上跑」。具体要做三件事:
- 选择正确的 CPU 架构:ARMv8 64位用
arm64,ARMv7 32位用arm。 - 配置串口驱动:virt 平台默认用 ns16550 UART,U-Boot 自带驱动。
- 设置启动设备: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 平台的核心流程。你可以把它当作一个检查清单。
这张图从左到右展示了移植的依赖关系。源码结构是基础,板级配置和设备树是核心,QEMU 适配是桥梁,编译和启动是最终验证。每一步都环环相扣。
U 也算成功。然后逐步添加功能:串口输出、内存识别、网络支持。每加一个功能就编译测试一次,这样出了问题能快速定位。