2. STM32启动流程与Flash分区

STM32上电或复位后,CPU并不会直接执行用户写在 main() 里的代码,而是先经过一段由芯片硬件和启动文件共同约定的固定路径。理解这条路径,是设计Bootloader与App分区的前提。因为OTA升级的本质,就是让芯片在“先运行谁、再跳到哪里”这件事上拥有可控的选择权。

复位后的第一段路:从向量表到Reset_Handler

以Cortex-M内核为例,复位时CPU从地址 0x0000_0000 处取出两个字:第一个字是初始主栈指针MSP的值,第二个字是复位向量,也就是 Reset_Handler 的入口地址。注意,这里取的是“地址0x00000000”,但STM32通过BOOT引脚和选项字节,把Flash、系统存储器或SRAM映射到该地址,因此实际取到的内容来自被映射的存储区。

在默认的Flash启动配置下,0x0800_0000 被映射到 0x0000_0000。所以向量表通常放在Flash起始处,前4字节是栈顶地址,紧接着是复位向量、NMI、HardFault等异常向量。启动文件 startup_stm32xxxx.s 中定义的向量表,正是这段内容的来源。复位向量指向 Reset_Handler,它完成两件关键事:把 .data 段从Flash拷贝到SRAM,把 .bss 段清零,然后调用 SystemInit() 配置时钟,最后跳入 __main,由C库完成运行时初始化并调用用户的 main()。

这里有一个容易被忽略的细节:SCB->VTOR(向量表偏移寄存器)在复位后默认为0,意味着异常向量从 0x0000_0000 取。当程序运行在Flash中时,这通常没问题;但一旦发生跳转,比如从Bootloader跳到App,App的向量表并不在地址0,就必须重定位VTOR,否则任何中断都会跳到错误的入口。

STM32 复位启动路径与 Flash 分区映射 复位 / 上电 CPU 从 0x0000_0000 取 MSP + Reset_Handler 向量表(Flash 起始) 0x0800_0000 初始 MSP 0x0800_0004 Reset_Handler 0x0800_0008 NMI 0x0800_000C HardFault … 其他异常 0x0800_00xx IRQ0…IRQn Reset_Handler 执行序列 ① 拷贝 .data 段 Flash → SRAM ② 清零 .bss 段 ③ 调用 SystemInit() 配置时钟 ④ 跳转 __main → main() (此时 VTOR 仍为 0) (中断向量从 0x0000_0000 取) Flash 分区规划示例(以 512KB Flash 为例) Bootloader 0x0800_0000 – 0x0800_7FFF 32KB App 运行区 0x0800_8000 – 0x0803_FFFF 224KB 下载/暂存区 0x0804_0000 – 0x0807_FFFF 256KB 参数/标志区 0x0807_F000 – 0x0807_FFFF 4KB Bootloader 校验后跳转 OTA 下载写入暂存区

Bootloader与App的分区规划

Bootloader是芯片上电后第一个运行的用户程序,它占据Flash起始位置,因此它的向量表天然位于 0x0800_0000,不需要重定位。App则被放置在Bootloader之后的某个偏移地址,例如 0x0800_8000。这个偏移量必须是Flash页大小的整数倍,因为擦除操作以页为单位。以STM32F103为例,页大小为1KB或2KB(取决于型号),而STM32F4系列通常为16KB、64KB或128KB不等,规划时必须查阅对应型号的参考手册。

分区规划的核心约束有三条。第一,Bootloader自身不能被App覆盖,否则升级过程中断电会导致设备无法再次启动。第二,App运行区与下载暂存区必须分离,这样新固件可以先完整写入暂存区,校验通过后再由Bootloader搬运或切换,避免“写一半断电”导致App区损坏。第三,参数区用于存放升级标志、固件版本、CRC校验值等元数据,通常放在Flash末尾,且需要独立擦写。

下面给出一个可直接参考的分区参数表,以512KB Flash、2KB页大小的STM32F103RET6为例:

区域起始地址结束地址大小用途
Bootloader0x0800_00000x0800_7FFF32KB启动判断、固件搬运、跳转
App运行区0x0800_80000x0803_FFFF224KB当前运行的应用程序
下载暂存区0x0804_00000x0807_EFFF252KB接收OTA新固件
参数区0x0807_F0000x0807_FFFF4KB升级标志、版本号、CRC

注意,App运行区与下载暂存区的大小不必完全相等,但暂存区必须能容纳最大可能的App镜像。如果App编译后超过224KB,就需要调整分区,或者启用压缩升级方案。

中断向量表重定位:跳转后中断为何会跑飞

当Bootloader决定跳转到App时,它需要做三件事:关闭全局中断、设置MSP为App向量表的第一个字、设置VTOR为App的起始地址,然后跳转到App的复位向量。其中VTOR的设置是关键。Cortex-M的 SCB->VTOR 寄存器低7位保留,实际写入的是向量表基地址。对于App位于 0x0800_8000 的情况,应写入 0x0800_8000。

在App的 SystemInit() 或 main() 最开始,通常需要显式执行:

SCB->VTOR = 0x08008000;
__DSB();
__ISB();

如果忘记这一步,App中一旦发生SysTick中断或任何外设中断,CPU仍会从 0x0000_0000 处的旧向量表取入口地址,结果要么跳回Bootloader的中断服务函数,要么跳到未定义区域导致HardFault。实际调试中,表现为“跳转后主循环能跑,但一开中断就死机”,这就是典型的VTOR未重定位症状。

在IAR或Keil中,App工程需要修改链接脚本,把Flash起始地址改为 0x0800_8000,同时设置向量表偏移。Keil中可在 Target 选项的 IROM1 起始地址处修改;IAR中修改 .icf 文件的 ROM_region 起始地址。此外,App的 system_stm32f1xx.c 中 VECT_TAB_OFFSET 宏也应改为对应偏移值。

跳转前的必要检查

Bootloader在跳转前,至少应确认三件事:App向量表的第一个字(初始MSP)是否落在SRAM地址范围内(例如 0x2000_0000 到 0x2000_FFFF),第二个字(复位向量)是否落在App运行区范围内,以及App区首地址是否包含有效的栈顶值。如果这些检查不通过,说明App区没有有效固件,应停留在Bootloader的升级模式,而不是盲目跳转。

一个简化的跳转函数如下:

typedef void (*pFunction)(void);
pFunction JumpToApp;
uint32_t appMsp = *(volatile uint32_t*)0x08008000;
uint32_t appReset = *(volatile uint32_t*)0x08008004;

if ((appMsp & 0x2FFE0000) == 0x20000000) {
    __disable_irq();
    SCB->VTOR = 0x08008000;
    __set_MSP(appMsp);
    JumpToApp = (pFunction)appReset;
    JumpToApp();
}

这段代码中,0x2FFE0000 掩码用于判断MSP是否指向SRAM区域,是一种常见的合法性检查。跳转后,App的启动文件会重新执行 Reset_Handler,完成它自己的 .data 拷贝和 .bss 清零,然后进入App的 main()。至此,控制权完成从Bootloader到App的交接。

跳转前必须关闭全局中断并清除所有挂起的中断标志,否则在 __set_MSP 到App初始化完成之间,任何中断都可能使用错误的栈指针,导致不可预测的崩溃。此外,如果Bootloader使用了SysTick,跳转前应关闭SysTick,由App自行重新配置。