2. 嵌入式系统启动流程:从复位向量到操作系统启动

嵌入式系统上电后,CPU 到底干了什么?

很多人觉得启动就是「上电 -> 跑系统」。其实没那么简单。从复位向量到操作系统跑起来,中间要经历好几层引导。我当年第一次调一块 i.MX6 板子,卡在 ROM Code 阶段整整两天,最后发现是启动介质选择引脚电平没拉对。

嗯,咱们今天就把这整条链路捋清楚。

2.1 复位向量:CPU 的第一口饭

CPU 上电或复位后,PC(程序计数器)会被硬件强制指向一个固定地址。这个地址就是复位向量。

架构 复位向量地址 说明
ARM Cortex-A 0x00000000 或 0xFFFF0000 由异常向量基址寄存器决定
ARM Cortex-M 0x00000000(向量表首地址) 前 4 字节存栈顶,后 4 字节存复位函数地址
RISC-V 0x80000000(典型值) 可配置,由硬件设计决定
x86 0xFFFFFFF0 最后 16 字节,跳转到 BIOS

复位向量里放的通常是一条跳转指令,直接跳到真正的启动代码入口。为什么不能直接放代码?因为复位向量位置往往在 ROM 或 Flash 里,空间极其有限,放一条跳转指令就够了。

关键点:复位向量不是代码,是地址。CPU 拿到这个地址后,才开始执行第一条指令。

2.2 ROM Code:芯片出厂自带的「最小引导器」

现代 SoC 内部都固化了一段 ROM Code。这段代码是芯片出厂时写死的,用户改不了。它的任务很简单:初始化最基本的硬件,然后从某个介质加载下一级代码。

ROM Code 通常做这几件事:

  • 配置系统时钟(PLL、分频器)
  • 初始化 SRAM(通常是芯片内部的小 SRAM,几 KB 到几百 KB)
  • 检测启动引脚电平,确定从哪个介质加载
  • 从介质读取固定大小(比如 4KB、8KB)的数据到 SRAM
  • 跳转到 SRAM 执行

我记得有一次调试,发现 ROM Code 加载完 SPL 后死活不跳转。最后用示波器量了量,发现时钟输出脚虚焊,PLL 根本没锁定。ROM Code 卡在时钟初始化里了。

个人习惯:拿到新板子,第一件事就是测 SoC 的时钟输出脚。ROM Code 跑没跑,看时钟就知道。

2.3 SPL 与 TPL:两级引导的由来

为什么需要 SPL(Secondary Program Loader)?

说白了,ROM Code 太小了。它只能加载很小的代码到内部 SRAM。但 Bootloader(比如 U-Boot)本身很大,动辄几百 KB。怎么办?

那就分两步走:

  1. SPL:由 ROM Code 加载到 SRAM,初始化 DDR 控制器,然后把完整的 Bootloader 从介质拷贝到 DDR。
  2. TPL(Tertiary Program Loader):某些复杂 SoC 在 SPL 和完整 Bootloader 之间再加一级。TPL 负责更复杂的硬件初始化,比如电源管理、安全启动校验。

典型链路:ROM Code → SPL(SRAM)→ TPL(可选)→ U-Boot(DDR)→ 操作系统

我建议你记住这个图:

嵌入式系统启动流程 上电 / 复位 CPU 复位向量 ROM Code 芯片固化代码 SPL SRAM 中运行 TPL(可选) 安全 / 电源初始化 U-Boot / Bootloader DDR 中运行 操作系统内核 Linux / RTOS 用户空间 应用程序 每一级加载器都负责初始化下一级运行所需的硬件环境

2.4 典型 SoC 启动介质

ROM Code 从哪加载 SPL?这取决于启动介质。不同的介质,特性完全不同。

介质类型 接口 读取方式 典型容量 启动速度 常见场景
NAND Flash NAND 控制器 页读取,需 ECC 128MB ~ 8GB 慢(页读取 + ECC 校验) 低成本嵌入式设备
eMMC MMC/SDIO 块读取,内置控制器 4GB ~ 256GB 快(支持 HS400) 手机、平板、高端嵌入式
SD 卡 SDIO/SPI 块读取 1GB ~ 2TB 中等(取决于 SD 协议版本) 开发板、单板计算机
SPI Flash SPI/QSPI 连续读取,支持 XIP 1MB ~ 256MB 快(QSPI 四线模式) BIOS、Bootloader 存储

2.4.1 NAND Flash 启动

NAND 启动是最麻烦的。为什么?因为 NAND 有坏块,而且读取需要 ECC 校验。ROM Code 必须内置简单的 ECC 算法,才能正确读取前几页的数据。

我曾经在一个项目里,NAND 的前 4 页刚好都是坏块。ROM Code 读不到有效的 SPL,板子直接变砖。后来我们在烧录时强制把 SPL 写到第 5 页,并在 ROM Code 里配置了跳页读取。

避坑指南:NAND 启动时,SPL 必须放在前几个好块里。烧录工具要支持坏块跳过,否则量产时你会哭。

2.4.2 eMMC 启动

eMMC 内置了控制器,坏块管理、ECC 都由 eMMC 自己处理。ROM Code 只需要通过 MMC 协议读取块数据就行。简单很多。

eMMC 启动分区有两种:

  • Boot Area Partition:专门用于存放 Bootloader,eMMC 上电后默认从这个分区读取。
  • User Data Partition:存放操作系统和文件系统。

我建议把 SPL 和 U-Boot 都放在 Boot Area Partition 里,这样最安全。

2.4.3 SD 卡启动

SD 卡启动和 eMMC 类似,但 SD 卡没有专门的 Boot Area Partition。ROM Code 会读取 SD 卡的前几个块(通常是块 0 ~ 块 4),这些块里存放着 MBR 或特定的启动签名。

很多开发板(比如 BeagleBone、Raspberry Pi)都支持 SD 卡启动。调试时特别方便,换张卡就能换系统。

2.4.4 SPI Flash 启动

SPI Flash 启动是最简单的。因为 SPI Flash 支持 XIP(eXecute In Place),代码可以直接在 Flash 里执行,不需要先拷贝到 RAM。

但注意:SPI Flash 的读取速度受限于 SPI 时钟频率。QSPI(四线 SPI)能快很多,但需要 SoC 的 ROM Code 支持。

个人经验:如果板子空间允许,我倾向于用 SPI Flash 存 Bootloader,用 eMMC 存系统。SPI Flash 启动快、稳定,eMMC 容量大、速度快。各取所长。

2.5 启动介质选择与优先级

SoC 怎么知道从哪个介质启动?

通常靠硬件引脚电平。比如:

  • BOOT[0] = 0, BOOT[1] = 0 → NAND
  • BOOT[0] = 1, BOOT[1] = 0 → eMMC
  • BOOT[0] = 0, BOOT[1] = 1 → SD 卡
  • BOOT[0] = 1, BOOT[1] = 1 → SPI Flash

ROM Code 会先读这些引脚,然后按优先级尝试。如果第一个介质没读到有效数据,就尝试下一个。

我记得有一次,客户说板子偶尔启动不起来。查了半天,发现是 BOOT 引脚的上拉电阻虚焊,导致电平浮动。ROM Code 随机选了一个介质,有时候能读到,有时候读不到。

核心原则:启动介质的选择是硬件决定的,但启动顺序是 ROM Code 固化的。你改不了 ROM Code,但可以改硬件引脚。

2.6 从 Bootloader 到操作系统

最后一步,U-Boot 把操作系统内核加载到 DDR,然后跳转过去。

跳转前,U-Boot 要做几件事:

  • 设置内核启动参数(ATAG 或设备树)
  • 关闭 MMU 和 Cache(某些架构需要)
  • 设置 CPU 寄存器(比如 r0、r1、r2)
  • 跳转到内核入口地址

内核拿到控制权后,会初始化自己的内存管理、中断系统、调度器,最后挂载根文件系统,启动 init 进程。

到这一步,整个启动流程才算走完。

注意:U-Boot 跳转内核前,必须确保 DDR 已经稳定。我见过有人跳过 DDR 初始化直接跳内核,结果内核跑起来后随机死机。查了三天,最后发现是 DDR 时序没配好。

嗯,嵌入式启动流程大概就是这样。从复位向量到操作系统,每一级都有它的职责。你想想看,如果没有 ROM Code,你连 SPL 都加载不了。如果没有 SPL,U-Boot 那么大,SRAM 根本放不下。每一级都是被逼出来的设计。