1. 嵌入式系统启动流程全景:从复位向量到操作系统加载的完整链路分析
嵌入式系统上电后,到底发生了什么?
很多人调了几年驱动,却说不清从按下电源键到系统启动,CPU 到底走了哪几步。我刚开始做嵌入式时也这样,总觉得启动是「黑盒」,直到有一次调试一块新板子死活起不来,才逼着自己把整个链路啃了一遍。
说白了,嵌入式启动就是一场接力赛。每一棒都有明确的任务,交棒时必须干净利落。今天我就带你从头到尾捋一遍。
1.1 启动流程总览
整个启动链路可以分成四个阶段。我习惯用一张图来记:
核心观点:嵌入式启动 = 硬件初始化 → Bootloader 加载 → 内核启动 → 根文件系统挂载。每一阶段出错,系统都会卡死。
1.2 第一阶段:复位向量与 BootROM
CPU 上电后,PC 指针会跳到一个固定地址——复位向量。ARM 架构通常是 0x00000000 或 0xFFFF0000,RISC-V 一般是 0x80000000 附近。
这个地址里放的是什么?
是芯片厂商固化在 ROM 里的一段代码,叫 BootROM。它很小,通常只有几 KB。它的任务只有一个:从存储介质里把下一级代码搬进 SRAM。
我的经验:有一次我拿到一块新芯片,发现 BootROM 支持从 SPI NOR、NAND、SD 卡、USB 四种介质启动。但启动顺序是固定的——先查 SPI,再查 SD。我焊了一块 SPI Flash 上去,结果 BootROM 一直卡在 SPI 初始化。后来才发现 SPI Flash 的 CS 引脚虚焊了。嗯,这种问题用示波器看波形最直接。
BootROM 的典型工作流程:
- 读取启动引脚电平——判断从哪个介质启动
- 初始化存储控制器——比如 SPI 控制器、MMC 控制器
- 读取固定偏移处的代码——通常是第 0 扇区或第 1 扇区
- 校验签名或 CRC——防止代码损坏
- 跳转到 SRAM 执行——此时 DDR 还没初始化
1.3 第二阶段:SPL / MLO
BootROM 加载的这段代码,在 U-Boot 里叫 SPL(Secondary Program Loader),在 TI 的芯片里叫 MLO。它的大小被严格限制——因为 SRAM 通常只有 64KB 到 256KB。
SPL 的核心任务:
- 初始化 DDR 控制器——配置时序、频率、电压
- 初始化时钟树——让 CPU 和总线跑在目标频率
- 初始化存储设备——比如从 eMMC 或 NAND 读取 U-Boot
- 加载 U-Boot 到 DDR——然后跳转过去
注意:SPL 阶段如果 DDR 初始化失败,系统会完全死掉,没有任何打印。我遇到过一块板子,DDR 的 VREF 电压不对,SPL 加载 U-Boot 时 CRC 校验总失败。查了两天才发现是电源纹波太大。所以,SPL 阶段建议加串口打印,哪怕只输出一个字符,也能帮你定位问题。
1.4 第三阶段:U-Boot 完整启动
U-Boot 被加载到 DDR 后,才算真正进入「Bootloader 的世界」。它比 SPL 大得多,功能也丰富得多。
U-Boot 启动流程:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | CPU 架构初始化 | 设置异常向量、中断控制器、MMU(可选) |
| 2 | 板级初始化 | 调用 board_init_f(),初始化 GPIO、串口、定时器 |
| 3 | 设备树解析 | 读取 .dtb 文件,获取内存布局、外设地址 |
| 4 | 驱动模型初始化 | DM(Driver Model)框架下,绑定设备和驱动 |
| 5 | 环境变量加载 | 从存储介质读取 bootargs、bootcmd 等 |
| 6 | 加载内核 | 从网络、存储或 USB 读取内核镜像到 DDR |
| 7 | 跳转到内核 | 传递设备树地址、内核参数,执行 bootm 命令 |
关键点:U-Boot 跳转到内核前,会做一件事——把设备树地址放在寄存器里(ARM 是 r2,RISC-V 是 a1)。内核启动时,第一件事就是解析这个设备树。如果设备树地址传错了,内核直接 panic。
1.5 第四阶段:内核启动与根文件系统
内核拿到控制权后,会做以下事情:
- 解压自身——如果内核是压缩的(zImage),先解压
- 初始化架构相关代码——设置页表、中断、时钟
- 解析设备树——遍历所有节点,匹配驱动
- 初始化驱动——按设备树中的 compatible 字段匹配驱动
- 挂载根文件系统——根据 bootargs 中的 root= 参数挂载
- 启动 init 进程——通常是 /sbin/init 或 /init
这里有个容易踩的坑:根文件系统挂载失败。我遇到过好几次,bootargs 里写的是 root=/dev/mmcblk0p2,但内核里 MMC 驱动没编进去,结果内核报 VFS: Unable to mount root fs。你想想看,内核都起来了,却卡在最后一步,多憋屈。
我的建议:调试阶段,先用 initramfs 做临时根文件系统。这样内核启动后直接进入 shell,可以手动检查存储设备、分区表、文件系统类型。等确认无误了,再切回真正的根文件系统。
1.6 各阶段耗时分布
不同阶段的时间开销差异很大。我整理了一份典型数据:
| 阶段 | 典型耗时 | 影响因素 |
|---|---|---|
| BootROM | 10~50 ms | 存储介质速度、校验算法 |
| SPL | 50~200 ms | DDR 初始化时序、时钟锁定时间 |
| U-Boot | 200~500 ms | 设备树大小、驱动初始化数量 |
| 内核 | 500 ms ~ 3 s | 内核大小、驱动加载、根文件系统挂载 |
如果你做的是消费类产品,启动时间要求 < 2 秒,那 U-Boot 阶段就要优化。我习惯的做法是:去掉不必要的驱动、使用 fastboot 模式、把设备树编译时裁剪掉不用的节点。
1.7 常见启动失败场景
最后,我列几个我实际踩过的坑,你调试时可以参考:
- 串口无输出——检查 BootROM 启动介质选择引脚,可能是焊错了电阻
- SPL 加载 U-Boot 失败——DDR 时序不对,或者存储设备的分区表错了
- U-Boot 启动后卡在 "Starting kernel..."——设备树地址传错了,或者内核镜像损坏
- 内核 panic 找不到根文件系统——bootargs 里的 root= 参数不对,或者驱动没编进去
我曾经踩过的坑:有一次移植 U-Boot 到新板子,SPL 能正常打印,但 U-Boot 启动后串口就没了。查了两天,发现是 U-Boot 的 pinmux 配置和 SPL 不一致——SPL 初始化了串口引脚,但 U-Boot 重新配置 pinmux 时把串口引脚改成了 GPIO。从那以后,我每次移植都会在 U-Boot 的 board_init_f() 里加一句串口回环测试。