3、SPI协议精讲:SPI的四种模式、时钟极性与相位、多从机配置
SPI 这玩意儿,说白了就是嵌入式世界里最常用的“串行聊天”方式之一。我做了这么多年硬件,跟它打交道最多。今天咱们就把 SPI 的底裤扒干净,从时钟极性相位到多从机配置,一次讲透。
3.1 SPI 的四种工作模式
SPI 有四种模式,区别就在于时钟极性(CPOL)和时钟相位(CPHA)怎么搭配。我刚开始学的时候总觉得这俩概念绕,后来在项目里吃过亏才彻底搞明白。
| 模式 | CPOL | CPHA | 时钟空闲电平 | 数据采样边沿 |
|---|---|---|---|---|
| 模式 0 | 0 | 0 | 低电平 | 上升沿 |
| 模式 1 | 0 | 1 | 低电平 | 下降沿 |
| 模式 2 | 1 | 0 | 高电平 | 下降沿 |
| 模式 3 | 1 | 1 | 高电平 | 上升沿 |
你想想看,CPOL 决定的是时钟线空闲时是什么电平。CPHA 决定的是数据在哪个边沿被采样。模式 0 和模式 3 最常用,我见过的 ADC 芯片 90% 都支持模式 0。
3.2 时钟极性与相位详解
咱们拿模式 0 举个例子。时钟空闲是低电平,数据在上升沿被主机采样,在下降沿被从机采样。说白了就是:主机在时钟上升沿“看”数据线,从机在下降沿“放”数据。
为什么会这样?因为 SPI 是全双工的,主机和从机同时发送和接收。一个边沿用来采样,另一个边沿用来更新数据。这样就不会打架。
3.3 多从机配置方案
一个 SPI 主机带多个从机,常见的有两种接法:独立片选和菊花链。我分别说说。
3.3.1 独立片选(最常见)
每个从机单独一根 CS 线。主机想跟谁说话,就把谁的 CS 拉低。其他从机的 CS 保持高电平,数据线处于高阻态。
- 优点: 简单直接,每个从机独立控制,互不干扰。
- 缺点: 从机多了,GPIO 不够用。比如带 8 个从机,就得占用 8 个 GPIO 做片选。
- 适用场景: 从机数量少(一般不超过 4 个),或者 GPIO 富裕。
3.3.2 菊花链(节省 GPIO)
所有从机共用一根 CS 线,数据像串糖葫芦一样从一个从机传到下一个。主机发一帧数据,第一个从机吃掉自己的那部分,剩下的往后传。
- 优点: 只占 1 个 CS 引脚,适合从机多的场景。
- 缺点: 延迟大,每个从机都要转发数据。而且所有从机必须支持菊花链模式。
- 适用场景: 移位寄存器、LED 驱动芯片这类需要级联的器件。
3.4 SPI 时序与知识体系
下面这张图把 SPI 的核心知识点串起来了。从时钟极性相位到多从机配置,再到实际项目中的避坑点,一目了然。
3.5 实际项目中的配置示例
拿一个常见的 ADC 芯片(比如 AD7680)来说,它工作在模式 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_16BIT;
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;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
HAL_SPI_Init(&hspi1);
嗯,这里要注意:片选 CS 要手动控制。拉低 CS 后,等一小段延时(至少半个时钟周期),再开始发数据。我习惯在 CS 拉低后加一个 nop 或者微秒级延时,确保从机准备好。
3.6 多从机配置的代码片段
下面是一个独立片选的多从机读写示例。两个从机,一个 ADC,一个 DAC。
// 片选引脚定义
#define CS_ADC_PIN GPIO_PIN_4
#define CS_DAC_PIN GPIO_PIN_5
#define CS_PORT GPIOA
// 选中 ADC
HAL_GPIO_WritePin(CS_PORT, CS_ADC_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(CS_PORT, CS_DAC_PIN, GPIO_PIN_SET);
delay_us(1);
HAL_SPI_TransmitReceive(&hspi1, adc_tx_buf, adc_rx_buf, 2, HAL_MAX_DELAY);
HAL_GPIO_WritePin(CS_PORT, CS_ADC_PIN, GPIO_PIN_SET);
// 选中 DAC
HAL_GPIO_WritePin(CS_PORT, CS_DAC_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(CS_PORT, CS_ADC_PIN, GPIO_PIN_SET);
delay_us(1);
HAL_SPI_Transmit(&hspi1, dac_tx_buf, 2, HAL_MAX_DELAY);
HAL_GPIO_WritePin(CS_PORT, CS_DAC_PIN, GPIO_PIN_SET);
你看,每次切换从机前,先把所有 CS 拉高,再拉低目标从机的 CS。这个顺序不能乱。我曾经因为偷懒没加那个 delay_us,结果 ADC 和 DAC 的数据串了,调试了一下午才发现。