第1章:SPI协议详解
大家好,我是你们的嵌入式驱动开发讲师。
今天咱们来聊聊SPI。说实话,在血氧仪这类医疗设备里,SPI和I2C就像左右手,缺一不可。我最早接触SPI是在做一款血氧探头驱动的时候,当时被时序折腾得够呛。嗯,今天就把这些经验掰开揉碎了讲给你听。
1.1 SPI总线物理层
SPI,全称是Serial Peripheral Interface,串行外设接口。它由摩托罗拉公司发明,现在已经是嵌入式领域的标配了。
物理层上,SPI总线至少需要4根线:
- SCLK(Serial Clock):时钟线,由主机产生
- MOSI(Master Out Slave In):主机输出,从机输入
- MISO(Master In Slave Out):主机输入,从机输出
- SS(Slave Select):从机选择线,也叫片选CS
你想想看,这四根线就构成了一个完整的通信链路。主机通过SCLK提供时钟,数据在时钟的上升沿或下降沿被采样。MOSI和MISO是两条独立的数据线,所以SPI天然支持全双工通信。
关键点:SPI没有像I2C那样的设备地址。它通过片选线SS来选择从机。谁被拉低,谁就参与通信。这一点我在项目中吃过亏,后面会细说。
我记得第一次画血氧仪的原理图时,把MOSI和MISO接反了。结果调试了整整两天,数据死活不对。后来用示波器一看,才发现是线序问题。所以啊,硬件设计时一定要仔细核对数据手册上的引脚定义。
1.2 SPI四种模式
SPI有四种工作模式,由时钟极性(CPOL)和时钟相位(CPHA)决定。说白了,就是决定时钟在空闲时是高还是低,以及数据在时钟的哪个边沿被采样。
| 模式 | CPOL | CPHA | 空闲时钟电平 | 数据采样边沿 |
|---|---|---|---|---|
| 模式0 | 0 | 0 | 低电平 | 上升沿 |
| 模式1 | 0 | 1 | 低电平 | 下降沿 |
| 模式2 | 1 | 0 | 高电平 | 上升沿 |
| 模式3 | 1 | 1 | 高电平 | 下降沿 |
为什么会这样?其实很简单。CPOL决定时钟空闲时的电平,CPHA决定数据是在第一个边沿还是第二个边沿采样。模式0和模式3是最常用的,我个人的习惯是优先用模式0,因为大多数外设都支持。
避坑指南:我曾经在驱动一款血氧传感器时,发现数据总是错位。查了半天,原来是传感器要求模式1,而我默认用了模式0。所以,拿到一个新外设,第一件事就是看数据手册里的时序图,确认它工作在哪种模式。
这里给个代码示例,配置STM32的SPI为模式0:
// SPI配置为模式0
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_64;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
HAL_SPI_Init(&hspi1);
1.3 SPI单工/半双工/全双工
SPI的通信方式有三种:单工、半双工和全双工。很多人以为SPI只能全双工,其实不然。
- 全双工:MOSI和MISO同时传输数据。主机发送的同时也能接收。这是SPI的默认模式,也是最常用的。
- 半双工:同一时刻只能一个方向传输。比如主机先发数据,然后切换方向接收。这种模式用得少,但有些外设会用到。
- 单工:只用一个方向传输。比如只发不收,或者只收不发。有些传感器只输出数据,不需要接收指令,就可以用单工模式。
你想想看,全双工的好处是效率高。但有些血氧传感器,比如MAX30102,它其实只需要主机发指令,然后读取数据。这种情况下,全双工的优势并不明显。
实际经验:我在调试一款血氧模块时,发现它的数据手册里写的是“只读设备”。这意味着它只需要MISO线,MOSI可以不用接。但很多工程师习惯性地把四根线都接上,结果发现通信不稳定。后来我查了原理图,发现MOSI引脚悬空时产生了干扰。所以,单工模式下,不用的引脚一定要处理好。
1.4 SPI多从机片选
SPI总线上可以挂多个从机,但每个从机需要独立的片选线。主机通过拉低某个从机的片选线来选择与之通信。
多从机片选有两种方式:
- 独立片选:每个从机一根片选线。主机用GPIO控制。这种方式最常用,也最灵活。
- 菊花链:从机串联,数据从一个从机传到下一个。这种方式节省引脚,但延迟大,不常用。
我记得在做一个多传感器血氧仪时,挂了三个SPI从机:一个血氧传感器、一个加速度计、一个存储芯片。每个从机一根片选线,主机用三个GPIO控制。调试时发现,切换从机时如果片选信号处理不当,会导致数据冲突。
注意事项:多从机通信时,切换从机前一定要确保当前从机释放总线。我曾经遇到过一个问题:从一个从机切换到另一个时,MISO线上还有残留数据,导致新从机的数据被污染。解决办法是在切换前加一个延时,或者先拉高所有片选线,再拉低目标片选线。
下面是一个多从机片选的代码示例:
// 定义片选引脚
#define CS_SENSOR GPIO_PIN_4
#define CS_ACCEL GPIO_PIN_5
#define CS_FLASH GPIO_PIN_6
// 选择血氧传感器
HAL_GPIO_WritePin(GPIOA, CS_SENSOR, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA, CS_ACCEL, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOA, CS_FLASH, GPIO_PIN_SET);
// 发送数据
HAL_SPI_Transmit(&hspi1, tx_data, len, 100);
// 释放片选
HAL_GPIO_WritePin(GPIOA, CS_SENSOR, GPIO_PIN_SET);
嗯,这里要注意:片选信号拉低后,要等一小段时间再开始传输数据。有些从机需要一定的建立时间。我一般会在片选拉低后加一个微秒级的延时,确保从机准备好。
好了,这一章的内容就到这里。SPI协议看似简单,但实际项目中坑不少。下一章我们会深入I2C协议,到时候再聊。