2. MCU固件升级基础

做固件升级,说白了就是让MCU自己把自己给“换掉”。听起来有点玄乎,但拆开来看,核心就三块:Bootloader怎么设计、Flash怎么操作、流程怎么走通。我做了这么多年嵌入式,见过太多升级失败翻车的案例,十有八九都是这三块没吃透。

这一章,咱们就把地基打牢。

1. Bootloader与App分区:MCU的“双系统”架构

MCU的Flash空间,通常被划分为两个区域:Bootloader区和App区。你可以把它想象成电脑的BIOS和操作系统。BIOS负责启动、自检、加载系统;操作系统负责干活。MCU里也一样。

核心原则:Bootloader和App在物理地址上是完全隔离的。Bootloader不能轻易被擦除,它是升级操作的“保险丝”。

1.1 分区布局

我习惯把Flash空间规划成下面这样。具体地址偏移量取决于芯片,但逻辑结构是通用的。

区域 起始地址(示例) 大小 内容
中断向量表(Boot) 0x08000000 4KB Bootloader的中断向量
Bootloader代码 0x08001000 48KB 升级逻辑、Flash驱动、通信协议
App中断向量表 0x08010000 4KB App的中断向量
App代码 0x08011000 400KB 用户应用程序
参数/标志区 0x0807F000 4KB 升级标志、版本号、校验值

注意看,App区也有自己的中断向量表。这是跳转的关键,后面会细说。

1.2 Bootloader的核心职责

Bootloader不是万能的,它只干三件事:

  • 检查升级请求:上电后,先看有没有升级标志。比如某个GPIO电平、或者Flash里的特定标志位。
  • 执行升级流程:如果有请求,就进入升级模式,接收数据、写入Flash。
  • 跳转到App:如果没有升级请求,或者升级完成,就跳转到App区执行。

我的经验:Bootloader代码要尽量精简。我见过有人把RTOS塞进Bootloader里,结果升级时自己把自己搞死机了。Bootloader越简单,越可靠。

2. Flash存储器特性:你得懂它的脾气

Flash不是RAM,不能随便写。它有自己的一套规矩。不懂这些规矩,升级代码写出来就是定时炸弹。

2.1 三大操作:读、写、擦除

操作 特点 注意事项
读 最快,直接寻址 无特殊限制,但要注意对齐(32位MCU通常4字节对齐)
写(编程) 只能将1写成0,不能将0写成1 写入前目标地址必须是擦除状态(全0xFF)
擦除 将整个扇区恢复成全0xFF 擦除单位是扇区(Sector),不是字节。扇区大小因芯片而异,从1KB到256KB不等。

为什么会这样?因为Flash的物理结构决定了它只能“放电”(写0)和“充电”(擦除成1)。你想想看,如果你要修改一个字节,得先把整个扇区读到RAM里,擦除扇区,再在RAM里改好,最后写回去。这就是经典的“读-改-写”流程。

避坑指南:我曾经在一个项目里,没注意扇区大小,直接按字节擦除。结果芯片锁死了,只能重新烧录。记住:擦除的最小单位是扇区,不是字节!

2.2 Flash的寿命与可靠性

Flash有擦写次数限制,一般是1万到10万次。对于Bootloader区,因为很少擦写,问题不大。但App区如果频繁升级,就得考虑磨损均衡。

另外,Flash在写入时如果掉电,数据会损坏。所以升级过程中,校验是必须的。我习惯在每包数据写入后立即读回校验,而不是等全部写完再校验。这样能尽早发现问题。

3. 升级流程:下载、校验、跳转

标准的MCU固件升级流程,可以概括为三步。下面这张图,是我画给团队新人的,你一看就明白。

MCU固件升级核心流程 上电启动 检查升级标志 有升级 请求? 接收固件数据 写入Flash 校验通过? 跳转到App App正常运行 校验通过,清除标志 校验失败,重试或回滚 跳转前需关闭所有中断

3.1 下载:怎么把固件塞进去

下载方式有很多种:UART、SPI、I2C、USB、CAN、以太网……不管用哪种,核心逻辑都一样:分包传输。

我一般把固件分成固定大小的包,比如512字节一包。每包带一个序号和CRC校验。接收方收到后,先校验,再写入Flash,然后回复ACK。发送方收到ACK才发下一包。这样即使中间断了,也能断点续传。

我的习惯:在参数区保存一个“已接收包序号”。下次重启后,Bootloader先读这个序号,从断点处继续接收。这个功能在产线升级时特别有用,能省不少时间。

3.2 校验:别让坏固件跑起来

校验分两个层面:

  • 包校验:每包数据写入前,用CRC32或SHA256校验。我推荐CRC32,速度快,硬件支持也好。
  • 整体校验:全部写完后,对整个App区做一次哈希校验,确保固件完整。

如果整体校验失败,怎么办?绝对不能跳转!我见过有人直接跳转,结果App跑飞,设备变砖。正确的做法是:保留旧固件,重新进入升级模式。这就是“双备份”的思想。

3.3 跳转:最关键的一步

跳转不是简单的goto。你需要做几件事:

  1. 关闭所有中断:包括全局中断和所有外设中断。
  2. 禁用SysTick:如果用了RTOS,还要停止调度器。
  3. 重置中断向量表:把SCB->VTOR寄存器指向App的中断向量表起始地址。
  4. 设置栈指针:从App的中断向量表首地址读取栈顶指针,赋值给MSP。
  5. 跳转到复位向量:从App的中断向量表第二个字(偏移4字节)读取复位地址,然后跳转。

代码实现大概是这样的:

/* Bootloader跳转到App的示例代码 */
typedef void (*pFunction)(void);

void JumpToApp(uint32_t app_addr)
{
    uint32_t stack_ptr;
    pFunction reset_handler;
    
    /* 1. 关闭所有中断 */
    __disable_irq();
    
    /* 2. 从App向量表读取栈顶指针 */
    stack_ptr = *(volatile uint32_t*)app_addr;
    
    /* 3. 从App向量表读取复位向量 */
    reset_handler = (pFunction)(*(volatile uint32_t*)(app_addr + 4));
    
    /* 4. 设置主栈指针 */
    __set_MSP(stack_ptr);
    
    /* 5. 重映射中断向量表到App区 */
    SCB->VTOR = app_addr;
    
    /* 6. 跳转 */
    reset_handler();
    
    /* 理论上不会执行到这里 */
    while(1);
}

避坑指南:我曾经在跳转前忘了关一个定时器中断。结果跳转后,App还没初始化完,中断就来了,直接跑飞。排查了两天才找到原因。记住:跳转前,把所有外设都复位到默认状态。

4. 总结一下

这一章的内容,说白了就是三个关键词:分区、特性、流程。

  • 分区要清晰,Bootloader和App物理隔离。
  • Flash操作要小心,擦除是扇区级的,写入前必须擦除。
  • 流程要严谨,下载、校验、跳转,一步都不能省。