STM32多扇区批量擦除实战:从原理到高效实现
你有没有遇到过这样的场景?设备正在进行固件升级,界面突然卡住十几秒——不是网络问题,也不是CPU跑不动,而是Flash正在擦除多个扇区。每擦一个扇区要几十毫秒,连续擦十个就是将近一秒,用户感知明显。
在嵌入式开发中,Flash擦除操作看似简单,实则暗藏陷阱。尤其当应用涉及OTA升级、日志循环写入或动态参数存储时,频繁的Flash操作会成为系统性能的“隐形瓶颈”。更严重的是,不当的操作可能导致数据丢失、写保护触发甚至芯片锁死。
本文将带你深入STM32平台下的多扇区批量擦除(multi-sector bulk erase)技术,不仅讲清楚底层机制,还会手把手构建一套可复用、高可靠的异步擦除框架。无论你是做工业控制、IoT终端还是边缘计算设备,这套方案都能显著提升系统的响应性和稳定性。
Flash为何必须先“擦”后“写”?
我们常说“往Flash里写数据”,其实这是一个简化说法。准确地说,应该是:“先擦除扇区,再编程写入”。为什么不能像RAM那样直接修改?
这要从Flash的物理结构说起。STM32内部使用的NOR Flash基于浮栅晶体管(Floating Gate Transistor),其存储原理是通过向浮栅注入或抽出电荷来改变阈值电压,从而表示0或1。而关键在于:
- 出厂状态为全‘1’(即每个bit=1)
- 只能将1变成0(通过编程操作)
- 无法单独把0变回1
- 必须整块施加高压才能清除电荷,使所有bit恢复为1
换句话说,擦除是唯一能让Flash“重置”的方式。这也是为什么哪怕只改一个字节,也得先把整个扇区擦掉,再把其他有效数据重新写回去。
举个形象的例子:你可以把Flash扇区想象成一块黑板。你想修改某个词,但不能局部擦除,只能整块擦干净,然后重写全部内容。
因此,在设计任何需要更新Flash数据的应用时,我们必须正视两个现实:
1. 擦除操作耗时长(典型值20~100ms/sector)
2. 擦除次数有限(约10万次寿命)
如果处理不好,轻则卡顿,重则提前报废Flash区域。
STM32的Flash组织结构与擦除单位
不同型号的STM32,其Flash扇区划分差异很大。以常见的STM32F4系列为例,其主存储区被划分为12个扇区,大小不一:
| 扇区 | 起始地址 | 大小 |
|---|---|---|
| Sector 0 | 0x08000000 | 16 KB |
| Sector 1 | 0x08004000 | 16 KB |
| … | … | … |
| Sector 11 | 0x0801E000 | 128 KB |
注意:STM32F1系列是按“页”擦除(如1KB/页),而F4/F7/H7等较新型号统一称为“扇区”。
这意味着:
- 最小擦除单位是扇区,哪怕你只想清空1字节
- 不同扇区大小不同,大容量程序通常占用后面的几个大扇区
- 擦除操作不可中断,一旦启动就必须完成
此外,还有几点关键特性不容忽视:
| 特性 | 说明 |
|---|---|
| 耐久性限制 | 典型支持10万次擦写周期,超出后可能出现读写错误 |
| 电压依赖性 | 需稳定供电(2.7V~3.6V),低电压下易导致擦除失败 |
| 原子性保证 | 单个扇区擦除是原子操作,不会中途断电导致半擦状态 |
| 无事务一致性 | 多个扇区之间不具备事务特性,部分成功需软件补偿 |
这些特性决定了我们在进行多扇区操作时,不能简单地循环调用单次擦除API了事,否则会带来严重的性能和可靠性问题。
为什么你需要“批量擦除”而不是逐个擦?
假设你的项目需要更新分布在Sector 2~5的旧固件,共4个扇区。如果采用传统的同步方式:
HAL_FLASH_Unlock(); for (int i = 2; i <= 5; i++) { FLASH_Erase_Sector(i); // 每次都要等待完成 } HAL_FLASH_Lock();会发生什么?
- 每次擦除前都要解锁Flash(开销固定)
- 每次擦除后轮询BSY标志位(CPU空转等待)
- 中间穿插错误检测、标志清除等重复逻辑
- 总耗时 ≈ 4 × 平均80ms =320ms以上
而这段时间内,主线程完全阻塞,UI卡死、通信超时、看门狗可能复位……
而如果我们能一次性提交所有待擦除扇区,后台自动依次执行,就能释放CPU资源去做别的事——这就是“批量擦除”的核心价值。
批量擦除的优势一览
| 优势 | 实际收益 |
|---|---|
| ✅ 减少上下文切换 | 合并多次独立操作,降低函数调用开销 |
| ✅ 提升吞吐效率 | 避免重复的状态检查与寄存器配置 |
| ✅ 增强系统响应性 | 主线程非阻塞,适合RTOS或多任务环境 |
| ✅ 简化错误处理 | 统一捕获异常,便于日志记录与恢复机制 |
更重要的是,这种模式天然支持与DMA、定时器、通信任务并发运行,真正实现“后台清理,前台服务”。
构建一个可靠的异步多扇区擦除引擎
接下来,我们要动手实现一个基于中断驱动的多扇区批量擦除模块。目标是:调用接口后立即返回,后续由中断自动推进,完成后通知回调。
整体设计思路
我们将采用“状态机 + 中断驱动 + 回调通知”的设计模式:
- 用户调用
FLASH_MultiSectorErase()提交扇区列表 - 模块启动第一个扇区擦除,并开启Flash中断
- 每次擦除完成触发EOP中断,在ISR中判断是否继续下一个
- 全部完成后关闭中断、锁定Flash,并调用完成回调
- 若发生错误(如写保护、地址越界),立即终止并进入错误处理流程
这种方式避免了长时间轮询,特别适合在FreeRTOS或其他实时系统中与其他任务并行运行。
核心代码实现
#include "stm32f4xx_hal.h" // 外部定义:待擦除扇区数组及数量 extern uint32_t sectors_to_erase[]; extern uint8_t num_sectors; // 全局状态标识 volatile uint8_t flash_erase_in_progress = 0; volatile uint8_t current_sector_index = 0; /** * @brief 启动多扇区批量擦除 * @param sectors: 扇区编号数组指针 * @param count: 扇区数量 * @retval HAL_StatusTypeDef: 操作结果 */ HAL_StatusTypeDef FLASH_MultiSectorErase(uint32_t *sectors, uint8_t count) { if (flash_erase_in_progress || count == 0) { return HAL_BUSY; // 正在执行中或参数无效 } // 解锁Flash控制器 if (HAL_FLASH_Unlock() != HAL_OK) { return HAL_ERROR; } // 清除所有可能存在的错误标志 __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR | FLASH_FLAG_PGAERR); // 设置全局状态 flash_erase_in_progress = 1; current_sector_index = 0; // 启用Flash操作完成和错误中断 __HAL_FLASH_ENABLE_IT(FLASH_IT_EOP | FLASH_IT_ERR); // 触发第一个扇区擦除 return FLASH_NextSectorErase(sectors[0]); } /** * @brief 擦除下一个扇区 * @param sector: 当前要擦除的扇区号 * @retval HAL_StatusTypeDef */ static HAL_StatusTypeDef FLASH_NextSectorErase(uint32_t sector) { FLASH_EraseInitTypeDef erase_config; uint32_t page_error = 0; erase_config.TypeErase = FLASH_TYPEERASE_SECTORS; erase_config.Sector = sector; erase_config.NbSectors = 1; erase_config.VoltageRange = FLASH_VOLTAGE_RANGE_3; // 3.3V系统 if (HAL_FLASHEx_Erase(&erase_config, &page_error) != HAL_OK) { // 擦除失败,立即终止流程 __HAL_FLASH_DISABLE_IT(FLASH_IT_EOP | FLASH_IT_ERR); HAL_FLASH_Lock(); flash_erase_in_progress = 0; return HAL_ERROR; } return HAL_OK; } /** * @brief Flash中断服务函数 */ void FLASH_IRQHandler(void) { // 是否为操作结束中断? if (__HAL_FLASH_GET_FLAG(FLASH_FLAG_EOP)) { __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP); current_sector_index++; // 还有更多扇区吗? if (current_sector_index < num_sectors) { uint32_t next_sector = sectors_to_erase[current_sector_index]; FLASH_NextSectorErase(next_sector); // 继续下一扇区 } else { // 所有扇区已擦完 __HAL_FLASH_DISABLE_IT(FLASH_IT_EOP | FLASH_IT_ERR); HAL_FLASH_Lock(); flash_erase_in_progress = 0; OnMultiSectorEraseComplete(); // 用户回调 } } // 错误中断处理 else if (__HAL_FLASH_GET_FLAG(FLASH_FLAG_WRPERR) || __HAL_FLASH_GET_FLAG(FLASH_FLAG_PGAERR) || __HAL_FLASH_GET_FLAG(FLASH_FLAG_OPTVERR)) { __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS); __HAL_FLASH_DISABLE_IT(FLASH_IT_EOP | FLASH_IT_ERR); HAL_FLASH_Lock(); flash_erase_in_progress = 0; OnMultiSectorEraseError(); // 错误回调 } }关键点解析
1.为什么使用中断而非轮询?
轮询会让CPU一直处于忙等状态,白白浪费资源。而中断方式让硬件自行完成操作,期间CPU可以处理通信、UI刷新等高优先级任务。
2.如何防止并发访问?
通过flash_erase_in_progress全局标志实现互斥访问。任何时刻只允许一次批量擦除进行,避免Flash控制器冲突。
3.错误处理是否完备?
是的。我们监听了三种常见错误:
-WRPERR:试图擦除受写保护的区域
-PGAERR:地址对齐错误
-OPTVERR:选项字节配置异常
一旦发生,立即终止流程并回调错误函数,确保系统安全退出。
4.回调函数怎么定义?
你需要在应用层实现以下两个函数:
void OnMultiSectorEraseComplete(void) { // 启动新固件写入、发送状态上报等 } void OnMultiSectorEraseError(void) { // 记录日志、报警、尝试恢复等 }实际应用场景:FOTA固件升级中的高效擦除
设想一个典型的远程固件升级流程:
[接收新固件包] → [校验完整性] ↓ [定位旧固件扇区] → [发起批量擦除请求] ↓ [非阻塞返回] → [继续解密/准备写入缓冲] ↓ [收到完成中断] → [开始写入新固件]在这种架构下,擦除阶段不再成为瓶颈。即使需要擦除10个扇区(约800ms),主线程也可以同时完成如下工作:
- 解密固件包
- 计算CRC校验
- 更新UI进度条
- 维持心跳通信
用户体验从“卡住等待”变为“平滑过渡”。
工程实践中的避坑指南
别以为写了代码就万事大吉。以下是我在真实项目中踩过的坑,帮你少走弯路:
❌ 坑点1:忘记喂狗 → 系统意外复位
长时间擦除可能超过IWDG超时时间。
✅秘籍:在每次EOP中断完成后喂一次狗。
IWDG->KR = 0xAAAA; // 喂狗(需根据具体型号调整)❌ 坑点2:电源不稳定 → 擦除失败率飙升
电池供电或LDO压降时,VDD低于2.7V会导致操作失败。
✅秘籍:增加电压监测,低于阈值时暂停擦除。
❌ 坑点3:扇区顺序混乱 → 控制器状态抖动
虽然不影响功能,但建议按地址升序排列扇区,有利于Flash控制器内部优化。
✅秘籍:擦除前排序sectors_to_erase[]
❌ 坑点4:堆栈溢出 → 中断内调用复杂函数
不要在FLASH_IRQHandler里做日志打印、内存分配等耗时操作。
✅秘籍:中断只负责推进状态,复杂逻辑移交主循环处理。
❌ 坑点5:跨系列兼容性差 → 移植成本高
F1/F4/L4/H7的扇区划分完全不同,硬编码扇区号难以维护。
✅秘籍:抽象封装一层映射表,按用途命名而非编号。
例如:
#define SECTOR_APP_FIRMWARE_START 2 #define SECTOR_APP_FIRMWARE_END 5 #define SECTOR_USER_CONFIG 6更进一步:结合磨损均衡延长Flash寿命
虽然STM32 Flash标称10万次擦写寿命,但在高频日志记录类应用中仍可能提前老化。
解决方案之一是引入磨损均衡(Wear Leveling)思想:
- 将日志区划分为多个逻辑块
- 每次写日志选择擦除次数最少的物理扇区
- 配合批量擦除机制,定期后台整理碎片
这样可以把原本集中在某一个扇区的压力分散到多个区域,理论上可将使用寿命延长数倍。
提示:对于复杂文件系统需求,可考虑集成LittleFS或SPIFFS,它们已内置磨损均衡算法。
如果你正在开发需要频繁操作Flash的嵌入式系统,不妨现在就把这个批量擦除模块加入你的基础库。它不仅能解决眼前的性能问题,更为未来的功能扩展打下坚实基础。
你不需要等到系统卡顿时才想起Flash管理的重要性,而应该在架构设计之初就把它当作核心模块来对待。
如果你在实现过程中遇到了其他挑战,欢迎在评论区分享讨论。