STM32H7双Bank闪存并行编程:实现OTA期间无缝代码执行

一、为什么需要双Bank闪存?

传统单Bank闪存方案中,OTA升级流程通常为:接收固件→擦除旧代码→写入新代码→跳转。擦写期间,CPU必须从闪存读取指令,若闪存被占用,代码执行将停滞,导致系统响应中断。对于电机控制、工业通信等实时性要求高的场景,这种停顿不可接受。

STM32H7系列(如STM32H743、H750)内置双Bank闪存,每个Bank容量相等(如1MB分两个512KB Bank)。硬件支持在Bank1执行代码的同时,对Bank2进行擦除/编程操作,反之亦然。这为OTA提供了天然的无缝切换基础。

二、双Bank并行编程原理

2.1 闪存架构

  • Bank划分:闪存被分为两个物理Bank,地址连续。例如,STM32H743的Flash起始地址0x08000000,Bank1覆盖0x08000000-0x0807FFFF,Bank2覆盖0x08080000-0x080FFFFF。
  • 独立控制:每个Bank有独立的控制寄存器(FLASH_CR1/CR2)和状态寄存器(FLASH_SR1/SR2),可独立执行擦写操作。
  • 总线访问:CPU取指和DMA访问可同时命中不同Bank,通过AHB总线交叉矩阵实现并行访问。

2.2 并行编程机制

当CPU从Bank1取指时,若对Bank2发起擦除或编程命令,Flash控制器会利用空闲的总线周期处理操作,不会阻塞Bank1的读取。关键点:

  • 擦除操作:按扇区擦除(典型扇区大小128KB),擦除期间Bank2不可读,但Bank1不受影响。
  • 编程操作:以双字(256位)为单位写入,编程时间短,冲突概率低。
  • 硬件仲裁:若CPU同时访问两个Bank,硬件自动插入等待周期,但不会导致错误。

三、配置步骤

3.1 使能双Bank模式

默认情况下,STM32H7可能处于单Bank模式(通过选项字节配置)。需在系统初始化时检查并设置:

void Flash_EnableDualBank(void) {
    if (READ_BIT(FLASH->OPTSR_CUR, FLASH_OPTSR_DUALBANK) == 0) {
        // 解锁选项字节
        HAL_FLASH_OB_Unlock();
        // 设置双Bank模式
        SET_BIT(FLASH->OPTSR_PRG, FLASH_OPTSR_DUALBANK);
        // 触发选项字节加载
        HAL_FLASH_OB_Launch();
        // 重新上电或复位后生效
        NVIC_SystemReset();
    }
}

注意:修改选项字节后需系统复位才能生效,因此建议在Bootloader中执行此操作。

3.2 配置Flash控制器

使用HAL库时,需初始化Flash等待周期和预取缓冲:

void Flash_Init(void) {
    HAL_FLASH_Unlock();
    // 设置等待周期,根据主频调整(例如主频480MHz时,等待周期为2)
    __HAL_FLASH_SET_LATENCY(FLASH_LATENCY_2);
    // 使能指令缓存和数据缓存
    HAL_FLASH_EnableIT(FLASH_IT_ICACHE_EN);
    HAL_FLASH_EnableIT(FLASH_IT_DCACHE_EN);
    HAL_FLASH_Lock();
}

3.3 双Bank并行编程实现

以下代码演示在Bank1执行时,对Bank2进行扇区擦除和编程:

// 假设当前代码在Bank1运行,目标Bank2地址0x08080000
void OTA_WriteFirmware(uint32_t destAddr, uint32_t *data, uint32_t len) {
    // 确保目标地址在Bank2
    if (destAddr < 0x08080000 || destAddr >= 0x08100000) return;

    // 解锁Flash
    HAL_FLASH_Unlock();

    // 擦除目标扇区(例如Bank2的第一个扇区)
    FLASH_EraseInitTypeDef erase = {0};
    erase.TypeErase = FLASH_TYPEERASE_SECTORS;
    erase.Banks = FLASH_BANK_2;  // 指定Bank2
    erase.Sector = 0;            // 扇区号
    erase.NbSectors = 1;
    erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
    uint32_t error = 0;
    if (HAL_FLASHEx_Erase(&erase, &error) != HAL_OK) {
        // 处理错误
    }

    // 编程数据(以双字为单位)
    for (uint32_t i = 0; i < len; i += 8) {
        uint64_t data64 = *(uint64_t*)(data + i/4);
        if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD, destAddr + i, data64) != HAL_OK) {
            // 处理错误
        }
    }

    HAL_FLASH_Lock();
}

在调用此函数时,主循环和中断照常运行,因为CPU取指在Bank1,而擦写操作在Bank2。

四、完整示例:OTA无缝升级流程

假设Bootloader位于Bank1起始,App1在Bank1后半部分,App2在Bank2。升级时,Bootloader将新固件写入Bank2,完成后设置跳转标志,复位后Bootloader检测标志并跳转至Bank2。

// Bootloader主逻辑
void Bootloader_Main(void) {
    // 检查跳转标志
    if (CheckJumpFlag() == 0xAA55) {
        // 跳转至Bank2的App2
        JumpToApp(0x08080000);
    }

    // 若收到升级命令
    if (OTA_CommandReceived()) {
        // 擦写Bank2(此时Bootloader在Bank1运行)
        OTA_WriteFirmware(0x08080000, newFwData, newFwLen);
        // 设置跳转标志
        SetJumpFlag(0xAA55);
        NVIC_SystemReset();
    }
}

void JumpToApp(uint32_t appAddr) {
    // 设置MSP和跳转
    uint32_t msp = *(volatile uint32_t*)appAddr;
    __set_MSP(msp);
    void (*appEntry)(void) = (void (*)(void))(*(volatile uint32_t*)(appAddr + 4));
    appEntry();
}

五、注意事项

  • 选项字节修改:双Bank模式必须在首次启动时配置,且修改后需复位。若在App中修改,需确保App有复位能力。
  • 中断向量表:跳转到新Bank后,需重定位中断向量表(通过SCB->VTOR设置)。
  • 擦除时间:虽然并行编程不阻塞执行,但擦除大扇区仍需时间(约几百毫秒),期间Bank2不可访问,需避免从Bank2读取数据。
  • 电源稳定性:擦写期间电流波动较大,需保证电源稳定,建议使用独立LDO或电容。
  • 错误处理:编程前检查地址对齐(双字对齐),编程后校验数据(读回比较)。
  • 缓存一致性:若使能了D-Cache,编程后需Clean&Invalidate相关地址,避免读到旧数据。

六、总结

STM32H7的双Bank闪存并行编程为OTA提供了优雅的解决方案,实现了固件升级过程中的零代码停顿。通过合理配置选项字节、利用HAL库的Bank指定接口,开发者可以轻松构建无缝升级系统。本文提供的代码和步骤可直接应用于实际项目,但需根据具体型号和主频调整参数。掌握这一技术,将显著提升嵌入式产品的可靠性和用户体验。