STM32F4 双 Bank Flash 在线升级(IAP)的地址映射陷阱与规避

一、双 Bank 模式与地址映射基础

STM32F4 系列(如 STM32F427/437)的 Flash 容量通常为 1MB 或 2MB,支持双 Bank 架构。每个 Bank 独立编址,但可通过选项字节配置为单一 Bank 模式(此时地址连续)或双 Bank 模式(地址交错)。在双 Bank 模式下,Flash 被划分为两个逻辑 Bank:

  • Bank 1:地址范围 0x08000000 ~ 0x0807FFFF(1MB 设备)
  • Bank 2:地址范围 0x08080000 ~ 0x080FFFFF

关键点:双 Bank 模式下,CPU 的取指和数据的地址映射是交错的,即物理扇区 0 映射到 Bank1 的地址 0,物理扇区 1 映射到 Bank2 的地址 0,以此类推。这意味着,当从 Bank1 跳转到 Bank2 执行代码时,向量表地址必须重新映射,否则中断将无法正确响应。

二、常见陷阱分析

陷阱 1:Bank 切换后向量表错位

在 IAP 升级中,Bootloader 通常位于 Bank1 起始地址,App 位于 Bank2。当 Bootloader 跳转到 App 时,若未正确设置向量表偏移(VTOR),CPU 仍会从 0x08000000 读取中断向量,导致执行 Reset_Handler 失败或进入 HardFault。

规避:在 App 启动代码中,必须设置 SCB->VTOR = APP_BASE_ADDRESS,其中 APP_BASE_ADDRESS 为 App 在 Bank2 的起始地址(如 0x08080000)。

陷阱 2:Flash 擦写操作跨越 Bank 边界

双 Bank 模式下,Flash 擦除操作以扇区为单位,但扇区编号是全局的。若开发者误以为 Bank1 的扇区编号从 0 开始,Bank2 从 1 开始,则可能擦除错误区域。实际上,STM32F4 的扇区编号是连续的(0~11),但物理映射交错。

规避:使用 HAL 库的 HAL_FLASHEx_Erase 时,必须指定正确的扇区编号和 Bank 选择(FLASH_BANK_1FLASH_BANK_2)。

陷阱 3:跳转函数未清理中断状态

在跳转前,若未关闭全局中断或未复位外设,可能导致中断在跳转瞬间触发,而向量表尚未切换,造成不可预知行为。

规避:跳转前执行 __disable_irq(),并调用 HAL_RCC_DeInit() 复位时钟,最后设置 MSP 和跳转。

三、双 Bank IAP 实现步骤

1. 配置选项字节启用双 Bank 模式

使用 STM32CubeProgrammer 或代码设置选项字节:

// 启用双 Bank 模式(以 STM32F427 为例)
HAL_FLASH_Unlock();
__HAL_FLASH_OPTIMOD_ENABLE();
FLASH_OBProgramInitTypeDef ob;
ob.OptionType = OPTIONBYTE_BYTE;
ob.BYTE = 0x01; // 设置 nDBANK=0,启用双 Bank
HAL_FLASHEx_OBProgram(&ob);
HAL_FLASH_OB_Launch();
HAL_FLASH_Lock();

2. Bootloader 跳转函数

#define APP_ADDRESS 0x08080000  // Bank2 起始地址

typedef void (*pFunction)(void);

void jump_to_app(void) {
    uint32_t app_stack = *(volatile uint32_t*)APP_ADDRESS;
    pFunction app_entry = (pFunction)(*(volatile uint32_t*)(APP_ADDRESS + 4));

    // 检查栈顶地址是否合法(RAM 范围)
    if ((app_stack & 0x2FFE0000) != 0x20000000) {
        return; // 非法地址
    }

    __disable_irq();
    HAL_RCC_DeInit();
    SysTick->CTRL = 0;
    SysTick->LOAD = 0;
    SysTick->VAL = 0;

    // 设置主栈指针并跳转
    __set_MSP(app_stack);
    app_entry();
}

3. App 端向量表重映射

在 App 工程的 system_stm32f4xx.cmain.c 中,于 main 函数最开头设置:

#define APP_BASE 0x08080000

void main(void) {
    SCB->VTOR = APP_BASE; // 重映射向量表
    // 其他初始化...
}

4. 升级流程(接收固件并写入 Bank2)

void write_firmware_to_bank2(uint8_t *data, uint32_t len, uint32_t offset) {
    FLASH_EraseInitTypeDef erase;
    uint32_t sector_error = 0;

    HAL_FLASH_Unlock();

    // 计算扇区(Bank2 起始扇区为 8,每个扇区 128KB,但需根据具体型号调整)
    erase.TypeErase = FLASH_TYPEERASE_SECTORS;
    erase.Banks = FLASH_BANK_2;
    erase.Sector = FLASH_SECTOR_8; // 根据实际偏移计算
    erase.NbSectors = 1;
    erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;

    if (HAL_FLASHEx_Erase(&erase, &sector_error) != HAL_OK) {
        // 错误处理
    }

    // 写入数据(注意地址对齐)
    for (uint32_t i = 0; i < len; i += 8) {
        uint64_t data64 = 0;
        for (int j = 0; j < 8; j++) {
            data64 |= ((uint64_t)data[i+j]) << (8*j);
        }
        if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, APP_ADDRESS + offset + i, data64) != HAL_OK) {
            // 错误处理
        }
    }

    HAL_FLASH_Lock();
}

四、完整示例:Bootloader 与 App 协作

Bootloader 端(简化)

int main(void) {
    HAL_Init();
    // 检查升级标志(如外部 EEPROM 或备份寄存器)
    if (upgrade_requested()) {
        receive_firmware(); // 接收并写入 Bank2
        clear_upgrade_flag();
    }
    jump_to_app();
}

App 端(简化)

int main(void) {
    SCB->VTOR = 0x08080000; // 必须设置
    HAL_Init();
    // 应用逻辑...
}

五、注意事项

  • 地址对齐:Flash 编程必须 8 字节对齐(双字),否则 HAL 库会报错。
  • 扇区大小:不同型号的 STM32F4 扇区大小不同(如 F427 的扇区 0~3 为 16KB,4 为 64KB,5~11 为 128KB),务必查阅参考手册。
  • 中断安全:跳转前关闭所有中断,并在 App 中重新初始化外设。
  • 校验机制:升级前建议对固件进行 CRC 校验,防止写入损坏数据。
  • 调试技巧:使用 ST-Link 的 set var 命令检查 SCB->VTOR 值,确认映射正确。

六、总结

双 Bank 模式为 IAP 提供了强大的灵活性,但地址映射的复杂性要求开发者深入理解硬件架构。通过正确设置 VTOR、谨慎处理 Flash 擦写、并遵循安全的跳转流程,可以避免绝大多数陷阱。希望本文能帮助你在 STM32F4 上实现稳定可靠的在线升级功能。