ARM64 启动全流程拆解:从 BootROM 到 U-Boot 到底发生了什么?

📅 2026/7/6 7:09:30 👁️ 阅读次数 📝 编程学习
ARM64 启动全流程拆解:从 BootROM 到 U-Boot 到底发生了什么?

ARM64 启动全流程拆解:从 BootROM 到 U-Boot 到底发生了什么?

记得第一次做嵌入式 Linux 移植的时候,对着开发板按了半天电源键,串口啥都没打印。我当时心态是:板子是不是坏了?

后来才发现,ARM64 的启动流程远比我以为的复杂。BootROM → SPL → ATF → U-Boot → Kernel,每一级都有自己的一套规矩。

今天我来把这套东西拆开揉碎,写清楚。

一、按下电源键之后,CPU 在干嘛?

先看整体流程,有个宏观印象:

初始化 DDR
加载下一级

EL3 → EL2
ATF BL31/BL32

EL2 → EL1
设备树 + Kernel

上电复位

BootROM
芯片内部固件

SPL/TF-A
初级阶段

U-Boot
完整引导加载

Linux Kernel

init 进程

ARM 处理器上电后,CPU 的Program Counter会被硬件强制设置到一个固定地址——这个地址指向芯片内部固化的一段 ROM 代码,这就是BootROM

BootROM 是芯片出厂就烧好的,你改不了它。它的任务极其简单:

  1. 初始化最基本的外设(通常是 SPI / SD / NAND 控制器)
  2. 从预定义的启动介质读取一小段代码到 SRAM(内部 RAM,不需要 DDR 初始化就能用)
  3. 跳转到这段代码执行

BootROM 支持的启动介质各有不同,典型的 BootROM 介质选择流程:

0001

0010

0100

1000

失败

成功

上电复位

跳转到 Reset Vector
0x0000_0000 / 0xFFFF_0000

执行 BootROM

检查启动引脚
BOOT_MODE[3:0]

从 SD/eMMC 启动

从 SPI NOR Flash 启动

从 NAND Flash 启动

USB DFU 下载模式

读取前 128KB
到 SRAM

等待 USB 下载

校验签名
/CRC

死循环等待

跳转到 SPL
SRAM 中执行

关键点:这时 DDR 还没初始化,代码只能在芯片内部的 SRAM 里跑。SRAM 通常只有 64KB~512KB,这也是为什么 BootROM 加载的代码必须很小。

二、SPL:比 BootROM 多一点,比 U-Boot 少一点

SPL(Secondary Program Loader),也有的芯片方案叫它 MLO 或 IBoot。

BootROM 把 SPL 加载到 SRAM 后,SPL 要干的事情:

最重要的一件事:初始化 DDR。DDR 的初始化参数(时序、频率、ODT、ZQ 校准)在硬件设计阶段就确定了,SPL 从 BootROM 那里得到这些参数,然后:

/* 伪代码:DDR 初始化序列 */voidddr_init(void){/* 1. 设置 DDR 控制器 */writel(DDR_CONF_2T,&ddrc->cfg);writel(ROW_BITS_16,&ddrc->row_config);/* 2. 设置 PHY 时序 */writel(PHY_DQS_SLEW_RATE,&ddr_phy->dqs_sr);writel(PHY_CLK_DELAY_VAL,&ddr_phy->clk_delay);/* 3. ZQ 校准 */set_zq_calibration(ZQ_CAL_LONG);/* 4. 等待 DDR 就绪 */while(!(readl(&ddrc->stat)&DDR_READY));/* 5. 简单的 DDR 读写测试 */volatileuint32_t*test=(uint32_t*)DDR_BASE;*test=0xDEADBEEF;if(*test!=0xDEADBEEF)panic("DDR init failed!");}

DDR 初始化成功后,SPL 就有了大块内存可用。接着它把完整的 U-Boot 镜像从 Flash/SD/NAND 加载到 DDR 的某个地址,然后跳过去。

SPL 和 U-Boot 是同一套代码树,编译时通过 CONFIG_SPL_BUILD 区分。这也是为什么很多开发板上用同一个u-boot.bin但前面有个偏移——前 128KB 是 SPL,后面是完整的 U-Boot。

三、ATF:EL3 的看门人

ARMv8 引入了Exception Level(异常等级)的概念,这是 ARM64 最核心的变化之一:

EL0
用户态

EL1
操作系统内核

EL2
虚拟机监视器

EL3 (最高特权级)
Secure Monitor

SMC 调用

启动

系统调用

ATF BL31
运行时代理

ATF BL32
OPTEE (可选)

U-Boot / UEFI
或 Hypervisor

Linux Kernel

App / libc

来看每个 EL 的角色:

异常等级典型软件权限
EL3ATF BL31 / Secure Monitor最高,控制 PSCI/电源管理/SMC
EL2U-Boot / Hypervisor虚拟化扩展,MMU 第二阶段翻译
EL1Linux Kernel操作系统内核,MMU 第一阶段翻译
EL0用户程序最低,无特权指令

ARM Trusted Firmware(ATF)承担了 EL3 的职责,提供PSCI(Power State Coordination Interface)服务。U-Boot 要关中断、重启系统、进入低功耗模式,全部要通过 SMC 指令陷入 EL3,由 ATF BL31 处理。

典型的 SMC 调用流程:

U-Boot (EL2) ATF BL31 (EL3) │ │ │ SMC #0 (PSCI_CPU_ON) │ │───────────────────────────>│ │ ├── 检查调用者权限 │ ├── 切换异常等级状态 │ ├── 配置目标 CPU 的上下文 │ │ │ SMC 返回 │ │<───────────────────────────│ │ │ │ 目标 CPU 已在指定地址启动 │

没有 ATF,ARM64 就启动不了。这是 ARM 生态在 v8 之后的强制要求。

四、U-Boot:真正的引导程序

在 ATF 准备好 EL3 环境之后,U-Boot 在 EL2 开始运行。

U-Boot 完整的加载和跳转流程:

Linux KernelU-Boot (DDR)ATF BL31 (DDR)SPL (SRAM)BootROMLinux KernelU-Boot (DDR)ATF BL31 (DDR)SPL (SRAM)BootROM初始化 DDR解析设备树加载内核镜像设置启动参数EL2 → EL1启动 init 进程加载到 SRAM加载 BL31 + BL33 到 DDR跳转到 BL31 Entry初始化 EL3 环境SMC 切换到 EL2跳转到 U-Boot跳转到 Kernel

U-Boot 启动后主要做这几件事:

4.1 加载设备树(DTB)

设备树是 ARM 嵌入式开发的基石。U-Boot 在board_init()阶段就把 DTB 从存储介质加载到内存了:

# U-Boot 命令行常见操作=>load mmc0:1${fdt_addr_r}/boot/kernel.dtb=>load mmc0:1${kernel_addr_r}/boot/Image.gz=>booti${kernel_addr_r}-${fdt_addr_r}

4.2 解析启动参数

设备树里记录了板级信息:DDR 大小、serial 地址、中断控制器 GIC 版本……

/* 设备树中的启动参数 */ / { chosen { bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2 rw"; stdout-path = &uart0; }; memory@40000000 { device_type = "memory"; reg = <0x0 0x40000000 0x0 0x40000000>; /* 1GB */ }; };

4.3 跳转到内核

U-Boot 最后调用booti(ARM64 专用,bootm是 ARM32 遗留)跳转到内核入口。

跳转时,CPU 的状态很讲究:

  • MMU: 关
  • Cache: 关
  • x0 寄存器:指向 DTB 地址
  • 异常等级:EL2

Kernel 在入口处读 x0 拿到 DTB 地址,然后开启 MMU、建立页表、转换到 EL1……Linux 启动了。

五、实际调试踩过的坑

说几个我实际遇到过的坑:

坑 1:SPL 太大超过 SRAM

有一次在某个主控上移植,芯片 SRAM 只有 128KB。SPL 编译出来 135KB,BootROM 加载到 SRAM 就直接盖到未初始化区域了,板子死得一塌糊涂。

解决方案:裁剪 SPL。去掉不用的驱动支持,用CONFIG_SPL_SIZE_LIMIT做编译检查。

坑 2:DDR 参数没校准

同一批板子,有的能启动有的不能。最后发现是 DDR 的 ODT(片内端接)值在量产时焊接偏差导致信号完整性不够。

解决:增加 DDR 训练流程,跑一遍写读训练(Write Leveling + DQS Gate Training)。

坑 3:ATF 版本不匹配

U-Boot 2024.01 配 ATF v2.8,SMC 调用号对不上,一 PSCI 操作就卡死。

解决:U-Boot 和 ATF 保持同源发布版本。

六、总结一下时序

00 ms00 ms00 ms00 ms00 ms00 ms00 ms00 ms00 ms00 ms00 ms00 ms00 ms00 ms00 ms加载 SPL 到 SRAM初始化 DDR加载 ATF + U-Boot跳转 BL31SMC 代理初始化切换到 EL2外设初始化加载设备树 + Kernel跳转 KernelMMU 初始化init 启动BootROMSPLATFU-BootKernelARM64 启动时序

ARM64 的启动不是一个简单的"加载 → 跳转"。BootROM → SPL → ATF → U-Boot → Kernel,每一级都在补充上一级的能力限制,最终把系统带到 Linux 可以接管的状态。

下次你按电源键的时候,脑子里过一遍这整个流程,是不是感觉板子也没那么神秘了?


这篇写完了。下期打算写 ARM64 异常处理的完整路径:中断来了 CPU 都做了啥?从 EL1 IRQ 到 EL3 FIQ,中断分发到底走哪条路径?有兴趣的可以关注。