ARM 启动流程

从上电到 Bootloader,再到内核或应用

Posted by Yvain Zhang on November 7, 2022 主题:技术

很多人第一次看 ARM 启动流程,会觉得它只是“上电以后把系统拉起来”。真往里看就会发现,这里面是一串接力:先决定从哪启动,再把最小硬件环境搭起来,再把更大的代码搬进内存,最后才轮到内核或应用接手。

如果只抓最关键的问题,其实就三件:

  1. 第一条指令从哪里取出来
  2. 系统靠什么逐步把硬件初始化到“可跑复杂软件”的程度
  3. 控制权最终如何从 ROM 交给 Bootloader,再交给内核或应用

1. 从上电到取第一条指令

处理器上电后并不会“自动进入 Linux”,它首先只会做一件最基础的事:从预定义的复位向量地址开始取指执行。

这个入口通常由 SoC 启动 ROM 控制。启动 ROM 一般固化在芯片内部,职责很明确:

  • 读取启动模式引脚或 eFuse 配置
  • 判断当前应该从哪种介质启动
  • 做非常少量的早期初始化
  • 把下一阶段代码搬运到指定位置并跳转

常见启动介质包括:

  • SPI Flash
  • NAND / NOR Flash
  • eMMC
  • SD 卡
  • USB 下载模式
  • 网络启动

这一阶段代码很小,能做的事也有限,但它的优先级最高。这里一旦出问题,后面连 Bootloader 都轮不上。

2. 硬件初始化阶段到底在做什么

启动早期的硬件初始化不是“把所有外设都初始化一遍”,而是建立一个最小可运行环境。通常先保证这几类能力:

  • 时钟已经工作正常
  • 复位链路完成释放
  • 片上 SRAM 可用
  • 串口可输出日志
  • 启动介质可读

很多芯片上电后只能先使用片上 SRAM,因为 DDR 此时还没有训练完成,也没有稳定可用。于是早期代码常常会采用“两段式启动”:

  1. 在片上 SRAM 中执行很小的一段初始化代码
  2. 初始化 DDR 后,再把更大的 Bootloader 主体搬到 DDR 中运行

因此,启动早期代码通常要重点处理:

  • 时钟树和 PLL 设置
  • 引脚复用
  • UART 初始化
  • 存储控制器初始化
  • DDR training / 校准

所以很多启动问题最开始的现象就是“串口一点动静都没有”。不是日志没开,而是连串口这件事本身都还没准备好。

3. Boot ROM、SPL、U-Boot 之间的关系

在很多 ARM Linux 系统中,启动链条并不是单一的 Bootloader,而是多段式:

Boot ROM -> SPL / TPL -> U-Boot -> Linux Kernel

Boot ROM

芯片内部固化代码,负责选择启动介质和加载下一阶段镜像。

SPL

SPL 可以先把它当成“前置版 Bootloader”。早期可用的 SRAM 往往很小,完整的 U-Boot 根本放不下,所以需要先跑一个瘦身版阶段:

  • 初始化 DDR
  • 初始化基础串口
  • 从更大的存储介质加载完整 Bootloader

U-Boot

U-Boot 是更完整的 Bootloader,职责通常包括:

  • 初始化更多外设
  • 提供命令行和调试能力
  • 识别文件系统、分区表、镜像格式
  • 加载 kernel、dtb、initramfs
  • 组装 bootargs
  • 跳转到内核入口

如果没有 SPL,U-Boot 也可以直接承担这些工作,但前提是芯片启动条件允许它被直接加载并运行。

4. 进入 Linux 内核前到底要准备什么

对于 Linux 场景,Bootloader 的目标不是“把所有系统功能都准备完”,而是把 Linux 内核启动所需的几个核心输入准备好:

  • 内核镜像
  • 设备树(dtb)
  • 可选的 initramfs / initrd
  • 启动参数 bootargs

其中设备树很关键,因为它告诉内核:

  • 这块板子上有哪些外设
  • 外设寄存器地址是什么
  • 中断号、时钟、GPIO 怎么关联
  • 内存布局是什么

Bootloader 在跳转前常见会做的事包括:

  • 把内核镜像放到约定地址
  • 把 dtb 放到约定地址
  • 设置内核命令行,例如 rootfs、console、loglevel
  • 清理或关闭不需要继承给内核的硬件状态

这一步如果参数错了,常见现象包括:

  • 内核能启动,但找不到根文件系统
  • 串口设备名不对,没有日志输出
  • 设备树和板级硬件不匹配,驱动初始化异常

5. Linux 和 RTOS 的启动差异

Linux 场景

典型路径是:

上电 -> Boot ROM -> SPL/U-Boot -> Kernel -> init/systemd -> 用户空间

它的特点是:

  • 启动链更长
  • 需要准备设备树和根文件系统
  • 内核初始化阶段更复杂
  • 更适合复杂驱动、多任务和完整用户态系统

RTOS / Bare-metal 场景

典型路径通常更短:

上电 -> Boot ROM / Bootloader -> RTOS 或应用主程序

特点是:

  • 组件更少
  • 启动更快
  • 镜像结构更简单
  • 更强调实时性和可预测性

Linux 启动更像是在先把舞台搭好,RTOS 启动则更接近把程序直接拉起来干活。

6. 分析 ARM 启动问题时的排查顺序

启动问题最怕一上来就从内核日志开始猜。更稳的做法是按阶段切:

1. 先确认卡在哪一层

  • 连 Boot ROM 都没走到
  • SPL 没起来
  • U-Boot 没起来
  • Kernel 没起来
  • Kernel 起了但用户空间没起来

2. 看最早日志从哪开始出现

如果第一条日志都没有,优先怀疑:

  • 电源 / 时钟
  • 启动介质
  • 串口配置
  • DDR 初始化前就崩溃

如果 U-Boot 正常但内核没起来,优先看:

  • 镜像地址
  • 设备树
  • bootargs
  • 根文件系统路径

3. 把“启动慢”拆成阶段时间

启动慢不一定是内核慢,也可能是:

  • Bootloader 读取存储太慢
  • DDR training 太久
  • 文件系统挂载等待超时
  • 用户态服务启动过多

按阶段记录时间点,比模糊地说“系统慢”更有价值。

7. 学 ARM 启动流程时最值得抓住的主线

如果要快速记住顺序,可以先记这个:

  1. Boot ROM 决定从哪里启动
  2. 早期代码建立最小硬件环境
  3. Bootloader 建立完整加载能力
  4. 内核接管硬件和内存管理
  5. 用户空间接管系统服务

真正理解之后,再去看具体芯片文档、U-Boot 启动日志、内核 start_kernel(),会容易很多。

8. 总结

ARM 启动流程说白了就是一场逐级接力:

  • 第一棒解决“能不能启动”
  • 第二棒解决“能不能把更大代码跑起来”
  • 第三棒解决“能不能把系统内核装载完成”
  • 第四棒解决“能不能进入真正的业务环境”

理解启动流程,不是为了背名字,而是为了出问题时能立刻判断:现在到底卡在哪一棒,上一棒做没做完,下一棒为什么没接上。