SPI 这玩意儿,说简单也简单,四根线一接,时钟一给,数据就能跑起来。但说坑也真坑,尤其是当你面对一个从来没用过的传感器或者 Flash 芯片,翻到数据手册的时序图那一页,看到 CPOL 和 CPHA 两个参数组合出来的四种模式,脑子里瞬间就开始打架:到底该用 Mode 0 还是 Mode 3?为什么同样的代码换一颗芯片就死活读不出 ID?
我这些年调试过的 SPI 设备少说也有几十款,从常见的 SPI Flash、各种传感器,到一些比较冷门的 ADC 和显示驱动,踩过的坑基本都和这四种模式的配置有关。很多人调 SPI 的习惯是:先随便选个 Mode 0,读不出来就换 Mode 3,再读不出来就换 Mode 1、Mode 2,四个模式轮一遍,总有一个能通。这个方法确实能解决大部分问题,但如果你一直停留在“试出来能用就行”的阶段,遇到多设备共用总线、高速通信丢数据、DMA 传输错位这些情况,还是会抓瞎。
所以这篇内容我打算把 SPI 四种模式彻底讲透,不是那种抄手册的定义复述,而是从实际调试的角度出发,把 CPOL 和 CPHA 到底在干什么、四种模式对应的时序波形长什么样、不同芯片该怎么选、配置错了会出现什么现象、以及实际项目中怎么快速定位模式问题,全部串起来讲一遍。不管你是刚接触 SPI 的新手,还是已经用过但一直没搞明白底层逻辑的老手,应该都能从里面找到对自己有用的东西。
1. 从一根时钟线的两种“空闲状态”说起
1.1 CPOL 决定的到底是什么
SPI 协议里最容易被忽略但又最基础的一个概念,就是时钟极性,也就是 CPOL(Clock Polarity)。很多人看到“极性”两个字就觉得抽象,其实它描述的事情特别简单:当 SPI 总线没有在传输数据的时候,时钟线 SCK 应该保持在高电平还是低电平。
就这么一个看似无关紧要的设定,却是四种模式分化的第一个维度。CPOL = 0 的时候,SCK 在空闲状态保持低电平;CPOL = 1 的时候,SCK 在空闲状态保持高电平。你可以把它理解成一个人站着的时候习惯把手指向地面还是指向天空,这个“习惯姿势”就是空闲状态。
为什么这个空闲状态很重要?因为 SPI 是同步通信,数据位的采样和移出都跟时钟边沿严格绑定。如果主机和从机对空闲状态的理解不一致,那么第一个时钟边沿到来的时候,双方对“这是第几个边沿”的判断就会错位,数据自然也就对不上了。
我刚开始接触 SPI 的时候,总觉得 CPOL 就是个可有可无的配置项,反正时钟一直在翻转,高低有什么关系。后来用逻辑分析仪抓了一次波形才明白,空闲电平决定了第一个有效边沿是上升沿还是下降沿,而这个“第一个有效边沿”恰恰是数据采样的起点。起点错了,后面全错。
1.2 CPHA 如何定义采样与移出的边沿
如果说 CPOL 决定了时钟的“静止姿势”,那 CPHA(Clock Phase)决定的就是“动作节奏”——数据到底在第一个边沿采样还是在第二个边沿采样。
CPHA = 0 的含义是:数据在第一个时钟边沿被采样,在第二个时钟边沿被移出。CPHA = 1 则反过来:数据在第一个时钟边沿被移出,在第二个时钟边沿被采样。
这里有一个很容易混淆的点:所谓“第一个边沿”和“第二个边沿”,是相对于每个时钟周期而言的。一个完整的时钟周期包含两个边沿,一个是前沿(Leading Edge),一个是后沿(Trailing Edge)。前沿是从空闲电平出发遇到的第一个跳变,后沿是回到空闲电平方向的那个跳变。
CPHA = 0 的时候,前沿用来采样,后沿用来移出下一位数据。CPHA = 1 的时候,前沿用来移出数据,后沿用来采样。你可以这样记:CPHA 等于 0,前沿就是采样沿;CPHA 等于 1,前沿是准备沿,后沿才是采样沿。
这个区别在实际波形上非常明显。CPHA = 0 时,数据线 MOSI 上的第一位数据必须在第一个时钟边沿到来之前就已经稳定有效;而 CPHA = 1 时,第一位数据是在第一个时钟边沿之后才变得有效,因为第一个边沿是用来触发移出的。
1.3 四种模式其实是两个维度的排列组合
把 CPOL 和 CPHA 各自两种取值组合起来,就得到了 SPI 的四种模式:
| 模式 | CPOL | CPHA | 空闲时钟电平 | 采样边沿 | 移出边沿 |
|---|---|---|---|---|---|
| Mode 0 | 0 | 0 | 低电平 | 上升沿 | 下降沿 |
| Mode 1 | 0 | 1 | 低电平 | 下降沿 | 上升沿 |
| Mode 2 | 1 | 0 | 高电平 | 下降沿 | 上升沿 |
| Mode 3 | 1 | 1 | 高电平 | 上升沿 | 下降沿 |
这张表建议你直接记住,尤其是采样边沿那一列。因为在实际调试中,用逻辑分析仪或者示波器抓波形的时候,你最需要确认的就是:数据线在哪个边沿上最稳定?那个边沿就是采样边沿,对应的模式也就确定了。
有一个很实用的记忆技巧:Mode 0 和 Mode 3 的采样边沿都是上升沿,区别只在于空闲电平不同;Mode 1 和 Mode 2 的采样边沿都是下降沿,同样区别只在空闲电平。换句话说,CPHA 决定了采样边沿是上升还是下降,CPOL 决定了时钟从哪个电平出发。
1.4 为什么大多数芯片默认推荐 Mode 0
如果你翻过各种 SPI 设备的数据手册,会发现一个很有意思的现象:大部分芯片都会在时序参数表里标注支持 Mode 0 和 Mode 3,有些还会额外支持 Mode 1 和 Mode 2,但几乎所有的芯片都会把 Mode 0 放在第一位。
这不是偶然的。Mode 0 之所以成为事实上的默认选择,有几个很实际的原因。第一,CPOL = 0 意味着空闲时时钟线是低电平,这对于大多数数字电路来说是最自然的默认状态,不需要额外的上拉或下拉。第二,CPHA = 0 意味着数据在第一个边沿就被采样,主机可以在片选拉低之后立刻开始输出时钟,不需要额外的准备时间,通信效率最高。
第三,也是最重要的一点:Mode 0 和 Mode 3 在采样边沿上是一致的,都是上升沿采样。这意味着如果一颗芯片支持 Mode 0,那么它在大多数情况下也能兼容 Mode 3,因为采样时刻相同,只是空闲电平不同。很多主控在配置为 Mode 3 的时候,如果从机对空闲电平不敏感,通信照样能正常进行。这也是为什么很多人“随便试”的时候,往往在 Mode 0 和 Mode 3 之间切换就能解决问题。
但 Mode 1 和 Mode 2 就没这么幸运了。它们的采样边沿是下降沿,和 Mode 0/3 完全相反。如果你把一颗只支持 Mode 0 的芯片配置成 Mode 1,数据采样时刻会整体偏移半个时钟周期,读出来的数据要么全错,要么时对时错,非常折磨人。
2. 用波形把四种模式的时序差异摊开来看
2.1 Mode 0 的完整时序拆解
Mode 0 是 CPOL = 0、CPHA = 0 的组合。空闲时 SCK 保持低电平,第一个时钟边沿是上升沿,数据在这个上升沿被采样,然后在接下来的下降沿被移出。
假设我们要发送一个字节 0xA5,二进制是 10100101,MSB 先发。在 Mode 0 下,整个时序是这样的:片选 CS 拉低之后,MOSI 线上首先出现最高位 1,这个 1 必须在第一个 SCK 上升沿到来之前就已经稳定。然后 SCK 产生第一个上升沿,从机在这个时刻读取 MOSI 上的 1。接着 SCK 下降,主机把下一位 0 放到 MOSI 上。再下一个上升沿,从机读取 0。如此循环八次,一个字节传输完成。
这里有一个很关键的细节:在 Mode 0 下,数据是在上升沿采样,但数据的切换是在下降沿。也就是说,MOSI 上的数据在 SCK 低电平期间发生变化,在 SCK 高电平期间保持稳定。这个规律对于用逻辑分析仪分析波形非常有用,如果你看到数据线在时钟高电平期间跳变,那基本可以确定模式配置有问题。
MISO 线上的行为也是类似的。从机在下降沿把数据放到 MISO 上,主机在上升沿采样。所以如果你同时抓 MOSI 和 MISO,会看到两条数据线都是在时钟下降沿附近发生变化,在上升沿附近保持稳定。
2.2 Mode 1 的采样时刻为什么“晚半步”
Mode 1 是 CPOL = 0、CPHA = 1。空闲时 SCK 仍然是低电平,但采样边沿变成了下降沿。
这个模式下,第一个时钟边沿是上升沿,但上升沿不做采样,而是用来移出数据。真正的采样发生在下降沿。换句话说,数据在上升沿被放到总线上,然后在下降沿被对方读取。
和 Mode 0 对比一下就很清楚了:Mode 0 是“先有数据,再来边沿采样”;Mode 1 是“先来边沿,再把数据放上去,等下一个边沿采样”。所以 Mode 1 的采样时刻比 Mode 0 晚了半个时钟周期。
这个“晚半步”在实际通信中会带来一个很直接的影响:如果你用 Mode 0 的代码去驱动一颗 Mode 1 的芯片,从机在第一个上升沿的时候还没有把数据准备好,因为它以为第一个边沿只是用来触发的。结果就是主机采样到的第一位数据是无效的,整个字节会错位一位,读出来的值完全不对。
我遇到过一颗压力传感器,手册上写的是支持 Mode 1,但我一开始没注意,直接用了 Mode 0 的配置。读出来的压力值一直在跳,而且跳得毫无规律。后来用逻辑分析仪抓波形才发现,数据线的变化时刻和时钟采样沿对不上,差了半个周期。改成 Mode 1 之后,数据立刻就稳定了。
2.3 Mode 2 与 Mode 0 的镜像关系
Mode 2 是 CPOL = 1、CPHA = 0。空闲时 SCK 保持高电平,第一个时钟边沿是下降沿,数据在这个下降沿被采样。
如果你把 Mode 0 的波形上下翻转一下,得到的就是 Mode 2 的波形。两者的采样逻辑完全一样——都是在前沿采样、后沿移出——只不过因为空闲电平不同,前沿从上升沿变成了下降沿。
这种镜像关系意味着,Mode 0 和 Mode 2 在代码层面的配置差异其实很小,主要就是 CPOL 这一位不同。但实际通信中,如果你把 Mode 0 的配置用在 Mode 2 的芯片上,会出现什么现象?片选拉低之后,SCK 本来是低电平,但芯片期望的是高电平。第一个时钟边沿,你发出的是上升沿,但芯片期望的是下降沿。结果就是芯片在整个通信过程中都处于“对不上拍”的状态,读出来的数据基本是随机的。
有些主控的 SPI 外设在切换 CPOL 的时候,SCK 线的空闲电平会立即改变。如果你在通信过程中动态切换模式,一定要确保片选已经拉高,总线处于空闲状态,否则会在总线上产生毛刺,甚至误触发从机。
2.4 Mode 3 为什么能和 Mode 0 “混用”
Mode 3 是 CPOL = 1、CPHA = 1。空闲时 SCK 保持高电平,第一个边沿是下降沿,但采样发生在第二个边沿,也就是上升沿。
前面提到过,Mode 3 和 Mode 0 的采样边沿都是上升沿。这意味着,如果你用 Mode 0 的主机去驱动一颗 Mode 3 的从机,或者反过来,采样时刻其实是一致的。唯一的区别在于空闲电平:Mode 0 的空闲电平是低,Mode 3 的空闲电平是高。
那为什么很多时候“混用”也能正常工作?因为很多从机芯片对空闲电平并不敏感,它只关心采样边沿。只要采样边沿对上了,数据就能正确读取。但这种情况并不是绝对的,有些芯片对空闲电平有严格要求,比如某些 Flash 芯片在片选拉低之后会检查 SCK 的电平状态,如果发现空闲电平不对,可能会进入错误的内部状态。
所以我的建议是:不要依赖“混用能通”这个巧合,该配置成什么模式就配置成什么模式。尤其是在批量生产或者高可靠性场景下,依赖巧合迟早会出问题。
3. 选型实战:拿到一颗新芯片怎么确定用哪个模式
3.1 数据手册里哪些参数是必须看的
拿到一颗新的 SPI 芯片,第一件事肯定是翻数据手册。但数据手册那么厚,到底该看哪些部分?
首先,找到 SPI 接口的时序参数表。这个表通常会列出 tSU、tHD、tV 等时间参数,但更重要的是它会明确写出支持的 SPI 模式。有些手册会直接写“Supports SPI Mode 0 and Mode 3”,有些则会用 CPOL 和 CPHA 的组合来表示。
其次,看时序图。时序图是最直观的判断依据。你只需要关注两个东西:SCK 在 CS 拉低之后的第一个边沿是上升还是下降,以及数据线在哪个边沿上被标注为“Valid”。如果数据在第一个边沿就被标注为有效,那就是 CPHA = 0;如果数据在第二个边沿才被采样,那就是 CPHA = 1。
第三,注意一些特殊的时序要求。比如有些芯片要求 CS 拉低之后必须等待一段时间才能开始输出时钟,有些芯片要求最后一个时钟边沿之后 CS 必须保持低电平一段时间才能拉高。这些细节虽然不直接决定模式选择,但会影响你的驱动代码能不能稳定工作。
3.2 用逻辑分析仪抓波形的判断流程
如果你手头没有数据手册,或者手册写得含糊不清,那就只能靠抓波形来判断了。逻辑分析仪是干这个活儿的最佳工具,几十块钱的入门款就够用。
具体怎么判断?把探头接到 SCK、MOSI、MISO 和 CS 四根线上,然后让主机用任意一种模式发送一个已知的数据,比如 0xAA 或者 0x55。抓到的波形按下面的步骤分析:
第一步,看 CS 拉低之后 SCK 的第一个边沿是上升还是下降。如果是上升沿,说明 CPOL = 0;如果是下降沿,说明 CPOL = 1。
第二步,看 MOSI 上的数据在哪个边沿发生变化。如果数据在第一个边沿之前就已经稳定,在第一个边沿之后才变化,说明 CPHA = 0;如果数据在第一个边沿之后才出现,在第二个边沿之后变化,说明 CPHA = 1。
第三步,对照 MISO 线上的数据。如果 MISO 上的数据在采样边沿附近是稳定的,说明模式匹配;如果 MISO 上的数据在采样边沿附近正在跳变,说明模式不匹配。
这个方法我用了很多次,基本上五分钟之内就能确定一颗芯片该用什么模式。唯一需要注意的是,有些芯片在 CS 拉低之后不会立即输出数据,可能需要几个时钟周期之后 MISO 才有效,这种情况下要先把前面的无效周期排除掉再分析。
3.3 常见芯片的模式选择参考
虽然每颗芯片都可能有自己的特殊性,但根据我的经验,大部分常见芯片的模式选择是有规律可循的:
| 芯片类型 | 常见模式 | 说明 |
|---|---|---|
| SPI Flash(W25Q系列等) | Mode 0 或 Mode 3 | 两者都支持,实际用 Mode 0 居多 |
| 加速度计/陀螺仪 | Mode 0 或 Mode 3 | 多数支持两种,Mode 3 也常见 |
| 压力/温度传感器 | Mode 1 或 Mode 0 | 部分型号只支持 Mode 1 |
| ADC 芯片 | Mode 0 或 Mode 1 | 取决于具体型号,需查手册 |
| 显示驱动 | Mode 0 或 Mode 3 | 多数支持 Mode 0 |
| 无线模块 | Mode 0 | 绝大多数只支持 Mode 0 |
这张表只是一个大致的参考,具体到某一颗芯片,还是要以数据手册为准。但如果你实在找不到手册,可以按照这个表的顺序去试,能提高不少效率。
3.4 配置错误时读出来的数据长什么样
模式配置错误的时候,读出来的数据往往有一些很典型的特征,了解这些特征可以帮助你快速判断问题方向。
如果读出来的数据全是 0xFF 或者全是 0x00,那可能不是模式问题,而是接线问题或者片选问题。但如果读出来的数据是“看起来有点规律但就是不对”,比如设备 ID 应该是 0x1234,你读出来是 0x091A 或者 0x2468,那基本可以确定是模式配置错了。
具体来说,如果采样边沿错了半个周期,读出来的数据会出现“整体移位”的现象。比如正确的数据是 10100101,你读出来可能是 01001011 或者 11010010,也就是整体左移或右移了一位。这是因为采样时刻偏移导致每一位都被采到了相邻位的位置上。
还有一种情况是数据“时对时错”,某些字节正确,某些字节错误。这通常是因为模式虽然大体匹配,但时序余量不够,在高速通信或者温度变化的时候就会出现偶发错误。这种情况下,除了检查模式配置,还要检查时钟频率是否超过了从机的最大支持频率。
4. 代码层面的模式配置与常见陷阱
4.1 STM32 HAL 库里的模式配置细节
以 STM32 为例,HAL 库配置 SPI 模式主要涉及两个参数:CLKPolarity 和 CLKPhase。在 CubeMX 里,这两个参数对应着四个选项的组合,直接决定了 SPI 工作在哪种模式。
// Mode 0 配置 hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // Mode 1 配置 hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; // Mode 2 配置 hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH; hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // Mode 3 配置 hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH; hspi1.Init.CLKPhase = SPI_PHASE_2EDGE;这里有一个很容易搞混的地方:SPI_PHASE_1EDGE 对应的是 CPHA = 0,SPI_PHASE_2EDGE 对应的是 CPHA = 1。我第一次用 HAL 库的时候,看到“1EDGE”和“2EDGE”这两个词,下意识以为 1EDGE 就是第一个边沿采样,结果发现 Mode 0 的配置里写的是 1EDGE,但 Mode 0 明明是第一个边沿采样,这没错啊。后来才反应过来,1EDGE 确实就是第一个边沿采样,也就是 CPHA = 0。这个命名其实挺直观的,只是我当时想复杂了。
还有一个细节:HAL 库在初始化 SPI 的时候,会先根据配置设置 SCK 的空闲电平,然后再使能 SPI。如果你在通信过程中动态修改 CPOL 或 CPHA,一定要先调用 HAL_SPI_DeInit 或者确保片选已经拉高,否则可能会在总线上产生意外的时钟脉冲。
4.2 硬件片选与软件片选对模式的影响
SPI 的片选信号有硬件片选(NSS)和软件片选两种方式。硬件片选由 SPI 外设自动控制,软件片选则由代码手动控制 GPIO。
从模式配置的角度来说,硬件片选和软件片选本身不影响 CPOL 和 CPHA 的选择,但它们会影响时序的精确性。硬件片选在最后一个时钟边沿之后会自动拉高,时序非常精确;软件片选则需要代码在传输完成之后手动拉高,如果代码里有中断或者其他任务插入,片选拉高的时刻可能会延迟。
这个延迟在某些对时序敏感的芯片上会导致问题。比如有些 Flash 芯片要求片选在最后一个时钟边沿之后的一个很短时间内拉高,如果延迟太长,芯片可能会认为通信异常。所以如果你的项目对时序要求很高,建议优先使用硬件片选,或者至少确保软件片选的拉高操作在传输完成之后立即执行。
另外,在多从机共用总线的场景下,软件片选更灵活,因为你可以用任意 GPIO 来控制不同从机的片选。但要注意,在切换从机的时候,一定要确保前一个从机的片选已经拉高,并且总线已经空闲,再拉低下一个从机的片选。否则两个从机同时被选中,MISO 线上会出现总线冲突。
4.3 DMA 传输时的模式配置注意事项
用 DMA 来传输 SPI 数据可以大大减轻 CPU 的负担,尤其是在高速、大数据量传输的场景下。但 DMA 和 SPI 模式配置之间有一些需要特别注意的地方。
首先,DMA 传输本身不改变 SPI 的时序特性,CPOL 和 CPHA 的配置和普通传输完全一样。但 DMA 传输的数据是连续不断的,如果模式配置错了,错误的数据会以很快的速度涌入,你可能来不及反应就已经收到了大量错误数据。
其次,DMA 传输的时钟频率通常比较高,这时候时序余量就变得很重要。如果你的模式配置虽然正确,但时钟频率接近从机的极限,DMA 传输可能会因为时序余量不足而出现偶发错误。这种情况下,可以适当降低时钟频率,或者调整 SPI 的时钟分频系数。
还有一个容易被忽略的点:DMA 传输完成之后,SPI 外设可能还有最后一个字节正在移位输出。如果你在 DMA 传输完成中断里立即拉高片选,可能会截断最后一个字节的传输。正确的做法是等待 SPI 的 BUSY 标志清零之后再拉高片选。
// 等待 SPI 传输完成后再拉高片选 while (HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);4.4 多设备共用总线时的模式冲突处理
在实际项目中,一条 SPI 总线上挂多个从机是很常见的。如果所有从机都支持同一种模式,那皆大欢喜。但如果不同从机支持的模式不同,就需要在切换从机的时候同时切换 SPI 的模式配置。
这种情况下,代码里需要维护一个从机配置表,记录每个从机对应的模式、时钟频率、片选引脚等信息。每次切换从机的时候,先拉高当前片选,然后重新配置 SPI 的模式和时钟,再拉低目标从机的片选,开始通信。
typedef struct { uint8_t mode; // 0-3 uint32_t baudrate; // 时钟频率 GPIO_TypeDef* cs_port; // 片选端口 uint16_t cs_pin; // 片选引脚 } SPI_DeviceConfig; void SPI_SwitchDevice(SPI_HandleTypeDef* hspi, SPI_DeviceConfig* dev) { // 拉高当前片选 HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); // 重新配置 SPI 模式 hspi->Init.CLKPolarity = (dev->mode & 0x02) ? SPI_POLARITY_HIGH : SPI_POLARITY_LOW; hspi->Init.CLKPhase = (dev->mode & 0x01) ? SPI_PHASE_2EDGE : SPI_PHASE_1EDGE; hspi->Init.BaudRatePrescaler = dev->baudrate; HAL_SPI_Init(hspi); // 拉低目标片选 HAL_GPIO_WritePin(dev->cs_port, dev->cs_pin, GPIO_PIN_RESET); }这段代码里有一个关键点:在重新配置 SPI 之前,一定要确保当前片选已经拉高,总线处于空闲状态。否则重新配置的过程中可能会在 SCK 线上产生毛刺,被从机误认为是有效的时钟边沿。
5. 那些年我踩过的 SPI 模式坑
5.1 把 Mode 3 当成 Mode 0 用了一整年
这是我早期做项目时踩过的一个典型坑。当时用的一颗 SPI Flash 芯片,手册上写的是支持 Mode 0 和 Mode 3。我随手配了 Mode 0,读写都正常,就没再管它。后来项目换了一颗同系列的 Flash,引脚兼容,我以为直接替换就行,结果新芯片读出来的 ID 全是 0xFF。
排查了半天,接线没问题,电源没问题,最后用逻辑分析仪抓波形才发现,新芯片在片选拉低之后会检查 SCK 的电平状态,如果 SCK 是低电平,它会进入一种低功耗待机状态,不响应任何命令。而旧芯片没有这个行为,所以 Mode 0 也能正常工作。
解决办法很简单,把模式改成 Mode 3,SCK 空闲电平变成高电平,新芯片就能正常识别了。但这个问题让我意识到,“能通”不等于“正确”,依赖巧合的配置迟早会出问题。
5.2 模式配置正确但数据仍然错误的排查过程
还有一次,我确认了芯片手册上写的是 Mode 1,代码也配置成了 Mode 1,但读出来的数据就是不对。逻辑分析仪抓波形,采样边沿对得上,数据线在采样时刻也是稳定的,看起来一切正常。
后来仔细看波形才发现,问题出在片选信号上。这颗芯片要求片选拉低之后必须等待至少 100 纳秒才能开始输出时钟,而我的代码在片选拉低之后立刻就调用了 SPI 传输函数,中间几乎没有延迟。虽然逻辑分析仪上看起来时钟和数据都对,但芯片内部可能还没有完成片选检测,导致第一个字节被忽略了。
解决办法是在片选拉低之后加一个短暂的延时:
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); for (volatile int i = 0; i < 10; i++); // 短暂延时 HAL_SPI_Transmit(&hspi1, data, len, timeout);这个延时不需要很精确,几个微秒就够了。但如果没有这个延时,通信就会时好时坏,非常难排查。
5.3 高速通信下模式匹配但偶发丢数据的解决
高速 SPI 通信下,即使模式配置正确,也可能出现偶发丢数据的情况。我遇到过一项目,SPI 时钟跑到 20MHz,大部分时候通信正常,但每隔几百次传输就会有一次数据错误。
用逻辑分析仪抓了很久,终于抓到了一次错误波形。发现错误发生的时候,MISO 线上的数据在采样边沿附近有一个很小的毛刺,导致主机采样到了错误的值。这个毛刺不是每次都有,只有在特定的数据模式下才会出现。
根本原因是 PCB 走线太长,MISO 线上有反射,导致信号完整性变差。解决办法有两个:一是降低 SPI 时钟频率,给信号更多的时间稳定;二是在 MISO 线上串联一个小的电阻(比如 22 欧姆),抑制反射。
我选择了串联电阻的方案,因为不想牺牲通信速度。串联电阻之后,毛刺明显减小,偶发错误也消失了。这个经验告诉我,SPI 模式配置只是通信正常的基础条件,信号完整性同样重要,尤其是在高速场景下。
5.4 用错模式导致的数据移位问题复盘
数据移位是模式配置错误最典型的表现。我曾经用 Mode 0 去驱动一颗 Mode 1 的传感器,读出来的数据整体右移了一位。比如正确的温度值是 0x1A,我读出来是 0x0D,正好是右移一位的结果。
当时我还没意识到是模式问题,以为是数据格式解析错了,折腾了很久。后来把读出来的原始数据和手册上的示例数据对比,发现每一位都错位了,才反应过来是采样边沿的问题。
这个问题的教训是:当你发现读出来的数据有规律地偏移时,优先检查 SPI 模式配置,而不是去怀疑数据解析代码。数据解析代码通常不会产生“整体移位”这种错误,只有采样时刻偏移才会导致每一位都错位。
6. 几个能帮你快速定位模式问题的实用技巧
6.1 用已知数据做回环测试
如果你不确定主机的 SPI 配置是否正确,可以做一个简单的回环测试:把 MOSI 和 MISO 短接,然后发送一个已知的数据,看接收到的数据是否和发送的一致。
这个测试可以排除从机的影响,单独验证主机的 SPI 配置。如果回环测试通过,说明主机的模式配置、时钟配置、DMA 配置都是正确的,问题出在从机侧或者接线侧。如果回环测试不通过,那问题肯定在主机配置上。
回环测试的时候,建议用 0xAA 和 0x55 这两个数据,因为它们的二进制是 10101010 和 01010101,每一位都和相邻位相反,最容易暴露移位和采样错误。
6.2 通过读取设备 ID 验证模式
大部分 SPI 芯片都有一个设备 ID 寄存器,读取这个寄存器的值可以快速验证通信是否正常。设备 ID 通常是固定的,手册上会给出预期值,你只需要读出来对比一下就知道模式对不对。
读设备 ID 的命令通常是 0x9F 或者 0x90,具体要看手册。发送命令之后,芯片会返回一个或多个字节的 ID 数据。如果读出来的 ID 和手册一致,说明模式配置正确;如果不一致,可以尝试切换模式再读。
这个方法比抓波形更简单,不需要额外的仪器,只需要几行代码就能完成。但前提是你知道正确的设备 ID 是什么,所以还是得看手册。
6.3 逻辑分析仪的关键触发设置
用逻辑分析仪抓 SPI 波形的时候,触发设置很关键。如果触发条件设得不好,可能抓不到你想要的波形,或者抓到的波形不完整。
我的习惯是把触发条件设在片选信号的下降沿,也就是 CS 从高变低的那一刻。这样可以确保抓到完整的通信过程,从片选拉低到片选拉高,一个字节都不漏。
如果通信数据量很大,可以设置成在特定数据出现时触发。比如你想看某个命令的响应波形,可以把触发条件设在 MOSI 上出现该命令的时候。这样逻辑分析仪只会在该命令出现时才抓取波形,避免了大量无用数据。
另外,采样率要设得足够高。SPI 时钟频率如果是 10MHz,逻辑分析仪的采样率至少要是 100MHz,才能保证每个时钟周期有足够的采样点,波形才不会失真。
6.4 模式切换时的总线空闲检查
前面提到过,动态切换 SPI 模式的时候,一定要确保总线处于空闲状态。但“空闲状态”到底怎么判断?
最可靠的方法是检查 SPI 外设的 BUSY 标志。在 STM32 的 HAL 库里,可以通过 HAL_SPI_GetState 函数来检查 SPI 是否处于忙碌状态。只有当状态为 HAL_SPI_STATE_READY 时,才说明传输已经完成,总线空闲。
另外,还要确保片选信号已经拉高。如果片选还是低电平,即使 SPI 外设不忙,从机也可能还在等待后续的时钟脉冲。这时候切换模式,从机会收到意外的时钟边沿,可能导致状态机错乱。
// 确保总线空闲后再切换模式 while (HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); // 现在可以安全地切换模式了6.5 从机不支持主机模式时的降级方案
有时候你会遇到一种情况:主控支持的 SPI 模式有限,而从机需要的模式恰好不在主控的支持范围内。比如某些低端主控只支持 Mode 0 和 Mode 1,而从机需要 Mode 3。
这种情况下,有几个降级方案可以考虑。第一,如果从机对空闲电平不敏感,可以尝试用 Mode 0 去驱动 Mode 3 的从机,因为两者的采样边沿都是上升沿,很多时候能正常工作。第二,如果从机对采样边沿有严格要求,那就只能换主控或者换从机了。
还有一种比较取巧的方案:用 GPIO 模拟 SPI 时序。虽然速度慢,但可以精确控制每一个边沿,支持任意模式组合。这种方法适合低速、小数据量的场景,比如配置寄存器或者读取状态。
// GPIO 模拟 SPI Mode 3 的时序 void SPI_Software_Transfer(uint8_t data) { for (int i = 0; i < 8; i++) { // Mode 3: 空闲高电平,上升沿采样 HAL_GPIO_WritePin(SCK_GPIO_Port, SCK_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(MOSI_GPIO_Port, MOSI_Pin, (data & 0x80) ? GPIO_PIN_SET : GPIO_PIN_RESET); data <<= 1; HAL_GPIO_WritePin(SCK_GPIO_Port, SCK_Pin, GPIO_PIN_SET); // 上升沿,从机采样 } }这段代码只是示意,实际使用的时候还需要考虑延时和 MISO 的读取。但思路就是这样:用 GPIO 手动控制时钟和数据线,想实现哪种模式就实现哪种模式。
7. 关于 SPI 模式选择的一些个人体会
调试 SPI 这么多年,我最大的体会是:模式配置这件事,看起来简单,但真正理解它的人并不多。很多人停留在“试出来能用就行”的阶段,遇到问题就四个模式轮一遍,轮通了就继续往下做,从来不深究为什么。
这种做法在简单项目里没问题,但一旦项目复杂度上来,比如多设备共用总线、高速通信、低功耗要求,就会暴露出各种问题。因为你不知道“为什么能用”,也就不知道“什么时候会不能用”。
我现在拿到一颗新芯片,第一件事就是确认它的 SPI 模式,然后配置好之后用逻辑分析仪抓一次波形,确认采样边沿和数据变化时刻都对得上。这个习惯帮我避免了很多后期调试的麻烦。
另外,关于 Mode 0 和 Mode 3 的选择,我的建议是:如果芯片同时支持这两种模式,优先选 Mode 0。因为 Mode 0 的空闲电平是低电平,更符合大多数电路的默认状态,而且在多设备共用总线的时候,Mode 0 的兼容性更好。但如果芯片明确要求 Mode 3,或者 Mode 0 下出现异常,那就果断换 Mode 3,不要犹豫。
最后再说一个细节:SPI 模式的选择不仅影响数据采样,还会影响片选信号的时序。有些芯片在片选拉低之后会立即检查 SCK 的电平状态,如果电平不对,可能会进入错误的内部状态。所以配置模式的时候,一定要确保片选拉低之前 SCK 已经处于正确的空闲电平。这一点在动态切换模式的时候尤其重要。
SPI 四种模式的核心其实就一句话:CPOL 决定空闲电平,CPHA 决定采样边沿。把这两个概念吃透,再结合波形去理解,四种模式就不再是四个需要死记硬背的选项,而是两个简单维度的自然组合。希望这篇内容能帮你把 SPI 模式这件事彻底搞清楚,以后调试的时候少走一些弯路。