news 2026/9/6 10:01:06

SPI通信协议深度解析:从CPOL/CPHA到调试避坑实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SPI通信协议深度解析:从CPOL/CPHA到调试避坑实战

年前帮一个朋友排查一块 SPI 屏的显示问题,波形抓出来看,SCK 和 MOSI 都正常,片选拉低的时间也对,可屏幕就是花屏。折腾了一下午,最后发现是 CPOL 和 CPHA 的搭配跟屏的时序要求正好反了。那一刻我意识到,SPI 这个协议表面上看只有四根线,简单得不能再简单,但真正把它用明白,靠的不是背时序图,而是理解它背后那套“主从换位”的机制。

这篇文章想写写我对 SPI 通信协议的完整理解,从底层机制到工程落地,再到实际调试中容易踩的坑。内容会覆盖 SPI 与 I2C、UART 的对比,时钟极性和相位的本质,硬件片选与软件片选的取舍,以及 Flash、屏幕、FPGA 从机这几类常见设备的实战经验。适合刚接触嵌入式通信的新手,也适合那些已经会配置 SPI 外设、但遇到问题还是只能靠试的开发者。

1. 为什么“会配 SPI”不等于“会用 SPI”:先破除三个常见误解

SPI 的名字叫 Serial Peripheral Interface,串行外设接口,由 Motorola 在 1979 年提出。它在嵌入式领域的普及程度,不亚于 UART,但有意思的是,越是看起来简单的东西,越容易在细节上翻车。

1.1 误解一:全双工就是主机发完数据,从机再回数据

这是很多人对 SPI 全双工的第一个错误认知。UART 的全双工是两条独立的数据线各走各的,发送和接收互不干扰,但 SPI 的全双工不是“先发后收”,而是“一发一收同时进行”。

SPI 的 MOSI 和 MISO 两条线,在每一个时钟脉冲内同时传输一位数据。主机通过 MOSI 发出数据的同时,从机也通过 MISO 把数据传回来。这意味着一次完整的 SPI 传输,主机发出的每一位,都会从从机那里换回一位数据。理解这一点非常重要,因为后面讲 SPI Flash 读操作的时候,你会发现读指令发出后,主机必须继续产生时钟,才能把从机的数据“换”回来。

1.2 误解二:从机标注的最高速率,在任何条件下都能跑满

SPI 没有像 UART 那样的波特率误差协商机制,也没有像 I2C 那样的时钟拉伸(Clock Stretching)功能,通信速率完全由主机产生的 SCK 决定,从机只能被动跟随。所以很多人觉得,只要从机手册上写着支持 50MHz,我直接配 50MHz 就行。

实际上这个“最高速率”往往是有前提条件的,比如供电电压、PCB 走线长度、负载电容、甚至温度。实际项目中电源纹波偏大,或者飞线太长,SPI 速率降到手册标称的四分之一才能稳定工作的情况很常见。我的习惯是:新器件第一次调通,速率先按手册标称的十分之一配,功能正常后再逐步往上升。

1.3 误解三:片选只是一根“使能线”,拉低就行

片选(CS/SS/NSS)在 SPI 通信中的地位,比大多数人想象的重要得多。它不仅仅是告诉从机“我要跟你说话”,还承担着从机内部状态复位的功能。很多从机芯片在片选拉高之后,内部的状态机才会回到空闲态,等待下一次通信的开始。

如果片选信号处理不好,比如拉低的时机不对、拉高的过程中有毛刺、或者时序上比 SCK 慢半拍,就会出现极其诡异的问题:第一次通信正常,第二次数据错位,第三次直接无响应。这一类问题在逻辑分析仪上非常难抓,因为它不是数据线上的问题,而是控制时序上的问题。

2. SPI 的底层机制:一组移位寄存器把主从双方串成一个环

要真正理解 SPI,不能只看时序图,要看它内部的移位寄存器结构。SPI 的本质,是主从双方各有一个移位寄存器,这两个寄存器通过 MOSI 和 MISO 首尾相连,形成一个环形结构。

2.1 四线制的分工逻辑

标准的 SPI 有四根线:

  • SCLK(SCK):串行时钟,由主机产生,决定数据传输的节奏
  • MOSI:Master Out Slave In,主机输出、从机输入
  • MISO:Master In Slave Out,从机输出、主机输入
  • CS/SS:片选信号,低电平有效,由主机控制

有的设备是三线制,比如部分温湿度传感器,只有 SCLK、DATA 和 CS,DATA 是双向的。这种三线制本质上是一种半双工的 SPI 变体,比如热搜词里提到的“IO口模拟三线SPI”,就是通过控制 GPIO 的方向寄存器,在发送和接收之间切换 DATA 线的方向,实现半双工通信。

2.2 为什么要“一个时钟脉冲交换一位”

主机在 SCK 的上升沿(或下降沿)把 MOSI 上的一位数据移出,同时从 MISO 上采样一位数据。与此同时,从机在相反的沿上采样 MOSI,并把自己的数据移到 MISO 上。这种“主发从收、从发主收”同时进行的机制,靠的就是移位寄存器在时钟边沿的触发移位。

我的理解方式是:SPI 的两个移位寄存器像两个人面对面站着,中间有两根管子,一根从左往右,一根从右往左。每响一次钟声,两个人同时把手里的一颗珠子推进管子,同时接住对方推过来的珠子。响八次钟,两个人手里的八颗珠子就完全交换了。

这也是为什么 SPI 一次传输的数据长度通常是 8 位、16 位、或者 32 位——移位寄存器的宽度决定的。STM32 的 SPI 外设支持 4 到 16 位的数据帧格式,如果从机需要非标准的位数(比如 12 位),就需要通过修改数据帧格式来实现,而不是简单地多发几个字节。

2.3 SPI 与 I2C、UART 的选型边界

很多初学者搞不清楚什么场景该用 SPI,什么场景该用 I2C,什么场景该用 UART。这三者的本质区别在于它们的“同步机制”和“设备寻址方式”。

协议同步方式设备寻址数据方向典型速率适用场景
UART异步,靠波特率对齐点对点,无寻址全双工9600bps ~ 数Mbps调试日志、GPS、蓝牙模组
I2C同步,带时钟拉伸7位/10位地址,总线多设备半双工100Kbps ~ 3.4Mbps传感器、EEPROM、低速外设
SPI同步,主机产生时钟片选线选择,无地址全双工可达数十MHzFlash、屏幕、ADC、SD卡

I2C 的优势是省引脚,两根线就能挂一堆设备,但半双工和速率上限决定了它不适合大数据量吞吐。SPI 的优势是速率高、全双工、时序简单直接,但代价是每个从机都要占一个片选引脚,设备多了引脚压力大。

实际项目中,我会按“数据量”和“实时性”来选。只是读个温度、湿度这种低频数据,I2C 足够;如果是刷屏、读写 Flash、采集高速 ADC,SPI 是更合理的选择。UART 则更多用于与外部模块通信,因为几乎所有无线模组、GPS 模组都原生支持 UART,它不需要额外的时钟线,接线最简单。

3. 时钟极性与相位:CPOL/CPHA 四个模式怎么选才不出错

SPI 最容易让人懵的就是四种工作模式,也就是 CPOL(Clock Polarity,时钟极性)和 CPHA(Clock Phase,时钟相位)的不同组合。我见过太多人在这上面翻车,包括我自己刚开始接触 STM32 的时候也栽过。

3.1 CPOL:空闲电平是低还是高

CPOL 决定的是 SCK 在空闲状态(没有数据传输时)的电平。

  • CPOL = 0:空闲时 SCK 为低电平,第一个时钟边沿是上升沿
  • CPOL = 1:空闲时 SCK 为高电平,第一个时钟边沿是下降沿

这个参数本身不难理解,难点在于它和 CPHA 配合起来后,决定了数据是在哪个边沿被采样。很多从机手册上的时序图会把 SCK 画成同一种极性,但实际工作时用的可能是另一种,这点在国产芯片的数据手册上尤其常见,需要特别注意。

3.2 CPHA:数据在哪个边沿被采样

CPHA 决定的是数据采样时刻相对于时钟边沿的位置。

  • CPHA = 0:在第一个时钟边沿(即 SCK 从空闲电平跳变的那个边沿)采样数据
  • CPHA = 1:在第二个时钟边沿采样数据

把 CPOL 和 CPHA 组合起来,就得到了常用的四种模式:

模式CPOLCPHA采样边沿数据移出边沿
Mode 000上升沿下降沿
Mode 101下降沿上升沿
Mode 210下降沿上升沿
Mode 311上升沿下降沿

Mode 0 和 Mode 3 是最常见的两种。很多 SPI Flash 和屏幕控制器默认支持 Mode 0 和 Mode 3,但部分传感器只支持其中一种。

3.3 实战选型建议:跟着从机手册走,别跟着惯例走

这里要强调一个原则:SPI 模式的最终依据,永远是从机芯片数据手册里的时序图,而不是你以为的“大家默认用 Mode 0”。

我踩过的一个真实案例:某款国产加速度传感器,手册上写的时序图看起来是 CPOL=0、CPHA=0(Mode 0),但实际测试发现 Mode 0 下读出来的数据偶尔会跳变,最后抓波形才发现,手册画的采样点位置模棱两可,实际支持的是 Mode 1。那次问题定位花了大半天,最后还是靠示波器同时抓 SCK 和 MISO,比对数据在哪个边沿稳定下来,才确定正确答案。

如果你手头没有示波器,可以用一个土办法:把速率降到 100kHz 以下,然后依次切换四种模式,读取从机的 ID 寄存器。如果某个模式下读到的 ID 是预期值且稳定重复,那大概率就是这个模式。这个方法虽然笨,但在没有测量设备的时候非常实用。

4. 片选信号处理:硬件片选与软件片选的高频场景取舍

片选信号在 SPI 通信里的重要性,前面已经提过。这里展开讲讲硬件片选和软件片选的区别,以及什么时候该用哪种。

4.1 硬件片选(NSS 硬件模式)的坑

STM32 等 MCU 的 SPI 外设支持硬件 NSS 模式,即片选信号由外设自动控制。主机在发送数据前自动拉低 NSS,发送完成后自动拉高。听起来很方便,实际上有不少坑。

最大的问题是时序不可控。硬件 NSS 的拉高时机由外设内部状态机决定,有些 MCU 的 SPI 外设在发送完最后一个字节后,会立刻拉高 NSS,但某些从机要求在最后一个时钟边沿之后保持片选低电平一段时间(比如 tCSH,片选保持时间),硬件模式可能不满足这个要求。

第二个问题是多从机场景。硬件 NSS 模式通常只能控制一个从机,如果需要接多个从机,就要用软件方式控制额外的 GPIO 作为片选,不然没法实现“选中 A、不选中 B”的独立控制。

4.2 软件片选为什么往往是更稳的选择

软件片选就是用普通 GPIO 来控制 CS 引脚,发送数据前手动拉低,发送完成后手动拉高。这样可以精确控制片选的时机,在片选拉低之后、数据发送之前,可以加一点延时,保证从机有足够时间准备接收。

我在实际项目里几乎全部使用软件片选,哪怕只有一个从机。原因很简单:软件片选的时序完全可控,遇到问题时可以通过调整延时来规避从机的时序约束。代价仅仅是每次传输前多写两行 GPIO 操作代码,对整个系统性能的影响可以忽略不计。

软件片选还有一个隐藏好处:可以很方便地实现“连续读”操作。比如 SPI Flash 的快速读指令,可以在一次片选有效期间连续读取多个字节,中间片选不能拉高。如果用软件片选,这个过程就是控制好一次片选低电平期间连续调用多次发送接收函数,逻辑非常清晰。

4.3 多设备总线仲裁与“总线空闲”问题

多个 SPI 从机共享同一组 SCK、MOSI、MISO 线,各用各的片选,这是常见的多设备接法。但这里有一个容易忽略的问题:从机的 MISO 输出必须在片选无效时处于高阻态,否则多个从机同时驱动 MISO,就会产生总线冲突。

有些从机芯片的 MISO 引脚在片选无效时并不是高阻态,而是持续输出某个电平。这种芯片就不适合直接并联在共享的 SPI 总线上,要么加一个三态缓冲器,要么给它单独分配一组 SPI 引脚。选型的时候一定要看芯片手册里对 MISO(或 SDO)输出使能的描述,确认是否支持“三态输出”或“高阻态”。

还有一个与总线空闲相关的细节:在所有片选都无效的情况下,MISO 线是悬空的,主机读到的是一个不确定的电平。某些从机协议要求主机在发起通信前先读一次 MISO 来检测从机是否在线,如果总线上有外部下拉电阻,读到的就是稳定的低电平,检测逻辑才能可靠工作。所以多设备共享 SPI 总线时,建议在 MISO 上加一个 10kΩ 下拉电阻,给总线一个确定的状态。

5. 从零写一套 SPI 驱动:GPIO 模拟与硬件外设的落地对比

不管你是用 STM32、Arduino 还是 ESP32,SPI 驱动的方式无非两种:GPIO 模拟和硬件外设。两种方式各有各的使用场景,我分别说说。

5.1 GPIO 模拟三线/四线 SPI 的适用场景

GPIO 模拟 SPI,就是用代码控制普通 IO 口来产生 SCK 和 MOSI 的波形,同时读取 MISO 的电平。这种方式的好处是几乎不受 MCU 型号限制,任何有 GPIO 的芯片都能实现。

模拟 SPI 的逻辑核心就是“一个时钟一个位”。发送一个字节的函数基本长这样:

void spi_soft_write_byte(uint8_t data) { for (int i = 7; i >= 0; i--) { // 先设置数据线电平 if (data & (1 << i)) { MOSI_GPIO_HIGH(); } else { MOSI_GPIO_LOW(); } // 产生时钟上升沿 SCK_GPIO_LOW(); delay_ns(100); SCK_GPIO_HIGH(); delay_ns(100); } }

所谓“IO口模拟三线SPI”,就是在这个基础上把 MOSI 和 MISO 合并成一根双向数据线,发送时把引脚配置为推挽输出,接收前把引脚切换为浮空输入或上拉输入,同时改变收发方向。每次操作 GPIO 的方向寄存器都会花几十个时钟周期,所以模拟三线 SPI 的速率一般上不去,实测在普通 STM32 上稳定跑 1MHz 就不错了。

GPIO 模拟适合以下几种场景:

  • MCU 没有硬件 SPI 外设(比如某些低成本 8 位单片机)
  • SPI 引脚被复用占用,只剩普通 GPIO
  • 需要极其灵活的时序控制(比如某些从机的时序很怪异)
  • 只是临时调试,不想初始化外设和中断

缺点是 CPU 占用高、速率上不去、时序抖动大。所以只要 MCU 有硬件 SPI,量产项目我一般不建议用 GPIO 模拟。

5.2 硬件 SPI 外设的初始化要点(以 STM32 为例)

硬件 SPI 的优势是速率高、CPU 介入少,配合 DMA 可以实现真正的后台传输。以 STM32 为例,用 CubeMX 配置 SPI 外设时,有几个关键参数:

  • 数据大小:通常选 8 Bits,部分传感器要求 16 Bits,按手册来
  • 时钟极性和相位:即前面说的 CPOL/CPHA,必须和从机匹配
  • 预分频器:决定 SCK 频率,初学阶段建议从小频率开始试
  • 帧格式:MSB First 还是 LSB First,大部分设备是 MSB First
  • 软件 NSS:强烈建议使能软件片选,用普通 GPIO 控制 CS

初始化完成后,STM32 的 SPI 发送一个字节的典型代码如下:

uint8_t spi_read_write_byte(uint8_t tx_data) { while (!(hspi1.Instance->SR & SPI_FLAG_TXE)); hspi1.Instance->DR = tx_data; while (!(hspi1.Instance->SR & SPI_FLAG_RXNE)); return (uint8_t)hspi1.Instance->DR; }

这段代码的逻辑是:等待发送缓冲区空,写入数据,然后等待接收缓冲区非空,读回数据。前面说过 SPI 是全双工的,发送一个字节的同时会收到一个字节,所以这个函数既能当发送用,也能当接收用——读操作的本质就是“发一个字节,收一个字节”。

5.3 收发流程:一次完整读操作要拆成两段

很多新手写 SPI Flash 读数据时经常卡住,原因就是没有理解“全双工交换”对读操作的约束。以常见的 W25Q128 为例,读数据的流程是:

  1. 拉低片选
  2. 发送读指令(0x03)
  3. 发送 24 位地址(分三个字节)
  4. 从第 4 个字节开始,每发一个字节(可以是任意值),从机返回一个字节的数据
  5. 读完后拉高片选

这里的关键在于第 4 步:你想读 N 个字节,就必须额外发出 N 个时钟周期的 SCK。主机发的是什么内容不重要,重要的是产生时钟。在实际代码里通常这样做:

uint8_t w25q_read_byte(void) { uint8_t dummy = 0x00; return spi_read_write_byte(dummy); }

发送一个 dummy 字节,目的就是为了产生 8 个 SCK 时钟脉冲,把从机移位寄存器里的数据“换”回来。如果只发指令不发时钟,MISO 上是不会有数据出来的。

这就是为什么 SPI 读操作比写操作多一个“假数据”环节。不理解这个机制,写出来的代码往往少发几个时钟,导致读回来的数据整体错位或者缺字节。

6. 高频场景实测:SPI Flash、屏幕刷新与 FPGA 从机的数据链路

SPI 协议本身是中立的,但不同类型的从机设备,对 SPI 的用法有各自的特点。这里挑三个最常见的场景展开讲讲我的实测经验。

6.1 读 SPI Flash W25Q 系列的正确姿势

W25Q 系列 SPI Flash 是市面上最常见的 SPI 存储芯片,ST 的开发板上几乎都能看到它的身影。它的 SPI 接口支持 Mode 0 和 Mode 3,使用前建议先读一下 JEDEC ID(Manufacturer Device ID)来验证通信是否正常。

读 ID 的命令是 0x9F,接下来需要发送 3 个 dummy 字节来换取 3 个 ID 字节。Java 代码里会有一个常见错误:发送完 0x9F 后直接调用接收函数,但接收函数内部没有产生时钟,结果读回来全是 0xFF。

正确的做法是使用前面提到的“交换式读写函数”,发送 0x9F 后继续发送三个 dummy 字节,每次发送的同时读取返回数据:

uint8_t id[3]; spi_cs_low(); spi_read_write_byte(0x9F); id[0] = spi_read_write_byte(0x00); id[1] = spi_read_write_byte(0x00); id[2] = spi_read_write_byte(0x00); spi_cs_high();

注意读回来的字节顺序:第一个返回的是厂商 ID(W25Q128 是 0xEF),第二个是内存类型(0x40),第三个是容量(0x18)。

实际测试中,我习惯把 SPI 速率先设为 1MHz 左右读 ID,确认通信正常后再调到 10MHz 或更高进行大数据量读写。因为 ID 读取时序最简单,任何模式不匹配都会直接导致 ID 错误,定位问题很快。

6.2 屏幕场景:SPI 刷新率与 DMA 收益

SPI 屏幕是另一个高频场景。很多 TFT 屏(比如 ST7789、ILI9341)都支持 SPI 接口,这类屏幕本身分辨率不高,用 SPI 驱动完全够用。刷一帧 240x240 的 RGB565 图像,数据量是 240 * 240 * 2 = 115200 字节,如果用 40MHz 的 SPI 速率,理论上大约需要 23ms,加上命令和延时,实际帧率能到 30fps 左右。

但这里有一个前提:MCU 不能一个字节一个字节地阻塞发送,否则 CPU 全都耗在等发送完成上,帧率会骤降到个位数。解决办法是 SPI DMA。

SPI DMA 的收益在屏幕驱动上非常明显。配置好 DMA 通道后,可以直接把显示缓冲区的内容一次性地搬运到 SPI 外设发送寄存器,整个传输过程中 CPU 只需要初始化传输和等待传输完成中断,中间可以处理其他任务。实测在 STM32F103 上,同样刷一屏,阻塞方式需要约 30ms CPU 全占用,DMA 方式只需不到 1ms 的 CPU 介入,其余时间由 DMA 控制器搬运数据。

配置 SPI DMA 的要点是:DMA 外设地址设为 SPI 的发送数据寄存器(SPI_DR),内存地址设为显示缓冲区;传输方向为内存到外设;数据宽度、传输大小要一致;使能 SPI 的 DMA 发送请求。CubeMX 里配置好后,代码层面只需要调用 HAL_SPI_Transmit_DMA 函数。

6.3 FPGA 做从机:时序边界更容易暴露问题

FPGA 作为 SPI 从机,是另一种常见的应用场景。MCU 作为主机通过 SPI 与 FPGA 通信,FPGA 内部用 Verilog 实现 SPI 从机逻辑。这个场景最考验对协议细节的理解,因为 FPGA 的逻辑是你自己写的,任何时序边界问题都会直接反映在数据上。

SPI slave Verilog 实现的核心是一个移位寄存器和一个位计数器。在 SCK 的采样沿,把 MOSI 的数据移入寄存器;在输出沿,把要发送的数据移到 MISO 上。需要注意的一点是:MISO 输出必须在片选无效时置为高阻态(用assign MISO = (cs_n == 1'b0) ? shift_reg[7] : 1'bz;),否则多个设备共享总线时会冲突。

用 FPGA 做从机时,我遇到过最典型的问题是:MCU 的 SPI 模式配置为 Mode 0,FPGA 在上升沿采样 MOSI、下降沿更新 MISO,逻辑上没问题,但实际抓波形发现 MISO 数据在下降沿之后有一小段毛刺,MCU 在下一个上升沿采样时刚好采到了毛刺。

解决方法是让 FPGA 的移位寄存器采 SCK 下降沿更新 MISO,或者在 MISO 输出路径上加一个寄存器打拍,保证数据变化沿和采样沿之间留出足够的建立保持时间。这是在 RTL 代码层面经常被忽略、却又影响稳定性的关键点。

7. 一次“读回全FF”问题的完整排查链路

下面还原一个真实的排查过程。这个案例来自一个使用 SPI 通信的温湿度传感器项目,现象是主机读取传感器数据时,读回来的字节全是 0xFF。这类问题在 SPI 调试中非常典型,排查链路也值得完整记录。

7.1 现象:MISO 一直是高电平,读回全 FF

设备正常上电,传感器数据线上拉电阻正常,主机发送读取指令后,读取函数返回的所有字节都是 0xFF。0xFF 表示所有位都是 1,说明 MISO 线上读到的一直是高电平。

遇到这个现象,第一反应不要直接改代码。0xFF 可能意味着从机根本没响应,也可能意味着主机根本没产生有效的采样时序,甚至可能意味着 MISO 线压根就没接对。信息不足的情况下改代码,只会把问题越改越乱。

7.2 排查链路:先看 SCK,再看片选,再看数据线

我习惯的排查顺序是固定的,这个顺序能快速缩小问题范围:

第一步,测 SCK。用示波器或逻辑分析仪抓主机的 SCK 引脚,确认主机确实产生了时钟脉冲,频率和预期一致。如果 SCK 没有波形,问题在主机的 SPI 外设配置或时钟使能,属于初始化层面的错误。

第二步,测片选。确认片选信号在传输期间确实被拉低,而且保持时间足够。如果片选根本没拉低,或者拉低的时间太短,从机根本不会进入接收状态,MISO 会保持高电平。

第三步,测 MOSI。确认主机发出去的数据确实在 MOSI 上出现了正确的电平序列。有时候问题出在 GPIO 复用配置上,比如 MOSI 引脚被其他外设占用,导致数据根本没输出到引脚上。

第四步,测 MISO。在保证前三步正常的情况下,仔细观察 MISO 的电平变化。如果 MISO 一直稳定在高电平,那问题多半在从机侧:供电、复位、或从机根本没进入正常工作状态。

7.3 最终根因:从机供电/复位/模式不匹配

回到那个温湿度传感器的案例。前三步都正常,SCK、片选、MOSI 波形都对,但 MISO 就是不动。后来用万用表量了从机 VCC 引脚的电压,发现只有 2.8V,而传感器要求的最小工作电压是 3.0V。

问题出在供电线路上:传感器由一颗低压差稳压器供电,稳压器的输入来自主电源,主电源在同时驱动其他负载时电压跌落,导致稳压器输出不到 3.0V。传感器在这种情况下并没有完全死掉,内部电路还能部分工作,但 SPI 接口的驱动器不在工作范围内,MISO 无法正常输出。

换了一颗输出能力更强的稳压器,电压稳定在 3.3V 之后,MISO 波形立刻正常,数据读取也恢复了。

这个案例说明一个道理:SPI 通信异常,不一定是 SPI 本身的问题,供电、复位、时钟这些“外围因素”往往是更隐蔽的元凶。尤其是从机的电源引脚,电压只要低于芯片的最低工作电压,即使没有触发掉电复位,芯片的数字接口也可能处于不确定状态。

7.4 排查清单:SPI 通信异常的通用定位表

结合多次调试经验,我整理了一个通用的排查清单:

现象可能原因排查方向
读回全 0xFFMISO 无输出、从机未响应、从机供电异常查 MISO 接线、从机供电、从机复位
读回全 0x00MISO 被拉低、从机输出三态但 MISO 无上拉查 MISO 是否有短路、总线电阻
数据错位模式不匹配、数据帧格式错误、采集的位不在正确位置查 CPOL/CPHA、数据位顺序、SCK 频率
第一次正常,后续出错片选时序异常、从机状态机未复位查片选保持时间、传输之间是否拉高片选
SCK 频率高了就不稳定走线过长、电平转换器件带宽不足、从机负载过重降速率、缩短飞线、检查电平匹配
数据偶尔跳变采样边沿接近数据变化沿、干扰耦合检查模式配置、增加滤波、优化布线

这张表不是万能药,但它能帮你在面对“不知从何查起”的 SPI 问题时,先给出一个有方向的起点。排查 SPI 问题最忌讳的是一上来就怀疑代码逻辑,然后在代码里反复试各种参数——没有数据支撑的瞎试,只会浪费时间。

8. 不同平台的 SPI 适配要点:STM32、Arduino 与 FPGA

最后聊聊不同平台上 SPI 的使用差异。虽然协议本身是标准的,但各个平台的 API 设计和底层实现各有不同,适配的时候有一些需要注意的地方。

8.1 STM32 的 SPI 外设配置细节

STM32 的 SPI 外设配置主要围绕 CubeMX,核心参数前面已经提过,这里补充几个容易忽略的细节。

第一个是 SPI 时钟的预分频问题。STM32 的 SPI 时钟源来自 APB 总线时钟,不同系列的 APB 时钟频率不同。同样是预分频 16,在 F103 上算出的 SCK 频率和 F429 上是不一样的。配置之前先用HAL_RCC_GetPCLK2Freq()之类的函数确认一下总线时钟频率,再反推预分频值。

第二个是 SPI 引脚的速度等级。GPIO 的输出速度配置会影响 SPI 的信号质量。SCK 速率在 10MHz 以上时,GPIO 输出速度至少要配到 High(50MHz)档,否则波形上升沿变缓,高速通信时容易出错。很多人 SPI 低速正常、高速失败,就是这个原因。

第三个是 SPI 外设的 CRC 功能默认是关闭的,但某些 HAL 库版本在初始化时会默认使能 CRC,导致正常通信时数据尾部的时序与从机期望不一致。遇到通信“偶尔多了一个字节”的现象,可以检查一下 SPI 的 CRC 是否被意外打开了。

8.2 Arduino 的 SPI 库与主从适配

Arduino 的 SPI 库封装得比较友好,核心操作就几个函数:SPI.begin()SPI.beginTransaction(settings)SPI.transfer(data)SPI.endTransaction()

其中SPI.beginTransaction()的参数是SPISettings,里面包含了速率、位序、模式三项内容。这里有个细节:SPISettings的构造函数是编译期常量,如果需要在运行时动态切换不同从机的参数,就要在每次切换时重新调用beginTransaction

Arduino 的默认片选引脚在大多数板子上是固定的(比如 Uno 的 10 号引脚),但调用beginTransaction并不会自动拉低片选,需要自己控制 CS 引脚。这就回到了前面说的“软件片选”逻辑——Arduino 库的设计默认就是让用户自己管片选。

Arduino 驱动 ST7789 这类 SPI 屏时,需要注意库函数SPI.transfer是阻塞式的,如果屏幕分辨率高、刷新频繁,建议使用SPI.transfer(buffer, size)这种批量传输版本,能显著减少函数调用开销。

8.3 FPGA 侧:Verilog 实现与跨时钟域提醒

FPGA 做 SPI 从机时,最容易出的问题反而是设计之外的“跨时钟域”。MCU 的 SCK 频率与 FPGA 的系统时钟频率不同,直接用系统时钟去采样 SCK 的话,容易采到亚稳态。

一个稳妥的做法是:先用两级触发器同步 SCK,再在同步后的 SCK 边沿上采样数据。这样可以显著降低亚稳态出现的概率。同时,位计数器要根据同步后的 SCK 来计数,保证片选拉低期间恰好采够一个数据帧的位数。

// SPI 从机接收部分 reg sck_d1, sck_d2; always @(posedge clk) begin sck_d1 <= sck; sck_d2 <= sck_d1; end wire sck_negedge = sck_d2 & ~sck_d1; // 下降沿检测 wire sck_posedge = ~sck_d2 & sck_d1; // 上升沿检测

上面这段是常见的打拍同步写法。收到一个有效的 SCK 边沿后,把 MOSI 的数据移入移位寄存器,同时把移位寄存器最高位的数据输出到 MISO。片选拉高时,位计数器清零,移位寄存器复位,等待下一次传输。

FPGA 侧还要注意一点:MISO 的输出时序要与主机的采样沿对齐。如果主机配置的是 Mode 0(上升沿采样),那么 FPGA 应该在下降沿更新 MISO,这样数据在上升沿来临时已经稳定,建立时间充足。另一种做法是直接用 SCK 下降沿驱动assign MISO = reg_data[7],前提是组合逻辑路径的延时足够小,不违背时序约束。

GPIO 模拟三线 SPI、硬件片选与软件片选、SPI DMA、SPI Flash、屏幕刷新、FPGA 从机,这几个方向的实践经验写下来,我自己又过了一遍这些年踩过的坑。SPI 的简单只是表面上的简单,真正稳定好用的 SPI 通信,靠的是对时序每一个细节的尊重——从模式选择到片选控制,从供电稳定到信号完整性,每一步都值得认真对待。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 9:59:36

STT-Agent-TTS:构建实时语音智能体的完整链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 9:56:33

2026丹东化工产品成分分析检测排名 TOP5 CMA 资质提供含量检测、纯度检测、元素分析 联系方式推荐

丹东的化工产业与新材料制造近年来蓬勃兴起&#xff0c;各类成分分析检测机构亦如雨后春笋般鳞次栉比&#xff0c;其中难免鱼龙混杂。本地化工企业、日化生产工厂、橡塑制造业以及食品医药研发实验室&#xff0c;在进行原料质检或配方研发时&#xff0c;稍有不慎便可能筛选到无…

作者头像 李华
网站建设 2026/9/6 9:53:57

凌晨三点的 Mac 自动重启:我是如何用命令行揪出“真凶“的

早上起来打开 MacBook&#xff0c;发现 Dock 上的应用全没了——系统似乎在夜里重启过。打开终端敲了一条命令&#xff0c;确认了我的猜测&#xff1a;$ last reboot shutdown | head -5reboot 六 9 5 03:22 shutdown 六 9 5 03:22 reboot 二 9 1 03:53 shutdown …

作者头像 李华
网站建设 2026/9/6 9:53:13

数据采集全链路解析:从传感器信号调理到数据文件生成

1. 采集链路全景概览 1.1 一次采集到底在说什么 传感器是感知物理世界的起点&#xff0c;但真正能让数据发挥作用&#xff0c;靠的是从传感器到数据文件的整个链路。很多人拿到一个传感器模块&#xff0c;接上开发板&#xff0c;读出来的数值在串口里能显示&#xff0c;就觉得…

作者头像 李华
网站建设 2026/9/6 9:53:01

HarmonyOS元服务开发全流程指南:从工程初始化到上架避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华