3. Boot流程移植:ATF启动流程、UEFI移植要点、设备树适配
好,咱们进入第三章。这一章讲的是系统上电后,CPU从复位向量到操作系统启动之间的那段“无人区”。说白了,就是Boot流程怎么移植到你的板子上。
ARMv9的启动链,比之前复杂了不少。我个人习惯把整个流程拆成三段:ATF(安全固件)拉起核心、UEFI(或者uboot)做硬件初始化、最后设备树把硬件描述交给内核。这三段,哪一段出问题,板子都起不来。
3.1 ATF启动流程:从EL3到EL2的接力
ATF是ARMv8/v9体系里最底层的固件。它运行在EL3,是系统的“上帝模式”。我刚开始接触ATF时,总觉得它就是个简单的bootloader,后来踩了坑才明白——它负责的事情远比你想象的多。
核心职责:
- 初始化安全世界(Secure World)的异常向量表
- 配置GIC(中断控制器)的路由策略
- 设置PSCI(电源状态协调接口)的CPU热插拔、挂起/恢复
- 最后,跳转到非安全世界的Bootloader(如UEFI)
ATF的启动流程,我习惯用这张图来理解:
嗯,这里要注意:BL1是固化在芯片ROM里的,你改不了。真正需要你动手的,是从BL2开始。我曾经在一个项目里,因为BL2的FIP(Firmware Image Package)打包顺序搞错了,导致BL31加载失败,板子一直卡在EL3出不来。排查了两天才发现——就是FIP里镜像的偏移地址写反了。
3.2 UEFI移植要点:EDK2的板级适配
UEFI在ARMv9平台上,通常用EDK2(TianoCore)来实现。它运行在EL2,负责给操作系统提供一个标准的启动环境。
移植UEFI,说白了就是告诉EDK2:你的板子长什么样、内存多大、外设挂在哪里。我个人觉得,最核心的移植工作就三块:
| 移植模块 | 关键文件/目录 | 常见坑点 |
|---|---|---|
| Platform Description | Platform/ |
DSC/FDF文件中的Flash布局与实际硬件不符 |
| Memory Map | MemoryInitPei 或 MemMap |
DRAM的地址范围和大小写错,导致UEFI认不全内存 |
| Peripheral Library | Library/ 下的 GicLib, TimerLib |
GIC版本不匹配(GICv2 vs GICv3)导致中断不响应 |
我的经验: 移植UEFI时,先别急着改代码。先把板子的 .dsc 和 .fdf 文件读透。这两个文件定义了整个固件的编译和打包规则。我见过太多人一上来就改C代码,结果编译出来的固件根本烧不进去——因为FDF里的分区大小设错了。
举个例子,EDK2里定义Flash布局的FDF文件,长这样:
[FD.Board_Flash]
BaseAddress = 0x08000000 # SPI Flash基地址
Size = 0x00400000 # 4MB
ErasePolarity = 1
# UEFI固件区域
0x00000000 | 0x00100000
FILE = Platform/MyBoard/Fd/BOOT.fd
# 变量存储区域
0x00100000 | 0x00040000
gEfiMdeModulePkgTokenSpaceGuid.PcdFlashNvStorageVariableBase
# FIP区域(ATF镜像)
0x00140000 | 0x000C0000
FILE = Platform/MyBoard/Fd/FIP.bin
你看,每个区域的偏移和大小都是硬编码的。如果你的板子Flash是8MB,但这里写的是4MB,那后面的FIP区域就会被截断。我曾经就因为没改这个Size,导致ATF的BL31镜像只加载了一半,系统启动到一半就挂了。
3.3 设备树(DTS)适配:把硬件“说”给内核听
设备树,说白了就是一份硬件描述文件。内核通过它知道:CPU有几个核、内存多大、外设挂在哪条总线上、中断号是多少。
ARMv9的设备树适配,和之前版本最大的区别在于:
- 多级中断控制器:GICv3/v4的ITS(中断翻译服务)需要在DTS里显式描述
- PPI(私有外设中断):每个CPU核的私有中断,必须绑定到对应的CPU节点
- 电源管理:PSCI的节点必须与ATF的BL31实现一致
我拿一个典型的ARMv9设备树片段来说明:
/dts-v1/;
/ {
model = "My ARMv9 Board";
compatible = "myvendor,armv9-board";
#address-cells = <2>;
#size-cells = <2>;
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-x2";
reg = <0x0>;
enable-method = "psci";
};
cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-x2";
reg = <0x1>;
enable-method = "psci";
};
};
psci {
compatible = "arm,psci-1.0";
method = "smc";
};
gic: interrupt-controller@2f000000 {
compatible = "arm,gic-v3";
reg = <0x0 0x2f000000 0x0 0x10000>, // GICD
<0x0 0x2f100000 0x0 0x200000>, // GICR
<0x0 0x2f300000 0x0 0x10000>, // ITS
<0x0 0x2f400000 0x0 0x10000>; // GICC
interrupts = <1 9 4>;
#interrupt-cells = <3>;
#address-cells = <2>;
#size-cells = <2>;
ranges = <0x0 0x0 0x0 0x2f000000 0x0 0x500000>;
};
memory@80000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x80000000>; // 2GB
};
};
注意: 设备树里的 reg 地址,必须和你的硬件设计完全一致。我遇到过最离谱的bug,是硬件工程师改了DDR的片选地址,但没通知我。结果设备树里写的还是旧地址,内核一启动就panic,因为访问的内存区域根本不存在。
适配设备树时,我建议你按这个顺序来:
- 先配CPU和内存:这是内核启动的基石,配不对连log都看不到
- 再配中断控制器:GIC的节点错了,所有外设的中断都收不到
- 然后配串口:串口能出log,后面调试就方便了
- 最后配其他外设:网卡、USB、PCIe等,一个一个来
嗯,这里有个小技巧:编译设备树时,用 dtc -I dts -O dtb -o board.dtb board.dts 生成dtb。然后用 dtc -I dtb -O dts board.dtb 反编译回来,看看有没有丢失信息。我经常用这招来检查设备树有没有被编译器优化掉关键节点。
3.4 启动链的调试思路
移植过程中,板子起不来是常态。我的调试思路是这样的:
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 上电后无任何输出 | ATF的BL1/BL2未正确加载 | 检查Flash烧录地址、FIP打包顺序 |
| ATF有log,但UEFI没起来 | BL33的入口地址或格式不对 | 确认FDF中BL33的BaseAddress与编译一致 |
| UEFI有log,内核启动panic | 设备树中内存或中断描述错误 | 用 devicetree.dump 检查内核收到的dtb |
| 内核启动后无外设 | 设备树中外设节点遗漏或地址错误 | 对照原理图逐项核对 reg 和 interrupts |
说白了,Boot流程移植就是一场“对答案”的过程——你的代码描述,必须和硬件设计完全一致。多一个0、少一个地址,板子都不买账。
本章小结:
- ATF的BL1不可改,重点在BL2的FIP打包和BL31的PSCI适配
- UEFI移植的核心是DSC/FDF文件中的Flash布局和内存映射
- 设备树是硬件与内核的“翻译官”,地址和中断号必须一字不差
- 调试时,从ATF log开始,逐级确认每一层的启动状态