SPI协议实战宝典:原理、时序、软硬实现与故障排查(STM32全解析)

聚焦嵌入式串行通信协议落地与问题解决

一、协议核心认知:SPI是什么?适合做什么?

SPI是摩托罗拉推出的同步串行通信协议,核心定位「短距离、高速全双工」外设通信,因协议简单、速率高、硬件实现成本低,成为嵌入式高速通信首选。

核心特征与选型逻辑

核心优势 典型应用场景 局限性
高速全双工(MHz级) SPI Flash、OLED、高速ADC/DAC 短距离(<10cm,高速下<5cm)
主从架构、协议轻量化 传感器、FPGA通信 无地址/仲裁,多从机需额外SS线
硬件实现简单、时序可控 电机驱动、音频Codec、SD卡(SPI模式) 4线制,不适合远距离组网

选型结论:短距离+高速+全双工选SPI;长距离+少布线+多设备组网选IIC。

二、硬件层:总线组成与连接规范(物理层避坑核心)

1. 核心信号线定义(4线制标准)

信号线 作用说明 主从方向 硬件要求
SCLK 主机生成同步时钟,控制传输节奏 主机→从机 推挽输出,高速需100~150Ω终端电阻
MOSI 主机发→从机收数据 主机→从机 推挽输出,与SCLK等长布线
MISO 从机发→主机收数据 从机→主机 上拉输入,避免浮空
SS(CS) 低电平选中从机,每个从机独占1根IO 主机→从机 默认高电平,禁止多从机同时拉低

2. 3线制简化配置

  • 适用:单向传输(仅写OLED/仅读ADC);
  • 连接:MOSI/MISO复用为SDIO,时序区分读写;
  • 缺点:失去全双工,速率略降。

3. 硬件连接黄金规范(90%物理层问题根源)

  1. 同名引脚直连(MOSI/MISO接反是高频错误);
  2. 每个从机独占SS线,未选中时SS必须高电平;
  3. 布线:≤10cm(高速≤5cm),SCLK/MOSI/MISO等长,双绞屏蔽线;
  4. 高速传输(≥5Mbps):总线末端(靠近从机侧)加120Ω终端电阻;
  5. 电平匹配(3.3V/5V),外设电源端并0.1μF去耦电容。

三、时序层:CPOL/CPHA与4种工作模式(通信兼容核心)

主从设备CPOL/CPHA必须完全匹配,否则必乱码。

1. 核心时序参数

参数 含义
CPOL 空闲电平:0=低,1=高
CPHA 采样边沿:0=第一个跳变沿,1=第二个跳变沿
传输顺序 99%外设默认高位优先(MSB First)
波特率 主机配置,需≤外设最大速率(如W25Q64支持80MHz,OLED≤5MHz)

2. 4种工作模式(实战速查)

模式 CPOL CPHA 空闲电平 采样边沿 典型外设
0 0 0 SCLK上升沿 OLED、多数ADC/DAC
1 0 1 SCLK下降沿 少数专用传感器
2 1 0 SCLK下降沿 工业级Flash
3 1 1 SCLK上升沿 W25Q系列Flash、SD卡

实战技巧:外设手册查“SPI Mode”/“CPOL/CPHA”,主机严格匹配即可。

四、传输逻辑:完整通信流程(全双工本质)

SPI全双工:主机发1字节必收1字节(无有效数据则收dummy)。

1. 单字节传输(模式0为例)

  1. 主机拉低SS选中从机;
  2. 主机生成SCLK,下降沿更新MOSI数据,上升沿从机采样MOSI、主机采样MISO;
  3. 8位传输完成后,主机拉高SS释放从机。

2. 读写操作实现

操作类型 实现逻辑
写操作 主机发有效数据,忽略MISO返回值
读操作 主机发0x00(dummy)触发时钟,从机MISO返回有效数据
读写混合 先发送命令/地址(写),再发dummy触发时钟,同时读取MISO数据(读)

五、实现方案:软件SPI vs 硬件SPI(STM32F103实战代码)

1. 选型对比

维度 软件SPI(GPIO模拟) 硬件SPI(MCU内置)
速率 <1Mbps(受CPU延时限制) MHz级(F103 SPI1最高9MHz)
CPU占用 高(循环控GPIO) 低(支持DMA)
灵活性 高(适配非标时序) 一般(仅支持标准模式)
适用场景 低速、非标外设、无硬件SPI的MCU 高速、批量传输(Flash/OLED)

2. 软件SPI实现(模式0,高移植性)

#include "stm32f10x.h"
#include "delay.h"  // SysTick实现us级延时

// 硬件抽象层(仅改此处适配不同MCU)
#define SPI_SCLK_PIN    GPIO_Pin_5
#define SPI_MOSI_PIN    GPIO_Pin_7
#define SPI_MISO_PIN    GPIO_Pin_6
#define SPI_SS_PIN      GPIO_Pin_4
#define SPI_GPIO_PORT   GPIOA
#define SPI_GPIO_CLK    RCC_APB2Periph_GPIOA

// 引脚操作宏
#define SPI_SCLK_HIGH()  GPIO_SetBits(SPI_GPIO_PORT, SPI_SCLK_PIN)
#define SPI_SCLK_LOW()   GPIO_ResetBits(SPI_GPIO_PORT, SPI_SCLK_PIN)
#define SPI_MOSI_HIGH()  GPIO_SetBits(SPI_GPIO_PORT, SPI_MOSI_PIN)
#define SPI_MOSI_LOW()   GPIO_ResetBits(SPI_GPIO_PORT, SPI_MOSI_PIN)
#define SPI_MISO_READ()  GPIO_ReadInputDataBit(SPI_GPIO_PORT, SPI_MISO_PIN)
#define SPI_SS_HIGH()    GPIO_SetBits(SPI_GPIO_PORT, SPI_SS_PIN)
#define SPI_SS_LOW()     GPIO_ResetBits(SPI_GPIO_PORT, SPI_SS_PIN)

// 软件SPI初始化
void SoftSPI_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;
    RCC_APB2PeriphClockCmd(SPI_GPIO_CLK, ENABLE);
    
    // SCLK/MOSI/SS:推挽输出
    GPIO_InitStruct.GPIO_Pin = SPI_SCLK_PIN | SPI_MOSI_PIN | SPI_SS_PIN;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(SPI_GPIO_PORT, &GPIO_InitStruct);
    
    // MISO:上拉输入
    GPIO_InitStruct.GPIO_Pin = SPI_MISO_PIN;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(SPI_GPIO_PORT, &GPIO_InitStruct);
    
    // 初始状态:空闲低SCLK,SS高
    SPI_SCLK_LOW();
    SPI_SS_HIGH();
}

// 全双工收发1字节(高位优先)
u8 SoftSPI_ReadWriteByte(u8 tx_data)
{
    u8 i, rx_data = 0;
    for(i=0; i<8; i++)
    {
        // 发送1位
        (tx_data & 0x80) ? SPI_MOSI_HIGH() : SPI_MOSI_LOW();
        tx_data <<= 1;
        
        // 上升沿采样
        SPI_SCLK_HIGH();
        delay_us(1);  // 1us≈500Kbps,调整延时改速率
        
        // 读取1位
        rx_data <<= 1;
        if(SPI_MISO_READ()) rx_data |= 0x01;
        
        // 下降沿更新数据
        SPI_SCLK_LOW();
        delay_us(1);
    }
    return rx_data;
}

// 写命令+读n字节(适配Flash/ADC)
void SoftSPI_WriteCmdReadData(u8 cmd, u8 *rx_buf, u32 len)
{
    u32 i;
    SPI_SS_LOW();
    delay_us(10);  // 从机稳定时间
    
    SoftSPI_ReadWriteByte(cmd);
    for(i=0; i<len; i++) rx_buf[i] = SoftSPI_ReadWriteByte(0x00);
    
    SPI_SS_HIGH();
}

3. 硬件SPI实现(模式0,高速传输)

#include "stm32f10x.h"
#include "delay.h"

#define SPIx            SPI1
#define SPIx_CLK        RCC_APB2Periph_SPI1
#define SPIx_GPIO_CLK   RCC_APB2Periph_GPIOA
#define SPIx_SCLK_PIN   GPIO_Pin_5
#define SPIx_MISO_PIN   GPIO_Pin_6
#define SPIx_MOSI_PIN   GPIO_Pin_7
#define SPIx_SS_PIN     GPIO_Pin_4
#define SPIx_GPIO_PORT  GPIOA

// 硬件SPI初始化(9MHz波特率,模式0)
void HardSPI_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;
    SPI_InitTypeDef SPI_InitStruct;
    
    // 使能时钟
    RCC_APB2PeriphClockCmd(SPIx_GPIO_CLK | SPIx_CLK, ENABLE);
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);  // DMA时钟
    
    // GPIO配置
    GPIO_InitStruct.GPIO_Pin = SPIx_SCLK_PIN | SPIx_MOSI_PIN;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;  // 复用推挽
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(SPIx_GPIO_PORT, &GPIO_InitStruct);
    
    GPIO_InitStruct.GPIO_Pin = SPIx_MISO_PIN;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(SPIx_GPIO_PORT, &GPIO_InitStruct);
    
    GPIO_InitStruct.GPIO_Pin = SPIx_SS_PIN;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_Init(SPIx_GPIO_PORT, &GPIO_InitStruct);
    
    // 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_Low;        // CPOL=0
    SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge;      // CPHA=0
    SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;         // 软件控SS
    SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8;  // 72/8=9MHz
    SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
    SPI_InitStruct.SPI_CRCPolynomial = 7;
    SPI_Init(SPIx, &SPI_InitStruct);
    
    // 使能SPI
    SPI_Cmd(SPIx, ENABLE);
    GPIO_SetBits(SPIx_GPIO_PORT, SPIx_SS_PIN);
}

// 硬件SPI收发1字节
u8 HardSPI_ReadWriteByte(u8 tx_data)
{
    while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_TXE) == RESET);
    SPI_I2S_SendData(SPIx, tx_data);
    
    while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_RXNE) == RESET);
    return SPI_I2S_ReceiveData(SPIx);
}

// DMA配置(SPI1_RX→DMA1_Channel2,批量读1KB)
void SPI_DMA_Config(u8 *rx_buf, u32 len)
{
    DMA_InitTypeDef DMA_InitStruct;
    DMA_DeInit(DMA1_Channel2);
    
    DMA_InitStruct.DMA_PeripheralBaseAddr = (u32)&SPIx->DR;
    DMA_InitStruct.DMA_MemoryBaseAddr = (u32)rx_buf;
    DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_InitStruct.DMA_BufferSize = len;
    DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
    DMA_InitStruct.DMA_Priority = DMA_Priority_High;
    DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
    
    DMA_Init(DMA1_Channel2, &DMA_InitStruct);
    DMA_Cmd(DMA1_Channel2, ENABLE);
    SPI_I2S_DMACmd(SPIx, SPI_I2S_DMAReq_Rx, ENABLE);
}

六、进阶应用:SPI多从机通信(冲突规避)

1. 硬件连接

所有从机SCLK/MOSI/MISO并联,每个从机SS接主机不同IO。

2. 软件控制逻辑(核心:同一时刻仅选1个从机)

// 从机SS定义
#define SS_FLASH    GPIO_Pin_4  // PA4
#define SS_OLED     GPIO_Pin_0  // PB0
#define SS_ADC      GPIO_Pin_1  // PB1
#define PORT_FLASH  GPIOA
#define PORT_OTHER  GPIOB

// 从机选择宏(先释放所有,再选目标)
#define SELECT_FLASH()  {GPIO_SetBits(PORT_OTHER, SS_OLED|SS_ADC); GPIO_ResetBits(PORT_FLASH, SS_FLASH); delay_us(1);}
#define SELECT_OLED()   {GPIO_SetBits(PORT_FLASH, SS_FLASH); GPIO_SetBits(PORT_OTHER, SS_ADC); GPIO_ResetBits(PORT_OTHER, SS_OLED); delay_us(1);}
#define SELECT_ADC()    {GPIO_SetBits(PORT_FLASH, SS_FLASH); GPIO_SetBits(PORT_OTHER, SS_OLED); GPIO_ResetBits(PORT_OTHER, SS_ADC); delay_us(1);}
#define RELEASE_ALL()   {GPIO_SetBits(PORT_FLASH, SS_FLASH); GPIO_SetBits(PORT_OTHER, SS_OLED|SS_ADC);}

// 多从机通信示例
void SPI_MultiSlave_Example(void)
{
    u8 flash_id[3], adc_data;
    
    // 读Flash ID
    SELECT_FLASH();
    SoftSPI_WriteCmdReadData(0x90, flash_id, 3);
    RELEASE_ALL();
    
    // 写OLED清屏指令
    SELECT_OLED();
    SoftSPI_ReadWriteByte(0x01);
    RELEASE_ALL();
    
    // 读ADC数据
    SELECT_ADC();
    adc_data = SoftSPI_ReadWriteByte(0x00);
    RELEASE_ALL();
}

3. 冲突规避要点

  1. 切换从机时,先拉高当前SS,延时1~2us再拉低目标SS;
  2. 未使用的SS默认高电平;
  3. 高速传输时,SS切换避开SCLK周期。

七、排障手册:SPI常见问题+根因+排查步骤

1. 通信失败/数据乱码

根因 排查步骤
CPOL/CPHA不匹配 1. 核对外设手册模式;2. 逻辑分析仪抓波形验证采样边沿;3. 切换主机模式测试
波特率过高 1. 查外设最大速率;2. 降速测试(如9MHz→1MHz)
引脚接反/SS未拉低 1. 核对MOSI/MISO;2. 测量SS电平(传输时是否为低)
软件SPI延时不足 增加delay_us值(如1us→2us)

2. 多从机冲突

根因 排查步骤
双从机同时选中 测量SS电平,确认同一时刻仅1根为低
SS切换时序干扰 切换时增加1~2us延时,逻辑分析仪查SS与SCLK时序

3. 高速传输数据丢失

根因 排查步骤
总线过长/信号反射 缩短布线至≤5cm,加120Ω终端电阻
电源纹波 外设电源并0.1μF电容,测量纹波≤100mV

核心总结

  1. SPI核心是「同步全双工+高速」,物理层连接和时序匹配是通信成功关键;
  2. 软件SPI灵活、硬件SPI高速低耗,按需选择;
  3. 多从机通信核心是SS线独立控制,避免同时选中;
  4. 调试优先级:物理层(连接/布线)→ 时序层(CPOL/CPHA/波特率)→ 软件逻辑。
Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐