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。

我的习惯: 拿到一个新器件,第一件事就是看数据手册里 SPI 时序图。上面会明确标出 CPOL 和 CPHA,或者直接写“Mode 0”。千万别猜,猜错了通信就是乱码。

3.2 时钟极性与相位详解

咱们拿模式 0 举个例子。时钟空闲是低电平,数据在上升沿被主机采样,在下降沿被从机采样。说白了就是:主机在时钟上升沿“看”数据线,从机在下降沿“放”数据。

为什么会这样?因为 SPI 是全双工的,主机和从机同时发送和接收。一个边沿用来采样,另一个边沿用来更新数据。这样就不会打架。

我曾经踩过的坑: 有一次用 STM32 跟一个温湿度传感器通信,怎么读都是 0xFF。查了两天,最后发现传感器要求模式 2,而我默认配成了模式 0。时钟空闲电平反了,数据全错位。从那以后,我每次画原理图都会在 SPI 接口旁边贴个标签,写上“Mode X”。

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 的核心知识点串起来了。从时钟极性相位到多从机配置,再到实际项目中的避坑点,一目了然。

SPI 协议知识体系图 SPI 协议核心知识体系 SPI 协议 时钟极性与相位 四种工作模式 多从机配置 CPOL(空闲电平) CPHA(采样边沿) 模式 0 / 1 / 2 / 3 独立片选 菊花链 ⚠️ 避坑提示 先看数据手册确认模式,再配寄存器。片选线要加上拉电阻,防止浮空误触发。

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 或者微秒级延时,确保从机准备好。

小技巧: 多从机共用一个 SPI 总线时,切换从机前一定要先把 CS 拉高,等至少一个时钟周期,再拉低下一个从机的 CS。否则上一个从机的数据可能会干扰下一个。

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 的数据串了,调试了一下午才发现。

重要提醒: 如果从机是 3.3V 而主机是 5V,电平转换是必须的。别指望 SPI 能直接跨电压域通信。我见过有人直接把 5V 的 MOSI 接到 3.3V 的从机上,结果从机烧了。电平转换芯片不贵,别省这个钱。