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的时序。
你看,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的时序一定要处理好,拉低和拉高的时机不对,数据就全乱了。