STM32H7 双 Bank 模式下利用 Flash 擦写中断实现 Bootloader 无缝跳转

1. 为什么需要双 Bank 与无缝跳转?

在嵌入式固件升级场景中,传统 Bootloader 通常将 App 写入固定地址,升级时需擦除整个 App 区,导致系统停机。STM32H7 系列(如 H743/H750)支持双 Bank Flash(Bank1 和 Bank2,各 1MB),允许在运行一个 Bank 的同时擦写另一个 Bank,实现“边运行边升级”。但跳转时若处理不当,中断向量表错乱或 Flash 操作未完成,会导致系统崩溃。本文基于 Flash 擦写中断(EOP)确保擦写完成后安全跳转,实现无缝切换。

2. 双 Bank 模式与 Flash 擦写中断原理

2.1 双 Bank 模式

  • STM32H7 Flash 分为 Bank1(地址 0x08000000)和 Bank2(地址 0x08100000),每个 Bank 独立擦写。
  • 通过选项字节(Option Bytes)配置 DBANK 位,可切换为单 Bank 或双 Bank 模式。默认双 Bank。
  • 当运行在 Bank1 时,可对 Bank2 进行擦写操作,反之亦然,但需注意 Flash 控制器(FLASH)的忙状态。

2.2 Flash 擦写中断(EOP)

  • Flash 操作(编程/擦除)完成后,硬件置位 EOP 标志(FLASH_SR_EOP),若使能中断(FLASH_CR_EOPIE),则触发中断。
  • 在中断服务函数中可清除标志,并设置标志位通知主程序操作完成,避免轮询阻塞 CPU。

2.3 无缝跳转的关键

  • 跳转前需重定位中断向量表(VTOR)到新 App 的起始地址,否则中断服务函数无法正确响应。
  • 确保 Flash 擦写操作完全结束(通过 EOP 中断确认),再执行跳转,防止数据损坏。

3. 系统架构与配置步骤

3.1 内存布局

  • Bootloader 位于 Bank1 起始地址(0x08000000),大小 32KB。
  • App1 位于 Bank1 偏移 0x8000(0x08008000),App2 位于 Bank2(0x08100000)。
  • 本文示例:Bootloader 从 Bank1 启动,升级时擦写 Bank2,完成后跳转到 Bank2 的 App。

3.2 配置步骤

  1. 使能 Flash 中断:在 FLASH_CR 中设置 EOPIE 位。
  2. 编写 EOP 中断服务函数:清除标志并置位全局标志。
  3. 擦写目标 Bank:调用 HAL 库函数擦除扇区并编程数据。
  4. 等待 EOP 中断:主循环等待标志位,超时处理。
  5. 跳转准备:设置 VTOR 到目标地址,关闭全局中断,初始化 MSP,跳转。

4. 完整代码示例

以下代码基于 STM32H743,使用 HAL 库。

/* 全局标志:Flash 操作完成 */
volatile uint8_t flash_op_complete = 0;

/* EOP 中断服务函数 */
void FLASH_IRQHandler(void)
{
    if (__HAL_FLASH_GET_FLAG(FLASH_FLAG_EOP)) {
        __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP);
        flash_op_complete = 1;
    }
}

/* Flash 擦写函数(以擦除 Bank2 为例) */
void Flash_Erase_Bank2(void)
{
    FLASH_EraseInitTypeDef erase_init;
    uint32_t sector_error = 0;

    flash_op_complete = 0;
    erase_init.TypeErase = FLASH_TYPEERASE_SECTORS;
    erase_init.Banks = FLASH_BANK_2;          // 选择 Bank2
    erase_init.Sector = 0;
    erase_init.NbSectors = 8;                 // 擦除所有扇区(假设每个扇区128KB)
    erase_init.VoltageRange = FLASH_VOLTAGE_RANGE_3;

    HAL_FLASH_Unlock();
    if (HAL_FLASHEx_Erase(&erase_init, &sector_error) != HAL_OK) {
        // 错误处理
    }
    HAL_FLASH_Lock();
}

/* 跳转到 App 函数 */
void Jump_To_App(uint32_t app_addr)
{
    uint32_t msp_value = *(volatile uint32_t *)app_addr;
    void (*app_reset_handler)(void) = (void (*)(void))(*(volatile uint32_t *)(app_addr + 4));

    // 关闭全局中断
    __disable_irq();

    // 重定位中断向量表
    SCB->VTOR = app_addr;

    // 设置主栈指针并跳转
    __set_MSP(msp_value);
    app_reset_handler();
}

/* 主函数示例 */
int main(void)
{
    HAL_Init();
    SystemClock_Config();

    // 使能 Flash EOP 中断
    HAL_FLASH_Unlock();
    __HAL_FLASH_ENABLE_IT(FLASH_IT_EOP);
    HAL_FLASH_Lock();
    HAL_NVIC_EnableIRQ(FLASH_IRQn);

    // 擦除 Bank2(假设升级数据已准备好)
    Flash_Erase_Bank2();

    // 等待擦除完成(超时 5 秒)
    uint32_t timeout = HAL_GetTick() + 5000;
    while (!flash_op_complete && HAL_GetTick() < timeout);

    if (flash_op_complete) {
        // 编程数据到 Bank2(此处省略具体编程代码)
        // ...

        // 跳转到 Bank2 的 App
        Jump_To_App(0x08100000);
    } else {
        // 超时处理
        Error_Handler();
    }

    while (1);
}

5. 注意事项

  • 中断向量表重定位:App 工程中必须设置 VECT_TAB_OFFSET 为对应偏移(如 0x100000),否则跳转后中断异常。
  • Flash 操作时序:擦写期间不能访问 Flash 代码区,否则总线忙。建议将擦写函数放在 RAM 中执行(如 __attribute__((section(".ramfunc"))))。
  • EOP 中断优先级:确保其优先级高于 App 中的关键中断,但跳转前应关闭所有中断,避免跳转过程中断。
  • 双 Bank 切换:若 App 需要切换 Bank 运行,需修改选项字节 DBANK 并复位,但本文跳转方式无需切换,直接跳转即可。
  • 电源稳定性:擦写 Flash 时电流较大,确保供电稳定,否则可能导致擦写失败。

6. 总结

通过利用 STM32H7 双 Bank 架构和 Flash 擦写中断,我们实现了 Bootloader 到 App 的无缝跳转,避免了传统升级的停机问题。关键在于正确配置 EOP 中断、等待操作完成,并重定位向量表。此方案适用于需要在线升级的高可靠性系统,如工业控制器、IoT 设备等。开发者可根据实际需求扩展为 A/B 分区升级策略,进一步提升系统鲁棒性。