STM32 双Bank切换实现OTA零停机:基于Flash双区映射的实战踩坑记录

引言

在嵌入式系统开发中,OTA(Over-The-Air)升级是常见需求,但传统升级方式通常需要设备重启,导致服务中断。对于要求高可用性的工业设备,零停机升级至关重要。STM32系列(如F4、H7等)支持双Bank Flash架构,允许在运行一个Bank的同时擦写另一个Bank,并通过硬件切换实现无缝跳转。本文将分享基于双Bank切换的OTA实现方案,并总结实战中的关键问题。

双Bank原理与硬件支持

什么是双Bank Flash?

STM32的部分型号(如STM32F427、STM32H743)将内部Flash划分为两个独立的Bank(Bank1和Bank2),每个Bank可独立擦写。当系统从Bank1启动时,可以擦写Bank2;反之亦然。通过设置选项字节(Option Bytes)中的BOR_LEVnBOOT1位,或使用软件触发FLASH_CR寄存器中的SW_BANK位,可以切换启动Bank。

零停机实现机制

零停机的关键在于:

  • 当前运行在Bank1,升级程序写入Bank2;
  • 写入完成后,设置跳转标志,然后软复位;
  • Bootloader检测到标志,切换Bank启动新程序;
  • 新程序运行后,可擦写旧Bank,准备下一次升级。

整个过程仅在复位瞬间有短暂中断(毫秒级),但避免了长时间停机。

硬件与软件配置

硬件要求

  • STM32F4xx/H7xx系列,具有双Bank Flash;
  • 外部Flash(可选)用于存储升级包;
  • 串口或网络接口用于接收固件。

软件架构

  • Bootloader:负责启动和Bank切换;
  • App1:运行在Bank1;
  • App2:运行在Bank2。

关键寄存器与配置步骤

1. 使能双Bank模式

在STM32F4中,双Bank模式通过选项字节配置。使用ST-Link或代码设置:

// 解锁Flash选项字节
FLASH_OB_Unlock();
// 设置双Bank模式(以F427为例)
FLASH_OB_Program(OB_DUAL_BANK, OB_DUAL_BANK_ENABLE);
// 重新加载选项字节
FLASH_OB_Launch();

注意:修改选项字节会导致系统复位,因此需在Bootloader中执行。

2. 编写Bootloader

Bootloader负责检查升级标志和启动Bank。

// Bootloader main
void bootloader_main(void) {
    // 检查升级标志(存储在备份寄存器或Flash末尾)
    if (check_update_flag()) {
        // 切换Bank
        if (current_bank() == BANK1) {
            set_boot_bank(BANK2);
        } else {
            set_boot_bank(BANK1);
        }
    }
    // 跳转到App
    jump_to_app();
}

// 切换Bank函数
void set_boot_bank(uint8_t bank) {
    FLASH_OB_Unlock();
    if (bank == BANK2) {
        FLASH_OB_Program(OB_BOOT_BANK, OB_BOOT_BANK2);
    } else {
        FLASH_OB_Program(OB_BOOT_BANK, OB_BOOT_BANK1);
    }
    FLASH_OB_Launch();
}

3. 编写App升级逻辑

App运行期间,接收新固件并写入非活动Bank。

// App中升级函数
void ota_update(uint8_t *data, uint32_t len) {
    uint32_t target_bank = (current_bank() == BANK1) ? BANK2_BASE : BANK1_BASE;
    // 擦写目标Bank
    flash_erase(target_bank);
    // 写入数据
    flash_write(target_bank, data, len);
    // 设置升级标志
    set_update_flag();
    // 软复位
    NVIC_SystemReset();
}

完整代码示例

以下是一个简化的Bootloader和App协作示例。

Bootloader代码(main.c)

#include "stm32f4xx.h"

#define APP1_ADDR 0x08008000  // Bank1 App起始地址
#define APP2_ADDR 0x08088000  // Bank2 App起始地址(假设Bank1为1MB,Bank2为1MB)

void jump_to_app(uint32_t app_addr) {
    uint32_t app_stack = *(volatile uint32_t*)app_addr;
    void (*app_main)(void) = (void (*)(void))(*(volatile uint32_t*)(app_addr+4));
    __set_MSP(app_stack);
    app_main();
}

int main(void) {
    // 初始化时钟等
    // 检查升级标志(使用备份寄存器)
    if (RTC->BKP0R & 0x01) {
        // 清除标志
        RTC->BKP0R &= ~0x01;
        // 切换Bank
        if (FLASH->OPTR & (1<<31)) { // 当前从Bank2启动
            // 设置启动Bank1
            FLASH_OB_Unlock();
            FLASH->OPTR &= ~(1<<31);
            FLASH_OB_Launch();
        } else {
            FLASH_OB_Unlock();
            FLASH->OPTR |= (1<<31);
            FLASH_OB_Launch();
        }
    }
    // 判断当前启动Bank
    if (FLASH->OPTR & (1<<31)) {
        jump_to_app(APP2_ADDR);
    } else {
        jump_to_app(APP1_ADDR);
    }
    while(1);
}

App升级代码(ota.c)

#include "stm32f4xx.h"

#define BANK1_BASE 0x08000000
#define BANK2_BASE 0x08080000
#define FLASH_SIZE 0x100000  // 1MB

uint32_t current_bank(void) {
    if (FLASH->OPTR & (1<<31)) {
        return 2;
    } else {
        return 1;
    }
}

void flash_erase_bank(uint32_t bank_base) {
    FLASH_Unlock();
    FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPERR);
    uint32_t addr = bank_base;
    while (addr < bank_base + FLASH_SIZE/2) {
        if (FLASH_EraseSector(addr, VoltageRange_3) != FLASH_COMPLETE) {
            // 错误处理
        }
        addr += 0x20000; // 扇区大小,根据型号调整
    }
    FLASH_Lock();
}

void flash_write_bank(uint32_t bank_base, uint8_t *data, uint32_t len) {
    FLASH_Unlock();
    uint32_t addr = bank_base;
    uint32_t idx = 0;
    while (idx < len) {
        uint32_t word = 0;
        for (int i=0; i<4; i++) {
            if (idx < len) {
                word |= (data[idx] << (8*i));
                idx++;
            }
        }
        FLASH_ProgramWord(addr, word);
        addr += 4;
    }
    FLASH_Lock();
}

void ota_start(uint8_t *fw_data, uint32_t fw_len) {
    uint32_t target_bank = (current_bank() == 1) ? BANK2_BASE : BANK1_BASE;
    flash_erase_bank(target_bank);
    flash_write_bank(target_bank, fw_data, fw_len);
    // 设置升级标志
    RTC->BKP0R |= 0x01;
    NVIC_SystemReset();
}

实战踩坑记录

坑1:选项字节修改导致意外复位

修改FLASH_OB_Launch()会立即复位系统,如果未保存上下文,可能导致数据丢失。解决方案:在修改前将关键数据存入备份寄存器或Flash末尾。

坑2:Bank大小与扇区映射

不同型号的Bank大小和扇区布局不同。例如,STM32F427的Bank1为1MB,Bank2为1MB,但扇区大小不一(前4个扇区16KB,后续64KB)。擦写时必须按扇区对齐,否则会擦除错误区域。

坑3:中断向量表重定位

App编译时需设置VECT_TAB_OFFSET,并在启动时重定位中断向量表。

// App中
SCB->VTOR = APP1_ADDR; // 或APP2_ADDR

否则中断无法正确响应。

坑4:Flash写入时禁止中断

Flash编程期间,如果发生中断,可能导致写入失败或死锁。建议在写入前关闭中断,或使用DMA传输。

坑5:升级包校验

在写入前必须校验固件完整性(如CRC32),否则写入损坏数据会导致设备变砖。建议在Bootloader中增加校验步骤。

总结

双Bank切换技术为STM32提供了高效的OTA零停机方案,但实现时需注意选项字节配置、Bank映射、中断向量重定位等细节。通过合理的Bootloader设计,可以确保升级的可靠性和安全性。希望本文的实战经验能帮助开发者少走弯路。

参考资料

  • STM32F4xx参考手册(RM0090)
  • AN3969: STM32F4 in-application programming
  • AN4657: STM32 dual-bank bootloader