MCU固件升级基础:启动流程与Flash分区

做嵌入式系统升级,绕不开一个核心问题:MCU是怎么启动的?

说白了,你得先搞清楚芯片上电后干了什么,才能设计出可靠的升级方案。我见过不少工程师,上来就写Bootloader,结果Flash分区一塌糊涂,升级一次就把自己搞死了。嗯,咱们今天就把这事彻底讲透。

2.1 MCU启动流程:从复位到App运行

MCU上电复位后,执行路径其实非常固定。以Cortex-M系列为例,流程大致如下:

  1. 硬件复位:CPU从0x00000000地址读取栈顶指针(MSP),从0x00000004地址读取复位向量(Reset_Handler)。
  2. 执行启动文件:跳转到Reset_Handler,完成系统时钟初始化、全局变量清零/赋值、C库初始化。
  3. 跳转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的起始映射。

具体做法是:

  1. 在链接脚本中定义App的向量表地址。
  2. Bootloader启动时,将App向量表从Flash复制到SRAM。
  3. 修改系统控制块(SCB)的向量表偏移,指向SRAM中的新向量表。

个人经验:我建议优先使用VTOR方式。如果芯片不支持,再考虑SRAM映射。但要注意,SRAM映射会增加启动时间,而且SRAM空间有限,别把向量表搞太大。

2.4 完整启动流程图

下面这张图,把整个流程串起来了。你可以看到Bootloader和App各自扮演的角色:

MCU启动与升级流程 上电复位 Bootloader 启动 检查升级请求 有升级 无升级 接收固件并写入Flash 校验并设置启动标志 设置VTOR并跳转App App 正常运行 关键点:Bootloader负责升级决策和跳转,App只关注业务逻辑,两者通过VTOR和分区约定解耦

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分区、向量表重映射,这三块是固件升级的基石。你把这些搞清楚了,后面讲升级协议、差分算法、安全校验,你才能听得明白。