1. 嵌入式系统启动流程全景:从复位向量到操作系统加载的完整链路分析

嵌入式系统上电后,到底发生了什么?

很多人调了几年驱动,却说不清从按下电源键到系统启动,CPU 到底走了哪几步。我刚开始做嵌入式时也这样,总觉得启动是「黑盒」,直到有一次调试一块新板子死活起不来,才逼着自己把整个链路啃了一遍。

说白了,嵌入式启动就是一场接力赛。每一棒都有明确的任务,交棒时必须干净利落。今天我就带你从头到尾捋一遍。

1.1 启动流程总览

整个启动链路可以分成四个阶段。我习惯用一张图来记:

核心观点:嵌入式启动 = 硬件初始化 → Bootloader 加载 → 内核启动 → 根文件系统挂载。每一阶段出错,系统都会卡死。

嵌入式系统启动流程全景图 阶段1:复位向量 ROM Boot / BootROM 阶段2:SPL / MLO 初始化DDR、时钟 阶段3:U-Boot 加载内核、设备树 阶段4:内核 挂载根文件系统 关键说明: • 复位向量:CPU上电后第一条指令地址(如0x00000000或0xFFFF0000) • SPL:Secondary Program Loader,负责初始化DDR和时钟,加载U-Boot • U-Boot:解析设备树,加载内核镜像到DDR,跳转到内核入口 • 内核:解压自身,初始化驱动,挂载根文件系统,启动init进程

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 的典型工作流程:

  1. 读取启动引脚电平——判断从哪个介质启动
  2. 初始化存储控制器——比如 SPI 控制器、MMC 控制器
  3. 读取固定偏移处的代码——通常是第 0 扇区或第 1 扇区
  4. 校验签名或 CRC——防止代码损坏
  5. 跳转到 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 第四阶段:内核启动与根文件系统

内核拿到控制权后,会做以下事情:

  1. 解压自身——如果内核是压缩的(zImage),先解压
  2. 初始化架构相关代码——设置页表、中断、时钟
  3. 解析设备树——遍历所有节点,匹配驱动
  4. 初始化驱动——按设备树中的 compatible 字段匹配驱动
  5. 挂载根文件系统——根据 bootargs 中的 root= 参数挂载
  6. 启动 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() 里加一句串口回环测试。