2. 启动流程分析:MCU上电启动流程、中断向量表与复位向量、启动模式选择
好,咱们直接进入正题。MCU 上电之后到底干了些什么?很多人写了好几年代码,其实对这块是模糊的。我刚开始做嵌入式那会儿,也觉得启动流程是芯片厂家的事,跟我没关系。直到有一次调试一个诡异的死机问题,折腾了两天,最后发现是中断向量表没放对位置……嗯,从那以后我再也不敢跳过这部分了。
2.1 MCU 上电启动流程
MCU 上电后,硬件会自动执行一系列固定动作。这些动作不是由你的代码控制的,而是芯片内部的状态机在跑。说白了,就是硬件先把“家底”收拾好,再让你的代码进场。
核心流程(以 Cortex-M 系列为例):
- 上电复位(POR):内部电压检测电路确认 VDD 稳定后,释放复位信号。
- 硬件自举:CPU 从固定地址(通常是 0x00000000)读取初始 SP(栈顶指针),从 0x00000004 读取复位向量(Reset_Handler 地址)。
- 执行 Reset_Handler:这是你代码的第一个入口。通常做三件事:初始化 .data 段、清零 .bss 段、调用 SystemInit() 配置时钟和基本外设。
- 跳转 main():一切就绪后,调用 __main(C 库初始化)或直接跳 main()。
这里有个细节:Cortex-M 的复位序列和 ARM7/9 完全不同。ARM7 是从 0x00000000 取第一条指令,而 Cortex-M 是从 0x00000004 取向量地址,再跳转。我当年从 ARM9 切到 Cortex-M3 时,就被这个区别坑过一次——直接把 ARM9 的启动代码搬过来用,结果板子压根不跑。
我的习惯:在 Reset_Handler 开头加一小段 GPIO 翻转代码,用来确认芯片确实跑起来了。调试时用示波器看一眼,比仿真器连半天快得多。
2.2 中断向量表与复位向量
中断向量表,说白了就是一张“地址跳转表”。每个中断源对应表中的一个条目,条目里存的是中断服务函数(ISR)的入口地址。CPU 响应中断时,硬件自动从这张表里取出地址,然后跳过去执行。
这张表必须放在芯片规定的起始位置。对于 Cortex-M,默认是 0x00000000。但很多芯片支持重映射(VTOR 寄存器),可以把表挪到 SRAM 或 Flash 的其他位置。这个特性在 Bootloader 里非常有用——你想想看,APP 和 Bootloader 各用各的向量表,互不干扰。
| 偏移地址 | 内容 | 说明 |
|---|---|---|
| 0x00000000 | 初始 SP(栈顶指针) | 上电后硬件自动加载 |
| 0x00000004 | 复位向量(Reset_Handler) | 程序入口地址 |
| 0x00000008 | NMI_Handler | 不可屏蔽中断 |
| 0x0000000C | HardFault_Handler | 硬件错误中断 |
| … | … | 其他外设中断 |
复位向量是特殊的那一个。它不是中断,但硬件上电后第一个找的就是它。我见过有人把复位向量写错成 0,结果芯片一直复位——因为硬件取到的地址是 0,跳过去执行空指令,然后触发异常,又复位,死循环。
避坑指南:我曾经在移植一个 RTOS 时,忘记更新链接脚本中的向量表基地址。结果系统一开中断就 HardFault。查了两天才发现,VTOR 寄存器指向了旧表,而旧表里 ISR 地址全是 0。所以,每次修改链接脚本后,务必检查向量表是否对齐到了 256 字节边界(Cortex-M 要求)。
2.3 启动模式选择
大部分 MCU 支持多种启动模式。比如 STM32 可以通过 BOOT0/BOOT1 引脚选择从 Flash、系统存储器(内置 Bootloader)或 SRAM 启动。这个设计是为了方便工厂烧录和调试。
三种模式的区别:
- Flash 启动:正常模式,用户代码放在主 Flash,上电后直接运行。
- 系统存储器启动:芯片出厂时预置了一段 Bootloader 代码,支持串口/USB/DFU 等协议下载固件。这是很多产品量产时用的方式。
- SRAM 启动:从 SRAM 执行代码,通常用于调试或临时运行小段程序。
我个人习惯在产品板上把 BOOT0 拉低(Flash 启动),BOOT1 随意。但在开发板上,我会留一个跳线帽,方便切换到系统存储器模式——万一你把 Flash 里的 Bootloader 刷坏了,还能通过串口救回来。
启动模式与 Bootloader 的关系:
你写的 Bootloader 通常放在 Flash 起始地址(0x08000000 附近)。上电后先执行 Bootloader,它检查升级标志,决定是进入升级模式还是跳转到 APP。而芯片自带的系统存储器 Bootloader 是“元老级”的——它负责把你自己写的 Bootloader 烧进去。所以,这两者是不同层级的东西,别搞混了。
2.4 知识结构图
下面这张图把上电启动、向量表、启动模式串在了一起。你可以把它当作本章的“地图”。
2.5 实际项目中的注意事项
讲完理论,说点实际的。你在写 Bootloader 时,启动流程这块有几个地方特别容易出问题:
- 向量表偏移:APP 的向量表不能放在 0x08000000 了,得往后挪。比如 Bootloader 占 32KB,APP 就从 0x08008000 开始。记得在 APP 的启动代码里设置 VTOR = 0x08008000。
- 中断使能时机:在 Reset_Handler 里不要开中断。我见过有人把外设初始化放在 SystemInit 里,结果中断一来,SP 还没设好,直接 HardFault。
- 栈指针验证:跳转到 APP 之前,检查一下 APP 起始地址的前 4 字节(初始 SP)是否在 RAM 范围内。如果是个非法值,说明 APP 区是空的或者被破坏了,这时候应该留在 Bootloader 里等待升级。
一个小技巧:在链接脚本里给每个段(.text、.data、.bss)都加上起始和结束符号。这样在启动代码里可以方便地计算大小,也方便做 CRC 校验。我自己的 Bootloader 项目里,每个段都配了一个 32 位 CRC 值,启动时校验,过不了就不跳转。