STM32双Bank切换实现OTA零停机:基于Flash双区映射的在线升级实战

1. 为什么需要双Bank?

传统OTA升级流程:下载固件到临时区 → 擦除主区 → 写入新固件 → 重启。此过程擦写期间应用无法运行,造成数秒甚至更长的停机。对于工业控制、医疗设备或边缘网关,这种中断不可接受。

STM32(如F4、L4、H7系列)支持双Bank Flash架构,将Flash划分为两个独立Bank(如Bank1和Bank2),每个Bank可独立擦写。通过硬件映射,CPU可从任一Bank启动,实现“后台下载+切换启动”的零停机升级。

2. 双Bank原理与映射机制

2.1 硬件架构

  • 以STM32F4为例,Flash容量1MB时,Bank1地址0x08000000~0x0807FFFF,Bank2地址0x08080000~0x080FFFFF。
  • 每个Bank拥有独立擦写控制器,可并行操作(部分型号支持)。
  • 启动映射由选项字节(Option Bytes)中的BOOT_SW位控制:置1时,CPU从Bank2启动,且Bank1映射到0x08000000,Bank2映射到0x08080000;置0时默认从Bank1启动。

2.2 切换流程

  1. 应用运行在Bank1,新固件下载到Bank2(通过Bootloader或应用自身)。
  2. 校验新固件CRC/签名。
  3. 设置BOOT_SW位,触发软复位。
  4. 复位后CPU从Bank2启动,应用无缝切换,停机时间仅复位周期(微秒级)。

3. 工程配置步骤

3.1 链接脚本调整

将Flash划分为两个区域,分别对应Bank1和Bank2。以STM32F407VG(1MB Flash)为例,修改链接脚本(.ld):

MEMORY
{
  FLASH_BANK1 (rx) : ORIGIN = 0x08000000, LENGTH = 512K
  FLASH_BANK2 (rx) : ORIGIN = 0x08080000, LENGTH = 512K
  RAM (xrw)       : ORIGIN = 0x20000000, LENGTH = 128K
}

编译时,为Bank1和Bank2分别生成固件,或使用同一固件通过宏控制起始地址。

3.2 选项字节设置

使用STM32CubeProgrammer或代码设置BOOT_SW位。代码方式:

// 解锁选项字节
FLASH_OB_Unlock();
// 设置BOOT_SW为1(从Bank2启动)
FLASH_OB_UserConfig(OB_BOOT_SW_SET);
// 保存选项字节
FLASH_OB_Launch();

注意:设置后需复位生效,且选项字节写入会触发一次Flash擦写,但时间极短(毫秒级)。

4. 完整代码示例

以下示例实现:应用运行在Bank1,接收新固件写入Bank2,校验后切换。

4.1 固件写入Bank2

#define BANK2_BASE 0x08080000
#define PAGE_SIZE  2048  // 根据芯片调整

void Write_Firmware_To_Bank2(uint32_t *data, uint32_t size)
{
    uint32_t addr = BANK2_BASE;
    FLASH_Unlock();
    
    // 擦除Bank2(按页擦除)
    for (uint32_t offset = 0; offset < size; offset += PAGE_SIZE) {
        FLASH_ErasePage(addr + offset);
    }
    
    // 写入数据
    for (uint32_t i = 0; i < size / 4; i++) {
        FLASH_ProgramWord(addr + i * 4, data[i]);
    }
    
    FLASH_Lock();
}

4.2 校验固件完整性

uint8_t Verify_Firmware_CRC(uint32_t expected_crc)
{
    uint32_t crc = 0;
    // 使用硬件CRC或软件CRC计算Bank2区域
    crc = HAL_CRC_Calculate(&hcrc, (uint32_t *)BANK2_BASE, FIRMWARE_SIZE / 4);
    return (crc == expected_crc) ? 1 : 0;
}

4.3 切换Bank

void Switch_To_Bank2(void)
{
    // 设置BOOT_SW位
    FLASH_OB_Unlock();
    FLASH_OB_UserConfig(OB_BOOT_SW_SET);
    FLASH_OB_Launch();
    
    // 软复位
    NVIC_SystemReset();
}

4.4 主流程

int main(void)
{
    // 初始化...
    
    // 检查是否有新固件待安装
    if (New_Firmware_Available()) {
        // 接收固件到RAM缓冲区(或直接流式写入)
        Receive_Firmware();
        
        // 写入Bank2
        Write_Firmware_To_Bank2(fw_buffer, fw_size);
        
        // 校验
        if (Verify_Firmware_CRC(expected_crc)) {
            Switch_To_Bank2();
        } else {
            // 校验失败,继续运行旧固件
        }
    }
    
    // 正常运行应用...
    while (1) {
        // 应用逻辑
    }
}

5. 注意事项与优化建议

  • 地址映射:切换后,Bank2映射到0x08000000,但中断向量表仍指向原地址。需在启动文件或SystemInit中重设VTOR(向量表偏移寄存器)。
    SCB->VTOR = 0x08000000; // 切换后Bank2基址
    
  • 固件大小:确保新固件不超过Bank容量(本例512KB)。
  • 擦写时间:虽然双Bank可并行,但写入Bank2时应用仍运行,需确保Flash控制器不冲突(部分型号支持同时读写不同Bank)。
  • 回滚机制:若新固件启动失败,可设置看门狗或标志位,在Bootloader中回滚到旧Bank。
  • 选项字节保护:写入选项字节前需解锁,且操作后重新锁定,防止误改。
  • 性能考量:双Bank切换仅复位,但若应用有状态需保存,需在切换前持久化关键数据。

6. 总结

利用STM32双Bank架构,OTA升级从“停机数秒”优化为“零停机”,极大提升设备可用性。本文从原理到代码,展示了完整实现路径。实际项目中,还需结合安全启动、加密校验等机制,打造更健壮的升级系统。希望本文能为你提供实战参考,助你在嵌入式领域更上一层楼。