深入解析STM32F103 SPI与IIC读写Flash实战:以W25Q128为例
1. SPI与IIC协议基础对比
在嵌入式系统中,SPI和IIC是两种最常用的串行通信协议。先打个比方:SPI就像两个人在用对讲机通话(全双工),而IIC更像是轮流发言的会议(半双工)。这两种协议在STM32F103上操作W25Q128 Flash时,底层实现差异非常明显。
SPI协议特点:
- 四线制(MOSI/MISO/SCLK/CS)
- 全双工同步通信
- 时钟速率可达18MHz(STM32F103)
- 无设备地址机制,靠片选信号选择设备
IIC协议特点:
- 两线制(SDA/SCL)
- 半双工通信
- 标准模式100kHz,快速模式400kHz
- 通过设备地址寻址(W25Q128的IIC地址是0x50)
实测发现,SPI模式下W25Q128的页写入(256字节)仅需1.2ms,而IIC模式需要8ms左右。这是因为SPI的硬件移位寄存器直接处理数据交换,而IIC需要软件模拟每个时钟周期的时序。
2. 硬件连接与初始化配置
2.1 SPI硬件连接方案
以STM32F103ZET6为例,推荐使用SPI1或SPI2接口:
// SPI2引脚配置(PB12-15) GPIOB->CRH &= ~(0xF000F000); // 清除PB13/15设置 GPIOB->CRH |= 0xB000B000; // PB13/15复用推挽输出 GPIOB->CRH |= 0x00030000; // PB12普通推挽输出(CS)特别注意:W25Q128的HOLD和WP引脚需要上拉到VCC,否则可能无法写入数据。我在调试时就遇到过因为WP引脚接触不良导致写操作失败的坑。
2.2 IIC硬件连接方案
虽然W25Q128主要支持SPI,但通过转换芯片可以用IIC控制:
// IIC1引脚配置(PB6/7) GPIOB->CRL &= ~(0xFF000000); GPIOB->CRL |= 0xFF000000; // PB6/7开漏输出IIC模式下需要特别注意上拉电阻(通常4.7kΩ),过小的阻值会导致信号上升沿不满足时序要求。曾经因为用了1kΩ电阻导致通信不稳定,折腾了半天才发现问题。
3. 底层驱动实现对比
3.1 SPI基础函数实现
SPI的优势在于STM32硬件原生支持:
uint8_t SPI_ExchangeByte(uint8_t data) { while(!(SPI2->SR & SPI_SR_TXE)); // 等待发送缓冲区空 SPI2->DR = data; while(!(SPI2->SR & SPI_SR_RXNE)); // 等待接收完成 return SPI2->DR; }这个函数同时完成发送和接收,体现了SPI全双工特性。实测发现,使用DMA传输大批量数据时,SPI速率可以达到IIC的20倍以上。
3.2 IIC基础函数实现
IIC需要完全用GPIO模拟时序:
void IIC_WriteBit(uint8_t bit) { SDA = bit ? 1 : 0; Delay_us(2); // 建立时间 SCL = 1; Delay_us(5); // 高电平保持 SCL = 0; Delay_us(2); // 低电平保持 }每个字节传输都需要9个时钟周期(8位数据+1位ACK),这就是IIC速度慢的根本原因。在调试IIC时,逻辑分析仪是必备工具,可以清晰看到每个时钟沿的数据变化。
4. W25Q128操作实战
4.1 关键操作指令对比
| 操作 | SPI指令码 | IIC等效操作 | 耗时对比 |
|---|---|---|---|
| 写使能 | 0x06 | 发送控制字节0x50 | SPI快3倍 |
| 页编程 | 0x02 | 写地址+数据 | SPI快8倍 |
| 扇区擦除 | 0x20 | 特殊命令序列 | SPI快5倍 |
4.2 SPI模式下的页编程
这是最常用的写入操作:
void W25Q128_SPI_WritePage(uint32_t addr, uint8_t *data) { SPI_CS_LOW(); SPI_ExchangeByte(0x02); // 页编程指令 SPI_ExchangeByte(addr >> 16); SPI_ExchangeByte(addr >> 8); SPI_ExchangeByte(addr); for(int i=0; i<256; i++) { SPI_ExchangeByte(data[i]); } SPI_CS_HIGH(); W25Q128_WaitBusy(); // 等待写入完成 }注意点:W25Q128的写入操作必须按页对齐(256字节),跨页写入会导致数据回卷。我曾经因为没注意这点,导致前256字节被意外覆盖。
4.3 IIC模式下的写入操作
虽然非标准但可以实现:
void W25Q128_IIC_Write(uint32_t addr, uint8_t data) { IIC_Start(); IIC_SendByte(0x50<<1); // 设备地址+写 IIC_WaitAck(); IIC_SendByte(addr>>16); // 24位地址 IIC_WaitAck(); IIC_SendByte(addr>>8); IIC_WaitAck(); IIC_SendByte(addr); IIC_WaitAck(); IIC_SendByte(data); IIC_WaitAck(); IIC_Stop(); Delay_ms(5); // 写入周期较长 }5. 工程选型建议
根据实测数据给出建议:
高速数据记录:必须选择SPI模式
- 最高18MHz时钟
- DMA支持批量传输
- 适合固件存储、音频数据等
引脚资源紧张:考虑IIC模式
- 仅需2个GPIO
- 适合低速配置存储
- 多设备共享总线
混合使用场景:SPI+硬件CS
- 多个SPI设备时分复用
- 通过片选切换设备
- 注意总线负载能力
特别提醒:当使用硬件SPI时,CLK相位和极性必须与W25Q128设置一致(模式0或模式3),否则会出现数据错位。配置示例:
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 时钟极性 SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 时钟相位6. 常见问题排查
SPI模式下的典型问题:
- 数据全为0xFF:检查CS信号是否有效
- 随机数据错误:确认CLK极性/相位设置
- 写入失败:确保WP引脚已上拉
IIC模式下的典型问题:
- 无ACK响应:检查设备地址和上拉电阻
- 时序错误:用示波器查看SCL/SDA波形
- 写入超时:适当延长等待时间
有个容易忽略的点:W25Q128上电后默认处于写保护状态,无论哪种接口都需要先发送写使能命令(0x06)才能修改数据。
7. 性能优化技巧
- SPI DMA传输:
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI2->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)buffer; DMA_InitStructure.DMA_BufferSize = 256; DMA_Cmd(DMA1_Channel5, ENABLE); SPI_I2S_DMACmd(SPI2, SPI_I2S_DMAReq_Tx, ENABLE);- 四线快速读取(需硬件支持):
SPI_ExchangeByte(0xEB); // Fast Read Quad I/O SPI_ExchangeByte(addr>>16); // ...发送地址后切换IO模式- 扇区预擦除:在空闲时提前擦除待用扇区
实测发现,使用DMA传输+四线模式,读取速度可以提升到标准SPI的4倍,这对大容量数据读取非常有利。
8. 代码架构设计建议
推荐的分层架构:
应用层 ├─ 文件系统层(FatFS) ├─ Flash操作层 │ ├─ SPI接口实现 │ └─ IIC接口实现 └─ 硬件抽象层 ├─ SPI驱动 └─ IIC驱动这种架构下,更换通信接口只需修改Flash操作层的实现,上层应用代码无需变动。我在实际项目中就遇到过需要从SPI切换到QSPI的情况,良好的分层设计让迁移工作变得非常轻松。
最后提醒:W25Q128的扇区擦除(4KB)需要较长时间(典型值400ms),建议在系统空闲时执行擦除操作,或者使用双Bank交替工作模式来避免等待。
