STM32 双 Bank 切换实现 OTA 零停机:基于 F4/F7 的 Flash 重映射细节

1. 为什么需要双 Bank?

传统 OTA 方案通常采用 Bootloader + App 模式,升级时需擦写 App 区,期间系统必须停机。对于要求 7x24 小时连续运行的系统(如电力监控、医疗设备),这是不可接受的。STM32 F4/F7 系列(如 STM32F427、STM32F746)提供双 Bank Flash,每个 Bank 容量相等,可独立擦写。通过硬件重映射,CPU 可瞬间从 Bank1 跳转到 Bank2,无需复位,实现零停机升级。

2. 双 Bank 与 Flash 重映射原理

2.1 内存映射基础

以 STM32F746 为例,Flash 总容量 1MB,分为 Bank1(0x08000000-0x0807FFFF)和 Bank2(0x08080000-0x080FFFFF)。默认情况下,CPU 从 Bank1 启动。当启用双 Bank 模式并设置重映射位后,Bank2 的地址会被映射到 0x08000000,而 Bank1 则被映射到 0x08080000。

2.2 关键寄存器

  • FLASH_CR (Flash Control Register):位 31 (DBANK) 用于选择单 Bank 或双 Bank 模式(仅部分型号支持,需在空片时设置)。
  • FLASH_CR 位 30 (SWAP_BANK):当置 1 时,交换 Bank1 和 Bank2 的映射地址。
  • FLASH_SR (Status Register):位 15 (BSY) 表示 Flash 忙,切换前必须等待。

2.3 切换流程

  1. 确保当前运行在 Bank1(或 Bank2),且目标 Bank 已写入新固件。
  2. 设置 FLASH_CR 的 SWAP_BANK 位。
  3. 等待 BSY 位清零。
  4. 硬件自动重映射,PC 指针无需改动,但需注意中断向量表位置。

3. 工程配置步骤

3.1 链接脚本调整

需为两个 Bank 分别生成固件,或生成一个包含双 Bank 的镜像。以 STM32F746 为例,修改 .ld 文件:

/* Bank1 应用 */
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
/* Bank2 应用 */
/* FLASH (rx) : ORIGIN = 0x08080000, LENGTH = 512K */

编译两个版本,分别烧录到对应 Bank。

3.2 中断向量表重定位

切换后,中断向量表仍指向旧地址,必须重定位。在系统初始化时,设置 SCB->VTOR:

#define APP_ADDRESS_BANK1 0x08000000
#define APP_ADDRESS_BANK2 0x08080000

void relocate_vector_table(uint32_t app_address) {
    SCB->VTOR = app_address;
    __DSB();
    __ISB();
}

4. 完整代码示例(基于 HAL 库)

4.1 双 Bank 切换函数

#include "stm32f7xx_hal.h"

#define FLASH_BANK1_BASE 0x08000000
#define FLASH_BANK2_BASE 0x08080000

/**
 * @brief 切换当前执行 Bank
 * @param target_bank: 1 或 2
 * @retval HAL_StatusTypeDef
 */
HAL_StatusTypeDef Flash_SwitchBank(uint8_t target_bank) {
    uint32_t current_bank;
    uint32_t primask;

    // 判断当前运行 Bank
    if ((SCB->VTOR & 0x000FFFFF) < 0x00080000) {
        current_bank = 1;
    } else {
        current_bank = 2;
    }

    if (current_bank == target_bank) {
        return HAL_OK; // 无需切换
    }

    // 关闭全局中断,防止切换期间中断触发
    primask = __get_PRIMASK();
    __disable_irq();

    // 等待 Flash 空闲
    while (FLASH->SR & FLASH_SR_BSY);

    // 设置 SWAP_BANK 位
    FLASH->CR |= FLASH_CR_SWAP_BANK;

    // 等待切换完成
    while (FLASH->SR & FLASH_SR_BSY);

    // 重定位中断向量表到新 Bank
    if (target_bank == 1) {
        SCB->VTOR = FLASH_BANK1_BASE;
    } else {
        SCB->VTOR = FLASH_BANK2_BASE;
    }
    __DSB();
    __ISB();

    // 恢复中断
    __set_PRIMASK(primask);

    return HAL_OK;
}

4.2 固件写入函数(简化)

HAL_StatusTypeDef Flash_WriteFirmware(uint32_t dest_addr, uint8_t *data, uint32_t len) {
    uint32_t i = 0;
    uint32_t word;

    // 解锁 Flash
    HAL_FLASH_Unlock();

    // 擦除目标 Bank(示例擦除整个 Bank2)
    FLASH_EraseInitTypeDef erase;
    erase.TypeErase = FLASH_TYPEERASE_SECTORS;
    erase.Banks = FLASH_BANK_2;
    erase.Sector = 0;
    erase.NbSectors = 8; // 根据实际扇区数调整
    erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
    uint32_t error;
    HAL_FLASHEx_Erase(&erase, &error);

    // 写入数据(按 32 位字)
    for (i = 0; i < len; i += 4) {
        word = (data[i] | (data[i+1] << 8) | (data[i+2] << 16) | (data[i+3] << 24));
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, dest_addr + i, word);
    }

    HAL_FLASH_Lock();
    return HAL_OK;
}

4.3 主程序示例

int main(void) {
    HAL_Init();
    SystemClock_Config();

    // 假设当前运行在 Bank1,新固件已下载到 RAM 或外部存储
    uint8_t new_firmware[1024]; // 示例数据

    // 1. 将新固件写入 Bank2
    Flash_WriteFirmware(FLASH_BANK2_BASE, new_firmware, sizeof(new_firmware));

    // 2. 验证固件(CRC 等)
    // ...

    // 3. 切换 Bank
    Flash_SwitchBank(2);

    // 4. 切换后,代码继续执行,但实际已运行在 Bank2 的固件中
    while (1) {
        // 应用逻辑
    }
}

5. 注意事项与陷阱

  • 双 Bank 模式启用:部分型号(如 STM32F4 早期版本)默认单 Bank,需在空片时通过选项字节设置 DBANK=1,否则无法使用双 Bank。
  • 中断安全:切换 Bank 期间必须关闭全局中断,否则中断服务函数可能访问旧向量表导致 HardFault。
  • Flash 擦写时间:擦写 Bank 时系统仍可运行(从另一 Bank 执行),但需确保代码不在被擦写的 Bank 中。
  • 固件验证:切换前务必验证新固件完整性(CRC32 或 SHA256),避免变砖。
  • 看门狗:若使用独立看门狗,需在擦写过程中喂狗,否则系统复位。
  • 编译器优化:链接脚本中需将只读数据(如字符串)放在当前 Bank,否则切换后可能访问错误地址。

6. 总结

双 Bank 切换技术利用 STM32 硬件重映射,实现了 OTA 升级的零停机。关键在于理解 Flash 映射机制、正确配置寄存器,并妥善处理中断向量表和固件验证。本文提供的代码可直接应用于 F4/F7 系列,为你的产品赋予无缝升级能力。