2. 启动流程深度解析:MCU上电复位流程、向量表与复位向量、启动模式选择

各位同学,咱们今天聊点硬核的。MCU 上电之后到底干了什么?很多人写了好几年代码,可能都没仔细想过这个问题。我刚开始做嵌入式那会儿,也觉得启动流程就是个黑盒子,能跑就行。直到有一次,板子死活起不来,查了三天,最后发现是向量表配置错了。嗯,从那以后,我再也不敢跳过这部分了。

说白了,MCU 的启动流程就是芯片的“开机自检”加“引导加载”。它决定了你的程序从哪里开始跑,怎么跑,以及跑之前要准备些什么。今天我们就把它彻底拆开看看。

2.1 上电复位流程:芯片醒来的第一步

当你给板子通电,或者按下复位键,MCU 内部会发生一系列事件。这个过程是硬件自动完成的,不需要你写任何代码。但理解它,能帮你解决很多疑难杂症。

我习惯把上电复位流程分成三个阶段:

  1. 电源稳定阶段:VDD 电压上升到 POR(Power-On Reset)阈值。芯片内部复位电路会保持复位状态,直到电压稳定。这个时间通常是几毫秒。
  2. 时钟启动阶段:内部 RC 振荡器开始起振。芯片会等待时钟稳定,然后释放复位信号。注意,此时用的是内部低速时钟,不是外部晶振。
  3. 硬件初始化阶段:CPU 从复位状态释放,开始执行第一条指令。但在此之前,硬件已经帮你做了几件事:
    • 清零大部分寄存器(除了 PC、SP 等特殊寄存器)
    • 设置默认的中断优先级
    • 从固定地址加载初始 SP 和 PC

关键点:上电复位和热复位(按下复位键)的流程基本一致,但有些 MCU 会区分 POR 和 NRST 引脚复位,内部寄存器的复位值可能不同。查手册时要注意这个细节。

你想想看,芯片刚上电时,RAM 里的数据是随机的,Flash 里是上次烧录的程序。CPU 怎么知道该从哪里开始跑?这就引出了我们的下一个话题——向量表。

2.2 向量表与复位向量:程序的入口地图

向量表,说白了就是一张地址映射表。它告诉 CPU:当中断发生时,你应该跳到哪里去执行。这张表通常放在 Flash 的起始地址,也就是 0x00000000(或者 0x08000000,取决于具体芯片)。

对于 Cortex-M 系列内核,向量表的前两个条目是固定的:

偏移地址 内容 说明
0x00000000 初始主栈指针 (MSP) 复位后 SP 寄存器的初始值
0x00000004 复位向量 (Reset_Handler) 复位后 PC 寄存器的初始值
0x00000008 NMI 异常入口 不可屏蔽中断
0x0000000C HardFault 异常入口 硬件错误处理

复位向量就是程序的真正入口。CPU 从复位状态释放后,会做两件事:

  1. 从 0x00000000 加载值到 SP 寄存器
  2. 从 0x00000004 加载值到 PC 寄存器

然后,CPU 就开始执行 PC 指向的那条指令。注意,这里加载的是地址,不是指令本身。Cortex-M 的复位向量必须是一个奇数(因为 Thumb 指令集要求 LSB 为 1),如果写成偶数,CPU 会进入错误状态。

我曾经踩过的坑:有一次我手动修改链接脚本,把向量表地址改到了 0x08001000,但忘了更新 SCB->VTOR 寄存器。结果中断一触发,CPU 跳到了默认的向量表位置,那里全是 0xFFFFFFFF,直接 HardFault。查了两天才发现是 VTOR 没同步。

在 Bootloader 工程中,我们经常需要重定向向量表。比如,你的 Boot 程序放在 0x08000000,应用程序放在 0x08010000。那么,在跳转到应用程序之前,必须把 VTOR 寄存器指向新的向量表基址:

/* 设置向量表偏移 */
SCB->VTOR = 0x08010000;

/* 或者使用 CMSIS 提供的宏 */
#define VECT_TAB_OFFSET  0x10000
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;

2.3 启动模式选择:Boot from Flash/RAM/System Memory

大多数 MCU 都支持多种启动模式。你可能会问:为什么要搞这么复杂?直接统一从 Flash 启动不就行了?

嗯,原因很简单:灵活性。不同的场景需要不同的启动方式:

  • 量产烧录:从 System Memory 启动,运行芯片厂家的内置 Bootloader,通过 UART/USB/CAN 烧录程序。
  • 调试阶段:从 RAM 启动,可以快速下载和调试,避免频繁擦写 Flash。
  • 正式运行:从 Flash 启动,程序固化,掉电不丢失。

启动模式的选择,通常是通过芯片的 BOOT0 和 BOOT1 引脚的电平来配置的。以 STM32F1 为例:

BOOT0 BOOT1 启动区域 说明
0 X Flash (0x08000000) 主 Flash,正常启动
1 0 System Memory (0x1FFF0000) 内置 Bootloader,用于 ISP 烧录
1 1 SRAM (0x20000000) 从 RAM 启动,调试用

注意,不同系列的 MCU,System Memory 的地址和大小都不一样。比如 STM32F4 的 System Memory 在 0x1FFF0000,而 STM32L0 在 0x1FF00000。一定要查手册确认。

我的个人习惯:在产品设计中,我会把 BOOT0 引脚通过一个 10kΩ 电阻下拉到 GND(默认从 Flash 启动),同时预留一个跳线或按键,方便量产时拉高进入系统 Bootloader。这样既保证了正常运行的可靠性,又保留了烧录的灵活性。

下面这张图,是我自己总结的 MCU 启动模式选择流程,画出来方便大家理解:

MCU 启动模式选择流程 MCU 启动模式选择流程 上电 / 复位 读取 BOOT0 引脚 BOOT0 = 0 → Flash BOOT0 = 1 → 判断 BOOT1 从 0x08000000 启动 BOOT1 = 0 BOOT1 = 1 System Memory (Bootloader) SRAM (0x20000000) 说明:不同 MCU 的 BOOT 引脚数量和逻辑可能不同,请以具体芯片参考手册为准。 例如 STM32F1 使用 BOOT0 + BOOT1,而 STM32G0 仅使用 BOOT0 一个引脚。

2.4 启动流程中的关键寄存器

除了向量表和启动模式,还有几个寄存器在启动过程中扮演着重要角色。我个人建议你把这些寄存器记牢,调试时能省不少时间。

寄存器 全称 作用
SCB->VTOR Vector Table Offset Register 设置向量表的基地址偏移量
RCC->CSR Clock Control & Status Register 记录复位原因(POR、NRST、WWDG 等)
SYSCFG->CFGR1 System Configuration Register 1 部分 MCU 用于配置启动模式

调试小技巧:在 Bootloader 的起始代码中,我习惯先读取 RCC->CSR 寄存器,判断复位原因。如果是看门狗复位,可以跳过一些耗时的初始化,快速恢复系统。如果是上电复位,则执行完整的初始化流程。

2.5 实战中的注意事项

最后,分享几个我在项目中遇到的实际问题,希望能帮你少走弯路:

  • 向量表对齐:VTOR 寄存器要求向量表基址必须按 512 字节对齐(Cortex-M0/M3/M4)。如果你把向量表放在 0x08000100,那就会出问题。我建议至少按 1KB 对齐,省心。
  • 中断使能时机:在 Bootloader 跳转到应用程序之前,一定要关闭所有中断,并清除挂起标志。否则,应用程序一启动就可能触发一个莫名其妙的中断。
  • 堆栈指针切换:如果 Bootloader 和应用程序使用不同的堆栈区域,跳转前要确保 SP 已经切换到应用程序的栈顶。否则,函数调用会崩。

我曾经踩过的坑:有一次做 OTA 升级,Bootloader 下载完固件后直接跳转,结果应用程序跑起来后,串口中断一直不响应。查了半天,发现是 Bootloader 里开了串口中断,跳转前忘了关。应用程序的向量表里,串口中断入口是新的地址,但中断标志还挂在旧的中断处理函数上。嗯,从那以后,我写跳转代码的第一行就是 __disable_irq();。