STM32存储管理中的erase机制通俗解释
2026/4/16 12:49:20 网站建设 项目流程

STM32中的Flash擦除机制:从原理到实战的深度解析

在嵌入式开发的世界里,有一类操作看似简单,却暗藏杀机——那就是对Flash存储器的擦除(Erase)。特别是在使用STM32系列微控制器时,一旦处理不当,轻则数据丢失,重则系统崩溃、芯片“变砖”。而这一切的核心,正是那个不起眼但至关重要的动作:erase

你有没有遇到过这样的问题?
- 修改一个参数,结果整个设备配置全丢了?
- OTA升级后,新固件无法启动?
- 程序运行中突然HardFault,定位发现是访问了正在擦除的Flash区域?

这些问题的背后,往往都指向同一个根源:对Flash擦除机制的理解不够深入

今天,我们就来彻底拆解STM32中的erase机制——不讲套话,不堆术语,而是从实际工程痛点出发,带你一步步看清它的本质、陷阱和最佳实践。


为什么必须先“擦”再“写”?

我们都知道RAM可以随意读写,但STM32片上Flash不是这样。它有一个铁律:

只能将“1”变成“0”,不能把“0”变回“1”——除非先擦除。

什么意思?举个例子。假设某个字节当前值是0xFF(所有位都是1),你可以通过编程把它改成0xFE0xFD……甚至0x00。但如果你现在想把它改回0xFF,对不起,硬件不允许。唯一的办法是:执行一次扇区擦除,让整个区域恢复成全“1”。

这就是为什么每次写入前必须先擦除。这不仅是规则,更是物理限制。

Flash是怎么存数据的?

STM32用的是NOR Flash,每个存储单元是一个浮栅晶体管。
- 写入(Program)时,施加高电压,电子被注入浮栅,改变阈值电压 → 表示“0”;
- 擦除(Erase)时,反向加压,抽出电子,恢复原始状态 → 回到“1”。

这个过程需要内部电荷泵生成约12V的高压脉冲,耗能大、速度慢、损耗高。所以,每一次erase,都在悄悄消耗Flash的寿命。


擦除操作到底怎么做?一步一步看清楚

别以为调个HAL库函数就万事大吉。真正要安全可靠地完成一次擦除,你得知道背后发生了什么。

第一步:解锁——否则一切免谈

默认情况下,STM32的Flash控制寄存器是锁定的,防止误操作导致程序破坏。你要做的第一件事就是“敲门”:

HAL_FLASH_Unlock();

底层其实是往FLASH_KEYR寄存器连续写两个密钥:
-0x45670123
-0xCDEF89AB

顺序错了都不行。这是ST设的第一道防线。

第二步:检查是否空闲

别急着动手!如果上一次Flash操作还没结束(比如还在编程或擦除),你现在强行开始,会出大事。所以必须查FLASH_SR寄存器里的BSY标志位:

while (__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY)) { // 等待忙完 }

CPU在这里可能卡几十毫秒,忍住别中断。

第三步:配置擦除参数

你要告诉控制器:我要擦哪个扇区?几个?工作电压多少?

FLASH_EraseInitTypeDef EraseInitStruct = {0}; EraseInitStruct.TypeErase = FLASH_TYPEERASE_SECTORS; EraseInitStruct.Sector = FLASH_SECTOR_2; EraseInitStruct.NbSectors = 1; EraseInitStruct.VoltageRange = FLASH_VOLTAGE_RANGE_3; // 2.7~3.6V

注意电压范围必须匹配你的供电情况,否则可能失败或损坏单元。

第四步:启动擦除

准备好之后,触发操作:

uint32_t SectorError = 0; if (HAL_FLASHEx_Erase(&EraseInitStruct, &SectorError) != HAL_OK) { // 失败处理 }

这时候,硬件开始干活了。CPU可以选择干等,也可以进入低功耗模式,但绝对不能去执行位于待擦除区域的代码

第五步:善后收尾

无论成功与否,最后一定要加锁:

HAL_FLASH_Lock();

不然别人随便就能改你的Flash,安全隐患极大。


常见坑点与避坑指南

你以为按流程走就安全了?下面这些“隐形炸弹”,很多老手都踩过。

❌ 坑点1:在Flash里运行擦除代码

最经典的错误:你在Sector 2里放了一段保存参数的函数,然后又去擦Sector 2。

会发生什么?
当控制器开始擦除时,那一片Flash瞬间变成无效状态。下一条指令取指失败 → 总线错误(BusFault)→ 系统崩塌。

✅ 正确做法:把擦除相关的函数放到SRAM中执行。

__attribute__((section(".sram_func"))) void flash_erase_in_sram(uint8_t sector) { HAL_FLASH_Unlock(); // ...擦除逻辑 HAL_FLASH_Lock(); }

并在链接脚本中定义.sram_func段映射到SRAM。

❌ 坑点2:中断服务里访问Flash

即使主程序没动Flash,但如果某个中断服务程序(ISR)恰好要跳转到Flash执行代码(比如调用库函数),而此时主程序正在擦除——Boom!

✅ 解法:在擦除期间禁用全局中断:

__disable_irq(); // 执行擦除 __enable_irq();

或者确保所有ISR使用的函数都不在目标扇区。

❌ 坑点3:电源不稳时强行擦除

Flash擦除需要稳定的VDD和内部高压。电压低于2.7V还硬来?轻则操作失败,重则留下“半擦除”状态——部分位没清干净,后续写入混乱。

✅ 推荐做法:启用PVD(可编程电压检测)

__HAL_RCC_PWR_CLK_ENABLE(); HAL_PWREx_EnablePVD(PWR_PVDLEVEL_2); // 设置阈值为2.9V

并在PVD中断中阻止任何Flash操作。


实战案例:如何安全保存用户参数?

很多项目没有外接EEPROM,又要掉电保存配置。怎么办?用内部Flash模拟。

设计思路

  1. 预留一个专用扇区(如Sector 7)用于存储参数;
  2. 定义结构体表示参数块;
  3. 更新时整块备份 → 擦除 → 重写。
typedef struct { uint32_t baudrate; uint16_t device_id; uint8_t sn[16]; uint32_t crc; // 校验用 } system_config_t; system_config_t config_backup; int save_config_to_flash(system_config_t *new_cfg) { __disable_irq(); // 关中断 // 1. 读出现有内容 memcpy(&config_backup, (void*)PARAM_BASE_ADDR, sizeof(system_config_t)); // 2. 更新字段 config_backup = *new_cfg; config_backup.crc = calc_crc32((uint8_t*)&config_backup, offsetof(system_config_t, crc)); // 3. 擦除原扇区 HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef ei = {0}; ei.TypeErase = FLASH_TYPEERASE_SECTORS; ei.Sector = PARAM_SECTOR; ei.NbSectors = 1; ei.VoltageRange = FLASH_VOLTAGE_RANGE_3; uint32_t error; if (HAL_FLASHEx_Erase(&ei, &error) != HAL_OK) { HAL_FLASH_Lock(); __enable_irq(); return -1; } // 4. 写回更新后的数据 uint32_t *src = (uint32_t*)&config_backup; for (int i = 0; i < sizeof(system_config_t)/4; i++) { if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, PARAM_BASE_ADDR + i*4, src[i]) != HAL_OK) { HAL_FLASH_Lock(); __enable_irq(); return -1; } } HAL_FLASH_Lock(); __enable_irq(); return 0; }

📌 提醒:每次保存都会擦一次扇区!频繁操作很快就会耗尽寿命。


如何延长Flash寿命?磨损均衡了解一下

前面提到,每个扇区最多支持约1万次擦写。如果每小时写一次,不到一年就报废了。

解决办法:磨损均衡(Wear Leveling)

思想很简单:别总盯着一个扇区擦,换着来。

例如,划出4个扇区组成“参数池”。每次保存时选择其中擦写次数最少的那个进行操作,并记录版本号或时间戳以便恢复最新数据。

更进一步,可以用LittleFS这类轻量级文件系统自动管理,既防磨损,又能支持多文件、断电恢复。


双Bank机制:OTA升级的安全底牌

对于支持双Bank的STM32型号(如H7、F7系列),我们可以玩更大胆的操作:在线升级而不中断运行

工作流程

  1. 当前固件运行在Bank1;
  2. 新固件通过串口/网络接收,写入Bank2;
  3. 写之前先擦除对应扇区;
  4. 全部写完后计算校验和;
  5. 校验无误,设置下次启动跳转到Bank2;
  6. 复位生效。

关键就在于:运行区和擦写区分离,互不影响。

如果没有双Bank怎么办?可以用“Bootloader + App”分区方案,原理类似,只是空间利用率低一些。


性能与寿命的关键参数(来自ST手册)

参数典型值说明
扇区擦除时间80 msF4系列,常温下
字编程时间~5 μs快速模式
擦写耐久性10,000次每扇区
数据保持期20年25°C环境

这意味着:
- 一次完整擦+写操作可能长达百毫秒级,需做好任务调度;
- 若每天写10次,单扇区可用约2.7年;
- 超过20年后,未刷新的数据可能逐渐丢失。

这些数字不是用来背的,而是设计系统时的决策依据。


最佳实践清单

必做项
- 擦除代码放在SRAM中执行
- 操作期间关闭中断
- 使用CRC或Flash ECC增强可靠性
- 擦除前后加锁保护
- 配合PVD防止低压操作

🔧优化建议
- 划分独立数据区,避免污染程序区
- 引入互斥锁防止多任务竞争
- 记录各扇区擦写次数,实现简单磨损均衡
- 对关键操作加入看门狗监控
- 错误发生时尝试进入安全模式并报警


写在最后:掌握erase,才算真正掌控STM32

很多人觉得STM32开发就是配时钟、调外设、跑RTOS。但真正决定系统稳定性的,往往是那些底层细节——尤其是像Flash擦除这种“高风险、低频次”的操作。

一次成功的OTA升级,背后是对擦除时序的精确控制;
一个十年不坏的工业设备,离不开合理的存储管理策略。

当你不再害怕动Flash,而是能从容规划它的每一次擦写,你就已经跨过了初级开发者与资深工程师之间的那道门槛。

如果你也在做参数存储、固件升级、日志记录之类的功能,欢迎在评论区分享你的实现方式和踩过的坑。我们一起把这条路走得更稳、更远。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询