4、SPI协议基础:从物理层到时序分析

各位同学,今天我们聊聊SPI。这个协议在MEMS传感器世界里太常见了,几乎每个加速度计、陀螺仪、磁力计都支持它。我做了十几年嵌入式,SPI的坑踩过不少,今天把这些经验掰开揉碎了讲给你听。

4.1 SPI总线物理层:四根线的故事

SPI全称是Serial Peripheral Interface,说白了就是「串行外设接口」。它由摩托罗拉在80年代推出,一直用到现在。为什么这么长寿?因为它简单、高效、可靠。

SPI总线只需要四根线:

  • SCLK(Serial Clock):时钟线,由主机产生
  • MOSI(Master Out Slave In):主机输出,从机输入
  • MISO(Master In Slave Out):主机输入,从机输出
  • SS(Slave Select):从机选择线,低电平有效

嗯,这里要注意:SS线也叫CS(Chip Select),不同厂家叫法不同,但功能一样。我见过不少新手把SS和CS当成两个不同的信号,其实一回事。

物理层上,SPI是同步、全双工的通信方式。什么叫全双工?就是主机发数据的同时,从机也能发数据回来。这一点比I2C强,I2C是半双工的,收发不能同时进行。

关键点:SPI没有像I2C那样的设备地址。每个从机独占一根SS线。所以你要接N个从机,就需要N根SS线。这是SPI的物理限制,也是它的优势——不需要地址仲裁,通信更直接。

我做过一个项目,一块板子上挂了8个MEMS传感器,每个传感器一根SS线,加上SCLK、MOSI、MISO,总共11根线。布线确实麻烦,但通信速度可以跑到20MHz以上,I2C在这种场景下根本做不到。

4.2 SPI四种模式:时钟极性和相位

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

  • CPOL(Clock Polarity):时钟极性,决定空闲时时钟电平
  • CPHA(Clock Phase):时钟相位,决定数据采样沿

组合起来就是四种模式:

模式 CPOL CPHA 空闲时钟电平 数据采样沿
模式0 0 0 低电平 上升沿
模式1 0 1 低电平 下降沿
模式2 1 0 高电平 下降沿
模式3 1 1 高电平 上升沿

你想想看,为什么要有四种模式?因为不同厂家的芯片对时序要求不一样。我遇到过最坑的一次,一个温湿度传感器手册上写的是模式0,结果实际测试发现必须用模式3才能正常通信。后来仔细看手册小字才发现,他们说的「模式0」其实是「CPOL=0, CPHA=1」——也就是模式1。这种命名混乱的情况在业界并不少见。

我的建议:拿到一个新传感器,先看数据手册里的时序图,别只看文字描述。时序图上会明确标出采样沿和时钟空闲电平,比文字靠谱得多。

4.3 SPI协议帧格式:数据怎么传

SPI的帧格式其实很简单。一次传输的基本单位是8位(也有支持16位的,但MEMS传感器基本都用8位)。

典型的SPI读写帧格式如下:

// 写操作
主机拉低SS
主机发送8位地址(或命令)
主机发送8位数据
主机拉高SS

// 读操作
主机拉低SS
主机发送8位地址(或命令)
主机发送8位虚拟数据(通常是0x00或0xFF)
从机在MISO上返回8位数据
主机拉高SS

这里有个细节:读操作时,主机必须继续发送时钟,从机才能把数据送出来。所以主机要发一个「虚拟字节」来产生时钟。我刚开始做的时候,以为读操作只需要发地址就行了,结果读回来的全是0。后来才明白,时钟是主机产生的,你不发时钟,从机怎么把数据给你?

不同厂家的MEMS传感器,帧格式略有差异。比如:

  • 有些传感器地址是8位,有些是16位
  • 有些支持多字节连续读写(burst mode)
  • 有些在地址后面需要加一个读写标志位

我建议你养成一个习惯:每次写驱动前,先把数据手册里的帧格式图画出来,标清楚每个字节的含义。这样写代码的时候不容易出错。

4.4 SPI时序分析:别让信号出问题

时序分析是SPI通信中最容易出问题的环节。我见过太多工程师,代码逻辑完全正确,但就是通信不稳定,最后发现是时序问题。

SPI时序有几个关键参数:

  • f_SCLK:时钟频率。MEMS传感器通常支持1MHz到10MHz,有些高速器件能到20MHz
  • t_SU:建立时间。数据在时钟沿之前必须稳定的时间
  • t_H:保持时间。数据在时钟沿之后必须保持稳定的时间
  • t_CS:片选到第一个时钟的延迟时间

下面这张图展示了SPI模式0的典型时序:

SPI模式0时序图(CPOL=0, CPHA=0) SS SCLK MOSI MISO Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0 ▲ 采样 ▲ 采样 ▲ 采样 ▲ 采样 ▲ 采样 ▲ 采样 ▲ 采样 ▲ 采样

从时序图可以看出,模式0下:

  • 空闲时SCLK为低电平
  • 数据在SCLK上升沿被采样
  • 数据在SCLK下降沿发生变化

避坑指南:我曾经在一个项目中,SPI通信偶尔出现错位。排查了两天才发现,是SS拉低后没有等足够的时间就发第一个时钟。有些传感器对t_CS(片选建立时间)有要求,至少需要几个微秒的延迟。后来我在代码里加了1微秒的延迟,问题就解决了。

另一个常见问题是信号反射。当SPI时钟频率超过10MHz时,信号反射会导致数据错误。我建议:

  • 走线尽量短,最好不超过10cm
  • 在MOSI和SCLK上串联22Ω到33Ω的电阻,抑制过冲
  • 如果从机离主机很远,考虑降低时钟频率

4.5 实际项目中的SPI配置示例

最后,给一个实际项目中初始化SPI的代码片段。以STM32为例,配置一个MEMS加速度计:

// SPI初始化 - 模式0,8位数据,MSB先行
void SPI_Init(void)
{
    SPI_HandleTypeDef hspi1;
    
    hspi1.Instance = SPI1;
    hspi1.Init.Mode = SPI_MODE_MASTER;
    hspi1.Init.Direction = SPI_DIRECTION_2LINES;
    hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
    hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;      // CPOL=0
    hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;          // CPHA=0
    hspi1.Init.NSS = SPI_NSS_SOFT;
    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8;  // 9MHz @72MHz
    hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
    hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
    hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
    hspi1.Init.CRCPolynomial = 10;
    
    HAL_SPI_Init(&hspi1);
}

// 读取传感器寄存器
uint8_t SPI_ReadRegister(uint8_t reg)
{
    uint8_t txData = reg | 0x80;  // 读操作,最高位置1
    uint8_t rxData = 0;
    
    HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);
    HAL_SPI_TransmitReceive(&hspi1, &txData, &rxData, 1, 100);
    txData = 0x00;  // 虚拟字节,产生时钟
    HAL_SPI_TransmitReceive(&hspi1, &txData, &rxData, 1, 100);
    HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);
    
    return rxData;
}

这段代码我用了很多年,基本没出过问题。唯一要注意的是,不同MCU的SPI外设实现细节有差异,但核心逻辑是一样的。

调试技巧:如果你用逻辑分析仪抓SPI波形,建议把采样率设到时钟频率的4倍以上。比如10MHz时钟,用40MHz采样率才能看清信号细节。我习惯用Saleae逻辑分析仪,便宜好用,抓SPI波形特别方便。

好了,SPI协议的基础就讲到这里。记住:物理层看布线,模式看时序图,帧格式看数据手册,时序分析靠示波器。把这四点吃透了,SPI通信基本不会出大问题。


公众号:蓝海资料掘金营,微信deep3321