3、SPI协议基础:从物理层到四种模式

SPI,全称是Serial Peripheral Interface。说白了,就是一根时钟线带着几根数据线,主设备跟从设备之间互相倒腾数据。我最早接触SPI是在一个温湿度传感器项目上,那时候I2C地址冲突搞得我焦头烂额,换成SPI之后,整个世界清净了。

这一节,咱们把SPI的物理层、四种工作模式,以及它跟I2C到底差在哪,一次性讲清楚。

3.1 SPI总线物理层:四根线搞定一切

SPI总线通常只需要四根线。你想想看,比I2C多了两根,但换来的是全双工和更高的速度。

信号线 全称 方向 作用
SCLK Serial Clock 主→从 时钟信号,由主设备产生
MOSI Master Out Slave In 主→从 主设备发送数据给从设备
MISO Master In Slave Out 从→主 从设备发送数据给主设备
CS Chip Select 主→从 片选信号,低电平有效

这里有个关键点:CS片选线。每个从设备独占一根CS线。主设备想跟谁说话,就把谁的CS拉低。我在一个项目里挂过4个SPI从设备,结果CS线占了4个GPIO,板子布线的时候差点没把我逼疯。

核心要点:SPI是全双工通信。主设备发数据的同时,从设备也在往回发数据。哪怕你只想读一个字节,主设备也必须先发一个字节(通常是0x00或0xFF)来"骗"出从设备的数据。

3.2 SPI四种模式:CPOL和CPHA说了算

SPI的四种模式,其实就两个参数在变:CPOL(时钟极性)和CPHA(时钟相位)。

  • CPOL = 0:空闲时SCLK为低电平
  • CPOL = 1:空闲时SCLK为高电平
  • CPHA = 0:在第一个时钟边沿采样数据
  • CPHA = 1:在第二个时钟边沿采样数据

组合起来就是四种模式。我刚开始做嵌入式的时候,经常搞混模式0和模式3,结果读出来的数据全是乱的。后来我养成了一个习惯:先看从设备的数据手册,找到它支持的模式,再配置主设备。

模式 CPOL CPHA 采样边沿 常见设备
模式0 0 0 上升沿 大多数Flash、ADC
模式1 0 1 下降沿 某些传感器
模式2 1 0 下降沿 部分LCD驱动
模式3 1 1 上升沿 SD卡、W5500

我的小技巧:如果你不确定从设备用哪种模式,先试试模式0。80%的SPI设备都支持模式0。如果不行,再试模式3。我曾经用这个"二分法"解决过好几个兼容性问题。

3.3 SPI与I2C的对比:选谁不选谁

很多初学者会问:SPI和I2C到底选哪个?我的回答是:看场景。下面这张表,是我在实际项目中总结出来的对比。

对比项 SPI I2C
线数 4根(SCLK, MOSI, MISO, CS) 2根(SCL, SDA)
通信方式 全双工 半双工
速度 通常10-50MHz,最高可达100MHz+ 标准100kHz,快速400kHz,高速3.4MHz
从设备数量 受限于主设备GPIO数量 理论上127个(7位地址)
硬件复杂度 简单,无地址概念 需要地址仲裁,稍复杂
功耗 较高(多根线同时翻转) 较低(开漏输出,上拉电阻)
典型应用 Flash、ADC、LCD、SD卡 传感器、EEPROM、RTC

我个人习惯是:如果只需要挂一两个设备,且对速度有要求,无脑选SPI。如果设备多、线数有限、速度要求不高,那就用I2C。

避坑指南:我曾经在一个项目里用SPI连接一个高速ADC,结果因为PCB走线太长,信号反射导致数据出错。后来我把SCLK频率从20MHz降到10MHz,问题就解决了。记住:SPI频率不是越高越好,信号完整性才是王道。

3.4 SPI通信流程:一次完整的数据交换

下面这张图,展示了SPI一次完整的数据交换过程。我建议你仔细看看,尤其是CS的时序。

SPI单次数据交换时序图 CS CS拉低 CS拉高 SCLK MOSI D7 D6 D5 D4 D3 D2 D1 D0 MISO D7 D6 D5 D4 D3 D2 D1 D0 关键时序说明 1. CS拉低后,从设备开始准备数据 2. 每个SCLK时钟周期,MOSI和MISO各传输1位数据 3. 8个时钟周期后,完成1字节的数据交换 4. CS拉高,从设备停止响应

你看,CS拉低之后,主设备开始产生时钟。每个时钟周期,MOSI上发一位,MISO上收一位。8个时钟之后,一个字节就交换完了。这就是SPI的全双工特性——发的同时也在收。

3.5 代码示例:STM32 SPI初始化(模式0)

下面是我在STM32上配置SPI模式0的代码。嗯,这里要注意:配置完SPI之后,一定要先拉高CS,再拉低CS,完成一次"伪传输"来复位从设备。这是我踩过的坑。

// SPI1 初始化 - 模式0
void SPI1_Init(void) {
    GPIO_InitTypeDef GPIO_InitStructure;
    SPI_InitTypeDef SPI_InitStructure;
    
    // 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    // 配置GPIO: PA5(SCLK), PA7(MOSI) 推挽复用输出
    // PA6(MISO) 浮空输入
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    // 配置SPI1 - 模式0
    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;      // CPOL=0
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;    // CPHA=0
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16;
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
    SPI_InitStructure.SPI_CRCPolynomial = 7;
    SPI_Init(SPI1, &SPI_InitStructure);
    
    SPI_Cmd(SPI1, ENABLE);
}

// SPI1 读写一个字节
uint8_t SPI1_ReadWriteByte(uint8_t txData) {
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
    SPI_I2S_SendData(SPI1, txData);
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
    return SPI_I2S_ReceiveData(SPI1);
}

经验之谈:SPI的速率配置要留有余量。比如从设备支持20MHz,我一般配到10MHz或16MHz。这样即使PCB走线不太理想,也能稳定工作。记住:稳定比速度重要。

3.6 本章小结

SPI协议的核心就三点:四根线、全双工、四种模式。跟I2C比,SPI速度快、硬件简单,但占用的IO口多。选型的时候,先看你的从设备支持哪种模式,再配置主设备。CS的时序一定要处理好,拉低和拉高的时机不对,数据就全乱了。