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。怎么办?
那就分两步走:
- SPL:由 ROM Code 加载到 SRAM,初始化 DDR 控制器,然后把完整的 Bootloader 从介质拷贝到 DDR。
- TPL(Tertiary Program Loader):某些复杂 SoC 在 SPL 和完整 Bootloader 之间再加一级。TPL 负责更复杂的硬件初始化,比如电源管理、安全启动校验。
典型链路:ROM Code → SPL(SRAM)→ TPL(可选)→ U-Boot(DDR)→ 操作系统
我建议你记住这个图:
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 根本放不下。每一级都是被逼出来的设计。