2. 启动流程分析:MCU上电启动流程、中断向量表与复位向量、启动模式选择

好,咱们直接进入正题。MCU 上电之后到底干了些什么?很多人写了好几年代码,其实对这块是模糊的。我刚开始做嵌入式那会儿,也觉得启动流程是芯片厂家的事,跟我没关系。直到有一次调试一个诡异的死机问题,折腾了两天,最后发现是中断向量表没放对位置……嗯,从那以后我再也不敢跳过这部分了。

2.1 MCU 上电启动流程

MCU 上电后,硬件会自动执行一系列固定动作。这些动作不是由你的代码控制的,而是芯片内部的状态机在跑。说白了,就是硬件先把“家底”收拾好,再让你的代码进场。

核心流程(以 Cortex-M 系列为例):

  1. 上电复位(POR):内部电压检测电路确认 VDD 稳定后,释放复位信号。
  2. 硬件自举:CPU 从固定地址(通常是 0x00000000)读取初始 SP(栈顶指针),从 0x00000004 读取复位向量(Reset_Handler 地址)。
  3. 执行 Reset_Handler:这是你代码的第一个入口。通常做三件事:初始化 .data 段、清零 .bss 段、调用 SystemInit() 配置时钟和基本外设。
  4. 跳转 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 知识结构图

下面这张图把上电启动、向量表、启动模式串在了一起。你可以把它当作本章的“地图”。

MCU 启动流程与向量表结构 MCU 上电启动流程与向量表结构 上电复位 (POR) 读取初始 SP (0x00000000) 读取复位向量 (0x00000004) 执行 Reset_Handler 跳转 main() 中断向量表 (IVT) 0x00000000: 初始 SP 0x00000004: 复位向量 0x00000008: NMI 0x0000000C: HardFault 0x00000010: MemManage 0x00000014: BusFault 0x00000018: UsageFault … 外设中断 … VTOR 可重映射基地址 对齐要求: 256 字节 启动模式选择 BOOT0=0: Flash 启动 BOOT0=1,BOOT1=0: 系统存储器 BOOT0=1,BOOT1=1: SRAM 启动 引脚电平决定映射地址

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 值,启动时校验,过不了就不跳转。