2. 姿态的数学描述

SPI 这东西,说白了就是四根线在跳舞。SCK、MOSI、MISO、CS,跳对了,数据就通了。我见过不少工程师,代码写得飞起,示波器一抓波形却傻了眼——数据对不上,时序全乱套。这一章,咱们就把 SPI 的时序掰开揉碎,从波形图到配置实战,一步步讲清楚。

2.1 SPI数据传输时序图:读懂波形里的“暗号”

先看一张最基础的 SPI 时序图。别怕,这东西看着复杂,其实就几个关键点。

SPI 单字节传输时序图(CPOL=0, CPHA=0) SCK ▲ ▲ ▲ ▲ ▲ ▲ ▲ ▲ ▲ T MOSI MSB D6 D5 D4 D3 D2 D1 LSB MISO MSB D6 D5 D4 D3 D2 D1 LSB CS 片选有效(低电平) 数据在 SCK 上升沿被采样,每个时钟周期传输 1 位数据

这张图展示的是最常用的模式——CPOL=0, CPHA=0。你注意看几个地方:

  • SCK(时钟线):空闲时是低电平。每个上升沿,主设备从 MOSI 上发送一位,从设备从 MISO 上回复一位。
  • MOSI(主出从入):数据在时钟上升沿之前就准备好了。主设备先发 MSB(最高位),最后发 LSB(最低位)。
  • MISO(主入从出):从设备同步在上升沿送出数据。主设备在同一个边沿采样。
  • CS(片选):低电平有效。传输开始前拉低,结束后拉高。
我的习惯: 调试 SPI 时,我第一件事就是抓 CS 和 SCK 的波形。CS 拉低后,SCK 必须稳定几个微秒才开始跳变。很多新手忽略了这个建立时间,导致第一个 bit 采样出错。

2.2 主从设备通信建立过程:握手不是握一次

SPI 的通信建立,其实就三步。但每一步都有坑。

  1. 主设备拉低 CS:告诉从设备“我要跟你说话”。从设备收到这个下降沿,开始准备接收数据。
  2. 主设备输出 SCK 时钟:时钟开始跳变。从设备根据时钟边沿,在 MOSI 上读取命令或数据,同时在 MISO 上返回数据。
  3. 主设备拉高 CS:传输结束。从设备检测到 CS 上升沿,停止响应,回到空闲状态。

你想想看,如果主设备拉低 CS 后,SCK 还没开始跳,从设备会怎样?

大部分从设备会进入“等待”状态。但有些芯片,比如某些加速度传感器,CS 拉低后就开始采样 MISO 上的电平了。如果 MOSI 上还是高阻态,就可能读到错误数据。

我曾经踩过的坑: 有一次调试一个温湿度传感器,死活读不到正确的 ID 寄存器。抓波形发现,CS 拉低后 SCK 延迟了 2 微秒才启动,而传感器手册要求 CS 到 SCK 的建立时间不超过 1 微秒。后来在代码里把 CS 拉低和 SPI 传输之间的延时去掉,问题就解决了。

2.3 时钟极性与相位配置实战:CPOL 和 CPHA 的“排列组合”

SPI 有四种模式,由 CPOL(时钟极性)和 CPHA(时钟相位)决定。很多初学者在这里绕晕了。我教你一个记忆方法:

模式 CPOL CPHA 空闲时钟电平 数据采样边沿 常见外设
模式 0 0 0 低电平 上升沿 W25Qxx Flash, ADXL345
模式 1 0 1 低电平 下降沿 部分 DAC 芯片
模式 2 1 0 高电平 下降沿 部分温度传感器
模式 3 1 1 高电平 上升沿 SD 卡, 部分 LCD 驱动

怎么记?我总结了一个口诀:

  • CPOL=0:空闲时时钟是低电平。就像“躺平”状态。
  • CPOL=1:空闲时时钟是高电平。就像“站岗”状态。
  • CPHA=0:在时钟的第一个边沿采样数据。不管是上升还是下降,反正第一个边沿就干活。
  • CPHA=1:在时钟的第二个边沿采样数据。第一个边沿只是“准备”,第二个边沿才“动手”。

举个例子:模式 0(CPOL=0, CPHA=0),时钟空闲低电平,第一个边沿是上升沿,所以数据在上升沿采样。模式 3(CPOL=1, CPHA=1),时钟空闲高电平,第一个边沿是下降沿,但 CPHA=1 意味着第二个边沿才采样,所以数据在上升沿采样。

配置实战: 在 STM32 上配置 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);
避坑指南: 我曾经遇到一个国产陀螺仪芯片,手册上写的是模式 0,但实际测试发现必须用模式 3 才能正常通信。后来跟 FAE 沟通才知道,手册写错了。所以,拿到一个新外设,我建议先用逻辑分析仪抓一下从设备返回的数据,确认模式对不对。

2.4 多从设备片选机制:让每个设备都“听话”

SPI 总线上挂多个从设备,靠的就是片选信号。每个从设备独占一根 CS 线,主设备通过拉低不同的 CS 来选择跟谁通信。

多从设备 SPI 总线拓扑 主设备 从设备 1 从设备 2 从设备 3 SCK MOSI MISO CS1 CS2 CS3 多从设备注意事项 • 每个从设备独占一根 CS 线 • SCK/MOSI/MISO 共享总线 • 未选中的从设备 MISO 必须高阻 • CS 切换间需留空闲时间

多从设备设计时,有几个关键点:

  • CS 独占:每个从设备必须有自己的 CS 引脚。不能共用,否则两个设备同时响应,MISO 上就打架了。
  • MISO 高阻:未选中的从设备,其 MISO 引脚必须输出高阻态(Hi-Z)。否则多个设备同时驱动 MISO,轻则数据错误,重则烧毁引脚。
  • CS 切换间隔:从一个设备切换到另一个设备时,中间要留一段空闲时间。我一般至少留 1 微秒,让总线“喘口气”。
我曾经犯过的错: 在一个项目里挂了三个 SPI 传感器,CS 切换太快,从设备 A 刚释放总线,从设备 B 的 CS 就拉低了。结果 B 的 MISO 还没完全进入高阻态,A 的最后一个 bit 和 B 的第一个 bit 在总线上重叠了。后来我在每次 CS 拉高后加了一个 5 微秒的延时,问题才解决。

2.5 实战:用逻辑分析仪抓取 SPI 波形

理论讲完了,咱们来点实际的。假设你手头有一个 STM32 和一个 W25Q64 Flash 芯片,配置为模式 0,读取 ID 寄存器。波形应该长什么样?

  1. CS 拉低。
  2. 主设备发送 0x9F(读 ID 命令),8 个时钟周期。
  3. 主设备继续发送 3 个字节的 dummy 数据(0x00),同时从设备在 MISO 上返回 3 个字节的 ID。
  4. CS 拉高。

用逻辑分析仪抓到的波形,你应该能看到:CS 低电平期间,SCK 有 32 个完整的时钟周期。MOSI 上依次是 0x9F、0x00、0x00、0x00。MISO 上,前 8 个周期是高阻或无效数据,后 24 个周期是 ID 值。

调试技巧: 我习惯在代码里先发一个已知的读 ID 命令,然后用逻辑分析仪抓 MISO 上的返回值。如果返回值跟手册一致,说明 SPI 配置对了。如果不一致,先检查 CPOL 和 CPHA,再检查时钟频率是否太高。W25Q64 最高支持 104MHz,但有些便宜的杜邦线在 10MHz 以上就开始出错了。