STM32多Bank Flash同时读写:总线忙硬错误规避实战指南
👁 2 阅读 · 2026-08-27 · 嵌入式
在STM32H7系列等双Bank Flash架构的MCU上,同时进行读写操作(如OTA升级时写Flash、同时从另一Bank执行代码)极易触发总线忙(Busy)硬错误。本文深入剖析Flash控制器仲裁机制,揭示硬错误根因,并给出基于中断屏蔽、轮询等待和Bank切换的三种实用规避策略,附完整可移植代码,助你彻底告别Flash并发访问的坑。
# 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切换是更稳妥的方案。理解底层机制,才能写出健壮的嵌入式代码。