1. 硬件连接与SPI基础配置
第一次接触W25Q系列Flash时,最让我头疼的就是硬件连接问题。记得有次调试,因为SCK和MISO接反了,整整浪费了两天时间。W25Q16/W25Q256这类SPI Flash通常采用标准的8引脚SOIC封装,引脚定义非常规范:
- 引脚1(CS):接STM32的任意GPIO,注意要软件控制片选
- 引脚2(DO/MISO):接STM32的SPI_MISO引脚
- 引脚3(WP):硬件写保护,通常直接接高电平
- 引脚4(GND):接地
- 引脚5(DI/MOSI):接STM32的SPI_MOSI引脚
- 引脚6(CLK/SCK):接STM32的SPI_SCK引脚
- 引脚7(HOLD):暂停控制,通常接高电平
- 引脚8(VCC):3.3V供电
实测中发现个细节:VCC一定要加0.1uF去耦电容,否则高速读写时会出现数据异常。我用示波器抓取波形时发现,不加电容的电源纹波能达到200mV,加了之后降到50mV以内。
SPI模式配置是另一个容易踩坑的点。华邦的W25Q系列支持模式0和模式3,我推荐使用SPI_MODE0(CPOL=0, CPHA=0)。在STM32CubeMX中配置时,要注意三点:
- 时钟极性选择Low
- 时钟相位选择1Edge
- 数据大小设置为8bits
// 典型SPI配置代码(HAL库) 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_LOW; hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; // 10.5MHz2. 驱动函数实现关键技巧
移植驱动时,最核心的是实现SPI收发函数。我对比过三种实现方式,发现HAL库的阻塞式传输最稳定:
uint8_t SPI_ReadWriteByte(uint8_t TxData) { uint8_t RxData; HAL_SPI_TransmitReceive(&hspi1, &TxData, &RxData, 1, 100); return RxData; }**写使能(WREN)**是很多新手容易忽略的指令。每次写入前必须先发送0x06,这个设计是为了防止误操作。我在代码中封装了安全写入流程:
void W25Q_WriteEnable(void) { CS_LOW(); SPI_ReadWriteByte(0x06); // WREN CS_HIGH(); Delay_us(5); // 等待tWREN时间 }**扇区擦除(Sector Erase)**要注意地址对齐问题。W25Q的扇区大小是4KB,擦除地址必须是4096的整数倍。我遇到过地址不对齐导致相邻数据被擦除的事故,后来加了校验:
void W25Q_EraseSector(uint32_t addr) { if(addr % 4096 != 0){ printf("Error: Address not aligned!"); return; } W25Q_WriteEnable(); CS_LOW(); SPI_ReadWriteByte(0x20); // Sector Erase SPI_ReadWriteByte((addr>>16) & 0xFF); SPI_ReadWriteByte((addr>>8) & 0xFF); SPI_ReadWriteByte(addr & 0xFF); CS_HIGH(); W25Q_WaitBusy(); // 等待擦除完成 }3. 环形缓冲区特性深度解析
在测试W25Q16时,我发现个有趣现象:向0x200000地址写入数据后,居然能从0x000000读到相同内容!经过反复验证,确认这是环形缓冲区特性——当访问地址超过实际容量时,会自动回绕到起始地址。
这个特性在日志系统中特别有用。我们可以设计循环写入策略,无需手动处理地址回滚:
#define FLASH_SIZE 0x200000 // 2MB void CircularWrite(uint32_t *pAddr, uint8_t *data, uint16_t len) { if(*pAddr + len > FLASH_SIZE){ uint16_t firstPart = FLASH_SIZE - *pAddr; W25Q_Write(data, *pAddr, firstPart); W25Q_Write(data+firstPart, 0, len-firstPart); *pAddr = len - firstPart; }else{ W25Q_Write(data, *pAddr, len); *pAddr += len; } }实测性能数据:
- 连续写入速度:528KB/s(SPI时钟10.5MHz)
- 随机读取速度:1.2MB/s
- 扇区擦除时间:典型值45ms
4. 实战优化方案
四字节地址模式是W25Q256特有的功能。当使用大于16MB的地址空间时,需要先发送0xB7指令:
void W25Q_Enable4ByteMode(void) { CS_LOW(); SPI_ReadWriteByte(0xB7); // Enter 4-Byte Address Mode CS_HIGH(); }写缓存优化能显著提升性能。我的方案是开辟4KB RAM缓存,攒够一个扇区再写入:
uint8_t cache[4096]; uint32_t cacheAddr = 0; uint16_t cachePos = 0; void CacheWrite(uint32_t addr, uint8_t *data, uint16_t len) { if(cachePos + len > 4096 || addr != cacheAddr + cachePos){ W25Q_Write(cache, cacheAddr, cachePos); // 刷写缓存 cacheAddr = addr; cachePos = 0; } memcpy(cache+cachePos, data, len); cachePos += len; }异常处理也很关键。我总结了几个常见错误码:
- 0x01:写保护触发
- 0x02:擦除/编程错误
- 0x04:非法指令
可以通过读取状态寄存器2(0x35指令)获取这些信息:
uint8_t W25Q_GetError(void) { CS_LOW(); SPI_ReadWriteByte(0x35); // Read Status Reg2 uint8_t status = SPI_ReadWriteByte(0xFF); CS_HIGH(); return status & 0x07; }最后分享一个调试技巧:用GPIO引脚触发示波器,可以直观观察SPI时序。我通常这样连接:
- CH1:CS信号
- CH2:SCK时钟
- CH3:MOSI数据
- CH4:MISO数据
通过分析波形,能快速定位相位错误、时序违规等问题。曾经发现过因为SCK频率过高导致的数据采样失败,将分频系数从2改为4后问题解决。