串行通信之SPI
本文介绍了SPI串行通信接口的工作原理及其在STM32上的应用。SPI是一种高速全双工同步通信协议,包含SCK、MOSI、MISO和NSS四条信号线,支持主从架构和四种工作模式。文章详细讲解了STM32标准库中SPI的初始化配置、数据收发函数实现,并通过代码示例展示了完整的使用流程。同时对比了SPI与I2C、UART的特性差异,并提供了常见问题解决方案和DMA传输等高级应用技巧,为开发人员使用SP

SPI(Serial Peripheral Interface)是一种高速全双工同步串行通信接口,由摩托罗拉公司开发,广泛应用于微控制器与各种外设(如Flash、传感器、ADC等)之间的通信。本文将介绍SPI工作原理,并通过STM32标准库的实际代码示例展示如何使用SPI接口。
一、SPI是什么?——通俗易懂的解释
SPI就像是一个高效的快递系统,它由四个关键部分组成:
- SCK(时钟线):相当于快递车的发车时间表,告诉收发双方何时该发送/接收数据
- MOSI(主发从收):主设备(如STM32)发送数据的专用通道,就像快递车的出库传送带
- MISO(主收从发):主设备接收数据的专用通道,相当于快递车的入库传送带
- NSS/CS(片选线):相当于快递仓库的门禁,只有被选中的设备才能参与通信
SPI最大的特点是全双工同步通信,这意味着数据可以同时双向传输,就像双向车道一样互不干扰
。它的传输速度通常比I2C快得多,可以达到几Mbps。

二、SPI工作原理——深入浅出
1. 主从模式
SPI采用主从架构,就像老师和学生的关系:
- 主设备(老师):控制时钟信号,决定什么时候开始和结束通信
- 从设备(学生):响应主设备的指令,只有在被选中时才会参与通信
一个主设备可以连接多个从设备,但需要通过不同的片选线(CS)来选择
2. 四种工作模式
SPI有四种工作模式,由两个参数决定:
-
CPOL(时钟极性):空闲时时钟线的状态
- 0:空闲时为低电平
- 1:空闲时为高电平
-
CPHA(时钟相位):数据采样的时机
- 0:在第一个时钟边沿采样
- 1:在第二个时钟边沿采样

相位补充知识点

3. 数据传输过程
SPI数据传输就像接力传递纸条:
- 主设备拉低对应从设备的CS线,选中它
- 主设备通过SCK提供时钟信号
- 主设备通过MOSI发送数据,同时通过MISO接收数据
- 数据以移位寄存器的方式一位一位传输,通常是高位(MSB)在前
- 传输完成后,主设备拉高CS线,结束通信

三、STM32标准库SPI配置实战
下面我们通过STM32标准库的代码示例,详细介绍如何配置和使用SPI接口。
1. SPI初始化配置
首先需要初始化SPI接口和相关的GPIO引脚:
#include "stm32f10x.h"
// 定义SPI和GPIO端口及引脚
#define SPIx SPI1
#define SPIx_CLK_ENABLE() RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE)
#define SPIx_GPIO_PORT GPIOA
#define SPIx_SCK_PIN GPIO_Pin_5
#define SPIx_MISO_PIN GPIO_Pin_6
#define SPIx_MOSI_PIN GPIO_Pin_7
#define NSS_GPIO_PORT GPIOB
#define NSS_PIN GPIO_Pin_0
// SPI配置结构体
SPI_InitTypeDef SPI_InitStructure;
void SPI1_Init(void)
{
// 1. 使能时钟
SPIx_CLK_ENABLE(); // 使能SPI1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE);
// 2. 配置GPIO引脚
GPIO_InitTypeDef GPIO_InitStructure;
// 配置SCK、MOSI为复用推挽输出
GPIO_InitStructure.GPIO_Pin = SPIx_SCK_PIN | SPIx_MOSI_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SPIx_GPIO_PORT, &GPIO_InitStructure);
// 配置MISO为浮空输入
GPIO_InitStructure.GPIO_Pin = SPIx_MISO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(SPIx_GPIO_PORT, &GPIO_InitStructure);
// 配置NSS为普通推挽输出(软件控制)
GPIO_InitStructure.GPIO_Pin = NSS_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(NSS_GPIO_PORT, &GPIO_InitStructure);
GPIO_SetBits(NSS_GPIO_PORT, NSS_PIN); // 初始状态不选中
// 3. 初始化SPI参数
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 双线全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 主模式
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8位数据
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 时钟极性:空闲低
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 时钟相位:第一个边沿采样
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件控制NSS
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16; // 波特率预分频
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // MSB优先
SPI_Init(SPIx, &SPI_InitStructure);
// 4. 使能SPI
SPI_Cmd(SPIx, ENABLE);
}
2. SPI数据发送函数
下面是主设备发送数据的函数实现:
void SPI_Master_Send(uint8_t* pData, uint16_t Size)
{
// 1. 拉低NSS,选中从设备
GPIO_ResetBits(NSS_GPIO_PORT, NSS_PIN);
// 2. 循环发送数据
while(Size--)
{
// 等待发送缓冲区空
while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_TXE) == RESET);
// 发送数据
SPI_I2S_SendData(SPIx, *pData++);
}
// 3. 等待传输完成
while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_BSY) == SET);
// 4. 拉高NSS,释放从设备
GPIO_SetBits(NSS_GPIO_PORT, NSS_PIN);
}
3. SPI数据接收函数
SPI是全双工的,发送数据的同时也会接收数据。下面是接收数据的实现:
uint8_t SPI_ReceiveByte(void)
{
// 发送虚拟数据(0xFF)以产生时钟信号
SPI_I2S_SendData(SPIx, 0xFF);
// 等待接收缓冲区非空
while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_RXNE) == RESET);
// 返回接收到的数据
return SPI_I2S_ReceiveData(SPIx);
}
void SPI_Master_Receive(uint8_t* pData, uint16_t Size)
{
// 1. 拉低NSS,选中从设备
GPIO_ResetBits(NSS_GPIO_PORT, NSS_PIN);
// 2. 循环接收数据
while(Size--)
{
*pData++ = SPI_ReceiveByte();
}
// 3. 拉高NSS,释放从设备
GPIO_SetBits(NSS_GPIO_PORT, NSS_PIN);
}
4. 主函数示例
下面是一个完整的主函数示例,演示如何发送和接收数据:
int main(void)
{
// 1. 初始化系统时钟等
SystemInit();
// 2. 初始化SPI
SPI1_Init();
// 3. 准备发送数据
uint8_t txData[] = {0x01, 0x02, 0x03, 0x04};
uint8_t rxData[4];
while(1)
{
// 4. 发送数据
SPI_Master_Send(txData, sizeof(txData));
// 5. 接收数据
SPI_Master_Receive(rxData, sizeof(rxData));
// 6. 简单延时
for(volatile uint32_t i = 0; i < 1000000; i++);
}
}
四、SPI使用中的常见问题与解决方案
在实际使用SPI时,可能会遇到各种问题。下面列举一些常见问题及其解决方法:
-
数据传输错误
- 可能原因:CPOL和CPHA设置与从设备不匹配
- 解决方法:检查从设备手册,确保主从设备的模式设置一致
-
通信完全无响应
- 可能原因:片选信号(NSS)未正确控制
- 解决方法:用逻辑分析仪检查NSS信号,确保在通信期间保持低电平
-
数据速率不稳定
- 可能原因:波特率预分频设置不当
- 解决方法:降低波特率或检查时钟配置
-
多从设备冲突
- 可能原因:多个从设备的片选线同时被激活
- 解决方法:确保同一时间只有一个从设备被选中
-
长距离通信问题
- 可能原因:SPI设计用于短距离通信,长距离会导致信号衰减
- 解决方法:增加驱动电路或改用其他通信协议如RS485
五、SPI与其他通信协议的比较
为了更好地理解SPI的特点,我们将其与常见的I2C和UART协议进行比较:
| 特性 | SPI | I2C | UART |
|---|---|---|---|
| 通信方式 | 同步全双工 | 同步半双工 | 异步全双工 |
| 线路数量 | 4线(最少3线) | 2线 | 2线(不含地) |
| 最大速率 | 几Mbps | 400Kbps-3.4Mbps | 通常<1Mbps |
| 寻址方式 | 硬件片选 | 软件地址 | 点对点 |
| 复杂度 | 中等 | 低 | 低 |
| 适用场景 | 高速外设 | 中低速外设 | 设备间通信 |
从比较可以看出,SPI在速度和全双工方面具有优势,但在多设备支持和线路复杂度方面稍逊于I2C
六、高级SPI应用技巧
对于需要更高性能或更复杂功能的应用,可以考虑以下高级技巧:
-
使用DMA传输
- 优点:减少CPU开销,提高传输效率
- 实现:配置SPI和DMA控制器,实现自动数据传输
-
中断驱动通信
- 优点:提高系统响应性
- 实现:使能SPI中断,在中断服务程序中处理数据
-
硬件NSS管理
- 优点:更精确的片选控制
- 实现:配置SPI_InitStructure.SPI_NSS = SPI_NSS_Hard;
-
16位数据模式
- 优点:提高传输效率(适合16位外设)
- 实现:设置SPI_InitStructure.SPI_DataSize = SPI_DataSize_16b;
七、总结
SPI是一种高效、灵活的串行通信接口,特别适合STM32与各种外设之间的高速数据交换。通过本文的介绍,你应该已经掌握了:
- SPI的基本工作原理和特点
- STM32标准库的SPI配置方法
- 完整的SPI发送接收代码实现
- SPI使用中的常见问题及解决方法
- SPI与其他通信协议的比较
- 高级SPI应用技巧
在实际项目中,建议根据具体外设的要求调整SPI参数,并充分利用STM32的硬件特性(如DMA)来优化性能。同时,使用逻辑分析仪等工具调试SPI通信可以大大提高开发效率。
更多推荐



所有评论(0)