1. W25Q128闪存芯片基础认知
第一次接触W25Q128是在去年一个智能家居项目里,当时需要存储大量设备配置和日志数据。这个指甲盖大小的芯片居然有16MB容量,实测下来发现它比想象中强大得多。W25Q128属于SPI NOR Flash家族,采用标准的8引脚封装,通过四线SPI接口就能实现高速数据传输。我特别喜欢它的宽电压特性(2.7V-3.6V),在电池供电场景下特别稳。
芯片内部采用分级存储结构:
- 256个块(Block),每块64KB
- 每个块包含16个扇区(Sector),每扇区4KB
- 每扇区又分为16页(Page),每页256字节
这种结构直接影响我们的操作方式:
- 最小擦除单位是扇区(必须整块擦除)
- 最小写入单位是页(单次最多写256字节)
- 读取则可以按字节操作
注意:擦除操作会把数据全部置为0xFF,写入只能将1改为0,所以必须先擦后写
2. 硬件连接与SPI配置
上周帮学弟调试STM32F103驱动W25Q128时,发现硬件连接有讲究。以常见SOIC-8封装为例:
| W25Q128引脚 | STM32对应引脚 | 功能说明 |
|---|---|---|
| /CS | PB12 | 片选信号(低有效) |
| DO(MISO) | PB14 | 主设备输入 |
| /WP | 接VCC | 写保护(高禁用) |
| GND | GND | 地线 |
| DI(MOSI) | PB15 | 主设备输出 |
| /HOLD | 接VCC | 保持功能禁用 |
| CLK | PB13 | 时钟信号 |
| VCC | 3.3V | 电源 |
在CubeMX里配置SPI要注意三个关键点:
- 时钟极性(CPOL)设为High
- 时钟相位(CPHA)选2Edge
- 波特率预分频建议先用SPI_BAUDRATEPRESCALER_8
// SPI初始化代码片段 hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH; hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;3. 底层驱动开发实战
3.1 基础通信函数
先实现最核心的字节收发函数,这里有个坑要注意:STM32的SPI收发是同步的,每次发送都会同时接收数据。
uint8_t SPI_TransmitReceive(uint8_t data) { while(!__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_TXE)); HAL_SPI_TransmitReceive(&hspi1, &data, &data, 1, 100); return data; }3.2 关键指令实现
读设备ID是最常用的调试手段,可以验证SPI通信是否正常:
uint16_t W25Q128_ReadID(void) { uint16_t id = 0; CS_LOW(); SPI_TransmitReceive(0x90); // 读ID指令 SPI_TransmitReceive(0x00); // 空字节 SPI_TransmitReceive(0x00); SPI_TransmitReceive(0x00); id |= SPI_TransmitReceive(0xFF) << 8; // 厂商ID id |= SPI_TransmitReceive(0xFF); // 设备ID CS_HIGH(); return id; // 正常应返回0xEF17 }写使能指令必须在每次写入前执行:
void W25Q128_WriteEnable(void) { CS_LOW(); SPI_TransmitReceive(0x06); // 写使能指令 CS_HIGH(); }4. 存储操作三件套
4.1 数据读取
读取是最简单的操作,支持连续读取:
void W25Q128_ReadData(uint8_t *pBuffer, uint32_t addr, uint16_t len) { CS_LOW(); SPI_TransmitReceive(0x03); // 读指令 SPI_TransmitReceive(addr >> 16); SPI_TransmitReceive(addr >> 8); SPI_TransmitReceive(addr); while(len--) { *pBuffer++ = SPI_TransmitReceive(0xFF); } CS_HIGH(); }4.2 数据写入
写入流程复杂些,必须遵循"擦除->写使能->写入"的顺序:
void W25Q128_WritePage(uint8_t *pData, uint32_t addr, uint16_t len) { // 检查是否跨页(每页256字节) if(((addr & 0xFF) + len) > 256) { len = 256 - (addr & 0xFF); } W25Q128_WriteEnable(); CS_LOW(); SPI_TransmitReceive(0x02); // 页编程指令 SPI_TransmitReceive(addr >> 16); SPI_TransmitReceive(addr >> 8); SPI_TransmitReceive(addr); while(len--) { SPI_TransmitReceive(*pData++); } CS_HIGH(); W25Q128_WaitBusy(); // 等待写入完成 }4.3 扇区擦除
擦除时间较长(约150ms),建议配合RTOS使用:
void W25Q128_EraseSector(uint32_t addr) { W25Q128_WriteEnable(); CS_LOW(); SPI_TransmitReceive(0x20); // 扇区擦除指令 SPI_TransmitReceive(addr >> 16); SPI_TransmitReceive(addr >> 8); SPI_TransmitReceive(addr); CS_HIGH(); W25Q128_WaitBusy(); }5. 实战案例:日志存储系统
去年给工业传感器项目做的日志系统,正好用上了W25Q128。分享下关键设计:
存储结构设计
- 前4KB存放元数据(日志指针、校验值等)
- 后续空间按4KB为单位存储日志包
- 采用循环写入方式
关键实现代码
#define LOG_START_ADDR 0x1000 // 跳过前4KB #define LOG_SECTOR_SIZE 4096 void Log_Write(uint8_t *data, uint16_t len) { static uint32_t log_ptr = LOG_START_ADDR; // 检查是否需要擦除新扇区 if((log_ptr % LOG_SECTOR_SIZE) == 0) { W25Q128_EraseSector(log_ptr); } // 写入数据 W25Q128_WritePage(data, log_ptr, len); log_ptr += len; // 到达末尾则循环 if(log_ptr >= (16*1024*1024 - LOG_SECTOR_SIZE)) { log_ptr = LOG_START_ADDR; } }- 性能优化技巧
- 使用双缓冲:一个缓冲接收新数据,另一个缓冲执行写入
- 定期整理碎片:每月全片擦除一次
- 添加ECC校验:每256字节增加1字节校验
6. 常见问题排查指南
问题1:读出的全是0xFF
- 检查/CS引脚是否正常拉低
- 确认SPI模式设置正确(模式0或3)
- 测量CLK信号是否正常输出
问题2:写入失败
- 确保执行了写使能指令
- 检查/WP引脚是否被拉高
- 验证目标区域已擦除(全为0xFF)
问题3:数据异常丢失
- 注意供电稳定性,建议在VCC加100nF电容
- 避免频繁写同一扇区(寿命约10万次)
- 重要数据建议双备份存储
最近在做一个物联网网关项目,发现W25Q128在-40℃~85℃工业温度范围内表现稳定,连续运行三个月没有出现数据异常。不过要注意的是,在极端低温环境下,擦除时间会延长约20%,需要适当调整等待超时时间。