STM32双Bank Flash在OTA升级中的零停机回滚实践

一、为什么需要双Bank?OTA的痛点

传统单Bank OTA流程:Bootloader擦除App区→写入新固件→跳转。这期间设备处于不可用状态,且一旦写入中途断电或校验失败,设备变砖。双Bank Flash将存储区分为Bank0和Bank1,两个Bank可独立擦写,且支持硬件级Bank切换——即CPU可以从一个Bank的App直接跳转到另一个Bank的App,无需搬移代码。

核心优势:

  • 升级时,新固件写入非活动Bank,旧固件继续运行,实现零停机。
  • 切换前通过软件标志确认新固件完整性,失败则回滚到旧Bank。
  • 硬件自动映射地址,无需修改链接脚本中的运行地址。

二、硬件原理:STM32双Bank架构解析

以STM32H743为例(2MB Flash,双Bank各1MB),关键寄存器:

  • FLASH_CR 的 BANK_SW 位:置1时,系统将Bank0和Bank1的地址映射互换(即Bank0映射到0x08000000,Bank1映射到0x08100000,反之亦然)。
  • FLASH_SR 的 BANK_BUSY 位:指示当前Bank切换是否完成。

地址映射规则:

  • 默认:Bank0 → 0x08000000,Bank1 → 0x08100000。
  • 切换后:Bank1 → 0x08000000,Bank0 → 0x08100000。
  • 因此,App代码必须链接在0x08000000(即当前活动Bank的起始地址),而Bootloader固定运行在0x08000000(但Bootloader本身不参与切换,它始终位于系统Flash起始区域,通常占用前64KB)。

关键点:

  • 双Bank切换是硬件行为,CPU的PC指针会立即映射到新地址,但需要确保切换时无中断冲突。
  • 擦写非活动Bank时,活动Bank的代码执行不受影响(但需注意Flash接口带宽,避免同时读写冲突)。

三、工程架构设计

3.1 内存布局

// Bootloader (固定0x08000000, 64KB)
#define BOOT_START     0x08000000
#define BOOT_SIZE      0x10000  // 64KB

// App区域:双Bank各占剩余空间
#define BANK0_START    0x08010000  // 1MB - 64KB = 960KB
#define BANK1_START    0x08110000  // 对应Bank1的起始
#define APP_MAX_SIZE   0xF0000     // 每个Bank最大固件大小

3.2 软件状态标志

在Flash的最后一个扇区(或系统存储区)保存升级状态:

typedef enum {
    APP_VALID = 0xA5A5A5A5,
    APP_INVALID = 0x5A5A5A5A,
    APP_NEW_READY = 0x12345678  // 新固件已写入,待切换
} AppState_t;

// 状态存储地址(使用独立扇区,避免与固件冲突)
#define STATE_ADDR 0x080FF000  // 假设最后一个扇区

3.3 双Bank切换流程

Bootloader启动 → 检查状态标志
  ├─ APP_VALID → 跳转到当前活动Bank(由BANK_SW决定)
  ├─ APP_NEW_READY → 执行Bank切换(置BANK_SW位)→ 验证新固件CRC → 成功则置APP_VALID,失败则回滚(清除BANK_SW)
  └─ APP_INVALID → 进入固件下载模式(如UART/USB)

四、核心代码实现

4.1 Flash操作驱动(基于HAL库)

// 解锁Flash并擦写指定Bank
HAL_StatusTypeDef Flash_WriteBank(uint32_t bank_addr, uint8_t *data, uint32_t len) {
    HAL_StatusTypeDef status;
    uint32_t addr = bank_addr;
    uint32_t remaining = len;
    
    // 解锁Flash
    HAL_FLASH_Unlock();
    
    // 擦除整个Bank(或按扇区擦除,这里简化)
    FLASH_EraseInitTypeDef erase;
    erase.TypeErase = FLASH_TYPEERASE_SECTORS;
    erase.Banks = (bank_addr == BANK0_START) ? FLASH_BANK_1 : FLASH_BANK_2;
    erase.Sector = 0;
    erase.NbSectors = 24;  // 根据实际扇区数调整
    erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
    uint32_t sector_error = 0;
    status = HAL_FLASHEx_Erase(&erase, &sector_error);
    if (status != HAL_OK) return status;
    
    // 编程数据(按双字写入)
    uint64_t temp;
    for (uint32_t i = 0; i < remaining; i += 8) {
        memcpy(&temp, data + i, 8);
        status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr + i, temp);
        if (status != HAL_OK) break;
    }
    
    HAL_FLASH_Lock();
    return status;
}

4.2 Bank切换与回滚

// 切换活动Bank
void Bank_Switch(void) {
    // 等待当前操作完成
    while (__HAL_FLASH_GET_FLAG(FLASH_FLAG_BANK_BUSY));
    
    // 设置BANK_SW位
    FLASH->CR |= FLASH_CR_BANK_SW;
    
    // 等待切换完成
    while (__HAL_FLASH_GET_FLAG(FLASH_FLAG_BANK_BUSY));
}

// 回滚:恢复原Bank
void Bank_Rollback(void) {
    // 清除BANK_SW位
    FLASH->CR &= ~FLASH_CR_BANK_SW;
    while (__HAL_FLASH_GET_FLAG(FLASH_FLAG_BANK_BUSY));
}

// 验证固件CRC(假设固件末尾存储CRC32)
uint32_t Verify_Firmware(uint32_t bank_addr, uint32_t size) {
    uint32_t crc = 0;
    // 计算CRC... 这里省略具体实现
    // 读取固件末尾的存储CRC并比较
    return (crc == stored_crc) ? 1 : 0;
}

4.3 Bootloader主逻辑

void Bootloader_Main(void) {
    AppState_t state = Read_State();
    
    if (state == APP_NEW_READY) {
        // 尝试切换Bank
        Bank_Switch();
        // 验证新固件(现在活动Bank是新固件)
        if (Verify_Firmware(0x08000000, APP_MAX_SIZE)) {
            Write_State(APP_VALID);
            Jump_To_App();
        } else {
            // 回滚
            Bank_Rollback();
            Write_State(APP_VALID);  // 旧固件仍有效
            Jump_To_App();
        }
    } else if (state == APP_VALID) {
        Jump_To_App();
    } else {
        // 进入下载模式,等待接收新固件
        Download_Firmware();
    }
}

// 跳转到App(设置向量表偏移)
void Jump_To_App(void) {
    uint32_t app_addr = 0x08000000;
    // 检查栈顶地址合法性
    if (((*(volatile uint32_t*)app_addr) & 0x2FFE0000) == 0x20000000) {
        // 设置主栈指针
        __set_MSP(*(volatile uint32_t*)app_addr);
        // 跳转
        void (*app_reset)(void) = (void (*)(void))(*(volatile uint32_t*)(app_addr + 4));
        app_reset();
    }
}

4.4 App端升级触发

// App中收到新固件包,写入非活动Bank
void OTA_Update(uint8_t *new_fw, uint32_t len) {
    // 判断当前活动Bank
    uint32_t inactive_bank = (FLASH->CR & FLASH_CR_BANK_SW) ? BANK0_START : BANK1_START;
    
    // 写入新固件
    Flash_WriteBank(inactive_bank, new_fw, len);
    
    // 设置状态为待切换
    Write_State(APP_NEW_READY);
    
    // 软复位,进入Bootloader执行切换
    NVIC_SystemReset();
}

五、注意事项与工程陷阱

  • 中断向量表:App的向量表必须重定位到0x08000000(活动Bank起始),在App启动代码中设置SCB->VTOR = 0x08000000。
  • Flash擦写时间:双Bank擦写时,活动Bank的代码执行可能因Flash接口竞争而变慢,需优化时序或使用缓存。
  • 掉电保护:在写入状态标志时,建议使用双字写入并加校验,防止半写状态。
  • Bank切换的原子性:切换过程不可中断,需关闭全局中断或使用临界区。
  • 链接脚本:Bootloader和App的链接脚本需严格区分地址,App的FLASH起始地址固定为0x08000000,但实际物理Bank由硬件决定。
  • 调试技巧:使用BANK_SW位时,在调试器中查看FLASH->CR值确认当前Bank状态。

六、总结

双Bank Flash方案将OTA升级的“停机窗口”压缩到微秒级(仅Bank切换瞬间),并通过硬件映射简化了固件管理。本文提供的工程代码可直接移植到STM32H7/L4+系列,配合CRC校验和状态机,可构建工业级可靠的远程升级系统。实际产品中,还需考虑固件签名、加密传输等安全措施,但双Bank机制本身已为高可用性打下坚实基础。