2. 硬件初始化:ROM Code、SPL、ATF、U-Boot、Kernel、Rootfs 的启动顺序与职责

嵌入式系统上电后,不是直接跳进 Linux 内核的。它要经历一段「接力跑」——从芯片内部固化代码开始,一级一级把硬件初始化好,最后才把控制权交给操作系统。这个链条如果理不清,联调时你会一头雾水。

我个人习惯把启动链分成六个阶段:ROM Code → SPL → ATF → U-Boot → Kernel → Rootfs。每个阶段都有明确的职责边界。下面我逐个拆开讲。

2.1 ROM Code:芯片出厂就有的「第一行代码」

ROM Code 是芯片流片时固化在内部 ROM 里的程序。你改不了它,也删不掉它。它的任务很简单:从预定义的启动介质加载下一级程序。

上电后,CPU 从复位向量(通常是 0x00000000 或 0xFFFF0000)开始执行。ROM Code 会做几件事:

  • 初始化最基本的时钟和 SRAM(通常是芯片内部 SRAM,几十 KB 到几百 KB)
  • 读取启动引脚电平(eMMC、SD 卡、NAND Flash、SPI NOR 等)
  • 从选中的介质读取 SPL 或 U-Boot 到内部 SRAM
  • 校验签名(如果使能了安全启动)
  • 跳转到 SPL 入口
我的经验:ROM Code 阶段最容易出问题的是启动介质选择。我遇到过一块板子,启动引脚虚焊导致 ROM Code 读不到任何介质,CPU 一直死循环。用示波器量启动介质的 CLK 和 CMD 线,能快速定位。

2.2 SPL:把 DDR 初始化好,为 U-Boot 铺路

SPL(Secondary Program Loader)是 U-Boot 的「瘦身版」。为什么需要它?因为 ROM Code 能用的内部 SRAM 太小了,装不下完整的 U-Boot。SPL 的任务就是:初始化 DDR,然后把 U-Boot 从存储介质搬到 DDR 里。

SPL 的典型流程:

  1. 设置 CPU 工作模式(关闭中断、设置栈指针)
  2. 初始化时钟和 PLL(让 CPU 和 DDR 跑在目标频率)
  3. 初始化 DDR 控制器(时序参数、ODT、ZQ 校准)
  4. 从存储介质读取 U-Boot 到 DDR 指定地址
  5. 跳转到 U-Boot 入口
注意:DDR 初始化是 SPL 里最关键的步骤。参数错了,系统直接死掉。我曾经在调一个新平台时,DDR 时序参数抄错了参考设计的值,结果 SPL 一跑 DDR 就挂。后来用芯片厂商提供的 DDR 训练工具重新校准才搞定。

2.3 ATF:ARM 平台的「安全管家」

ATF(ARM Trusted Firmware)是 ARMv8 架构引入的固件层。它运行在 EL3(异常级别 3),是系统里权限最高的代码。它的职责包括:

  • 初始化安全世界(Secure World)的硬件
  • 设置电源管理(PSCI,Power State Coordination Interface)
  • 提供安全监控调用(SMC)接口
  • 跳转到非安全世界的 U-Boot

在启动链里,ATF 通常由 SPL 加载,或者由 U-Boot 的早期阶段加载。它执行完后,系统会降级到 EL2 或 EL1,把控制权交给 U-Boot。

关键点:ATF 不是必须的。如果你的芯片是 ARMv7 或更早架构,或者你不需要安全世界功能,可以跳过 ATF,让 SPL 直接跳 U-Boot。但 ARMv8 平台上,ATF 几乎是标配。

2.4 U-Boot:真正的「硬件初始化主力」

U-Boot 是启动链里最复杂的阶段。它负责初始化大部分外设,为内核准备好运行环境。U-Boot 的启动流程大致分两步:

第一阶段(汇编部分):

  • 设置异常向量表
  • 初始化 CPU 和缓存
  • 设置栈指针
  • 跳转到 C 语言入口

第二阶段(C 语言部分):

  • 初始化串口(让你能看到打印信息)
  • 初始化定时器
  • 初始化 GPIO、I2C、SPI 等外设
  • 初始化存储设备(eMMC、SD 卡、NAND)
  • 初始化网络(如果支持网络启动)
  • 解析设备树(DTB)
  • 加载内核和设备树到 DDR
  • 跳转到内核入口
避坑指南:我曾经在 U-Boot 里加了一个新的 SPI Flash 驱动,结果每次启动到内核就死。查了两天才发现是 U-Boot 初始化 SPI 时改了时钟分频,内核驱动没重新配置。后来我在 U-Boot 的退出函数里把外设恢复到默认状态,问题解决。

2.5 Kernel:接管系统,启动进程

内核是启动链的「主角」。U-Boot 跳过来时,内核已经拿到了设备树和 initramfs(或根文件系统路径)。内核的启动过程:

  1. 解压自身(如果是压缩内核)
  2. 初始化架构相关代码(MMU、中断控制器、定时器)
  3. 解析设备树,注册平台设备
  4. 初始化驱动(按设备树里的顺序)
  5. 挂载根文件系统
  6. 启动 init 进程(PID 1)

内核启动时,打印信息会从 U-Boot 的串口继续输出。如果你看到 VFS: Mounted root 之类的信息,说明根文件系统挂载成功了。

2.6 Rootfs:用户空间的起点

Rootfs 是启动链的最后一环。内核启动 init 进程后,系统就进入了用户空间。Rootfs 的职责:

  • 提供 init 程序(通常是 BusyBox 的 init 或 systemd)
  • 执行初始化脚本(/etc/init.d/rcS 等)
  • 挂载额外的文件系统(/proc、/sys、/dev、/tmp)
  • 启动系统服务(网络、日志、看门狗等)
  • 启动应用程序

在 Buildroot 里,Rootfs 的构建是自动化的。你只需要配置好包列表,Buildroot 会生成完整的根文件系统镜像。

2.7 启动链全景图

下面这张图把整个启动链串起来了。你可以看到每个阶段的加载者和被加载者,以及它们之间的跳转关系。

嵌入式系统启动链全景图 ROM Code 芯片固化,不可修改 SPL 初始化 DDR ATF 安全世界初始化 U-Boot 外设初始化 Kernel 驱动加载 Rootfs 存储介质 eMMC / SD / NAND / SPI 加载 SPL 读取 SPL 读取 U-Boot 加载 ATF 跳转到 U-Boot 加载内核 挂载 说明 • 实线箭头表示加载/跳转关系 • ATF 在 ARMv8 平台是必须的,ARMv7 可跳过 • SPL 和 U-Boot 通常放在同一存储介质的不同分区

2.8 各阶段职责对比

为了方便你快速查阅,我把每个阶段的职责、运行位置和典型大小整理成了表格。

阶段 运行位置 典型大小 核心职责
ROM Code 内部 ROM 4-32 KB 加载 SPL,安全校验
SPL 内部 SRAM 16-64 KB 初始化 DDR,加载 U-Boot
ATF DDR(安全区域) 64-256 KB 安全世界初始化,PSCI
U-Boot DDR 256 KB - 2 MB 外设初始化,加载内核
Kernel DDR 2-10 MB 驱动加载,进程调度
Rootfs DDR / Flash 1-100 MB 用户空间初始化

总结一下:启动链的本质是「逐级放大」——ROM Code 用 KB 级的 SRAM 加载 SPL,SPL 初始化 MB 级的 DDR 后加载 U-Boot,U-Boot 再加载几 MB 的内核,内核最后挂载几十 MB 的根文件系统。每一级都在为下一级准备更大的运行空间。