1. SPI总线协议基础解析
SPI(Serial Peripheral Interface)是一种高速全双工同步串行通信协议,由摩托罗拉在1980年代提出。它凭借简单高效的特性,在嵌入式系统中广泛应用,尤其适合与Flash存储器、传感器等外设进行数据交换。我第一次接触SPI是在调试一个温湿度传感器时,当时被它"四线制"的精巧设计所吸引。
SPI采用主从架构,仅需4根信号线即可完成通信:
- SCLK(Serial Clock):主设备产生的同步时钟,像乐队的指挥棒一样协调数据传输节奏。我在调试中发现,时钟频率可高达数十MHz,远超I2C的400KHz。
- MOSI(Master Out Slave In):主设备输出数据线,如同单向行驶的高速公路,专门输送主设备发出的指令和数据包。
- MISO(Master In Slave Out):从设备输出数据线,与MOSI方向相反,形成完美的双向数据通道。
- CS/SS(Chip Select):从设备使能信号,低电平有效。就像点名时的举手应答,只有被选中的从设备才会响应通信。
实际项目中曾遇到一个经典问题:当多个SPI设备共用总线时,CS信号切换不及时会导致数据冲突。后来通过增加5μs的延时解决了这个问题,这也让我深刻理解了CS信号的重要性。
2. SPI工作模式深度剖析
SPI最让人着迷也最容易出错的就是它的四种工作模式,这由CPOL(时钟极性)和CPHA(时钟相位)两个参数决定:
| 模式 | CPOL | CPHA | 时钟空闲状态 | 数据采样边沿 |
|---|---|---|---|---|
| 0 | 0 | 0 | 低电平 | 上升沿 |
| 1 | 0 | 1 | 低电平 | 下降沿 |
| 2 | 1 | 0 | 高电平 | 下降沿 |
| 3 | 1 | 1 | 高电平 | 上升沿 |
在调试W25Q128 Flash时,我最初因为模式设置错误导致读取的数据全是0xFF。后来用逻辑分析仪抓取波形才发现,Flash要求模式3(CPOL=1, CPHA=1),而我的初始化代码设置成了模式0。这个教训让我养成了查阅器件手册的好习惯。
时钟极性和相位的配合:
- CPOL=0时,时钟空闲为低电平,第一个边沿是上升沿
- CPOL=1时,时钟空闲为高电平,第一个边沿是下降沿
- CPHA决定采样时刻:0表示第一个边沿采样,1表示第二个边沿采样
3. STM32硬件SPI配置实战
下面以STM32F407驱动W25Q128为例,展示完整的SPI初始化流程:
void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; SPI_InitTypeDef SPI_InitStruct; // 使能时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); // 配置GPIO复用功能 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5; // PB3~5 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStruct.GPIO_OType = GPIO_OType_PP; GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP; GPIO_Init(GPIOB, &GPIO_InitStruct); // 引脚复用映射 GPIO_PinAFConfig(GPIOB, GPIO_PinSource3, GPIO_AF_SPI1); GPIO_PinAFConfig(GPIOB, GPIO_PinSource4, GPIO_AF_SPI1); GPIO_PinAFConfig(GPIOB, GPIO_PinSource5, GPIO_AF_SPI1); // SPI参数配置 SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStruct.SPI_Mode = SPI_Mode_Master; SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b; SPI_InitStruct.SPI_CPOL = SPI_CPOL_High; // 模式3 SPI_InitStruct.SPI_CPHA = SPI_CPHA_2Edge; // 模式3 SPI_InitStruct.SPI_NSS = SPI_NSS_Soft; // 软件控制CS SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; // 21MHz SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStruct); SPI_Cmd(SPI1, ENABLE); }这段代码有几个关键点需要注意:
- 使用软件控制NSS(CS)信号更灵活
- 预分频系数设为4,当APB2时钟为84MHz时,SPI时钟为21MHz
- 模式3配置适合大多数SPI Flash器件
- 必须使能GPIO的复用功能(AF)
4. W25Q128 Flash操作详解
W25Q128是Winbond推出的16MB SPI Flash,采用标准的SPI指令集。其存储结构组织为:
- 256个块(Block),每块64KB
- 每个块包含16个扇区(Sector),每扇区4KB
- 最小擦除单位是扇区
Flash操作三大基本操作:
4.1 读取器件ID
uint16_t W25Q_ReadID(void) { uint16_t id = 0; W25Q_CS(0); // 使能器件 SPI_ReadWriteByte(0x90); // 发送读ID指令 SPI_ReadWriteByte(0x00); // 发送3个空字节 SPI_ReadWriteByte(0x00); SPI_ReadWriteByte(0x00); id |= SPI_ReadWriteByte(0xFF)<<8; // 读取制造商ID id |= SPI_ReadWriteByte(0xFF); // 读取设备ID W25Q_CS(1); // 禁用器件 return id; }正常返回值应为0xEF17,其中EFh代表Winbond,17h表示128Mbit容量。
4.2 扇区擦除
Flash编程前必须先擦除,这是由其物理特性决定的:
void W25Q_EraseSector(uint32_t addr) { W25Q_WriteEnable(); // 使能写操作 W25Q_CS(0); SPI_ReadWriteByte(0x20); // 扇区擦除指令 SPI_ReadWriteByte(addr>>16); // 发送24位地址 SPI_ReadWriteByte(addr>>8); SPI_ReadWriteByte(addr); W25Q_CS(1); W25Q_WaitForWriteEnd(); // 等待擦除完成 }擦除一个4KB扇区通常需要50-200ms,期间可以读取状态寄存器判断是否完成。
4.3 数据读写操作
void W25Q_Read(uint8_t *buf, uint32_t addr, uint16_t len) { W25Q_CS(0); SPI_ReadWriteByte(0x03); // 读数据指令 SPI_ReadWriteByte(addr>>16); // 地址 SPI_ReadWriteByte(addr>>8); SPI_ReadWriteByte(addr); while(len--) *buf++ = SPI_ReadWriteByte(0xFF); W25Q_CS(1); } void W25Q_Write(uint8_t *buf, uint32_t addr, uint16_t len) { W25Q_WriteEnable(); W25Q_CS(0); SPI_ReadWriteByte(0x02); // 页编程指令 SPI_ReadWriteByte(addr>>16); // 地址 SPI_ReadWriteByte(addr>>8); SPI_ReadWriteByte(addr); while(len--) SPI_ReadWriteByte(*buf++); W25Q_CS(1); W25Q_WaitForWriteEnd(); }注意Flash的页编程限制:单次写入不能跨页(每页256字节)。实际项目中我封装了一个自动处理跨页写入的函数,大大提高了开发效率。
5. SPI通信优化技巧
经过多个项目的积累,我总结出以下SPI优化经验:
时钟配置:在信号质量允许的情况下,尽量使用更高的时钟频率。曾通过将SPI时钟从1MHz提升到18MHz,使Flash读写速度提升近20倍。
DMA传输:对于大数据量传输,使用DMA可以显著降低CPU负载:
// STM32 DMA配置示例 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)txBuffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE; DMA_Init(DMA1_Channel3, &DMA_InitStructure); SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE);- 信号完整性:
- 保持SCK和MOSI/MISO线等长
- 在高速传输时添加33Ω串联电阻
- 避免信号线平行走线过长
- 错误处理:
- 增加超时机制防止死等
- 定期检查SPI状态寄存器
- 重要操作前读取状态寄存器确认设备就绪
记得有一次硬件同事将SPI走线布得过于靠近射频模块,导致通信误码率飙升。后来通过重新布局和增加屏蔽层解决了这个问题,这也让我意识到高速数字信号完整性的重要性。