STM32双Bank Flash在线升级失败后利用BANK2自举恢复的实战方法

1. 为什么需要双Bank自举恢复?

在线升级(OTA)在嵌入式设备中越来越普遍,但升级失败(如断电、通信错误、固件校验失败)可能导致设备无法启动,即“变砖”。传统方案是使用外部看门狗或Bootloader+App双区,但STM32的双Bank Flash提供了更优雅的硬件级解决方案:将Flash分为两个独立的Bank,每个Bank可独立擦写和启动。当Bank1中的App升级失败时,系统可以自动从Bank2启动,运行一个安全的备份固件,从而恢复设备。

2. 双Bank Flash原理

STM32系列(如F4、L4、H7等)的部分型号支持双Bank Flash。以STM32F4为例,Flash容量为1MB时,分为Bank1(地址0x08000000-0x0807FFFF)和Bank2(地址0x08080000-0x080FFFFF)。每个Bank有独立的擦除/编程操作,且可以通过选项字节配置启动模式。

关键点:

  • 启动引脚BOOT0/BOOT1:决定芯片从主Flash、系统存储器还是SRAM启动。但双Bank自举不是通过引脚,而是通过选项字节中的nBOOT1位Flash控制寄存器
  • 自举机制:当设置选项字节BOOT_CFG为双Bank模式,且检测到Bank1的启动地址无效(如全0xFF或校验失败),硬件会自动从Bank2启动。
  • 软件触发:也可以通过软件设置FLASH_CRBOR_LEV或调用NVIC_SystemReset()配合选项字节实现。

3. 系统架构设计

  • Bank1 (0x08000000):存放主应用程序(App1),支持在线升级。
  • Bank2 (0x08080000):存放备份固件(App2),功能精简,仅包含基础通信和恢复逻辑(如恢复出厂设置、重新升级)。
  • Bootloader:可放在Bank1起始区域或独立区域,但为了简化,我们假设Bootloader位于Bank1的0x08000000-0x0800FFFF,App1从0x08010000开始。

升级流程:

  1. 设备运行App1,接收新固件,写入Bank1的App区域。
  2. 写入完成后,校验CRC或SHA。
  3. 若校验失败或写入过程中断电,复位后硬件检测到Bank1的App无效,自动从Bank2启动App2。
  4. App2运行后,可通过通信接口提示用户升级失败,并等待重新升级。

4. 配置步骤

4.1 启用双Bank模式

在STM32CubeMX或代码中,通过修改选项字节启用双Bank。以STM32F4为例,使用HAL库:

void Enable_DualBank(void)
{
    FLASH_OBProgramInitTypeDef pOBInit;
    HAL_FLASH_Unlock();
    HAL_FLASH_OB_Unlock();
    
    pOBInit.OptionType = OPTIONBYTE_BANK;
    pOBInit.Banks = FLASH_BANK_1;
    pOBInit.BankConfig = FLASH_BANK_1_2; // 双Bank模式
    HAL_FLASH_OBProgram(&pOBInit);
    
    HAL_FLASH_OB_Lock();
    HAL_FLASH_Lock();
    // 需要重新上电或系统复位生效
    NVIC_SystemReset();
}

4.2 设置启动地址和自举配置

在双Bank模式下,需要设置选项字节中的BOOT_CFG0b10(从Bank2启动当Bank1无效)。实际上,当启用双Bank后,硬件自动根据Bank1的复位向量有效性决定启动Bank。但为了确保,我们可以在代码中检查:

uint32_t CheckBank1Valid(void)
{
    // 检查Bank1的复位向量是否有效(栈顶地址在RAM范围,复位向量在Flash范围)
    uint32_t stack_addr = *(volatile uint32_t*)0x08010000;
    uint32_t reset_vector = *(volatile uint32_t*)0x08010004;
    if ((stack_addr & 0xFFF00000) != 0x20000000) return 0; // 栈顶不在RAM
    if ((reset_vector & 0xFFF00000) != 0x08000000) return 0; // 复位向量不在Flash
    return 1;
}

4.3 编写Bootloader(可选)

如果Bootloader位于Bank1起始,它需要负责跳转到App1或App2。但双Bank自举是硬件行为,Bootloader可以简化。实际上,当Bank1无效时,硬件直接执行Bank2的复位向量,无需Bootloader干预。但为了灵活,我们可以在App2中实现恢复逻辑。

5. 完整代码示例

以下代码演示如何在App1中实现升级失败检测并触发自举恢复。

// main.c (App1)
#include "stm32f4xx_hal.h"

// 定义Bank1 App起始地址和大小
#define APP1_ADDR 0x08010000
#define APP1_SIZE 0x70000 // 448KB,留空间给Bootloader

// 定义升级标志(存储在备份寄存器或Flash末尾)
#define UPGRADE_FLAG_ADDR 0x0807FF00

void Check_Upgrade_Status(void)
{
    uint32_t flag = *(volatile uint32_t*)UPGRADE_FLAG_ADDR;
    if (flag == 0xDEADBEEF) // 升级进行中标志
    {
        // 检查App1是否有效
        if (!CheckBank1Valid())
        {
            // 升级失败,清除标志,复位后硬件将从Bank2启动
            *(volatile uint32_t*)UPGRADE_FLAG_ADDR = 0;
            NVIC_SystemReset();
        }
        else
        {
            // 升级成功,清除标志
            *(volatile uint32_t*)UPGRADE_FLAG_ADDR = 0;
        }
    }
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    
    Check_Upgrade_Status();
    
    // 正常App1运行
    while (1)
    {
        // 主循环
    }
}

// 在升级过程中,写入新固件前设置标志
void Start_Upgrade(void)
{
    // 擦除Bank1的App区域
    FLASH_EraseInitTypeDef eraseInit;
    uint32_t pageError = 0;
    eraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
    eraseInit.Sector = Get_Sector_From_Addr(APP1_ADDR);
    eraseInit.NbSectors = 8; // 根据实际大小
    eraseInit.VoltageRange = FLASH_VOLTAGE_RANGE_3;
    HAL_FLASH_Unlock();
    HAL_FLASH_Erase(&eraseInit, &pageError);
    
    // 设置升级标志
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, UPGRADE_FLAG_ADDR, 0xDEADBEEF);
    HAL_FLASH_Lock();
    
    // 接收新固件并写入...
}

App2(备份固件)的启动代码

App2位于Bank2,其向量表偏移需设置为0x08080000。在系统初始化时,需要重设向量表:

// App2的system_stm32f4xx.c或main中
#define VECT_TAB_OFFSET 0x08080000

在main函数中,设置向量表偏移:

void SystemInit(void)
{
    // ...
    SCB->VTOR = 0x08080000;
}

App2的功能:

  • 初始化通信(如UART、USB或以太网)。
  • 提示用户升级失败,等待重新升级。
  • 提供恢复出厂设置或重新升级的接口。

6. 注意事项

  • 选项字节修改:修改选项字节后必须复位才能生效,且操作前需解锁Flash和选项字节。
  • 向量表偏移:App1和App2的向量表必须正确设置,否则中断异常。
  • Flash大小:确保双Bank总容量足够,且每个Bank的App大小不超过其容量。
  • 升级标志存储:使用备份寄存器(RTC域)或Flash末尾扇区,注意写入次数限制。
  • 硬件自举条件:并非所有STM32型号都支持双Bank自举,需查阅参考手册确认。
  • 测试:实际测试断电和写入错误场景,确保恢复机制可靠。

7. 总结

利用STM32双Bank Flash的自举恢复机制,可以显著提高OTA升级的可靠性。通过合理设计Bootloader、App1和App2,以及配置选项字节,即使升级失败,设备也能自动回退到安全固件,避免“变砖”。本文提供的代码和步骤可直接应用于实际项目,但需根据具体芯片型号调整Flash地址和大小。