第二章 嵌入式系统启动流程:从复位向量到操作系统启动的完整链路

大家好,我是你们的讲师。今天我们来聊聊嵌入式系统上电后,到底发生了什么。很多工程师写了几年固件,对启动流程还是一知半解。说实话,这玩意儿要是搞不清楚,后面做安全启动、做固件升级,迟早要踩坑。

我刚开始做嵌入式那会儿,以为上电就是直接跑main函数。后来被一个启动异常的问题折磨了整整两周,才老老实实把启动流程从头到尾捋了一遍。嗯,从那以后,我再也不敢跳过这部分了。

2.1 启动流程全景图

一个典型的嵌入式系统,从按下复位键到操作系统跑起来,中间要经过好几个阶段。每个阶段都有自己的使命,也有自己的“地盘”。

你想想看,CPU一上电,内存还没初始化,代码该放哪儿跑?Flash里是压缩过的镜像,谁来解压?这些事儿,都得靠分阶段启动来解决。

核心观点:嵌入式启动是一个“由简到繁、逐级接力”的过程。每一级Bootloader只做一件事,做完就把控制权交给下一级。

下面这张图,是我个人习惯用来给团队新人讲启动流程的。它把整个链路画得很清楚:

嵌入式系统启动完整链路 复位向量 0x00000000 ROM Code 芯片出厂固化 SPL 初始化DDR/时钟 FBL 固件升级入口 SBL 加载OS镜像 操作系统 Linux/RTOS 说明: • 复位向量是CPU上电后第一条指令的地址,通常指向ROM Code入口 • ROM Code由芯片厂商固化,用户不可修改 • SPL/FBL/SBL存储在Flash中,属于用户可编程的Bootloader 关键节点: • 从ROM Code到SPL:内存初始化完成,代码从SRAM搬到DDR • 从SBL到OS:系统从实模式切换到保护模式/虚拟地址模式

2.2 复位向量:一切开始的地方

CPU上电或复位后,会去一个固定的地址取第一条指令。这个地址,就是复位向量。对于ARM Cortex-M系列,通常是0x00000000;对于Cortex-A系列,可能是0x00000000或0xFFFF0000。

复位向量里放的是什么?其实就两个东西:

  • 栈指针初始值——给CPU一个能用的栈
  • 复位中断服务程序入口——也就是第一段代码的地址

小提示:我见过有人把复位向量写错了,结果芯片一上电就跑飞。排查方法很简单——用示波器抓复位引脚,再看PC指针是不是跳到了预期地址。嗯,这招我用了很多次。

2.3 ROM Code:芯片出厂就有的“老大哥”

ROM Code是芯片出厂时固化在ROM里的代码。用户改不了,也删不掉。它的任务很明确:

  1. 基本的硬件初始化——比如关闭看门狗、配置系统时钟
  2. 检测启动介质——看看是从NAND Flash、eMMC还是SD卡启动
  3. 加载下一级Bootloader——把SPL从Flash搬到内部SRAM

说白了,ROM Code就是个“引路人”。它自己能力有限(SRAM通常只有几十KB),但能把更复杂的Bootloader请进来。

特性 ROM Code SPL/FBL/SBL
存储位置 芯片内部ROM 外部Flash
可修改性 不可修改 可升级
运行环境 内部SRAM SRAM或DDR
大小限制 几十KB 几百KB到几MB
主要任务 加载SPL 初始化硬件、加载OS

注意:有些芯片的ROM Code有安全漏洞。比如某款芯片的ROM Code在检测启动介质时,没有校验镜像签名。这就给了攻击者可乘之机——他们可以伪造一个恶意SPL,绕过安全启动。所以,选芯片时一定要看ROM Code的安全设计。

2.4 SPL:第二级程序加载器

SPL,全称是Secondary Program Loader。它是ROM Code加载进来的第一个用户可编程代码。

SPL的核心任务就两个:

  • 初始化DDR控制器——让外部内存能用
  • 把FBL或SBL从Flash搬到DDR——为下一级做准备

为什么不让ROM Code直接初始化DDR?因为DDR初始化需要复杂的时序配置,ROM Code那点空间根本放不下。而且不同板子的DDR颗粒不一样,配置参数也不同。这些参数放在SPL里,用户可以根据实际硬件来调整。

我记得有个项目,DDR初始化死活过不去。查了两天,最后发现是SPL里的时序参数跟实际DDR颗粒对不上。嗯,从那以后我每次换DDR颗粒,都会重新量一遍时序。

2.5 FBL:固件升级的“安全卫士”

FBL,Factory Bootloader,也叫恢复模式Bootloader。它跟SBL是平级的,但职责完全不同。

FBL的主要场景:

  1. 出厂首次烧录——空片时,FBL负责接收烧录工具发来的镜像
  2. 固件升级失败后的恢复——如果SBL或OS坏了,FBL还能通过特定方式(比如USB、UART)接收新镜像
  3. 安全校验——FBL通常会校验SBL和OS的签名,防止被篡改

设计要点:FBL必须放在Flash的独立分区里,而且这个分区在正常启动时不能被擦写。否则,一旦FBL被破坏,设备就真的变砖了。

2.6 SBL:操作系统的“搬运工”

SBL,Secondary Bootloader,是启动流程中最后一级Bootloader。它的任务是把操作系统镜像从Flash搬到DDR,然后跳转过去。

SBL做的事情比SPL多得多:

  • 解析镜像格式(比如FIT、uImage)
  • 校验镜像签名和哈希
  • 解压压缩过的内核镜像
  • 设置设备树(Device Tree)
  • 准备启动参数(比如内核命令行)
  • 跳转到内核入口

你想想看,如果SBL出了问题,整个系统都起不来。所以SBL的稳定性至关重要。

个人经验:我建议在SBL里加一个“启动计数”功能。如果连续三次启动失败,就自动回滚到上一个版本。这个机制救过我一次——有一次OTA升级包有问题,设备批量变砖,幸亏有这个回滚逻辑,才没酿成大祸。

2.7 各阶段的内存布局

不同阶段的代码,运行在不同的内存区域。我画了个表格,方便你对照:

阶段 运行位置 大小 特点
ROM Code 内部ROM 16~64 KB 只读,不可修改
SPL 内部SRAM 32~128 KB SRAM容量有限,SPL不能太大
FBL DDR 128~512 KB DDR已初始化,空间充裕
SBL DDR 256 KB~2 MB 功能复杂,体积最大
OS DDR 几MB~几十MB 内核+设备树+initramfs

2.8 启动流程中的常见坑

做启动这块,有几个坑我几乎每次都会遇到。列出来给你提个醒:

  • 栈溢出——SPL阶段栈空间很小,一不小心就溢出了。我建议在SPL里加一个栈保护标记,启动时检查一下。
  • DDR时序不对——DDR初始化参数跟硬件强相关。换板子、换颗粒,都得重新调。
  • 镜像加载地址冲突——SPL把自己加载到某个地址,结果FBL或SBL也用了这个地址,互相覆盖。嗯,这个坑我踩过。
  • 安全校验顺序——是先校验再加载,还是先加载再校验?顺序错了,可能被攻击者利用。

避坑指南:我曾经在一个项目里,把SPL和FBL放在同一个Flash分区。结果一次升级失败,两个都坏了,设备彻底变砖。后来我强制要求:每个Bootloader阶段必须独占一个分区,而且分区之间要有保护机制。

2.9 小结

嵌入式系统的启动流程,说白了就是一场接力赛。ROM Code起跑,SPL接棒,FBL或SBL冲刺,最后把操作系统送进终点。每个阶段都有自己的职责,也有自己的限制。

理解了这个流程,你才能知道:

  • 安全启动应该在哪个阶段做校验
  • 固件升级失败后,怎么保证设备还能恢复
  • 启动时间优化,该从哪个阶段下手