ARM64 启动全流程拆解:从 BootROM 到 U-Boot 到底发生了什么?
ARM64 启动全流程拆解:从 BootROM 到 U-Boot 到底发生了什么?
记得第一次做嵌入式 Linux 移植的时候,对着开发板按了半天电源键,串口啥都没打印。我当时心态是:板子是不是坏了?
后来才发现,ARM64 的启动流程远比我以为的复杂。BootROM → SPL → ATF → U-Boot → Kernel,每一级都有自己的一套规矩。
今天我来把这套东西拆开揉碎,写清楚。
一、按下电源键之后,CPU 在干嘛?
先看整体流程,有个宏观印象:
ARM 处理器上电后,CPU 的Program Counter会被硬件强制设置到一个固定地址——这个地址指向芯片内部固化的一段 ROM 代码,这就是BootROM。
BootROM 是芯片出厂就烧好的,你改不了它。它的任务极其简单:
- 初始化最基本的外设(通常是 SPI / SD / NAND 控制器)
- 从预定义的启动介质读取一小段代码到 SRAM(内部 RAM,不需要 DDR 初始化就能用)
- 跳转到这段代码执行
BootROM 支持的启动介质各有不同,典型的 BootROM 介质选择流程:
关键点:这时 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 最核心的变化之一:
来看每个 EL 的角色:
| 异常等级 | 典型软件 | 权限 |
|---|---|---|
| EL3 | ATF BL31 / Secure Monitor | 最高,控制 PSCI/电源管理/SMC |
| EL2 | U-Boot / Hypervisor | 虚拟化扩展,MMU 第二阶段翻译 |
| EL1 | Linux 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 完整的加载和跳转流程:
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 保持同源发布版本。
六、总结一下时序
ARM64 的启动不是一个简单的"加载 → 跳转"。BootROM → SPL → ATF → U-Boot → Kernel,每一级都在补充上一级的能力限制,最终把系统带到 Linux 可以接管的状态。
下次你按电源键的时候,脑子里过一遍这整个流程,是不是感觉板子也没那么神秘了?
这篇写完了。下期打算写 ARM64 异常处理的完整路径:中断来了 CPU 都做了啥?从 EL1 IRQ 到 EL3 FIQ,中断分发到底走哪条路径?有兴趣的可以关注。