MCU固件升级基础:启动流程与Flash分区
做嵌入式系统升级,绕不开一个核心问题:MCU是怎么启动的?
说白了,你得先搞清楚芯片上电后干了什么,才能设计出可靠的升级方案。我见过不少工程师,上来就写Bootloader,结果Flash分区一塌糊涂,升级一次就把自己搞死了。嗯,咱们今天就把这事彻底讲透。
2.1 MCU启动流程:从复位到App运行
MCU上电复位后,执行路径其实非常固定。以Cortex-M系列为例,流程大致如下:
- 硬件复位:CPU从0x00000000地址读取栈顶指针(MSP),从0x00000004地址读取复位向量(Reset_Handler)。
- 执行启动文件:跳转到Reset_Handler,完成系统时钟初始化、全局变量清零/赋值、C库初始化。
- 跳转main():调用用户主函数。
但这里有个关键点——向量表的位置。默认情况下,向量表固定在0x00000000。如果你把App放在0x08010000,那CPU上电后读到的还是Bootloader的向量表,根本不会执行App。
核心结论:MCU启动时,永远从复位向量指向的地址开始执行。谁控制复位向量,谁就控制启动流程。
我在项目中遇到过一个问题:某款芯片的Bootloader和App都放在内部Flash,但Bootloader升级后,App突然跑不起来了。查了半天,发现是Bootloader修改了向量表偏移寄存器,但App启动时又把它重置了。这种坑,你想想看,定位起来多痛苦。
2.2 Flash分区规划策略
Flash分区,说白了就是给Bootloader和App各自划地盘。怎么划?我个人的习惯是:先定大小,再定位置,最后定保护策略。
2.2.1 典型分区结构
以一个512KB的STM32F4为例,我常用的分区方案是这样的:
| 分区 | 起始地址 | 大小 | 内容 |
|---|---|---|---|
| Bootloader | 0x08000000 | 64KB | 启动代码、升级协议、Flash驱动 |
| App | 0x08010000 | 384KB | 用户应用程序 |
| 参数区 | 0x08070000 | 32KB | 校准参数、配置信息 |
| 备份区 | 0x08078000 | 32KB | 升级失败时的回滚镜像 |
为什么Bootloader只给64KB?因为Bootloader的功能很纯粹:检查升级请求、擦写Flash、跳转App。不需要塞太多东西。我曾经见过有人把Bootloader做到256KB,里面塞了完整的RTOS和文件系统……嗯,我只能说,这有点过度设计了。
2.2.2 分区对齐原则
Flash分区不是随便划的。有几个硬性约束:
- 扇区对齐:Flash擦除的最小单位是扇区(Sector),分区边界必须落在扇区边界上。否则你擦一个扇区,可能把隔壁分区的数据也擦掉了。
- 地址对齐:向量表要求256字节对齐(Cortex-M要求)。App起始地址必须是0x200的整数倍。
- 大小预留:给Bootloader留足未来升级的空间。我一般多留一个扇区的余量。
避坑指南:我曾经在一个项目里,把Bootloader和App的分区边界设在了扇区中间。结果App升级时,擦除操作把Bootloader的尾部数据也抹掉了。那次之后,我每次做分区都会拿数据手册对着扇区表一个一个核对。
2.3 向量表重映射原理
好,分区划好了,Bootloader也写好了。现在Bootloader要跳转到App,怎么跳?
直接跳转地址?不行。因为CPU的中断机制依赖向量表。如果向量表还在0x08000000,App里发生中断,CPU还是会去Bootloader的向量表里找中断服务函数。结果就是:要么跑飞,要么死机。
解决方案就是向量表重映射。Cortex-M提供了两种方式:
2.3.1 方式一:修改VTOR寄存器
VTOR(Vector Table Offset Register)是Cortex-M3/M4/M7内核提供的专用寄存器。你只需要把App的向量表基地址写进去:
// 假设App向量表在0x08010000
#define APP_VECTOR_TABLE ((uint32_t)0x08010000)
// 设置VTOR
SCB->VTOR = APP_VECTOR_TABLE;
设置完VTOR后,所有中断都会从新的向量表里查找入口地址。这是最推荐的方式,简单可靠。
2.3.2 方式二:修改链接脚本
有些低端MCU没有VTOR寄存器(比如Cortex-M0)。这时候只能通过修改链接脚本,把向量表放在Flash的起始位置,然后通过Bootloader把App的向量表复制到SRAM中,再修改SRAM的起始映射。
具体做法是:
- 在链接脚本中定义App的向量表地址。
- Bootloader启动时,将App向量表从Flash复制到SRAM。
- 修改系统控制块(SCB)的向量表偏移,指向SRAM中的新向量表。
个人经验:我建议优先使用VTOR方式。如果芯片不支持,再考虑SRAM映射。但要注意,SRAM映射会增加启动时间,而且SRAM空间有限,别把向量表搞太大。
2.4 完整启动流程图
下面这张图,把整个流程串起来了。你可以看到Bootloader和App各自扮演的角色:
2.5 跳转实现代码
最后,给出一段标准的Bootloader跳转代码。这段代码我用了很多年,基本没出过问题:
typedef void (*pFunction)(void);
void JumpToApp(uint32_t app_addr)
{
uint32_t msp_value;
pFunction app_entry;
// 1. 检查App向量表有效性(栈顶指针必须在SRAM范围内)
msp_value = *(volatile uint32_t*)app_addr;
if ((msp_value < SRAM_BASE) || (msp_value >= (SRAM_BASE + SRAM_SIZE)))
{
// 无效的App,停留在Bootloader
return;
}
// 2. 关闭全局中断
__disable_irq();
// 3. 设置VTOR
SCB->VTOR = app_addr;
// 4. 设置主栈指针
__set_MSP(msp_value);
// 5. 获取复位向量并跳转
app_entry = (pFunction)(*(volatile uint32_t*)(app_addr + 4));
app_entry();
// 永远不会执行到这里
}
重要提醒:跳转前一定要关闭全局中断。否则跳转过程中发生中断,CPU会去新的向量表找中断入口,但此时App还没初始化完成,大概率会死机。我早期就因为这个原因,调试了整整两天。
好了,MCU启动流程、Flash分区、向量表重映射,这三块是固件升级的基石。你把这些搞清楚了,后面讲升级协议、差分算法、安全校验,你才能听得明白。