1. SPI通信基础:从四线制到全双工
SPI(Serial Peripheral Interface)是一种高速、全双工的同步串行通信协议,最早由摩托罗拉公司提出。在实际项目中,我经常用它来连接传感器、存储芯片等外设。与I2C相比,SPI的最大优势在于传输速率——我曾经用STM32的SPI接口实现过18MHz的通信速率,这比I2C的400kHz快得多。
SPI采用主从架构,通常由一个主设备(Master)和一个或多个从设备(Slave)组成。硬件连接只需要四根线:
- SCK(Serial Clock):时钟信号,由主设备产生
- MOSI(Master Out Slave In):主设备输出,从设备输入
- MISO(Master In Slave Out):主设备输入,从设备输出
- NSS/CS(Slave Select):从设备片选信号(低电平有效)
这里有个实际应用中的经验:当需要连接多个从设备时,可以采用两种方案。一种是每个从设备单独使用一个CS引脚(适合从设备数量少的场景),另一种是通过74HC138等译码器扩展CS信号(我曾在项目中用这种方法同时控制8个SPI Flash芯片)。
2. STM32硬件SPI初始化详解
2.1 GPIO配置与复用功能设置
在STM32上使用硬件SPI,第一步是正确配置GPIO。以STM32F103的SPI1为例,需要将PA5(SCK)、PA6(MISO)和PA7(MOSI)配置为复用推挽输出模式。这里有个容易踩坑的地方:虽然MISO是主机输入,但仍需配置为复用模式而非输入模式。
// SPI1 GPIO配置示例 GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽输出 GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);对于NSS引脚,我建议使用软件控制模式(SPI_NSS_SOFT),这样可以更灵活地管理片选信号。实际项目中遇到过硬件NSS模式下的异常锁定问题,改用软件控制后稳定性大幅提升。
2.2 SPI参数配置关键点
STM32的SPI初始化结构体包含多个重要参数,这里重点说明几个容易配置错误的:
SPI_InitTypeDef SPI_InitStruct = {0}; SPI_InitStruct.Mode = SPI_MODE_MASTER; // 主模式 SPI_InitStruct.Direction = SPI_DIRECTION_2LINES; // 全双工 SPI_InitStruct.DataSize = SPI_DATASIZE_8BIT; SPI_InitStruct.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0 SPI_InitStruct.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0 SPI_InitStruct.NSS = SPI_NSS_SOFT; // 软件控制NSS SPI_InitStruct.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 9MHz@72MHz SPI_InitStruct.FirstBit = SPI_FIRSTBIT_MSB; // MSB先行 HAL_SPI_Init(&hspi1, &SPI_InitStruct);特别注意CPOL和CPHA的组合决定了SPI的四种工作模式。曾经调试一个加速度计时,因为模式设置不匹配导致数据读取全为0,后来用逻辑分析仪抓取波形才发现模式配置错误。
3. 主从设备数据交换实战
3.1 单字节传输机制
SPI的数据传输本质上是主从设备间移位寄存器的内容交换。每次传输至少包含两个阶段:
- 主设备通过MOSI发送数据
- 从设备通过MISO返回数据
// HAL库单字节收发示例 uint8_t SPI_TransmitReceiveByte(SPI_HandleTypeDef *hspi, uint8_t txData) { uint8_t rxData; HAL_SPI_TransmitReceive(hspi, &txData, &rxData, 1, 100); return rxData; }这里有个性能优化技巧:使用DMA传输可以大幅提高效率。我在实现SPI Flash读写时,采用DMA后传输速度提升了5倍以上。
3.2 多字节传输与帧格式
对于需要连续传输多字节的场景,要注意STM32的SPI数据寄存器是16位的。当配置为8位数据长度时,连续写入两个字节会导致数据覆盖。解决方案是:
// 正确的多字节发送方式 void SPI_SendMultiBytes(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size) { while(Size--) { while(!__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_TXE)); *((__IO uint8_t *)&hspi->Instance->DR) = *pData++; } }实际项目中,我通常会为SPI通信定义特定的帧格式。例如:
- 第1字节:命令字
- 第2字节:数据长度
- 后续字节:有效数据
- 最后1字节:校验和
4. 常见问题排查与性能优化
4.1 典型故障排查指南
根据我的调试经验,SPI通信常见问题包括:
- 无数据通信:检查SCK信号是否正常产生,确认CPOL/CPHA设置匹配
- 数据错位:检查MSB/LSB设置,必要时用逻辑分析仪捕获波形
- 从设备无响应:确认CS信号有效,测量供电电压是否正常
曾经遇到一个棘手案例:SPI通信间歇性失败。最终发现是PCB布局问题,SCK走线过长导致信号质量差,添加33Ω串联电阻后问题解决。
4.2 性能优化技巧
- 时钟配置:在STM32F4系列上,SPI时钟可配置到42MHz(APB2时钟的1/2)
- DMA应用:对于大数据量传输,使用DMA可释放CPU资源
- 中断优化:合理使用TXE和RXNE中断,避免轮询等待
以下是DMA配置示例:
// SPI TX DMA配置 hdma_tx.Instance = DMA2_Stream3; hdma_tx.Init.Channel = DMA_CHANNEL_3; hdma_tx.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_tx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_tx.Init.MemInc = DMA_MINC_ENABLE; hdma_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_tx.Init.Mode = DMA_NORMAL; hdma_tx.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_tx); __HAL_LINKDMA(hspi, hdmatx, hdma_tx);通过以上优化,我在最近的一个工业传感器项目中实现了稳定的10MHz SPI通信,持续传输超过8小时无错误。