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的典型时序:
从时序图可以看出,模式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