STM32 双Bank切换实现OTA无缝回滚的地址映射陷阱与规避

1. 双Bank切换的基本原理

在STM32系列(如F4、L4、H7等)中,部分型号支持Flash的双Bank结构。每个Bank拥有独立的地址空间,通常为Flash总容量的一半。例如,STM32F429的2MB Flash分为Bank1(0x08000000-0x080FFFFF)和Bank2(0x08100000-0x081FFFFF)。

双Bank切换的核心思想是:当前运行在Bank A,升级时写入Bank B,完成后通过切换启动地址,使CPU从Bank B启动,实现无缝升级。若升级失败,可切换回Bank A,实现回滚。

2. 地址映射的陷阱

2.1 中断向量表错位

陷阱描述:当CPU从Bank A切换到Bank B后,中断向量表仍指向Bank A的起始地址(0x08000000),导致中断无法正确响应。

原因:STM32的向量表默认位于0x08000000,切换Bank后需重新定位。

规避方法:在启动代码中,通过设置SCB->VTOR寄存器,将向量表偏移到当前Bank的起始地址。

#define BANK1_BASE 0x08000000
#define BANK2_BASE 0x08100000

void set_vector_table(uint32_t bank_base) {
    SCB->VTOR = bank_base;
    __DSB();
    __ISB();
}

2.2 跳转地址偏差

陷阱描述:从Bank A跳转到Bank B时,若直接使用函数指针跳转,可能因编译链接时的绝对地址导致跳转失败。

原因:编译时生成的代码地址基于当前Bank的链接脚本,跳转时需使用Bank B的绝对地址。

规避方法:使用函数指针,并确保目标地址为Bank B的起始地址加偏移。

typedef void (*pFunction)(void);

void jump_to_bank(uint32_t bank_base) {
    uint32_t jump_addr = *(volatile uint32_t *)(bank_base + 4); // 复位向量
    pFunction jump_func = (pFunction)jump_addr;
    __set_MSP(*(volatile uint32_t *)bank_base); // 设置主栈指针
    jump_func();
}

2.3 Flash操作冲突

陷阱描述:在Bank A运行时,若对Bank B进行擦写操作,可能因Flash控制器状态冲突导致操作失败。

原因:某些STM32型号的Flash操作需要等待当前操作完成,且双Bank同时操作需注意时序。

规避方法:在操作Bank B前,确保Bank A的Flash操作已完成,并正确配置Flash等待周期。

void flash_erase_bank2(void) {
    FLASH_EraseInitTypeDef erase_init;
    uint32_t page_error = 0;
    erase_init.TypeErase = FLASH_TYPEERASE_PAGES;
    erase_init.PageAddress = BANK2_BASE;
    erase_init.NbPages = 1;
    HAL_FLASH_Unlock();
    if (HAL_FLASHEx_Erase(&erase_init, &page_error) != HAL_OK) {
        // 错误处理
    }
    HAL_FLASH_Lock();
}

3. 配置步骤

3.1 链接脚本配置

为每个Bank创建独立的链接脚本,确保代码段、数据段和向量表定位正确。

/* bank1.ld */
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1M
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 192K

/* bank2.ld */
FLASH (rx) : ORIGIN = 0x08100000, LENGTH = 1M
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 192K

3.2 启动代码修改

在启动文件(如startup_stm32f429xx.s)中,增加向量表重定位逻辑。

Reset_Handler:
    ; 设置向量表偏移
    LDR R0, =0xE000ED08
    LDR R1, =0x08000000  ; 当前Bank基址
    STR R1, [R0]
    ; 其他初始化...

3.3 OTA升级流程

  • 接收固件包,校验完整性(如CRC32)。
  • 擦写非活动Bank。
  • 写入固件,并设置升级标志。
  • 重启,Bootloader根据标志切换Bank。

4. 完整代码示例

以下是一个简化的Bootloader切换逻辑:

#include "stm32f4xx_hal.h"

#define APP_BANK1_ADDR 0x08010000  // 应用起始地址(Bank1)
#define APP_BANK2_ADDR 0x08110000  // 应用起始地址(Bank2)

void bootloader_main(void) {
    uint32_t active_bank = get_active_bank(); // 读取标志位
    uint32_t app_addr = (active_bank == 1) ? APP_BANK1_ADDR : APP_BANK2_ADDR;

    // 检查应用有效性(如栈指针范围)
    if (is_valid_app(app_addr)) {
        set_vector_table(app_addr & 0xFFFFF000); // 对齐到页
        jump_to_app(app_addr);
    } else {
        // 回滚到另一Bank
        app_addr = (active_bank == 1) ? APP_BANK2_ADDR : APP_BANK1_ADDR;
        if (is_valid_app(app_addr)) {
            set_vector_table(app_addr & 0xFFFFF000);
            jump_to_app(app_addr);
        }
    }
}

void jump_to_app(uint32_t app_addr) {
    uint32_t msp_val = *(volatile uint32_t *)app_addr;
    uint32_t reset_handler = *(volatile uint32_t *)(app_addr + 4);
    __set_MSP(msp_val);
    ((void (*)(void))reset_handler)();
}

5. 注意事项

  • Flash等待周期:切换Bank后,需根据系统时钟调整FLASH_ACR的等待周期,否则可能因时序错误导致程序崩溃。
  • 中断优先级:在切换过程中,建议关闭全局中断,避免中断干扰。
  • 编译优化:确保链接脚本中的符号地址与运行时地址一致,避免使用绝对地址的优化。
  • 测试覆盖:在真机上测试升级失败、回滚、断电等场景,确保稳定性。

6. 总结

双Bank切换是OTA无缝回滚的利器,但地址映射的细节决定成败。通过正确设置向量表、谨慎处理跳转地址、合理配置Flash操作,并配合完善的Bootloader逻辑,可有效规避陷阱,实现可靠的嵌入式升级方案。