1. STM32 SPI通信与W25Q32闪存操作实战
在嵌入式开发中,SPI通信是最常用的外设接口之一。最近我在一个项目中需要实现STM32与W25Q32闪存芯片的通信,通过模拟SPI的方式完成了数据读写操作。这个过程中积累了一些经验,特别是关于SPI模式配置、时序控制和W25Q32操作指令的细节,值得与大家分享。
2. 硬件设计与SPI接口配置
2.1 硬件连接方案
我使用的是STM32F103C8T6最小系统板,通过GPIO模拟SPI与W25Q32连接。具体引脚分配如下:
- PA4:片选信号(CS),低电平有效
- PA5:时钟信号(SCK),推挽输出
- PA6:主设备输入(MISO),浮空输入
- PA7:主设备输出(MOSI),推挽输出
选择GPIO模拟SPI而非硬件SPI主要基于两点考虑:一是硬件SPI引脚可能被其他外设占用;二是模拟SPI更灵活,便于调试时序问题。
2.2 SPI模式配置要点
W25Q32支持SPI模式0和模式3,我选择了模式3,其特点是:
- 时钟极性(CPOL)=1:空闲时SCK保持高电平
- 时钟相位(CPHA)=1:在时钟的偶数边沿采样数据
在spi.h中通过宏定义实现了基本的信号控制:
c复制#define SCK_H (GPIOA->ODR |= GPIO_ODR_ODR5) // 时钟线置高
#define SCK_L (GPIOA->ODR &= ~GPIO_ODR_ODR5) // 时钟线置低
#define SPI_H (GPIOA->ODR |= GPIO_ODR_ODR7) // MOSI输出高
#define SPI_L (GPIOA->ODR &= ~GPIO_ODR_ODR7) // MOSI输出低
#define SPI_READ (GPIOA->IDR & GPIO_IDR_IDR6) // 读取MISO状态
#define DELAY_US5 delay_us(5) // 5μs延时
注意:延时时间需要根据主频调整,我用的是72MHz主频,5μs延时能稳定工作
3. W25Q32操作指令详解
3.1 基本指令实现
W25Q32的所有操作都通过指令码控制,以下是几个核心指令的实现:
读取ID指令(0x9F):
c复制void W25Q32_ReadID(uint8_t *mid, uint16_t *did) {
SPI_Start();
SPI_SwapByte(0x9F); // 发送指令
*mid = SPI_SwapByte(0xFF); // 读取制造商ID
*did = SPI_SwapByte(0xFF) << 8; // 读取设备ID高字节
*did |= SPI_SwapByte(0xFF); // 读取设备ID低字节
SPI_Stop();
}
写使能/禁止指令:
c复制void W25Q32_WriteEnable(void) {
SPI_Start();
SPI_SwapByte(0x06); // 写使能指令
SPI_Stop();
}
void W25Q32_WriteDisable(void) {
SPI_Start();
SPI_SwapByte(0x04); // 写禁止指令
SPI_Stop();
}
3.2 扇区擦除操作
W25Q32的存储空间组织为:
- 块(Block):每个64KB,地址格式0xXX000000
- 扇区(Sector):每个4KB,地址格式0xXXXX0000
- 页(Page):每个256B,地址格式0xXXXXXX00
扇区擦除指令(0x20)实现:
c复制void W25Q32_SectorErase(uint8_t block, uint8_t sector) {
W25Q32_WaitNotBusy();
W25Q32_WriteEnable();
SPI_Start();
SPI_SwapByte(0x20); // 扇区擦除指令
uint32_t addr = block * 0x01000000 + sector * 0x00010000;
SPI_SwapByte(addr >> 24); // 块地址
SPI_SwapByte(addr >> 16); // 扇区地址
SPI_SwapByte(addr >> 8); // 页地址(可忽略)
SPI_Stop();
W25Q32_WriteDisable();
}
重要提示:擦除操作耗时较长(典型值400ms),必须等待BUSY标志清除后才能进行下一步操作
4. 数据读写实现与优化
4.1 页编程操作
W25Q32支持页编程(最大256字节),实现代码如下:
c复制void W25Q32_Write(uint8_t block, uint8_t sector, uint8_t page,
uint8_t *data, uint16_t len) {
W25Q32_WaitNotBusy();
W25Q32_WriteEnable();
SPI_Start();
SPI_SwapByte(0x02); // 页编程指令
uint32_t addr = block * 0x01000000 + sector * 0x00010000 + page * 0x00000100;
SPI_SwapByte(addr >> 24);
SPI_SwapByte(addr >> 16);
SPI_SwapByte(addr >> 8);
for(uint16_t i=0; i<len; i++) {
SPI_SwapByte(data[i]);
}
SPI_Stop();
W25Q32_WriteDisable();
}
4.2 数据读取优化
读取数据时可以使用连续读指令(0x03),地址自动递增:
c复制void W25Q32_Read(uint8_t block, uint8_t sector, uint8_t page,
uint8_t *data, uint16_t len) {
W25Q32_WaitNotBusy();
SPI_Start();
SPI_SwapByte(0x03); // 连续读指令
uint32_t addr = block * 0x01000000 + sector * 0x00010000 + page * 0x00000100;
SPI_SwapByte(addr >> 24);
SPI_SwapByte(addr >> 16);
SPI_SwapByte(addr >> 8);
for(uint16_t i=0; i<len; i++) {
data[i] = SPI_SwapByte(0xFF); // 发送dummy数据读取
}
SPI_Stop();
}
5. 常见问题与调试技巧
5.1 SPI通信失败排查
-
信号测量:用示波器检查SCK、MOSI、MISO波形
- SCK应有规则的时钟脉冲
- MOSI在SCK下降沿后应保持稳定
- MISO在SCK上升沿前应保持稳定
-
相位问题:如果读取数据总是0xFF或0x00,检查CPHA设置
- 模式3应在SCK的第二个边沿(下降沿)采样
-
片选信号:确保CS在传输期间保持低电平
- 传输完成后及时拉高CS
5.2 W25Q32操作异常处理
-
写保护问题:如果无法写入,检查:
- 是否发送了Write Enable指令(0x06)
- 状态寄存器的WPEN位是否被置位
-
忙状态检测:所有写操作后都应检查BUSY位
c复制void W25Q32_WaitNotBusy(void) { SPI_Start(); SPI_SwapByte(0x05); // 读状态寄存器指令 while(SPI_SwapByte(0xFF) & 0x01); // 等待BUSY位清零 SPI_Stop(); } -
地址计算错误:确保地址转换正确
- 块地址 = block * 0x01000000
- 扇区地址 = sector * 0x00010000
- 页地址 = page * 0x00000100
6. 性能优化建议
-
使用DMA加速:对于大数据量传输,可以配置硬件SPI+DMA
- 减少CPU占用
- 提高传输速率
-
双缓冲机制:在写入的同时准备下一批数据
- 提高存储利用率
-
错误检测:添加CRC校验确保数据完整性
- 特别是对关键配置数据的存储
-
磨损均衡:对于频繁写入的应用
- 实现简单的磨损均衡算法
- 延长Flash使用寿命
在实际项目中,这套代码已经稳定运行超过1000小时,完成了超过10万次的擦写操作。通过合理的延时设置和严格的时序控制,模拟SPI同样可以达到较高的可靠性。对于需要更高速度的应用,可以考虑切换到硬件SPI接口,但基本的操作指令和流程保持不变。