STM32 双Bank切换实现OTA零停机:基于F4/F7的Bank交错映射实战

1. 双Bank架构原理

STM32 F4/F7系列(如STM32F427、STM32F746)的Flash被划分为两个独立的Bank,每个Bank大小相等(例如1MB Flash分为两个512KB Bank)。这种设计允许在运行一个Bank时,对另一个Bank进行擦写操作,从而实现OTA升级不中断当前应用。

关键特性:

  • Bank交错映射:通过配置选项字节(Option Bytes)中的BOR_LEV和DBANK位,可以切换Flash的映射方式。默认情况下,Flash从地址0x08000000开始连续映射;启用双Bank后,地址空间被交错映射,使得两个Bank的起始地址均为0x08000000,但通过高位地址选择Bank。
  • 无缝切换:升级完成后,通过软件复位或跳转指令,将PC指针切换到新Bank的复位向量,实现快速切换,无需外部干预。

2. 硬件与软件准备

  • 硬件:STM32F4/F7开发板(如Nucleo-F746ZG),外部Flash可选(用于备份固件)。
  • 软件:STM32CubeIDE(或Keil MDK),STM32 HAL库。

3. 配置步骤

3.1 启用双Bank模式

  1. 在STM32CubeMX中,选择目标芯片,进入Option Bytes配置。
  2. 设置DBANK位为1(双Bank模式),并配置BOR_LEV为合适的电平(如3.3V)。
  3. 生成初始化代码。

3.2 链接脚本调整

修改链接脚本(.ld文件),将Flash区域划分为两个Bank:

/* 假设Flash总大小1MB,每个Bank 512KB */
MEMORY
{
  FLASH_BANK0 (rx) : ORIGIN = 0x08000000, LENGTH = 512K
  FLASH_BANK1 (rx) : ORIGIN = 0x08080000, LENGTH = 512K
  RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 256K
}

3.3 编写双Bank切换代码

#include "stm32f7xx_hal.h"

// 定义Bank地址
#define BANK0_START 0x08000000
#define BANK1_START 0x08080000

// 当前运行Bank标志
extern uint32_t _estack; // 栈顶地址

void JumpToBank(uint32_t bank_addr) {
    // 检查栈顶地址是否合法
    if ((*(volatile uint32_t*)bank_addr & 0x2FFE0000) != 0x20000000) {
        return;
    }
    
    // 设置主栈指针
    __set_MSP(*(volatile uint32_t*)bank_addr);
    
    // 获取复位向量地址
    uint32_t reset_vector = *(volatile uint32_t*)(bank_addr + 4);
    
    // 跳转
    void (*reset_handler)(void) = (void (*)(void))reset_vector;
    reset_handler();
}

void OTA_Update(void) {
    // 假设新固件已写入非活动Bank(例如当前运行Bank0,则写入Bank1)
    uint32_t target_bank = (GetCurrentBank() == BANK0_START) ? BANK1_START : BANK0_START;
    
    // 擦除目标Bank(示例:擦除Bank1)
    FLASH_EraseInitTypeDef erase_init;
    erase_init.TypeErase = FLASH_TYPEERASE_SECTORS;
    erase_init.Banks = FLASH_BANK_1;
    erase_init.Sector = 0;
    erase_init.NbSectors = 8; // 根据实际扇区数
    erase_init.VoltageRange = FLASH_VOLTAGE_RANGE_3;
    uint32_t sector_error = 0;
    HAL_FLASHEx_Erase(&erase_init, &sector_error);
    
    // 写入固件(此处省略具体写入逻辑,通常从外部存储或网络读取)
    // WriteFirmwareToBank(target_bank);
    
    // 设置标志位,表示新固件就绪
    SetUpdateFlag(target_bank);
    
    // 软件复位
    NVIC_SystemReset();
}

uint32_t GetCurrentBank(void) {
    // 通过PC指针判断当前运行Bank
    uint32_t pc = (uint32_t)GetPC();
    if (pc >= BANK0_START && pc < BANK0_START + 0x80000) {
        return BANK0_START;
    } else {
        return BANK1_START;
    }
}

// 获取PC指针(内联汇编)
__attribute__((always_inline)) static inline uint32_t GetPC(void) {
    uint32_t pc;
    __asm volatile ("mov %0, pc" : "=r" (pc));
    return pc;
}

3.4 启动代码修改

在系统启动时,检查更新标志,决定跳转到哪个Bank:

void SystemInit(void) {
    // ... 其他初始化
    
    if (CheckUpdateFlag() == BANK1_START) {
        JumpToBank(BANK1_START);
    }
    // 否则继续运行Bank0
}

4. 完整示例:双Bank升级流程

以下是一个简化的升级流程示例,展示如何从Bank0切换到Bank1:

int main(void) {
    HAL_Init();
    SystemClock_Config();
    
    // 检查是否有待升级固件
    if (CheckUpdateFlag() == BANK1_START) {
        // 验证固件完整性(如CRC)
        if (VerifyFirmware(BANK1_START)) {
            // 清除标志
            ClearUpdateFlag();
            // 跳转到Bank1
            JumpToBank(BANK1_START);
        } else {
            // 固件无效,回滚到Bank0
            JumpToBank(BANK0_START);
        }
    }
    
    // 正常运行当前Bank应用
    while (1) {
        // 应用逻辑
    }
}

5. 注意事项

  • 地址映射:启用双Bank后,Flash地址映射发生变化,务必确保链接脚本和代码中的地址正确。
  • 中断向量表:每个Bank的应用必须有自己的中断向量表,并在启动时重定位到对应Bank的起始地址。
  • Flash擦写时间:虽然双Bank允许并行操作,但擦写期间CPU仍可能被阻塞(取决于Flash接口),建议使用中断或DMA优化。
  • 固件验证:切换前必须验证新固件的完整性(如CRC、签名),避免启动失败。
  • 回滚机制:设计回滚逻辑,若新固件启动失败,自动回退到旧Bank。
  • 电源稳定性:升级过程中确保电源稳定,防止Flash写入失败。

6. 总结

通过STM32双Bank架构,我们实现了OTA升级的零停机切换。核心在于利用Bank交错映射,使两个应用独立运行,并通过跳转实现快速切换。本文提供了原理、配置、代码和注意事项,为高可靠性嵌入式系统提供了实用方案。实际项目中,还需结合具体硬件和需求优化细节。