# STM32多Bank Flash同时读写:总线忙硬错误规避实战指南 ## 一、问题现象与根因分析 在STM32H743等双Bank Flash(每Bank 1MB)的MCU上,当CPU从Bank1执行代码,同时通过Flash接口对Bank2进行编程/擦除操作时,偶尔会触发HardFault。调试发现错误发生在Flash操作指令执行瞬间,且错误状态寄存器(FLASH_SR)的`PGSERR`或`WRPERR`位未置位,而是`BSY`位卡死。 **根因**:STM32的Flash控制器(FLASH)是单通道仲裁器,同一时刻仅允许一个访问请求(读或写)。当CPU取指(读Bank1)与编程(写Bank2)同时到达时,Flash控制器会优先处理写操作,但若读请求在写操作进行中到达,且读请求未被正确挂起,则总线会返回等待状态(Wait States)。若此时CPU的指令预取缓冲区已空,且等待周期超过总线超时阈值,则触发总线错误(Bus Fault),进而升级为HardFault。 ## 二、硬件仲裁机制与关键寄存器 - **FLASH_CR**:控制寄存器,`PG`(编程)和`PER`(擦除)位用于启动操作。 - **FLASH_SR**:状态寄存器,`BSY`位指示Flash忙,`CFGBSY`指示配置忙。 - **FLASH_CCR**:清除错误标志寄存器。 - **FLASH_OPTSR**:选项字节配置,可设置`BOR_LEV`等,但注意`DBANK`位决定单Bank还是双Bank模式(H7系列默认双Bank)。 **关键点**:在双Bank模式下,两个Bank共享同一Flash控制器,但各自有独立的编程/擦除缓冲。然而,**仲裁粒度是操作级**,即一个完整的编程(如128位数据写入)或擦除(扇区/块)期间,总线被独占。 ## 三、三种规避策略 ### 策略1:轮询等待BSY位(最基础) 在发起Flash写操作前,检查`BSY`位,若忙则等待。但此方法无法解决“写操作进行中,CPU取指导致读请求”的问题,因为读请求是硬件自动发起的。因此,**仅靠轮询BSY不够**,需配合中断屏蔽。 ### 策略2:屏蔽中断+轮询(推荐) 在Flash写操作期间,屏蔽所有可屏蔽中断(PRIMASK),防止中断服务程序(ISR)中的代码执行触发Flash读操作。同时,主循环中轮询BSY。此方法确保写操作期间无其他代码执行,避免并发访问。 ### 策略3:Bank切换(终极方案) 将代码从Bank1复制到RAM或Bank2,然后从RAM执行写操作,彻底避免Flash读请求。适用于OTA等场景,但需额外RAM开销。 ## 四、完整代码示例(策略2实现) 以下代码基于STM32H7 HAL库,实现安全的Flash编程函数。 ```c #include "stm32h7xx_hal.h" // 自定义Flash编程函数,带中断屏蔽和BSY轮询 HAL_StatusTypeDef FLASH_Program_Protected(uint32_t address, uint64_t *data, uint32_t size_in_double_words) { HAL_StatusTypeDef status = HAL_OK; uint32_t primask; // 1. 屏蔽可屏蔽中断 primask = __get_PRIMASK(); __disable_irq(); // 2. 解锁Flash(若未解锁) if (HAL_FLASH_Unlock() != HAL_OK) { __set_PRIMASK(primask); return HAL_ERROR; } // 3. 清除错误标志 __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS); // 4. 逐双字编程 for (uint32_t i = 0; i < size_in_double_words; i++) { // 等待BSY位清零(确保上次操作完成) while (__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY) != RESET) { // 可加超时机制 } // 设置编程地址和数据(H7使用FLASH_CR的PG位和FLASH_CR的PSIZE) FLASH->CR |= FLASH_CR_PG; *(volatile uint64_t *)(address + i * 8) = data[i]; __DSB(); // 数据同步屏障,确保写入完成 // 等待BSY位清零 while (__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY) != RESET) { // 可加超时机制 } // 检查编程错误 if (__HAL_FLASH_GET_FLAG(FLASH_FLAG_PGSERR) != RESET) { status = HAL_ERROR; break; } } // 5. 锁定Flash HAL_FLASH_Lock(); // 6. 恢复中断状态 __set_PRIMASK(primask); return status; } ``` **使用示例**: ```c // 定义要写入的数据(双字对齐) uint64_t data[2] = {0x0123456789ABCDEF, 0xFEDCBA9876543210}; // 编程Bank2的某个地址(例如0x08100000) HAL_StatusTypeDef ret = FLASH_Program_Protected(0x08100000, data, 2); if (ret != HAL_OK) { // 错误处理 } ``` ## 五、注意事项与陷阱 - **中断屏蔽时间**:Flash编程一个双字约需几十微秒(H7在VOS1下典型50μs),屏蔽中断期间不能执行实时性要求高的任务。若需长时间擦除(如扇区擦除约1s),建议使用策略3。 - **超时机制**:上述代码中的BSY轮询未加超时,实际产品中应加入超时计数,防止硬件异常导致死循环。 - **缓存一致性**:若开启了D-Cache,编程后需调用`SCB_CleanDCache_by_Addr`或`SCB_InvalidateDCache_by_Addr`,否则可能读到旧数据。 - **Bank切换**:使用策略3时,需确保代码在RAM中运行,且链接脚本正确分配。可参考HAL的`FLASH_Program`内部实现,但需注意其未屏蔽中断。 - **选项字节**:若需修改选项字节(如切换DBANK),必须遵循特定序列,且操作期间同样需要屏蔽中断。 ## 六、总结 STM32多Bank Flash并发访问的硬错误源于Flash控制器仲裁机制,通过屏蔽中断+轮询BSY可有效规避,但需权衡实时性。对于复杂场景,Bank切换是更稳妥的方案。理解底层机制,才能写出健壮的嵌入式代码。