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编程函数。
#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;
}
使用示例:
// 定义要写入的数据(双字对齐)
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切换是更稳妥的方案。理解底层机制,才能写出健壮的嵌入式代码。