news 2026/3/1 16:02:52

从零实现SPI Flash的erase功能驱动代码

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零实现SPI Flash的erase功能驱动代码

从零实现SPI Flash的erase功能驱动:不只是写代码,更是理解存储的本质

你有没有遇到过这种情况——OTA升级失败,设备卡在启动阶段;或者配置参数突然丢失,系统行为变得诡异?很多时候,这些看似“玄学”的问题,根源就藏在一个不起眼的操作里:擦除(erase)

在嵌入式开发中,我们习惯性地调用flash_write(),却常常忽略了它背后的前提条件:必须先擦除,才能写入。RAM可以随意覆写,但Flash不行。它的物理特性决定了每一次写入前都必须经历一场“清场仪式”——这就是erase的意义。

今天,我们就来亲手实现一个真正可靠的SPI Flasherase驱动。不抄库、不套壳,从硬件原理到代码落地,一步步构建属于你的底层能力。


为什么不能直接写?Flash的“单向门”机制

要搞懂erase,首先要明白Flash是怎么存数据的。

SPI Flash内部使用的是浮栅晶体管(Floating Gate Transistor)。你可以把它想象成一个带电荷陷阱的小房间:

  • 房间空着 → 表示“1”
  • 房间塞满电子 → 表示“0”

编程(program)操作就是往房间里“扔电子”,把“1”变成“0”。这个过程相对容易,靠隧道效应就能完成。

但你想把电子“拿出来”呢?难了。这需要施加高压(约12V),触发Fowler-Nordheim隧穿,强行把电子拉出来——也就是擦除操作。

所以,Flash有个铁律:

只能将位由1变为0,无法反向操作

这意味着:如果你想改写某个字节,比如原来是0xFF(全是1),现在想写成0x55,没问题,直接program就行。但如果原来已经是0xAA(部分为0),你还想变回0xFF?唯一办法是——整块擦除,全部重置为1。

这就引出了我们的核心任务:安全、可靠地执行擦除操作


擦除不是一键清空,而是精密时序的艺术

别以为erase就是发个命令就完事了。它是一套严格的流程控制,稍有不慎就会导致芯片“罢工”。

以常见的Winbond W25Q64为例,执行一次扇区擦除(4KB)需要以下步骤:

  1. 发送写使能(Write Enable)
  2. 发送擦除命令 + 地址
  3. 轮询状态寄存器,等待完成

看起来简单?但每一步都有坑。

第一步:写使能 —— 芯片的“解锁开关”

几乎所有SPI Flash芯片在上电后默认处于“只读”状态。你要修改内容?先喊一声:“我要写了!”

这就是写使能命令0x06的作用。它会置位状态寄存器中的WEL(Write Enable Latch)标志位,告诉芯片:“接下来我要发起写或擦除操作。”

重点来了:

WEL是瞬态的!每次上电、复位或操作完成后都会自动清零。

也就是说,哪怕你刚刚发过0x06,只要中间执行了一次读操作,WEL可能就被清除了。所以——
✅ 正确做法:每次擦除前,必须重新发送0x06

第二步:地址对齐 —— 硬件说了算

Flash的擦除单位是固定的。W25Q系列中:

擦除类型大小命令
扇区擦除(Sector Erase)4KB (4096B)0x20
块擦除(Block Erase)32KB / 64KB0x52,0xD8
芯片擦除(Chip Erase)全部0xC7

这意味着:你不能随便指定一个地址去擦除。例如,你想擦除地址0x1234,而这个地址位于第5个扇区(起始于0x1000)。那么你必须传入0x1000,而不是0x1234

否则会发生什么?
❌ 芯片不会报错!但它会根据你给的地址,自动计算所属扇区并擦除整个扇区。如果你没意识到这一点,可能会误删不该动的数据。

所以我们必须做一件事:地址校验与对齐检查

#define SECTOR_SIZE (4096U) /** * @brief 判断地址是否为4KB扇区对齐 */ static inline int is_sector_aligned(uint32_t addr) { return (addr & (SECTOR_SIZE - 1)) == 0; }

在调用擦除函数时,先检查:

if (!is_sector_aligned(addr)) { // 可选处理方式: // 1. 返回错误码 // 2. 自动向下对齐(危险!) // 3. 断言终止(调试期推荐) return -1; }

我建议在调试阶段使用断言,上线后返回错误码并记录日志。

第三步:等待完成 —— 别让CPU干等

擦除一个扇区要多久?

查手册就知道:典型值400ms,最大可达3秒(高温下更久)!

如果主线程一直卡在这里,系统基本就瘫痪了。但我们又不能不管它——因为后续任何操作都会失败。

解决方案只有一个:轮询状态寄存器

SPI Flash有一个状态寄存器(Status Register),通常通过命令0x05读取。其中最关键的一位是:

  • Bit 0: BUSY—— 1表示正在忙,0表示空闲

于是我们可以这样写:

void spi_flash_wait_ready(void) { uint8_t status; do { spi_flash_send_cmd(0x05); // 发送读状态命令 HAL_SPI_Receive(&hspi1, &status, 1, 10); // 接收1字节 } while (status & 0x01); // BUSY位仍为1 }

注意这里HAL_SPI_Receive的超时设为10ms,避免因通信异常导致永久阻塞。

但这还不够优雅。更好的做法是加入最大等待时间限制,防止死循环:

#define ERASE_TIMEOUT_MS 5000 // 最大等待5秒 uint32_t start = get_tick_ms(); // 获取当前毫秒计数 do { if ((get_tick_ms() - start) > ERASE_TIMEOUT_MS) { // 超时处理:记录错误、尝试复位等 break; } spi_flash_send_cmd(0x05); HAL_SPI_Receive(&hspi1, &status, 1, 10); } while (status & 0x01);

这里的get_tick_ms()可以来自SysTick、RTC或其他定时源。


核心驱动代码:简洁、健壮、可移植

下面是我们最终封装的扇区擦除函数:

/** * @brief 擦除指定地址所在的4KB扇区 * @param addr: 目标地址(需4KB对齐) * @return 0: 成功, -1: 参数错误, -2: 超时 */ int spi_flash_erase_sector(uint32_t addr) { // 1. 地址对齐检查 if ((addr & 0xFFF) != 0) { return -1; // 非法地址 } // 2. 写使能 spi_flash_send_cmd(0x06); // 3. 发送扇区擦除命令 + 24位地址 uint8_t tx[4] = {0x20, (uint8_t)(addr >> 16), (uint8_t)(addr >> 8), (uint8_t)addr}; HAL_SPI_Transmit(&hspi1, tx, 4, 100); // 4. 等待完成(带超时) uint32_t start = get_tick_ms(); uint8_t status; do { if (get_tick_ms() - start > ERASE_TIMEOUT_MS) { return -2; // 超时 } spi_flash_send_cmd(0x05); HAL_SPI_Receive(&hspi1, &status, 1, 10); } while (status & 0x01); return 0; }

这段代码有几个关键设计点:

  • 参数验证前置:尽早发现问题
  • 写使能必发:确保WEL置位
  • 命令+地址打包传输:减少CS切换次数,提升稳定性
  • 超时机制:避免无限等待
  • 返回值语义清晰:便于上层错误处理

实际应用中的三大陷阱与应对策略

坑点1:断电导致“半擦除”状态

最可怕的不是擦不掉,而是擦了一半断电。此时部分页为全0xFF,部分仍是旧数据,整个扇区处于不确定状态。

秘籍
- 使用双备份机制:如A/B分区交替更新
- 引入日志标记:在擦除前写入“正在更新”标志,完成后清除
- 支持恢复模式:启动时检测标志位,发现异常则回滚

坑点2:频繁擦写加速老化

SPI Flash寿命一般为10万次擦写周期(P/E cycles)。如果你的日志系统每秒写一次,不到两天就会耗尽一个扇区寿命!

秘籍
- 实施磨损均衡(Wear Leveling):动态分配写入位置
- 合并小写操作:缓存多次修改,批量写入
- 使用环形日志结构:固定区域循环覆盖,平均磨损

坑点3:多任务环境下的并发冲突

RTOS下多个任务同时访问Flash?灾难现场。

秘籍
- 使用互斥锁(Mutex)保护SPI总线和Flash操作
- 抽象出串行化队列:所有Flash请求排队处理
- 关键操作期间禁用中断(慎用)


更进一步:如何让它真正“通用”?

目前代码还绑定在STM32 HAL库上。要想跨平台复用,我们需要做一层抽象:

// 定义底层接口 typedef struct { void (*transmit)(const uint8_t *data, size_t len); void (*receive)(uint8_t *data, size_t len); void (*delay_ms)(uint32_t ms); } spi_flash_io_t; // 全局句柄 static spi_flash_io_t *io; // 初始化时注入具体实现 void spi_flash_init(spi_flash_io_t *ops) { io = ops; } // 替换原函数中的HAL调用 static void send_cmd(uint8_t cmd) { io->transmit(&cmd, 1); }

这样,无论是裸机、FreeRTOS还是Zephyr,只需提供对应的transmit/receive函数即可无缝迁移。


结语:擦除虽小,责任重大

erase只是一个小小的函数,但它承载的是整个系统的数据完整性。

当你写下spi_flash_erase_sector(0x00100000)时,不只是在清除一片内存,更是在执行一项高风险操作:
- 它可能影响OTA升级成败
- 它关系到用户配置能否保存
- 它决定设备断电后是否还能正常启动

所以,请务必做到:

✅ 每次擦除前发送写使能
✅ 严格校验地址对齐
✅ 绝不忽略状态轮询
✅ 加入超时与错误处理
✅ 在系统层面设计容灾机制

掌握了erase,你就掌握了Flash管理的第一把钥匙。下一步,我们可以基于它实现page_programread,最终构建完整的Flash抽象层(FAL),甚至集成LittleFS这样的轻量文件系统。

如果你正在做固件升级、日志存储或配置管理,欢迎在评论区分享你的挑战,我们一起探讨解决方案。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/26 3:04:16

Centrifuge实战:构建高可用实时消息系统的完整指南

Centrifuge实战:构建高可用实时消息系统的完整指南 【免费下载链接】centrifuge Real-time messaging library for Go. The simplest way to add feature-rich and scalable WebSocket support to your application. The core of Centrifugo server. 项目地址: ht…

作者头像 李华
网站建设 2026/2/21 3:51:34

VERT文件转换工具终极指南:从零开始掌握本地化格式转换

VERT文件转换工具终极指南:从零开始掌握本地化格式转换 【免费下载链接】VERT The next-generation file converter. Open source, fully local* and free forever. 项目地址: https://gitcode.com/gh_mirrors/ve/VERT 在数字文件格式日益丰富的今天&#xf…

作者头像 李华
网站建设 2026/2/27 9:18:13

Open-AutoGLM移动端落地难?资深工程师亲授手机端高效部署秘诀

第一章:Open-AutoGLM移动端落地难?资深工程师亲授手机端高效部署秘诀在将 Open-AutoGLM 这类大型语言模型部署至移动端时,许多开发者面临推理延迟高、内存占用大和设备兼容性差等问题。然而,通过合理的模型压缩与运行时优化策略&a…

作者头像 李华
网站建设 2026/2/27 8:49:03

Apache Arrow与PostgreSQL集成:7种高效数据连接方案完整教程

Apache Arrow与PostgreSQL集成:7种高效数据连接方案完整教程 【免费下载链接】arrow Apache Arrow is a multi-language toolbox for accelerated data interchange and in-memory processing 项目地址: https://gitcode.com/gh_mirrors/arrow13/arrow Apach…

作者头像 李华
网站建设 2026/2/26 15:34:21

【大模型自动化新纪元】:Open-AutoGLM为何成为AI工程师的必备工具?

第一章:Open-AutoGLM的核心价值与行业影响Open-AutoGLM 作为新一代开源自动化通用语言模型框架,正在重塑企业级AI应用的开发范式。其核心价值不仅体现在模型性能的显著提升,更在于对开发效率、部署成本和行业适配性的全面优化。推动AI工程化落…

作者头像 李华
网站建设 2026/2/24 11:54:00

超详细版LCD显示屏驱动时序分析:适合新手学习

搞懂LCD显示时序:从原理到实战,新手也能轻松上手你有没有遇到过这样的情况?屏幕背光亮了,但画面却是花屏、错位,甚至完全黑屏——明明代码烧进去了,引脚也接对了,为什么就是出不来图像&#xff…

作者头像 李华