乐山市网站建设_网站建设公司_定制开发_seo优化
2025/12/31 7:22:48 网站建设 项目流程

手把手教你实现 Flash 擦除:从原理到实战,彻底搞懂嵌入式存储底层操作

你有没有遇到过这样的情况:在做固件升级时,明明写入了新代码,设备却始终运行旧程序?或者尝试保存一个配置参数,结果读出来还是老值?这类“数据没更新”的诡异问题,90% 的根源都出在一个被很多人忽略的基础操作上——Flash 擦除(erase)

今天我们就来揭开这个嵌入式开发中的“隐形门槛”。不讲空话,不堆术语,带你从零开始,亲手实现一个可靠的 Flash erase 功能,并深入理解它背后的机制与陷阱。


为什么不能直接往 Flash 写数据?

我们都知道 RAM 可以随意读写,但 Flash 不行。这不是设计缺陷,而是物理结构决定的。

Flash 存储单元基于浮栅晶体管,它的特性很特别:

只能将 1 改为 0,不能将 0 改为 1。

这意味着什么?
假设你有一块刚擦过的区域,所有位都是1(即字节值为0xFF)。这时你可以写入任意数据,比如把某些位改成0来表示0x5A

但如果你想再改回来呢?比如从0x5A变成0xFF—— 这意味着要把一些0改成1,而这是硬件不允许的!

唯一的办法就是先执行erase,把整个扇区恢复成全1状态。这就像白板上的记号笔字迹,想修改就得先用板擦清空整片区域。

所以,在任何写入之前,必须先擦除。这是铁律。


Flash 擦除的本质是什么?

擦除不是软件层面的“清零”,而是一次高压物理操作

控制器会在目标存储单元施加反向电压,迫使电子通过隧穿效应离开浮栅,从而使晶体管回到导通状态(逻辑 1)。这个过程耗时较长(通常几毫秒到几十毫秒),且会对材料造成微小损耗。

因此:

  • 每个扇区有寿命限制(常见 1万~10万次擦写)
  • 擦除单位远大于写入单位(最小常为 4KB 扇区)
  • 一旦启动,不可中断(断电可能导致扇区损坏)

这些特性决定了我们必须谨慎对待每一次 erase 操作。


实战:在 STM32 上实现安全的扇区擦除

我们以 STM32F4 系列为例(ARM Cortex-M4 内核),使用 HAL 库完成一次完整的扇区擦除。这套流程适用于绝大多数现代 MCU,只是寄存器名略有差异。

第一步:包含头文件并初始化系统

#include "stm32f4xx_hal.h" #include "stm32f4xx_hal_flash.h" int main(void) { HAL_Init(); SystemClock_Config(); // 用户定义的时钟配置函数

别忘了调用HAL_Init()初始化 HAL 层,否则后续操作可能失败。


第二步:确定你要擦的是哪一块

STM32F4 的 Flash 划分为多个扇区,不同型号大小不同。例如 STM32F407VG 拥有 1MB Flash,共 12 个扇区:

扇区起始地址大小
00x0800000016 KB
10x0800400016 KB
20x0800800016 KB
50x0802000064 KB

我们要擦除扇区 5,对应宏定义为FLASH_SECTOR_5

uint32_t target_sector = FLASH_SECTOR_5;

第三步:解锁 Flash 控制器

Flash 默认处于写保护状态,防止意外修改。要进行擦除或编程,必须先解锁。

HAL_FLASH_Unlock();

这条指令会清除FLASH_CR寄存器中的LOCK位。如果不解锁就直接操作,硬件会拒绝并返回错误。


第四步:配置擦除参数

我们需要告诉控制器:“我要擦哪个扇区、擦几个、供电电压是多少”。

FLASH_EraseInitTypeDef erase_config; uint32_t error_code; erase_config.TypeErase = FLASH_TYPEERASE_SECTORS; // 类型:扇区擦除 erase_config.Sector = target_sector; // 目标扇区 erase_config.NbSectors = 1; // 擦 1 个 erase_config.VoltageRange = FLASH_VOLTAGE_RANGE_3; // 2.7V ~ 3.6V

其中VoltageRange很关键。如果你的系统工作在 3.3V,选RANGE_3;如果是 1.8V 平台,则需选择对应范围。配错可能导致命令被忽略或误动作。


第五步:执行擦除并检查结果

调用库函数触发擦除:

if (HAL_FLASHEx_Erase(&erase_config, &error_code) != HAL_OK) { // 擦除失败!处理错误 Error_Handler(); }

函数会自动轮询忙标志位(BSY),直到操作完成。如果中途发生错误(如地址非法、电压异常等),会通过返回值和error_code告知具体原因。


第六步:重新上锁,保护系统

操作完成后,务必重新加锁!

HAL_FLASH_Lock();

这是很多初学者忽略的关键一步。不上锁的话,后续代码万一误触发写操作,可能会覆盖关键固件,导致系统崩溃。


封装成通用函数:可复用才是好代码

把上面逻辑打包成一个独立函数,方便以后调用:

/** * @brief 擦除指定 Flash 扇区 * @param sector: 扇区编号(如 FLASH_SECTOR_5) * @retval HAL_OK 成功,HAL_ERROR 失败 */ HAL_StatusTypeDef Flash_Erase_Sector(uint32_t sector) { FLASH_EraseInitTypeDef cfg; uint32_t error; HAL_FLASH_Unlock(); cfg.TypeErase = FLASH_TYPEERASE_SECTORS; cfg.Sector = sector; cfg.NbSectors = 1; cfg.VoltageRange = FLASH_VOLTAGE_RANGE_3; if (HAL_FLASHEx_Erase(&cfg, &error) != HAL_OK) { HAL_FLASH_Lock(); // 出错也要上锁 return HAL_ERROR; } HAL_FLASH_Lock(); return HAL_OK; }

现在你可以在 OTA 升级、参数保存等场景中放心调用了:

if (Flash_Erase_Sector(FLASH_SECTOR_10) == HAL_OK) { printf("扇区擦除成功,准备写入新数据\n"); } else { printf("擦除失败,请检查电源或地址合法性\n"); }

那些年踩过的坑:新手常见误区与应对策略

别以为写了代码就能高枕无忧。以下是实际项目中最容易翻车的几个点:

❌ 错误 1:擦了自己的代码

// 千万别这么干! Flash_Erase_Sector(FLASH_SECTOR_0); // 正在运行的代码就在这一区!

CPU 正在从 Flash 取指执行,你却把它擦了……轻则 HardFault,重则变砖。

正确做法:只擦非当前运行区。OTA 升级时,通常保留两份应用空间(A/B 分区),交替擦写。


❌ 错误 2:电源不稳定还强行擦除

Flash 擦除需要稳定电压。若低于 2.7V 强行操作,可能造成部分单元未完全释放电荷,导致后续写入失败或数据混乱。

建议
- 添加电压监测电路(PVD)
- 在低电压下禁止执行 erase
- 使用备份电源(超级电容)确保关键操作完成


❌ 错误 3:频繁擦写同一扇区,寿命耗尽

商用 Flash 寿命约 10,000 次。如果你每分钟记录一次日志到同一个扇区,不到一周就会报废。

解决方案:引入磨损均衡(Wear Leveling)

思路很简单:不要总盯着一个扇区写,换着来。

比如你有 4 个备用扇区,每次写日志前先找最“年轻”的那个(擦写次数最少)使用。

uint32_t best_sector = find_least_used_sector(log_sectors, 4); Flash_Erase_Sector(best_sector); write_log_to_sector(best_sector, log_data); update_usage_counter(best_sector);

哪怕不用复杂算法,简单的轮询也能显著延长整体寿命。


❌ 错误 4:多任务环境下并发访问

在 FreeRTOS 或其他 RTOS 中,多个任务同时调用 Flash 操作?

后果不堪设想:擦一半又被另一个任务打断,状态寄存器错乱,甚至锁死控制器。

解决方法:加互斥锁

osMutexId_t flash_mutex; void safe_erase(uint32_t sector) { osMutexWait(flash_mutex, osWaitForever); Flash_Erase_Sector(sector); osMutexRelease(flash_mutex); }

确保同一时间只有一个任务能操作 Flash。


如何验证擦除是否真的成功?

别信“返回 HAL_OK”就万事大吉。最好手动读回数据确认:

bool is_erased = true; uint32_t *addr = (uint32_t*)0x08020000; // 扇区起始地址 for (int i = 0; i < 1024; i++) { // 检查前 4KB if (addr[i] != 0xFFFFFFFF) { is_erased = false; break; } } if (is_erased) { printf("擦除验证通过 ✅\n"); } else { printf("警告:擦除不完整 ❌\n"); }

注意:不需要检查整个扇区,抽样即可。毕竟全读一遍也挺耗时。


它到底用在哪?真实应用场景解析

掌握了基础技能,来看看它如何支撑核心功能。

场景一:无线固件升级(OTA)

流程如下:

  1. 下载新固件包 → 校验完整性
  2. 调用Flash_Erase_Sector(APP_AREA)清空旧程序区
  3. 分页写入新固件(HAL_FLASH_Program
  4. 更新启动标志 → 重启跳转

⚠️ 关键点:erase 必须在写入前完成。否则原有数据残留,新固件无法正确烧录。


场景二:参数存储(替代 EEPROM)

许多低成本 MCU 没有内置 EEPROM,开发者常用一小块 Flash 模拟。

典型做法:

  • 划出 1~2 个扇区专用于存储配置
  • SRAM 中缓存当前参数
  • 修改后标记“脏”,定时批量刷入 Flash
  • 每次刷入前先擦除该扇区

为了进一步提升可靠性,还可采用双缓冲机制

  • A 扇区正在用,B 扇区备用
  • 修改时擦 B、写 B、切换指针
  • 原 A 成为新备用区

这样即使断电,至少有一个副本是完整的。


场景三:运行日志记录

工业设备常需记录故障码、操作轨迹等信息。

挑战在于:日志不断增长,而 Flash 容量有限。

解决方案:

  • 使用循环日志(Circular Log)
  • 设定 N 个日志扇区
  • 按顺序写,满后擦最早的那个继续写

结合前面提到的磨损均衡,可让每个扇区均匀分担负载。


更进一步的设计思考

当你已经能熟练实现基本功能,下一步该关注哪些工程细节?

✅ 合理规划 Flash 分区

建议在项目初期就明确划分:

区域用途是否允许擦除
Bootloader启动代码极少,仅升级时
Application主程序OTA 时整体擦除
Config配置参数允许,频率较低
Log日志允许,需磨损均衡
Backup关键数据备份仅故障恢复时使用

避免混用,降低风险。


✅ 抽象出统一接口,便于移植

不要把 HAL 库调用散落在各处。封装一层驱动层:

typedef struct { int (*init)(void); int (*erase_sector)(uint32_t sector); int (*write_page)(uint32_t addr, const uint8_t *data); int (*read)(uint32_t addr, uint8_t *buf, size_t len); } flash_driver_t;

将来换芯片或换 SDK,只需替换底层实现,上层逻辑不动。


✅ 加入 ECC 和校验机制(高端应用)

部分 MCU 支持 Flash ECC(错误纠正码),可在读取时自动修复单比特错误。开启后能显著提升长期数据可靠性。

此外,每次写入后计算 CRC 并保存,下次读取时校验,也是一种简单有效的防护手段。


写在最后:为什么每一个嵌入式工程师都要懂 Flash 擦除?

因为它不只是一个 API 调用,而是连接软件与硬件、理想与现实的桥梁。

你写的每一行代码,最终都要落在 Flash 上才能持久存在。而每一次成功的写入背后,都有一次沉默的擦除在默默铺路。

掌握它,你就掌握了:

  • 固件升级的主动权
  • 数据存储的可控性
  • 系统稳定性的底线

更重要的是,你会开始用“物理视角”看待内存管理——不再认为“存个数而已有什么难的”,而是意识到每一次操作背后的代价与约束。

随着新型存储技术的发展,QLC NAND、HyperFlash、甚至 MRAM 逐渐进入嵌入式领域,但“先擦后写”的基本逻辑依然成立。今天你在 STM32 上练的手感,未来一样可以用在更复杂的平台上。


如果你正在做一个需要保存数据的项目,不妨停下来问问自己:

“我有没有在写入前正确地擦除目标区域?”

如果答案不确定,那就赶紧去补上这一课吧。

有任何疑问或实战经验分享,欢迎留言讨论 👇

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

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

立即咨询