2、SPI 通信协议详解:四线制、时钟极性相位、数据帧格式与多从机拓扑

SPI 这玩意儿,说穿了就是「同步串行总线里的老实人」——结构简单,速度不慢,硬件开销也低。我在不少项目里都用它来连接 AD 转换器、SD 卡、显示屏之类的从设备。但你要是毛手毛脚地把时钟相位搭错了………嗯,数据读出来全是乱的,我当年就吃过这亏。

这一章咱们把 SPI 的几根线、时钟怎么配、数据怎么打包、以及多个从机怎么挂在一起,彻底捋一遍。你把它想清楚,后面 DMA 带宽计算才能算得准。


2.1 四线制:哪四根?各管什么事?

标准 SPI 总线通常就四根信号线,所以我常说「四线制」是它的名片。多一根或少一根都得另起名字了。

  • SCLK(Serial Clock) —— 时钟线,由主机驱动。它决定数据移位的节奏。
  • MOSI(Master Out Slave In) —— 主机输出、从机输入。数据从主机发往从机。
  • MISO(Master In Slave Out) —— 主机输入、从机输出。数据从从机返回主机。
  • SS / CS(Slave Select / Chip Select) —— 从机片选线。低电平有效,选中某个从机开始通信。

我习惯把 MOSI 和 MISO 想象成两条单向高速公路,一个去程一个回程,互不干扰。这也正是 SPI 能跑高速的原因之一——全双工,同时收发。

关键点: SPI 没有设备地址的概念,靠片选线实现「点名」。你想跟哪个从机说话,就把它的 CS 拉低,其他从机保持高阻态的 MISO。

2.2 时钟极性(CPOL)与时钟相位(CPHA)

这两个寄存器位是新手最容易翻车的地方。我见过一个工程师调了两天 SPI,最后发现是 CPOL 配反了——数据总是在时钟的另一个边沿采样,读回来的字节全都是 0x00 或 0xFF。

2.2.1 CPOL – 时钟空闲电平

  • CPOL = 0:空闲时 SCLK 为低电平。有效边沿为上升沿或下降沿(取决于 CPHA)。
  • CPOL = 1:空闲时 SCLK 为高电平。有效边沿相应改变。

2.2.2 CPHA – 数据采样边沿

  • CPHA = 0:在时钟的第一个边沿采样数据(上升沿还是下降沿取决于 CPOL)。
  • CPHA = 1:在时钟的第二个边沿采样数据。

组合起来就是四种 SPI 模式,很多从机芯片手册里会直接告诉你「SPI Mode 0」或「SPI Mode 3」。我把它们列出来,你直接对照着配就行:

SPI 模式CPOLCPHA空闲电平数据采样边沿
Mode 000低上升沿(第一个边沿)
Mode 101低下降沿(第二个边沿)
Mode 210高下降沿(第一个边沿)
Mode 311高上升沿(第二个边沿)

小技巧: 我习惯先用逻辑分析仪抓 SPI 波形,看 CS 拉低后 SCLK 第一个边沿是上升还是下降,再对比手册上要求的数据采样时刻。这比盯着代码寄存器值硬猜快得多。

2.3 数据帧格式:位序、字节宽度与 MSB/LSB

SPI 的数据帧通常以字节为单位传输,但有些控制器支持 4 位、8 位、16 位甚至 32 位帧。不过主流 8 位最常用。

  • MSB First / LSB First:大部分 SPI 设备默认先传高位(MSB First),但有些传感器(比如某些温度芯片)要求 LSB First。你需要在初始化时配好。
  • 数据长度:STM32、NXP 等 MCU 的 SPI 外设通常支持编程帧大小,比如 4~16 位。如果你接的是 12 位的 ADC,直接把帧长设为 12 位,省掉一次软件移位。
  • 时钟速率:每个 SCLK 周期移出一位。8 位数据需要 8 个 SCLK 脉冲。如果你想算一帧的时间,就是 8 / f_SCLK。

举个例子,我的一个项目里用 20 MHz SCLK 驱动 SD 卡,8 位帧的传输时间只有 400 ns。配合 DMA,CPU 几乎不用管。

// STM32 标准库配置 SPI 模式 0,8 位数据,MSB First 的示例
SPI_InitTypeDef SPI_InitStruct;
SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStruct.SPI_Mode = SPI_Mode_Master;
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low;        // Mode 0
SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge;      // 第一个边沿采样
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;          // 软件控制片选
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_Init(SPI1, &SPI_InitStruct);

2.4 多从机拓扑:菊花链 vs 独立片选

SPI 总线上挂多个从机,主要有两种接法。我分别说说它们的优缺点。

2.4.1 独立片选(Parallel / Star)

每个从机单独一根 CS 线,主机有多少个 GPIO 就能接多少个从机。MOSI、MISO、SCLK 全部并联。

  • 优点: 每个从机独立寻址,速度不受其他从机影响;可连接不同型号的设备。
  • 缺点: 需要大量 GPIO,走线复杂,且所有从机的 MISO 必须三态输出,否则会打架。

注意: 我曾经踩过一个坑——两个从机在未选中时 MISO 没有释放为高阻,结果两者同时驱动,直接把 IO 引脚烧了。所以务必确保从机片选拉高后 MISO 输出高阻。

2.4.2 菊花链(Daisy Chain)

从机依次串接:主机的 MOSI 接第一个从机的 SI,第一个从机的 SO 接第二个从机的 SI,以此类推。SCLK 和 CS 是并联的。

  • 优点: 节省 GPIO(只需一根 CS),接线简单。
  • 缺点: 每个从机内部有移位延迟,时钟频率不能太高;所有从机必须同时被选中,不能单独操作某一个。

我用菊花链接过多片移位寄存器做 LED 驱动,很方便。但接 AD 转换器不太推荐,因为延时积累会降低实时性。

为了让你一眼看清这两种拓扑的结构,我画了一张对比图:

SPI 多从机拓扑对比 主机 独立片选 从机 A 从机 B 从机 C 公共 SCLK + MOSI + MISO 菊花链 从机 A 从机 B 从机 C MOSI → SI_A → SO_A → SI_B → SO_B → … 💡 选型建议 • 独立片选:适合从机类型不同、需要单独配置的场景;注意 MISO 三态控制。 • 菊花链:适合同型号从机、对延迟不敏感的场景(如 LED 驱动、移位寄存器)。 • 时钟频率受最远从机的信号质量限制;菊花链的总延迟是各从机延迟之和。

2.5 实践中的匹配要点

好了,理论讲完,说点实在的。当你拿到一块新的 SPI 从机芯片,应该按什么步骤配?我自己的习惯是这样的:

  1. 读手册:找到 SPI 时序图,确认 CPOL/CPHA,以及 MSB/LSB。
  2. 配时钟:从机最大频率是多少?主机分频后别超它。低于 10 MHz 基本不用太担心信号完整性。
  3. 设帧格式:8 位还是 16 位?如果是 8 位,MCU 默认就是 8 位,省心。
  4. 选拓扑:如果只用 1 个从机,就不用纠结;如果多个,优先独立片选,除非 GPIO 不够。
  5. 验证波形:上逻辑分析仪,抓 CS 拉低后第一个 SCLK 边沿对应的数据位,跟预期对比。

我记得有一次调一个加速度计,手册写 Mode 3,我配了 Mode 0,结果读回来的 ID 寄存器一直是 0xFF。抓波形才发现采样边沿正好在数据跳变上。真是教科书级的错误,但我一辈子不会忘。

一个治本的建议: 在你的 SPI 驱动初始化代码里,把模式、帧长、比特序以宏定义写清楚,注释里直接贴手册上的截图描述。以后别人接手,或者半年后你自己回来看,都不用重新翻手册。