第二章 启动流程分析:CPU复位后的第一条指令
各位同学,咱们今天聊聊CPU复位后到底发生了什么。说实话,这个问题我当年刚入行时也琢磨了很久。你想想看,芯片一上电,寄存器全是随机值,内存还没初始化,CPU怎么就知道该去执行哪条指令?
嗯,这里面的门道,咱们一层层剥开来看。
2.1 复位后的第一条指令
CPU复位后,硬件会自动做一件事——从某个固定地址读取第一条指令。这个地址,通常叫复位向量(Reset Vector)。
不同的架构,复位向量地址不一样:
| 架构 | 复位向量地址 | 说明 |
|---|---|---|
| ARM Cortex-M | 0x00000000 | 向量表起始地址,第一条指令在偏移4字节处 |
| ARM Cortex-A | 0x00000000 或 0xFFFF0000 | 取决于CPU配置 |
| x86 | 0xFFFFFFF0 | 靠近4GB地址空间顶端 |
| RISC-V | 0x00000000 或 0x80000000 | 由硬件设计决定 |
我个人习惯把复位向量理解成「CPU的出生证明」——它告诉CPU:你从这里开始活。我在项目中遇到过一个问题,某款芯片复位后总是跑飞,查了半天发现是复位向量地址配错了,硬件把向量表映射到了SRAM里,而SRAM上电后内容是随机的。嗯,那场面,真是让人头大。
关键点:复位向量里存的不是代码,而是地址。CPU先读这个地址,然后跳转过去执行真正的启动代码。
2.2 中断向量表
中断向量表,说白了就是一张「紧急联系人名单」。CPU遇到中断时,会查这张表,找到对应的处理函数地址。
这张表通常放在内存的最前面。以ARM Cortex-M为例:
; 向量表示例(汇编)
.section .vectors
.word _estack ; 0x00: 栈顶指针
.word Reset_Handler ; 0x04: 复位处理函数
.word NMI_Handler ; 0x08: 不可屏蔽中断
.word HardFault_Handler ; 0x0C: 硬错误中断
; ... 其他中断向量
你想想看,为什么栈顶指针要放在第一个位置?因为复位后第一条指令执行前,CPU需要先设置好栈指针。Cortex-M的设计很巧妙——它自动从0x00加载SP,从0x04加载PC。两步搞定,干净利落。
避坑指南:我曾经在移植Bootloader时,忘了把中断向量表重定位到RAM里。结果一开中断,CPU直接跑飞到默认地址,系统死机。后来我养成了习惯:每次修改向量表偏移,一定检查VTOR(向量表偏移寄存器)的值。
2.3 启动模式(Boot Mode)选择
大多数芯片支持多种启动模式。说白了就是:CPU可以从不同的地方启动。
常见的启动模式有:
- 内部Flash启动:从芯片内置的Flash读取代码
- 系统存储器启动:从ROM里的Bootloader启动
- SRAM启动:从内部SRAM启动(通常用于调试)
- 外部存储器启动:从NAND、SD卡、eMMC等启动
启动模式的选择,通常通过芯片的BOOT引脚电平来决定。比如STM32:
| BOOT0 | BOOT1 | 启动模式 |
|---|---|---|
| 0 | X | 主Flash启动 |
| 1 | 0 | 系统存储器启动 |
| 1 | 1 | SRAM启动 |
我记得有一次做量产测试,发现一批板子死活烧不进程序。查了半天,原来是产线工人把BOOT0引脚虚焊了,芯片一直处于系统存储器模式。嗯,硬件上的小问题,往往最让人头疼。
注意:启动模式的选择必须在复位前确定。复位后,芯片会锁存BOOT引脚的状态。如果你想在运行时切换启动模式,得先复位。
2.4 启动介质分析
不同的启动介质,特性差异很大。咱们一个一个看。
2.4.1 NOR Flash
NOR Flash的特点是:支持XIP(就地执行)。说白了就是,代码可以直接在Flash里跑,不用搬到RAM里。
- 读取速度快,适合存代码
- 写入速度慢,不适合频繁写
- 容量通常较小(几MB到几十MB)
- 接口简单,通常是SPI或并行接口
我在项目中用过NOR Flash做Bootloader存储。优点是启动快,上电就能跑。缺点是贵,容量小。有一次客户要求增加OTA功能,NOR Flash空间不够,最后只能换成NAND。
2.4.2 NAND Flash
NAND Flash就不一样了。它不支持XIP,代码必须先搬到RAM里才能执行。
- 容量大,价格便宜
- 读写速度快(尤其是连续读写)
- 有坏块问题,需要ECC校验
- 接口复杂,通常是8位或16位并行
你想想看,NAND Flash的启动流程是什么样的?CPU上电后,先执行内部ROM里的一段小程序(叫BootROM),这段程序负责从NAND Flash里把Bootloader读到SRAM里,然后跳转过去执行。
关键点:NAND启动时,BootROM需要知道NAND的参数(页大小、块大小、时序等)。这些参数要么硬编码在BootROM里,要么通过外部引脚配置。
2.4.3 SD卡
SD卡启动在嵌入式设备里很常见,尤其是树莓派这类板子。
- 使用SPI或SDIO接口
- 需要初始化SD卡控制器
- 启动代码通常放在SD卡的特定扇区(比如第0扇区)
- 支持热插拔(但启动时不行)
我曾经调试过一个SD卡启动的问题:板子插上SD卡能启动,拔掉就死机。查了三天,发现是BootROM里SD卡检测逻辑有bug——它只检查了卡是否存在,没处理卡不存在的情况。嗯,这种问题,说大不大,说小不小,但足够让你加班到深夜。
2.4.4 eMMC
eMMC本质上就是NAND Flash加了一个控制器,对外提供MMC接口。
- 内置坏块管理和ECC
- 接口标准化,兼容性好
- 启动速度快
- 支持分区(Boot分区、RPMB分区等)
eMMC的启动比较特殊。它有两个专用的Boot分区,容量通常很小(几MB)。启动时,eMMC控制器会把Boot分区的内容自动输出到数据线上,CPU直接读取就行。
避坑指南:我曾经遇到过eMMC启动分区内容被意外擦除的情况。原因是Bootloader里有个bug,在更新固件时误写了Boot分区。后来我加了一个保护机制:在写Boot分区前,必须读取一个特定的标志位。这个习惯,我一直保留到现在。
2.5 启动流程总结
好了,咱们把整个启动流程串起来看看:
- 上电复位:CPU从复位向量读取第一条指令地址
- 初始化硬件:设置栈指针、时钟、内存控制器等
- 选择启动介质:根据BOOT引脚或配置,决定从哪启动
- 加载Bootloader:从启动介质读取Bootloader代码
- 跳转执行:CPU跳转到Bootloader的入口地址
这个过程,说起来简单,做起来全是坑。我刚开始做Bootloader时,总觉得不就是跳转一下嘛,有什么难的。结果第一次调试,板子直接变砖。嗯,从那以后,我再也不敢小看启动流程了。
下一章,咱们聊聊Bootloader的核心——如何从启动介质里把固件加载到内存里。到时候我会分享一些我在实际项目中踩过的坑,保证让你少走弯路。