深入解析SPI FLASH扇区擦除:从时序图到代码实现的完整指南

发布时间:2026/7/15 6:43:38
深入解析SPI FLASH扇区擦除:从时序图到代码实现的完整指南 1. SPI FLASH扇区擦除的核心原理第一次接触SPI FLASH时我被它的擦除机制搞得很困惑——为什么不能像操作RAM那样直接修改数据后来才发现这跟FLASH的物理结构密切相关。想象FLASH存储单元就像一排排小灯泡写操作只能把亮着的灯1熄灭0而擦除操作相当于把所有灯重新点亮恢复为1。这就是为什么在写入新数据前必须先擦除。SPI FLASH的扇区擦除指令20h会清除4KB大小的存储区域。这个过程中最关键的四个步骤构成了完整的操作闭环首先用06h指令解除写保护就像打开保险柜的锁然后发送20h指令指定要擦除的地址接着持续检查状态寄存器直到BUSY位清零确认擦除完成最后用04h指令重新上锁。这种严谨的流程设计能有效防止误操作导致数据丢失。2. 时序图深度解析拿到芯片手册时我最先看的就是时序图。以W25Q系列为例扇区擦除的时序包含几个关键阶段片选信号拉低CS引脚从高电平跳变到低电平相当于按下对讲机的通话键告诉FLASH准备接收指令。指令传输阶段在SCK时钟上升沿主机通过MOSI线发送20h指令码。这里要注意SPI模式设置——常见FLASH通常工作在模式0CPOL0, CPHA0或模式3CPOL1, CPHA1。地址传输阶段紧接指令码之后发送24位地址分三个字节传输。例如要擦除0x001000地址开始的扇区需要依次发送0x00、0x10、0x00。片选信号拉高CS引脚上升沿触发擦除操作就像松开对讲机按键表示指令发送完毕。实际调试时我发现CS拉高后的保持时间(tSHSL)很关键。有次因为没留足时间导致擦除失败后来在代码里加了10μs延时才解决。不同厂商的FLASH对这个参数要求不同Winbond通常要求至少1μs而某些国产芯片可能需要更长。3. 代码实现详解基于STM32的标准外设库我总结出最稳定的实现方案。先看写使能函数void FLASH_WriteEnable(void) { GPIO_ResetBits(FLASH_CS_PORT, FLASH_CS_PIN); // CS拉低 SPI_SendData(SPI1, 0x06); // 发送06h指令 GPIO_SetBits(FLASH_CS_PORT, FLASH_CS_PIN); // CS拉高 Delay_us(1); // 保持时间 }这个简单的函数却有个易错点有些工程师会忘记CS拉高后的保持时间。我曾在产品量产时遇到偶发性的写使能失败最后发现是硬件上CS走线过长导致信号畸变增加延时后问题消失。扇区擦除的核心函数需要更严谨的处理void FLASH_EraseSector(uint32_t addr) { FLASH_WriteEnable(); // 第一步写使能 GPIO_ResetBits(FLASH_CS_PORT, FLASH_CS_PIN); SPI_SendData(SPI1, 0x20); // 发送擦除指令 SPI_SendData(SPI1, (addr 16) 0xFF); // 地址字节1 SPI_SendData(SPI1, (addr 8) 0xFF); // 地址字节2 SPI_SendData(SPI1, addr 0xFF); // 地址字节3 GPIO_SetBits(FLASH_CS_PORT, FLASH_CS_PIN); Delay_us(10); // 关键延时 while(FLASH_IsBusy()); // 等待擦除完成 FLASH_WriteDisable(); // 最后写保护 }实际项目中我建议在擦除前先备份该扇区数据。有次OTA升级时突然断电导致固件扇区被擦除但新数据没写完设备变砖只能返厂。后来我们在代码里增加了擦除前自动备份到其他扇区的机制。4. 状态寄存器的正确用法状态寄存器是FLASH与主机对话的窗口其中第0位BUSY标志最重要。读取状态的典型实现uint8_t FLASH_ReadStatus(void) { uint8_t status; GPIO_ResetBits(FLASH_CS_PORT, FLASH_CS_PIN); SPI_SendData(SPI1, 0x05); // 读状态指令 status SPI_ReceiveData(SPI1); // 获取状态值 GPIO_SetBits(FLASH_CS_PORT, FLASH_CS_PIN); return status; }调试时我发现个有趣现象某些国产FLASH芯片的BUSY位反应较慢直接连续读取可能导致误判。后来改为首次读取后延迟1ms再二次确认稳定性大幅提升。对于需要高效率的场景可以采用中断方式代替轮询。例如在RTOS中创建专用任务void FLASH_Task(void *pArg) { while(1) { if(erase_request) { StartErase(); while(FLASH_IsBusy()) { osDelay(1); // 释放CPU资源 } PostEraseComplete(); } osDelay(10); } }5. 常见问题排查指南问题1擦除后校验发现未清除干净可能原因电压不稳导致擦除中断扇区地址计算错误芯片已超过擦写寿命通常10万次问题2擦除时间远超规格书标注排查步骤检查SPI时钟是否达到最大允许频率测量CS信号上升沿是否干净确认电源电压在3.0V-3.6V范围内问题3偶尔出现写使能失败解决方案在WriteEnable()后增加状态寄存器检查缩短CS走线长度或降低SPI速度尝试在指令间插入微小延时去年调试某工业项目时FLASH在低温环境下频繁擦除失败。最终发现是PCB布局问题导致信号完整性下降重新设计布线后通过-40℃测试。这个案例让我深刻体会到硬件设计同样影响软件可靠性。6. 性能优化技巧批量擦除策略 对于需要清理大块区域的场景可以组合使用不同擦除指令// 先尝试64KB块擦除(D8h) if((addr % 65536) 0 size 65536) { UseBlockErase64K(); } // 次选32KB块擦除(52h) else if((addr % 32768) 0 size 32768) { UseBlockErase32K(); } // 最后用扇区擦除 else { UseSectorErase(); }并行操作优化 在支持双通道的MCU上可以用DMA传输数据的同时执行擦除操作。以STM32H7为例// 启动DMA传输 HAL_SPI_Transmit_DMA(hspi1, txData, length); // 在SPI1忙时用SPI2执行擦除 if(HAL_SPI_GetState(hspi1) ! HAL_SPI_STATE_READY) { FLASH_EraseSector_SPI2(addr); }预读取加速 对于已知要修改的部分数据可以先读取整个扇区到RAM在内存中修改后再整体擦除写入void SmartUpdate(uint32_t addr, uint8_t *newData, uint16_t len) { uint8_t buffer[4096]; FLASH_Read(addr, buffer, 4096); // 预读取 memcpy(buffer (addr % 4096), newData, len); // 内存修改 FLASH_EraseSector(addr); FLASH_Write(addr, buffer, 4096); // 整页写入 }7. 安全防护机制意外写保护 建议在关键代码段添加保护锁__disable_irq(); FLASH_CriticalOperation(); __enable_irq();电源监测 使用MCU的PVD功能在电压跌落时紧急保护void PVD_IRQHandler(void) { if(FLASH_OperationInProgress) { FLASH_WriteDisable(); // 紧急上锁 } // ...其他处理 }操作日志 在单独扇区记录重要操作void WriteOperationLog(uint8_t cmd, uint32_t addr) { uint32_t timestamp HAL_GetTick(); uint8_t log[8]; memcpy(log, timestamp, 4); log[4] cmd; memcpy(log5, addr, 3); FLASH_Write(LOG_SECTOR_ADDR log_offset, log, 8); log_offset (log_offset 8) % 4096; }在智能家居网关项目中我们通过这种日志机制成功复现了现场设备异常擦除的原因——原来是电源模块故障导致的异常复位。