1. 课程导入

当你按下开发板电源键,或是在QEMU中敲下启动命令的那一刻,一颗ARM64处理器并不会立刻运行Linux内核的start_kernel()。在它真正进入操作系统之前,必须经历一段极其精密、层层递进的“唤醒”过程:从复位向量取指、异常等级切换、页表建立、内存与缓存初始化,到设备树解析、平台驱动注册,最终才把控制权交给内核通用初始化代码。这段过程就是本课程要系统讲解的ARM64早期启动与平台初始化。

本课程面向嵌入式Linux工程师、BSP开发人员、内核移植与调试人员,以及希望深入理解ARM64体系结构启动机制的开发者。课程不满足于“知道流程”,而是要求你能够读懂启动代码、定位启动故障、修改平台初始化逻辑、验证启动阶段状态。因此,学习目标分为四个层次:理解ARM64异常等级与启动状态、掌握从复位到内核入口的完整执行路径、熟悉平台初始化中的关键数据结构与接口、具备早期启动问题的分析与排障能力。

学习本课程后,你应当能够回答:CPU上电后第一条指令在哪里?为什么需要EL3/EL2/EL1切换?页表在什么时候建立?设备树如何从Bootloader传递到内核?平台初始化代码在哪个阶段执行?启动卡死时如何用最少的工具定位到具体阶段?

核心概念:先建立一张“启动地图”

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从复位到平台初始化的整体框架与关键交接点,建议在学习每个章节时都回到这张图定位当前所处阶段。

ARM64 早期启动与平台初始化整体框架 BootROM / BL1 复位取指 · 启动介质 加载下一阶段镜像 BL2 / 二级引导 DDR初始化 · 时钟配置 加载BL31/BL33 BL31 / EL3固件 安全监控 · PSCI 异常等级切换 BL33 / U-Boot 外设驱动 · 环境变量 加载内核与DTB x0=DTB地址 Linux内核入口 head.S · stext · 恒等映射 setup_arch() DTB解析 · 内存布局 · 平台初始化 start_kernel() 通用内核初始化 · 进入调度 贯穿启动过程的关键机制 异常等级切换 EL3 → EL2 → EL1 → EL0 MMU与页表 恒等映射 → 虚拟地址空间 设备树传递 x0寄存器 · DTB解析 平台初始化 时钟 · 中断 · 串口 · 电源 启动调试与排障 串口日志 · JTAG · 启动阶段标记 · 寄存器快照 学习路径:体系结构基础 → 启动阶段流程 → 内核早期代码 → 设备树与平台初始化 → 调试与实战

方法流程:从复位到平台初始化的执行链

理解启动流程最有效的方法,是把它看作一条“交接链”。每一级固件或引导程序完成自己的任务后,把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 ...就再无下文。按照本课程的框架,可以按以下顺序排查:

  1. 确认U-Boot传递给内核的x0是否为有效DTB地址,DTB是否与内核匹配。
  2. 检查内核入口是否因页表建立失败而在head.S中卡住,可通过早期串口earlycon输出验证。
  3. 确认DDR初始化是否由BL2正确完成,内核加载地址是否落在有效内存范围。
  4. 检查异常等级切换是否正确,EL2到EL1的配置是否与内核期望一致。
  5. 使用JTAG或启动阶段GPIO标记,定位具体卡死位置。
早期启动调试的第一原则:先让串口输出尽可能早地工作。很多平台在BL2阶段就初始化了UART,内核阶段可使用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需正确配置并降级
启动问题往往不是单一原因造成的。DDR时序、时钟频率、页表属性、DTB内容、异常等级配置可能相互耦合。排障时应逐阶段确认交接状态,而不是一次性修改多个参数。

学习路径与章节安排

本课程共30章,按照“先建立框架,再深入细节,最后实战排障”的思路组织。建议的学习路径如下:

  • 第一阶段:体系结构基础——ARM64异常等级、寄存器、复位行为、启动介质。
  • 第二阶段:启动阶段流程——BL1/BL2/BL31/BL33职责、镜像加载、交接约定。
  • 第三阶段:内核早期代码——head.S、恒等映射、页表建立、start_kernel()入口。
  • 第四阶段:设备树与平台初始化——DTB解析、setup_arch()、时钟/中断/串口初始化。
  • 第五阶段:调试与实战——串口日志、JTAG、启动阶段标记、常见故障树分析。

学习过程中,建议始终带着三个问题阅读代码:当前处于哪个异常等级?当前使用物理地址还是虚拟地址?下一阶段的入口和参数是什么?只要这三个问题能回答清楚,ARM64早期启动的脉络就已经掌握了大半。