Flash Memory擦除操作全解析:从原理到实战,新手也能轻松上手
你有没有遇到过这样的情况?在做固件升级时,新程序写进去却无法运行;或者保存配置后重启发现数据“消失”了。如果你用的是SPI Flash芯片,比如W25Q64、MX25L系列,那问题很可能出在一个被很多人忽视的关键步骤——Erase(擦除)。
别小看这个操作。它不是可有可无的“清理动作”,而是Flash存储器工作的铁律。不理解它,轻则功能异常,重则把整个系统搞“砖”。
今天我们就来彻底讲清楚:为什么必须先擦除才能写入?怎么安全高效地执行一次擦除?常见的坑有哪些?代码该怎么写?
一、为什么Flash不能直接“覆盖写”?
我们平时用U盘或硬盘的时候,改个文件直接保存就行,但在嵌入式开发中,对Flash的操作却要复杂得多。核心原因在于它的物理结构。
它的本质是“浮栅晶体管”
Flash memory的基本存储单元是一种叫做浮栅晶体管(Floating Gate Transistor)的器件。你可以把它想象成一个带“电子陷阱”的开关:
- 当浮栅里没有电子时 → 表示“1”
- 当电子被注入并困在浮栅里时 → 表示“0”
而关键来了:
✅编程(Program)操作:可以把某个bit从“1”变成“0”(往浮栅注入电子)
❌但不能反向操作:无法单独把“0”变回“1”
所以,如果你想修改一个原本是“0”的位置为“1”,唯一的办法就是——整块区域一起清空,也就是擦除(Erase)。
这就引出了那句每个嵌入式开发者都该记住的话:
🔧一切写入之前,必先erase。这不是限制,而是规则。
二、擦除单位到底是啥?Sector、Block、Chip有啥区别?
既然不能按字节擦除,那就得知道最小能擦多少。
Flash的擦除是以固定大小的“块”为单位进行的,常见层级如下:
| 擦除单位 | 典型大小 | 使用场景说明 |
|---|---|---|
| Page | 256B ~ 1KB | 最小写入单位,不可单独擦除 |
| Sector | 4KB / 32KB | 最常用擦除粒度,适合配置区、日志等 |
| Block | 64KB / 128KB | 大范围擦除,效率高但灵活性低 |
| Chip | 整颗芯片 | 出厂测试或完全重置时使用 |
举个例子,以W25Q64为例:
- 支持4KB扇区擦除
- 也支持64KB块擦除
- 还能整片擦除(Chip Erase)
这意味着:哪怕你只想改一个字节,也必须先把包含它的那个4KB扇区全部擦掉,再重新写入这一页的数据。
听起来有点“浪费”?确实如此。但这正是Flash和EEPROM的最大区别之一。
| 对比项 | Flash | EEPROM |
|---|---|---|
| 擦除粒度 | 扇区/块(几KB以上) | 字节级 |
| 写入速度 | 快(页写入) | 慢 |
| 成本与密度 | 高密度、低成本 | 低密度、高成本 |
| 适用场景 | 固件、大文件、OTA包 | 参数、标志位、小变量 |
所以选型时要权衡:追求容量和性价比 → 用Flash;需要频繁微调单个字节 → 考虑外挂EEPROM。
三、一次完整的擦除是怎么发生的?
让我们看看当你调用spi_flash_erase_sector(0x00001000)时,背后到底发生了什么。
第一步:发命令前先“申请权限”——Write Enable
Flash芯片默认是“保护状态”,不会响应任何写或擦除命令。你必须先告诉它:“我要开始写了!”
这就是Write Enable(WREN)命令(0x06)的作用。
void spi_flash_write_enable(void) { uint8_t cmd = 0x06; spi_select(); // CS低电平选中设备 spi_transfer(&cmd, 1); spi_deselect(); // CS拉高结束 }注意:这个使能只对下一条写/擦命令有效。也就是说,每执行一次擦除或写入前,都得先发一遍WREN!
第二步:发送擦除命令 + 地址
不同擦除类型对应不同的指令:
| 命令(Hex) | 名称 | 功能 |
|---|---|---|
| 0x20 | Sector Erase | 擦除4KB扇区 |
| 0x52 | Block Erase (32K) | 擦除32KB块 |
| 0xD8 | Block Erase (64K) | 擦除64KB块 |
| 0xC7 | Chip Erase | 整片擦除(慎用!) |
例如,我们要擦除地址0x00001000所在的4KB扇区:
cmd[0] = 0x20; // 命令:Sector Erase cmd[1] = (address >> 16) & 0xFF; // A23~A16 cmd[2] = (address >> 8) & 0xFF; // A15~A8 cmd[3] = address & 0xFF; // A7~A0然后通过SPI发送出去。
⚠️重要提醒:地址必须对齐到扇区边界!
比如4KB扇区要求地址低12位为0 → 即addr & 0xFFF == 0。否则可能擦错区域甚至失败。
第三步:等待完成——轮询BUSY位
擦除不是瞬间完成的。典型的4KB扇区擦除需要约400ms,期间芯片处于“忙”状态。
你怎么知道它干完了?查状态寄存器!
do { spi_select(); spi_transfer(&0x05, 1); // Read Status Register spi_receive(&status, 1); spi_deselect(); } while (status & 0x01); // BUSY bit = 1 表示还在忙只要状态寄存器第0位(BUSY)为1,就说明还在擦,不能进行其他操作。
💡 小技巧:实际项目中建议加超时机制,防止死循环卡死。
四、完整代码示例:可移植的扇区擦除函数
下面是一个经过实战验证的C语言实现模板,适用于大多数SPI NOR Flash芯片(如W25Qxx、MX25Lxx等):
#include "spi_flash.h" // 启用写操作 void spi_flash_write_enable(void) { uint8_t cmd = 0x06; spi_select(); spi_transfer(&cmd, 1); spi_deselect(); } // 等待芯片空闲 void spi_flash_wait_busy(void) { uint8_t status; uint8_t cmd = 0x05; uint32_t timeout = 1000000; // 防止无限等待 do { if (--timeout == 0) { // TODO: 错误处理,记录日志或复位 return; } spi_select(); spi_transfer(&cmd, 1); spi_receive(&status, 1); spi_deselect(); } while (status & 0x01); } // 擦除指定地址所在的4KB扇区 void spi_flash_erase_sector(uint32_t address) { uint8_t cmd[4]; // 步骤1:开启写使能 spi_flash_write_enable(); // 步骤2:构造并发送擦除命令 cmd[0] = 0x20; // 4KB Sector Erase cmd[1] = (address >> 16) & 0xFF; cmd[2] = (address >> 8) & 0xFF; cmd[3] = address & 0xFF; spi_select(); spi_transfer(cmd, 4); spi_deselect(); // 步骤3:等待擦除完成 spi_flash_wait_busy(); } // 示例:擦除0x00001000处的扇区 int main(void) { system_init(); spi_flash_init(); uint32_t addr = 0x00001000; if ((addr & 0xFFF) != 0) { // 地址未对齐,应警告或调整 } spi_flash_erase_sector(addr); return 0; }📌关键点总结:
- 每次擦除前必须调用write_enable
- 地址需对齐到4KB边界
- 必须等待BUSY位清零后再进行后续操作
- 加入超时保护避免系统挂死
五、真实应用场景:OTA固件升级怎么做?
假设你要做一个WiFi远程升级功能(OTA),流程大致如下:
- 接收新的固件包 → 存入RAM缓冲区
- 找到目标写入地址(如
0x100000) - 查询该地址所属扇区是否已擦除?
- 如果没擦 → 调用erase_sector - 分页写入(每次最多256字节,遵循页边界)
- 写完后计算CRC校验
- 标记新固件有效,下次启动跳转执行
🔍注意事项:
- OTA过程中禁止同时读取Flash(如XIP运行代码),否则会总线冲突
- 建议使用双Bank机制:一半运行旧版,一半写新版,无缝切换
- 引入磨损均衡(Wear Leveling):不要每次都写同一个sector,否则寿命很快耗尽
六、那些年踩过的坑:常见错误与解决方案
| 现象 | 原因分析 | 解决方法 |
|---|---|---|
| 擦除后仍无法写入 | 忘了调write_enable() | 每次操作前务必使能 |
| 擦除卡住不动 | BUSY一直为1 | 检查供电是否稳定,SPI时序是否正确 |
| 数据写入混乱 | 地址未对齐或跨页 | 确保页写入不越界 |
| 系统变砖 | 不小心擦了Bootloader区 | 启用写保护,锁定关键区域 |
| 寿命提前报废 | 同一sector反复擦写 | 使用动态磨损均衡算法 |
🔧特别提醒:
很多初学者喜欢用逻辑分析仪抓包看命令是否发出,但忽略了电源噪声和SPI信号完整性。如果VCC波动大或MISO采样不准,也可能导致命令丢失或状态读错。
七、设计建议:如何构建可靠的Flash管理系统?
1. 合理划分分区
建议在项目初期就定义好Flash布局,例如:
| 区域 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader | 0x000000 | 32KB | 引导程序,只读 |
| Config | 0x008000 | 4KB | 用户参数,定期更新 |
| Firmware A | 0x010000 | 512KB | 应用程序镜像 |
| Log Buffer | 0x100000 | 64KB | 循环日志,高频擦写 |
可以用链接脚本.ld文件或JSON格式的分区表管理。
2. 抽象出统一驱动层
不要到处散落spi_flash_erase_sector()这种底层调用。封装成模块:
flash_driver.erase_sector(addr); flash_driver.program_page(addr, data, len); flash_driver.read_data(addr, buf, len);这样未来换芯片或加功能(如加密、压缩)更容易扩展。
3. 监控擦写次数
调试阶段可以在内存中维护一个计数器数组,记录每个sector被擦了多少次:
uint16_t erase_count[SECTOR_COUNT];跑一段时间后查看哪些区域成了“热点”,及时优化策略。
4. 注意电源设计
Flash在擦除时电流可达20mA以上,尤其是大块擦除。如果你用的是LDO供电,压降太大会导致芯片复位或操作失败。
✅ 解决方案:
- 使用DC-DC而非LDO
- 增加去耦电容(推荐10μF + 0.1μF组合)
- 关键操作期间禁用低功耗模式
5. 安全是底线
- 擦除boot区前弹出二次确认(可通过串口交互)
- 写入完成后做CRC32或SHA256校验
- 支持回滚机制:旧版本保留一份,升级失败自动还原
最后一句话
在Flash的世界里,一切写入之前,必先erase。
这不是繁琐的限制,而是由物理规律决定的必然规则。掌握它,你就掌握了嵌入式系统中最基础也最关键的存储管理能力。
当你下次面对“数据写不进去”、“程序跑飞”、“Flash变砖”等问题时,不妨先问自己一句:
“我……是不是忘了擦除?”