1. 课程导入
当你按下开发板电源键,或是在QEMU中敲下启动命令的那一刻,一颗ARM64处理器并不会立刻运行Linux内核的start_kernel()。在它真正进入操作系统之前,必须经历一段极其精密、层层递进的“唤醒”过程:从复位向量取指、异常等级切换、页表建立、内存与缓存初始化,到设备树解析、平台驱动注册,最终才把控制权交给内核通用初始化代码。这段过程就是本课程要系统讲解的ARM64早期启动与平台初始化。
本课程面向嵌入式Linux工程师、BSP开发人员、内核移植与调试人员,以及希望深入理解ARM64体系结构启动机制的开发者。课程不满足于“知道流程”,而是要求你能够读懂启动代码、定位启动故障、修改平台初始化逻辑、验证启动阶段状态。因此,学习目标分为四个层次:理解ARM64异常等级与启动状态、掌握从复位到内核入口的完整执行路径、熟悉平台初始化中的关键数据结构与接口、具备早期启动问题的分析与排障能力。
核心概念:先建立一张“启动地图”
ARM64启动涉及大量体系结构术语,如果一开始就陷入细节,很容易迷失。建议先抓住以下核心概念,它们构成了后续所有章节的骨架。
- 异常等级(Exception Level, EL):ARM64定义EL0~EL3四个特权层级。EL0为用户态,EL1通常运行内核,EL2用于虚拟化,EL3为安全监控模式。启动过程本质上就是一次从高异常等级向低异常等级、从固件向操作系统的“降级交接”。
- 复位向量与启动地址:CPU复位后从固定地址取指,该地址通常由SoC的BootROM映射。不同芯片的启动介质(SPI Flash、eMMC、UART下载)会影响第一阶段加载方式。
- 启动阶段划分:常见划分为BL1、BL2、BL31、BL32、BL33,或简化为BootROM → 一级引导 → 二级引导 → U-Boot → Linux内核。每一阶段都有明确的职责和交接约定。
- 设备树(Device Tree):ARM64平台普遍使用DTB描述硬件。Bootloader将DTB地址通过
x0寄存器传递给内核,内核在早期启动阶段解析它,完成平台设备初始化。 - MMU与页表:在开启MMU之前,CPU按物理地址取指;开启后必须已有合法页表。ARM64早期启动通常先建立恒等映射,再逐步切换到虚拟地址空间。
- 平台初始化:包括时钟、串口、中断控制器、内存控制器、电源管理等。部分初始化在Bootloader完成,部分延迟到内核
setup_arch()阶段。
这些概念不是孤立的,而是通过一条清晰的执行链串联起来。下图展示了ARM64从复位到平台初始化的整体框架与关键交接点,建议在学习每个章节时都回到这张图定位当前所处阶段。
方法流程:从复位到平台初始化的执行链
理解启动流程最有效的方法,是把它看作一条“交接链”。每一级固件或引导程序完成自己的任务后,把CPU状态、内存布局、硬件描述信息交给下一级。下面用伪代码形式概括关键交接点,帮助你在阅读真实代码时快速定位。
/* 典型ARM64启动交接链(简化) */
BootROM:
CPU复位 → 从复位向量取指
初始化最小SRAM → 加载BL2到SRAM
跳转BL2,传递启动介质信息
BL2:
初始化DDR控制器 → 初始化时钟
加载BL31到安全内存、BL33到普通内存
跳转BL31,传递BL33入口地址
BL31 (EL3):
配置安全监控 → 初始化PSCI
切换异常等级:EL3 → EL2或EL1
跳转BL33 (U-Boot)
U-Boot (EL2/EL1):
初始化串口、网口、存储
加载Linux内核Image与DTB到DDR
设置x0=DTB地址,跳转内核入口
Linux内核:
head.S → 建立恒等映射 → 开启MMU
start_kernel() → setup_arch() → 解析DTB
平台初始化 → 驱动注册 → 进入调度
这条链中,任何一环出错都会导致启动失败。例如DDR未初始化就加载内核,会在解压阶段崩溃;DTB地址传递错误,内核会在setup_arch()中找不到内存节点;异常等级切换配置错误,则可能在eret指令后直接跑飞。
案例场景:一次典型的启动卡死分析
假设你在某ARM64开发板上移植新内核,串口只输出到Starting kernel ...就再无下文。按照本课程的框架,可以按以下顺序排查:
- 确认U-Boot传递给内核的
x0是否为有效DTB地址,DTB是否与内核匹配。 - 检查内核入口是否因页表建立失败而在
head.S中卡住,可通过早期串口earlycon输出验证。 - 确认DDR初始化是否由BL2正确完成,内核加载地址是否落在有效内存范围。
- 检查异常等级切换是否正确,EL2到EL1的配置是否与内核期望一致。
- 使用JTAG或启动阶段GPIO标记,定位具体卡死位置。
earlycon和earlyprintk。没有串口输出时,再考虑JTAG和GPIO打点。
误区与复盘:初学者最容易踩的坑
在多年教学与工程支持中,以下误区出现频率最高,建议在学习过程中反复对照。
| 误区 | 典型表现 | 正确认识 |
|---|---|---|
| 把启动流程等同于U-Boot启动 | 忽略BL1/BL2/BL31阶段 | U-Boot只是BL33,前面还有安全固件和DDR初始化 |
| 认为内核一启动就有完整内存管理 | 在head.S中使用未映射地址 |
MMU开启前必须使用物理地址或恒等映射地址 |
| 忽略设备树传递约定 | 内核找不到串口、内存大小错误 | DTB地址通过x0传递,且必须位于内核可访问内存 |
调试时只依赖printk |
早期卡死无任何输出 | 需使用earlycon、JTAG、GPIO打点等早期手段 |
| 认为异常等级切换与内核无关 | EL2配置错误导致内核无法启动 | 内核期望在EL1运行,EL2需正确配置并降级 |
学习路径与章节安排
本课程共30章,按照“先建立框架,再深入细节,最后实战排障”的思路组织。建议的学习路径如下:
- 第一阶段:体系结构基础——ARM64异常等级、寄存器、复位行为、启动介质。
- 第二阶段:启动阶段流程——BL1/BL2/BL31/BL33职责、镜像加载、交接约定。
- 第三阶段:内核早期代码——
head.S、恒等映射、页表建立、start_kernel()入口。 - 第四阶段:设备树与平台初始化——DTB解析、
setup_arch()、时钟/中断/串口初始化。 - 第五阶段:调试与实战——串口日志、JTAG、启动阶段标记、常见故障树分析。
学习过程中,建议始终带着三个问题阅读代码:当前处于哪个异常等级?当前使用物理地址还是虚拟地址?下一阶段的入口和参数是什么?只要这三个问题能回答清楚,ARM64早期启动的脉络就已经掌握了大半。