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,否则任何中断都会跳到错误的入口。
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为例:
| 区域 | 起始地址 | 结束地址 | 大小 | 用途 |
|---|---|---|---|---|
| Bootloader | 0x0800_0000 | 0x0800_7FFF | 32KB | 启动判断、固件搬运、跳转 |
| App运行区 | 0x0800_8000 | 0x0803_FFFF | 224KB | 当前运行的应用程序 |
| 下载暂存区 | 0x0804_0000 | 0x0807_EFFF | 252KB | 接收OTA新固件 |
| 参数区 | 0x0807_F000 | 0x0807_FFFF | 4KB | 升级标志、版本号、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未重定位症状。
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自行重新配置。