SPI(Serial Peripheral Interface)是一种​​高速全双工同步串行通信接口​​,由摩托罗拉公司开发,广泛应用于微控制器与各种外设(如Flash、传感器、ADC等)之间的通信。本文将介绍SPI工作原理,并通过STM32标准库的实际代码示例展示如何使用SPI接口。

一、SPI是什么?——通俗易懂的解释

SPI就像是一个​​高效的快递系统​​,它由四个关键部分组成:

  1. ​SCK(时钟线)​​:相当于快递车的发车时间表,告诉收发双方何时该发送/接收数据
  2. ​MOSI(主发从收)​​:主设备(如STM32)发送数据的专用通道,就像快递车的出库传送带
  3. ​MISO(主收从发)​​:主设备接收数据的专用通道,相当于快递车的入库传送带
  4. ​NSS/CS(片选线)​​:相当于快递仓库的门禁,只有被选中的设备才能参与通信

SPI最大的特点是​​全双工同步通信​​,这意味着数据可以同时双向传输,就像双向车道一样互不干扰

。它的传输速度通常比I2C快得多,可以达到几Mbps。

二、SPI工作原理——深入浅出

1. 主从模式

SPI采用​​主从架构​​,就像老师和学生的关系:

  • ​主设备(老师)​​:控制时钟信号,决定什么时候开始和结束通信
  • ​从设备(学生)​​:响应主设备的指令,只有在被选中时才会参与通信

一个主设备可以连接多个从设备,但需要通过不同的片选线(CS)来选择

2. 四种工作模式

SPI有四种工作模式,由两个参数决定:

  • ​CPOL(时钟极性)​​:空闲时时钟线的状态

    • 0:空闲时为低电平
    • 1:空闲时为高电平
  • ​CPHA(时钟相位)​​:数据采样的时机

    • 0:在第一个时钟边沿采样
    • 1:在第二个时钟边沿采样

相位补充知识点

3. 数据传输过程

SPI数据传输就像​​接力传递纸条​​:

  1. 主设备拉低对应从设备的CS线,选中它
  2. 主设备通过SCK提供时钟信号
  3. 主设备通过MOSI发送数据,同时通过MISO接收数据
  4. 数据以​​移位寄存器​​的方式一位一位传输,通常是高位(MSB)在前
  5. 传输完成后,主设备拉高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时,可能会遇到各种问题。下面列举一些常见问题及其解决方法:

  1. ​数据传输错误​

    • ​可能原因​​:CPOL和CPHA设置与从设备不匹配
    • ​解决方法​​:检查从设备手册,确保主从设备的模式设置一致
  2. ​通信完全无响应​

    • ​可能原因​​:片选信号(NSS)未正确控制
    • ​解决方法​​:用逻辑分析仪检查NSS信号,确保在通信期间保持低电平
  3. ​数据速率不稳定​

    • ​可能原因​​:波特率预分频设置不当
    • ​解决方法​​:降低波特率或检查时钟配置
  4. ​多从设备冲突​

    • ​可能原因​​:多个从设备的片选线同时被激活
    • ​解决方法​​:确保同一时间只有一个从设备被选中
  5. ​长距离通信问题​

    • ​可能原因​​:SPI设计用于短距离通信,长距离会导致信号衰减
    • ​解决方法​​:增加驱动电路或改用其他通信协议如RS485

五、SPI与其他通信协议的比较

为了更好地理解SPI的特点,我们将其与常见的I2C和UART协议进行比较:

特性 SPI I2C UART
通信方式 同步全双工 同步半双工 异步全双工
线路数量 4线(最少3线) 2线 2线(不含地)
最大速率 几Mbps 400Kbps-3.4Mbps 通常<1Mbps
寻址方式 硬件片选 软件地址 点对点
复杂度 中等
适用场景 高速外设 中低速外设 设备间通信

从比较可以看出,SPI在​​速度​​和​​全双工​​方面具有优势,但在​​多设备支持​​和​​线路复杂度​​方面稍逊于I2C

六、高级SPI应用技巧

对于需要更高性能或更复杂功能的应用,可以考虑以下高级技巧:

  1. ​使用DMA传输​

    • 优点:减少CPU开销,提高传输效率
    • 实现:配置SPI和DMA控制器,实现自动数据传输
  2. ​中断驱动通信​

    • 优点:提高系统响应性
    • 实现:使能SPI中断,在中断服务程序中处理数据
  3. ​硬件NSS管理​

    • 优点:更精确的片选控制
    • 实现:配置SPI_InitStructure.SPI_NSS = SPI_NSS_Hard;
  4. ​16位数据模式​

    • 优点:提高传输效率(适合16位外设)
    • 实现:设置SPI_InitStructure.SPI_DataSize = SPI_DataSize_16b;

七、总结

SPI是一种高效、灵活的串行通信接口,特别适合STM32与各种外设之间的高速数据交换。通过本文的介绍,你应该已经掌握了:

  1. SPI的基本工作原理和特点
  2. STM32标准库的SPI配置方法
  3. 完整的SPI发送接收代码实现
  4. SPI使用中的常见问题及解决方法
  5. SPI与其他通信协议的比较
  6. 高级SPI应用技巧

在实际项目中,建议根据具体外设的要求调整SPI参数,并充分利用STM32的硬件特性(如DMA)来优化性能。同时,使用逻辑分析仪等工具调试SPI通信可以大大提高开发效率。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐