STM32 双Bank切换实现OTA零停机:原理与边界条件分析

一、为什么需要双Bank?

传统OTA升级流程通常包括:下载固件 → 擦除旧应用 → 写入新应用 → 跳转重启。其中擦除和写入阶段,系统必须停止运行,造成数秒甚至更长的停机。对于需要7x24小时运行的设备(如电力监控、医疗仪器),这种停机不可接受。

STM32的双Bank Flash(如F7、H7系列)将内部Flash划分为两个独立的Bank(Bank1和Bank2),每个Bank可独立擦写。升级时,系统在Bank1运行,同时擦写Bank2;完成后通过切换启动地址,重启后直接运行Bank2的新固件,整个过程无需停机等待Flash操作。

二、双Bank切换的硬件原理

2.1 Flash组织架构

以STM32H743为例,其Flash总容量2MB,分为两个Bank:

  • Bank1:地址0x08000000 ~ 0x080FFFFF(1MB)
  • Bank2:地址0x08100000 ~ 0x081FFFFF(1MB)

每个Bank内部又分为多个扇区(Sector),支持独立擦写。关键寄存器:

  • FLASH_CR:控制Flash操作,其中BANK_SW位用于切换启动Bank
  • FLASH_OPTCR:配置选项字节,决定系统从哪个Bank启动

2.2 切换机制

系统复位后,CPU从0x08000000(Bank1)或0x08100000(Bank2)取指,具体取决于选项字节nBOOT1BANK_SW位的组合。切换流程:

  1. 在Bank1运行,将新固件写入Bank2
  2. 设置FLASH_CRBANK_SW位为1
  3. 触发软件复位(NVIC_SystemReset()
  4. 复位后,CPU从Bank2启动,运行新固件

注意:切换是原子性的,复位后立即生效,无需外部干预。

三、软件配置步骤

3.1 链接脚本调整

需要为两个Bank分别创建链接脚本,确保固件链接到正确的地址。以GCC为例,Bank1的FLASH起始地址为0x08000000,Bank2为0x08100000

/* Bank1.ld */
MEMORY
{
  FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1M
  RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 512K
}

/* Bank2.ld */
MEMORY
{
  FLASH (rx) : ORIGIN = 0x08100000, LENGTH = 1M
  RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 512K
}

3.2 中断向量表重定位

每个Bank的固件必须将中断向量表重定位到自己的起始地址。在SystemInit()main()开头执行:

void vector_relocate(void)
{
    // 获取当前运行的Bank基地址
    uint32_t app_addr = (FLASH_BASE & 0x1FFFFF) ? 0x08100000 : 0x08000000;
    // 设置VTOR寄存器
    SCB->VTOR = app_addr;
    __DSB();
    __ISB();
}

3.3 双Bank写入流程

以下代码演示在Bank1运行时,将新固件写入Bank2:

void ota_write_bank2(uint8_t *data, uint32_t len, uint32_t offset)
{
    // 解锁Flash
    HAL_FLASH_Unlock();
    
    // 擦除Bank2的扇区(假设从扇区0开始)
    FLASH_EraseInitTypeDef erase;
    erase.TypeErase = FLASH_TYPEERASE_SECTORS;
    erase.Banks = FLASH_BANK_2;
    erase.Sector = FLASH_SECTOR_0;
    erase.NbSectors = 8;
    erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
    uint32_t error = 0;
    HAL_FLASHEx_Erase(&erase, &error);
    
    // 写入数据到Bank2地址
    uint32_t addr = 0x08100000 + offset;
    for (uint32_t i = 0; i < len; i += 8)
    {
        uint64_t val = *(uint64_t*)(data + i);
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr + i, val);
    }
    
    HAL_FLASH_Lock();
}

3.4 切换Bank并复位

void switch_to_bank2(void)
{
    // 设置BANK_SW位
    FLASH->CR |= FLASH_CR_BANK_SW;
    // 等待就绪
    while (FLASH->SR & FLASH_SR_BSY);
    // 软件复位
    NVIC_SystemReset();
}

四、边界条件与注意事项

4.1 中断向量表必须重定位

如果忽略VTOR设置,中断将跳转到旧Bank的向量表,导致程序崩溃。务必在启动早期完成重定位,且确保两个固件的中断处理函数地址正确。

4.2 Flash操作时序约束

  • 擦写期间,CPU不能从同一Bank取指,否则会触发总线错误。因此,擦写Bank2时,代码必须运行在Bank1或RAM中。
  • 建议将Flash操作函数放在RAM中执行(__attribute__((section(".ramfunc")))),避免取指冲突。

4.3 固件版本校验

切换前必须验证新固件的完整性(如CRC32、SHA256)。否则,损坏的固件可能导致设备变砖。建议在写入完成后、切换前进行校验。

bool verify_firmware(uint32_t bank_addr, uint32_t size, uint32_t crc_expected)
{
    uint32_t crc = HAL_CRC_Calculate(&hcrc, (uint32_t*)bank_addr, size / 4);
    return (crc == crc_expected);
}

4.4 回滚机制

若新固件启动失败(如看门狗超时),应能回滚到旧Bank。实现方式:

  • 在固件头部存储状态标志(如0xA5表示有效)
  • 启动时检查标志,若无效则切换回另一Bank
void check_and_rollback(void)
{
    uint32_t *app_header = (uint32_t*)APP_ADDR;
    if (app_header[0] != 0xA5A5A5A5)
    {
        // 切换回另一Bank
        FLASH->CR ^= FLASH_CR_BANK_SW;
        NVIC_SystemReset();
    }
}

4.5 双Bank容量限制

双Bank模式将Flash容量减半,仅适用于固件大小不超过单个Bank的场合。对于大固件,需考虑压缩或差分升级。

五、总结

STM32双Bank切换通过硬件级Bank隔离和原子切换,实现了OTA零停机升级。关键在于:正确配置链接脚本、重定位中断向量表、处理Flash时序约束,并设计可靠的校验与回滚机制。掌握这些边界条件,你就能构建工业级可靠的OTA系统。

实际项目中,建议结合RTOS和文件系统,实现固件下载、存储、校验和切换的流水线化,进一步提升升级效率和安全性。