ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SPI通信协议完全指南:时序、模式、DMA与调试实战

2026/9/5 13:26:34 拓冰建站 浏览量
SPI通信协议完全指南:时序、模式、DMA与调试实战 手头同时挂着三四个传感器、一块屏幕、一个 Flash 存储I2C 总线慢得让人着急UART 又点对点不好扩展这时候 SPI 基本就是嵌入式开发者的默认答案。这篇文章我想把 SPI 通信协议从原理到实操完整捋一遍包括时序图怎么看、四种模式怎么选、硬件片选和软件片选到底差在哪、DMA 怎么配合以及我在 STM32 和 FPGA 上实际调试时踩过的一些坑希望能给正在调 SPI 的朋友省点时间。1. SPI 到底是什么——从“三根线打天下”说起SPI 全称 Serial Peripheral Interface串行外设接口是 Motorola 在 20 世纪 80 年代提出的一种同步串行通信协议。同步这两个字是理解 SPI 的关键它不像 UART 那样收发双方各自约定波特率而是由主机主动产生时钟信号所有数据位的传输都跟着这个时钟走所以 SPI 的通信速率可以做得很高常见应用轻松跑几十 MHz有些器件甚至支持上百 MHz。SPI 通常需要四根线SCLK串行时钟、MOSI主机输出从机输入、MISO主机输入从机输出、CS/SS片选低电平有效。有时候我们听到“三线 SPI”“两线 SPI”那是砍掉了某些线的简化变体比如只写不读的屏幕驱动可以省掉 MISO硬件固定片选的器件也可以省掉 CS但完整形态就是这四根线。片选线的工作方式特别像开会点名——主机把某个从机的 CS 拉低等于跟它说“现在我只跟你说话”其他从机看到自己的 CS 是高电平就乖乖把 MISO 置为高阻态不参与总线竞争。这种一主多从的架构加上全双工能力发数据和收数据同时进行让 SPI 在嵌入式领域几乎无处不在Flash 存储芯片、SD 卡、各类传感器、显示屏驱动、ADC/DAC、CAN 控制器芯片到处都有 SPI 的身影。SPI 和 I2C 经常被放在一起比较。I2C 只有两根线SDA 和 SCL靠设备地址寻址可以挂很多设备但通信速率一般限制在 400kHz标准模式到几 MHz而且有应答机制、地址仲裁这些额外开销。SPI 没有地址概念靠片选线选设备速率上限高得多也没有复杂的应答机制收发双方只要时序对上就行。代价就是线多——每个从机都要占用一个片选引脚从机数量多了之后引脚开销很可观。简单说追求速率和简单直接选 SPI追求省引脚和多设备挂载选 I2C。我当年第一次接触 SPI 的时候有个误区以为 SPI 协议像 TCP/IP 那样有复杂的报文结构。实际上 SPI 就是一个纯粹的物理层协议它不定义任何帧格式、错误检测、应答机制只是一个“时钟同步下按位传输”的搬运工。具体的数据格式、命令字、状态寄存器全部由器件厂商自己定义。这也是为什么 SPI 调试有点麻烦——协议简单意味着所有复杂东西都暴露在应用层你得对着数据手册慢慢抠。但反过来讲它也足够灵活可以适配千奇百怪的外设。2. SPI 数据流与时序——四种模式背后的“时钟相位”和“时钟极性”时序是 SPI 通信的核心大部分调不通的问题最后都回到时序上。SPI 定义了四种工作模式Mode 0~3由 CPOL时钟极性和 CPHA时钟相位两个参数组合得出。CPOL 决定 SCLK 空闲时的电平CPOL0空闲低电平CPOL1空闲高电平。CPHA 决定数据在时钟哪个沿被采样CPHA0第一个时钟沿对 CPOL0 来说是上升沿采样数据CPHA1第二个时钟沿采样。两种参数组合出来的四种模式模式CPOLCPHA数据采样沿数据输出沿Mode 000上升沿下降沿Mode 101下降沿上升沿Mode 210下降沿上升沿Mode 311上升沿下降沿实际应用中最常见的是 Mode 0 和 Mode 3比如 W25Q 系列 Flash 默认支持 Mode 0 和 Mode 3ST7789 这类屏幕驱动 IC 一般也是 Mode 0。Mode 1 和 Mode 2 相对少见但总有一些特殊器件用所以配置 SPI 时千万不要想当然一定要查数据手册的时序图。关于采样沿这件事我多说两句。SPI 通信双方必须约定好在哪个沿采样如果主机在上升沿把数据放到 MOSI 上从机又恰好也在上升沿采样那采到的就是上一个数据位或者还没稳定的电平整个数据流就串位了表现出来就是读回来的寄存器值乱七八槽或者屏幕上显示的内容错位花屏。我遇到过最典型的案例是一个屏幕在 Mode 3 下画面只有轻微噪点换到 Mode 0 就完全正常当时排查了很久最后发现是时钟极性和相位没配对。数据格式方面最常见的配置是 8 位一帧MSB 先发。但也不是绝对的有些音频芯片支持 16 位或 32 位帧有些传感器支持 LSB 先行。STM32 的 SPI 外设可以通过寄存器配置帧格式、数据长度和位序如果是 GPIO 模拟 SPI那这些完全由代码决定。这里有个容易被忽略的细节如果主机和从机的位序不一致主机 MSB从机 LSB接收到的数据就会按位镜像反转比如 0x01 变成 0x80。遇到这种情况先别急着怀疑电气问题检查一下位序配置。2.1 SPI 时序图到底怎么读读时序图是 SPI 调通的必备技能。一张典型的 SPI 时序图会画出 CS、SCLK、MOSI、MISO 四根线的波形标注出建立时间、保持时间、采样点这些关键参数。手册里的时序参数一般包括这么几个SCLK 频率上限、SCLK 高低电平最小时间、CS 建立时间CS 拉低到第一个时钟沿的时间、CS 保持时间最后一个时钟沿到 CS 拉高的时间、数据建立时间、数据保持时间。主机侧必须满足从机手册里这些最小时间要求否则从机可能采样失败。比如有些 Flash 芯片要求 CS 拉低后至少等 100ns 才能来第一个时钟如果你的 MCU 在 CS 拉低后立刻进 while 循环发数据时钟马上就来了器件可能不响应。解决办法是在 CS 拉低和开始时钟之间加一点点延时哪怕几条 NOP 指令都行。另外SPI 是全双工协议也就是说每个时钟周期主机从 MOSI 发出去一位数据同时从 MISO 采回来一位数据。这两个过程并行发生不存在“先发后收”的概念。所以主机向从机读数据时通常要先通过 MOSI 发送一条命令然后再发若干个“空数据”一般是 0x00 或 0xFF当作时钟从机才能趁机把数据通过 MISO 送回来。初学者最容易犯的错就是读操作只发命令不发空时钟结果读回来全是一堆 0xFF 或者 0x00。这个问题的本质是从机没有独立时钟没有时钟它就无法移位输出数据主机的 MOSI 必须一路发下去把时钟续上。2.2 一次完整读操作的数据流演示拿 W25Q256 Flash 芯片举例读数据的操作是拉低 CS发送命令 0x03Read Data再发送 24 位地址高字节在前然后主机连续发送空字节0x00 或 0xFF 都行产生时钟从机就在 MISO 上一位一位地把 Flash 里的数据吐出来读到想要的长度后拉高 CS 结束操作。整个过程中 MOSI 上到后期全是无效数据重点看 MISO。这个过程里主机和从机是同步在跑的主机每发出一位同时就收回一位。所以如果你在逻辑分析仪上看到 MOSI 和 MISO 的数据是错位对应的不要慌SPI 天然就是这样同一个时钟沿MOSI 上是主机当前发的一位MISO 上是从机当前回的一位两位并不是同一个逻辑操作里的“一来一回”而是并行的两条数据通道。3. 硬件 SPI 与软件模拟 SPI——该用哪个为什么单片机驱动 SPI 外设有两条路走硬件 SPIMCU 内部 SPI 外设接管引脚和软件模拟 SPI用普通 GPIO 按时序手动翻转电平。这两条路我都走过各有优劣选择标准其实很明确。硬件 SPI 的优点是速率高、不占 CPU。STM32 的 SPI 外设跑在 APB2 总线上时钟可以分频到几十 MHz配合 DMA 可以做到数据零 CPU 干预。缺点是引脚分配是固定的虽然 STM32 大部分引脚有重映射功能但终究不如任意 GPIO 灵活配置相对复杂中断和 DMA 的联动需要一定经验。软件模拟 SPI 的优点是引脚任意、配置灵活、代码可控性强Debug 时可以在关键位置打断点看波形出问题容易定位。缺点是速率低、占 CPU。一个 8 位数据的发送可能需要几十条指令在系统主频不高的 MCU 上很难跑过 1MHz。而且模拟 SPI 的时序稳定性不如硬件外设——如果在数据传输过程中来了一个中断GPIO 翻转就会被延迟时钟周期忽长忽短某些时序要求严格的从机可能就出错了。我的经验是如果 MCU 有硬件 SPI 且引脚能满足要求优先用硬件 SPI只有在硬件资源不够、引脚冲突、或者需要特殊时序比如某个器件违反常规时序要求时才用软件模拟。举个实际案例我之前一个项目里 MCU 的硬件 SPI 引脚被 I2C 屏幕占用了另一个 SPI Flash 只能接在普通 GPIO 上我就写了一个模拟 SPI 驱动Flash 读写速率虽然上不去但项目对速率要求不高完全够用。这种情况软件模拟就是救命的方案。3.1 软件模拟 SPI 的代码套路模拟 SPI 的代码其实非常简单核心就是 GPIO 翻转加延时。主机发送一个字节的核心逻辑void SPI_Soft_TransmitByte(uint8_t data) { for (uint8_t i 0; i 8; i) { if (data 0x80) { SPI_MOSI_HIGH(); } else { SPI_MOSI_LOW(); } data 1; SPI_SCLK_HIGH(); // 上升沿从机采样 delay_ns(50); SPI_SCLK_LOW(); // 下降沿主机切换数据 delay_ns(50); } }接收数据的逻辑也很类似区别在于要在时钟的某个沿去读 MISO 引脚的电平。对于 Mode 0CPOL0, CPHA0数据在上升沿被采样所以主机应该在 SCLK 拉高之后读取 MISO这时从机输出的数据已经被锁存稳定了。这里要注意一个细节模拟 SPI 的延时不能机械地卡在手册标称的最小时间上因为 GPIO 翻转本身有开销代码执行也有开销。我一般会在手册要求的时间基础上留 2 到 3 倍余量确保稳定。如果后续发现速率不够再逐步优化比如去掉多余延时、精简循环体。另外模拟 SPI 的 SCLK 占空比不一定要严格的 50%只要高低电平时间都满足从机的最小要求就行。从机的模拟 SPI 实现也不难核心就是检测 SCLK 的边沿。我写过 FPGA 和单片机上的模拟从机最典型的做法是用一个外部中断引脚接 SCLK在上升沿中断里把 MOSI 采进来移位在下降沿中断里把 MISO 的一位数据输出。当然这种实现方式对中断响应时间要求很高如果 MCU 在忙别的中断延迟一大时序就崩了所以模拟从机更适合用状态机轮询的方式在循环里不断检查 SCLK 电平来判断时钟边沿。3.2 硬件 SPI 初始化配置示例以 STM32 为例STM32 上用 CubeMX 配置 SPI 是大多数人的选择但配置完自动生成的代码不等于万事大吉关键参数还得自己确认。一个典型的主机配置包括传输模式全双工Transmit Only 模式用于只写设备也可以但注意读不到数据数据帧格式8 位还是 16 位必须跟器件一致时钟极性 CPOL 和相位 CPHA必须匹配从机预分频器决定 SCLK 频率不能超过从机上限位序MSB First 还是 LSB First同样要匹配器件片选信号硬件片选还是软件片选这个后面细讲CubeMX 界面上这些选项都有对应下拉框但生成的 HAL 代码里初始化结构体的字段名是 SPI_InitTypeDef 的成员比如hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_16; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB;配置完成后最稳妥的验证方式是先用逻辑分析仪抓一下 SCLK 和 MOSI确认空闲电平、时钟频率和数据格式都符合预期再连从机。千万别信“我配置应该没问题”这种错觉用仪器看一眼永远比盲调快。4. 硬件片选与软件片选——选错了你可能会遇到“幽灵数据”SPI 主机的片选信号有两种控制方式硬件片选NSS 引脚由 SPI 外设自动控制和软件片选NSS 引脚由普通 GPIO 手动控制。这个选择虽然看起来不起眼但实际影响非常大特别是多从机场景。硬件片选模式下SPI 外设在通信开始时自动拉低 NSS通信结束后自动拉高不需要代码干预。但问题也出在这里很多 MCU 的硬件 NSS 控制逻辑不够灵活它在完成一次数据帧传输后就立刻释放片选而某些从机要求 CS 在整个“命令地址数据”的操作序列期间保持低电平。比如 W25Q 系列 Flash 的一次页编程操作可能包含多个字节的写入中途 CS 被拉高操作就中止了。所以实际项目中哪怕是 STM32 这样带有硬件 NSS 的 MCU很多人依然选择用普通 GPIO 做软件片选。软件片选模式下CS 引脚就是一个普通 GPIO由你手动拉低和拉高。好处是很灵活你可以在一个完整的操作序列比如读 Flash 的整个数据块里保持 CS 低电平也可以随时决定提前终止通信。代价是你要自己保证时序——CS 拉低后要等从机准备好再发时钟CS 拉高前要确保最后一个时钟沿已经过去并满足保持时间。我在实际项目中基本都用软件片选除非是那种硬件 NSS 行为完全满足要求的简单单从机场景。一个需要注意的坑是如果多个从机芯片连接在同一个 SPI 总线上所有从机的 MISO 引脚输出都是三态门只有被片选拉低的那一个才会真正输出。如果片选搞错了两个从机同时输出MISO 线上就会发生总线冲突可能出现数据错误甚至损坏引脚的风险。所以软件片选务必保证同一时间只有一个 CS 是低电平千万不要一个操作还没结束就去操作另一个设备。还有一个小细节某些从机芯片的 CS 上需要加一个上拉电阻确保上电瞬间 CS 不会处于低电平否则芯片可能在上电时误进入通信状态。另外线长、从机数量、信号完整性这些也会影响片选信号质量如果 CS 抖动严重从机可能产生误触发。总之片选线虽然功能简单但绝不是随便拉根线就行。5. SPI 通信速率——为什么你的 SPI 跑不快以及怎么调优SPI 能跑多快取决于从机的最高 SCLK 频率和主机的分频能力但实际工程里更常见的问题是“理论上能跑 20MHz实际一跑就出错”。SPI 的速率瓶颈往往不在协议本身而在信号完整性和代码实现上。先说分频。STM32 的 SPI 时钟源是 PCLKAPB 外设时钟通过预分频器分频得到 SCLK。分频系数一般是 2 的幂2/4/8/16/32/64/128/256所以你很难精确得到想要的频率。比如 PCLK 是 72MHz你想让 SCLK 不超过 20MHz用 4 分频是 18MHz用 2 分频是 36MHz那只能用 18MHz。这个很好理解但有些新手会直接把分频系数设得很低结果 SCLK 远超从机上限通信时好时坏。再说信号完整性。当 SCLK 跑到 20MHz 以上时PCB 走线的寄生电容、引脚的电平转换速率、从机输入引脚的负载电容都会影响信号质量。最典型的症状是时钟边沿变缓、数据线上的振铃导致误采样。解决办法包括缩短 SPI 走线长度加串联匹配电阻一般在 22~33Ω降低 SCLK 速率或者尝试换一个 SPI 模式让数据在更有利的时钟沿被采样。然后是代码层面的调优。用 HAL 库的阻塞式发送每发一个字节都要检查 TXE 标志位、写入数据寄存器、等待 BSY 标志这个过程中 CPU 的循环判断本身就是瓶颈。同样是 STM32用 HAL 阻塞发送跑 18MHz SCLK 时 CPU 占用率很高但用 DMA 方式后 CPU 基本可以不管 SPI 了。如果你的项目对实时性要求高SPI 数据吞吐量大DMA 是必须的。最后提一个很多人忽视的问题SCLK 空闲电平和极性。极性和相位配错了即使速率很低也可能读不到正确数据。我在调试一个外部 ADC 时手册写的是 Mode 1我按 Mode 0 初始化速率怎么降都不对最后看了时序图才发现自己抄错了参数。这类问题用逻辑分析仪一眼就能看出来别像我一样对着寄存器和波形猜半天。6. SPI 的 DMA 配置——当数据量大到 CPU 扛不住的时候SPI DMA 是高性能嵌入式系统的常见组合。先理解 DMA 解决什么问题如果没有 DMA每发一个字节CPU 都要等数据寄存器从空变成满、再写下一个字节这个过程循环往复占用了大量 CPU 时间。而 DMA 可以在 CPU 不干预的情况下把内存里的数据自动搬运到 SPI 的数据寄存器或者把 SPI 收到的数据自动搬到内存缓冲区完成后才触发一次中断通知 CPU。以 STM32 为例使用 DMA 的典型流程是初始化 SPI 外设配置为全双工模式、合适的分频、正确的极性和相位。配置 DMA 通道指定外设地址SPI-DR和内存地址设置传输方向和传输长度。使能 DMA 通道使能 SPI。DMA 传输完成后触发中断或事件在回调中处理数据。我实际用到的场景是往一块 QVGA 分辨率的 ST7789 屏幕推全屏图片数据。屏幕分辨率 240x320RGB565 格式一帧数据约 150KB如果不用 DMACPU 会卡死在 SPI 发送循环里其他任务全部停摆。用了 DMA 之后CPU 只需要启动一次传输然后就可以去处理其他逻辑传输完成后由 DMA 中断通知。这个体验上的差距是质的飞跃。但 DMA 也不是没有坑。最经典的问题是 SPI 发送完成并不等于 DMA 搬运完成——DMA 把最后一个字节塞进 SPI 数据寄存器后SPI 硬件可能还在移位发送这个字节如果你立刻操作片选拉高或者进入低功耗模式最后一个字节的尾巴可能就被截断了。解决办法是等 SPI 的 BSY 标志位变低或者等待 TXE 和 BSY 同时满足条件再片选拉高。还有一个坑是 DMA 的传输长度和数据宽度配置。SPI 数据寄存器是 8 位还是 16 位DMA 的 PSIZE 和 MSIZE 要对应起来否则会产生意想不到的数据错位。另外某些 MCU 的 DMA 在整个传输结束时会自动产生一次“传输完成”中断但在半传输时也有中断使用时要区分清楚避免在数据传输一半时就去读缓冲区导致数据不完整。7. SPI 多从机与总线共享——屏和 Flash 抢总线谁先谁后一个 SPI 总线上挂多个从机非常常见比如 ESP32 上同时挂 ST7789 屏幕和 SD 卡或者 STM32 上挂 Flash 和传感器。多从机在设计上有个核心矛盾所有从机共用 SCLK、MOSI、MISO 三根线片选线各自独立同一个时刻只能有一个从机被选中。所以总线共享的系统设计重点是片选管理和通信时序的互斥。我在 ESP32 上遇到过屏幕和 SD 卡共用 SPI 总线的场景。当时先在逻辑分析仪上看时序发现屏幕刷新过程中会频繁占用总线导致 SD 卡的读取操作迟迟得不到执行或者执行一半被打断。解决方案有两个方向一是调整驱动的调度策略保证 SD 卡操作期间屏幕不发起通信关闭屏幕刷新或者切换到其他任务二是给两个设备分配不同的 SPI 总线代价是引脚占用变多。如果两个设备可以接受分时共用用互斥锁管理总线访问就足够了如果冲突严重建议直接分总线。多从机共享总线时MISO 线上的“三态竞争”问题要特别注意。根据 SPI 协议只有被 CS 选中的从机才会驱动 MISO 输出其他从机必须处于高阻态。但有些芯片的 MISO 引脚在未选中时是推挽输出高电平个别老芯片这样两个从机同时驱动 MISO 就会打架轻则电平被拉低读不到数据重则损坏引脚。解决办法是检查从机数据手册确认 MISO 是否支持高阻态必要时在 MISO 线上串联电阻限制电流。另外总线上的上拉电阻也会影响从机。有些设计中 MISO 和 MOSI 线会接上拉电阻但上拉会降低信号边沿的转换速率在高速 SPI 下可能引入时序问题。如果使用的是 3.3V 系统SPI 上拉电阻的取值一般选 4.7kΩ 到 10kΩ既能起到稳定电平的作用又不至于严重影响边沿速率。8. 用逻辑分析仪抓 SPI——调试时必须掌握的一招调试 SPI 最有效率的手段就是逻辑分析仪。我见过太多人出问题后第一反应是改代码、改配置折腾大半天发现还是一头雾水其实只需用逻辑分析仪抓一下波形问题原因一目了然。逻辑分析仪的使用步骤很简单把通道接到 SCLK、MOSI、MISO、CS 四个引脚点击采样然后复现通信过程。采完之后重点看几处CS 拉低的时机和保持时间对不对有没有中途意外拉高SCLK 的频率、占空比、空闲电平是否和配置一致MOSI 上的数据和预期是否一致数据位序对不对MISO 在从机响应期间是否有波形数据是否正确是否存在毛刺、抖动、电平不稳定等问题如果手头只有一个普通示波器也能抓 SPI 波形但效率不如逻辑分析仪因为示波器用例少通道、解码功能弱。便携式逻辑分析仪比如几百块的 8 通道 USB 逻辑分析仪已经是嵌入式调试的标配了配合开源的 PulseView 或者厂商自带软件就能完成协议解码。我诊断过一些“诡秘”的 SPI 问题最终都是靠逻辑分析仪找到根因的。比如说某个从机在高温环境下偶尔读错寄存器值抓波形发现 CS 信号有毛刺导致从机误判片选。再比如某个项目里 SPI 在开机后能正常通信运行一段时间后突然挂掉抓波形发现 SCLK 在某个瞬间出现了一个额外的高电平脉冲——后来排查发现是某个中断服务程序里不小心把 SPI 的时钟脚配置成了输出口中断执行时改写了引脚电平。这些问题如果不看波形光靠代码审查不知道要排查多久。所以我的建议是SPI 调不通第一件事别改代码先抓波形。硬件工程师说过一句我很赞同的话“时序问题万事先上仪器。”9. GPIO 模拟 SPI 从机——如何在资源紧张的 MCU 上实现 SPI 接收有时候我们面临的问题是MCU 没有硬件 SPI 从机接口或者硬件从机接口被占用了但必须用 SPI 跟其他设备通信这时候可以尝试用 GPIO 模拟 SPI 从机。这个需求在低成本 MCU 上非常常见。GPIO 模拟从机有两个关键点时钟边沿检测和数据移位。如果 SCLK 速度不高几百 kHz 到 1MHz可以用外部中断在 SCLK 的采样沿触发中断在中断服务函数里采集 MOSI、移位移位寄存器、写入接收缓冲区同时在发送数据时在中断服务函数里把下一位数据输出到 MISO。如果 SCLK 速度很高中断方式跟不上就得用轮询方式在 while 循环里不断检测 SCLK 的电平变化来模拟边沿检测。轮询方式的伪代码思路// 等待 SCLK 空闲电平CPOL0 时是低电平 while (SPI_SCLK_READ() 1); // 检测第一个下降沿到上升沿的过程 while (1) { while (SPI_SCLK_READ() 0); // 等待上升沿 // 此刻 MOSI 上的电平就是下一位数据 rx_byte (rx_byte 1) | SPI_MOSI_READ(); bit_count; if (bit_count 8) { // 一个字节接收完成交给应用层处理 buffer[head] rx_byte; bit_count 0; } // 把下一位要发送的数据写到 MISO SPI_MISO_WRITE(tx_byte 0x80); tx_byte 1; while (SPI_SCLK_READ() 1); // 等待下降沿 }GPIO 模拟从机的最大问题是时序精度。如果 MCU 的主频不够高或者中断响应时间不确定很容易在高速 SCLK 下丢位。我的建议是如果从机协议允许把通信速率刻意压低主机侧的 SCLK 不要超过 500kHz这样 GPIO 模拟从机才有足够的处理余量。如果通信速率必须在 MHz 级别以上老老实实换带硬件 SPI 从机接口的 MCU或者用 FPGA/CPLD 做协议桥接。10. FPGA 上的 SPI 实现——用 Verilog 撸一个 SPI Master/SlaveFPGA 上做 SPI 接口是很多逻辑工程师绕不开的活而且和单片机上的 SPI 编程完全是两个世界。单片机是拿现成外设配置寄存器FPGA 是用 Verilog/VHDL 自己把协议从底层的时序逻辑“长”出来。这里我分享一个简单的思路。SPI Master 的核心是一个状态机和移位寄存器。状态机负责管理片选、时钟产生和字节传输流程。对于一个 8 位数据的传输大概的流程是IDLE 状态等待传输请求拉高 CSSCLK 保持空闲电平。拉低 CS进入传输状态。产生 8 个 SCLK 时钟脉冲在每个时钟沿发送/接收一位数据。完成后拉高 CS回到 IDLE。Verilog 的核心逻辑简化示意always (posedge clk or negedge rst_n) begin if (!rst_n) begin bit_cnt 0; sclk 1b0; cs_n 1b1; end else if (tx_start) begin if (bit_cnt 8) begin cs_n 1b0; sclk ~sclk; if (sclk) begin // 下降沿更新 MOSI mosi tx_data[7 - bit_cnt]; bit_cnt bit_cnt 1; end end else begin cs_n 1b1; tx_done 1b1; end end end当然这只是一个极其简化的框架实际工程里还要考虑跨时钟域、FIFO 缓冲、突发传输、中断标志等等。FPGA 的好处是你可以精确控制每一个时钟周期的行为不像 MCU 受制于外设的固定逻辑。坏处是调试波形的时间成本更高而且如果你本来就不熟悉触发器、状态机的设计方法上手难度比单片机大很多。我在高云 FPGA 上做过一个 SPI 接口的远程升级模块核心就是把 MCU 的升级命令通过 SPI 转发到 FPGA 内部的 Flash 控制器这里 SPI 协议本身不复杂复杂的是升级过程中 Flash 擦写状态的管理和超时处理。11. SPI 调试常见问题速查表把我在项目里遇到过的 SPI 问题汇总成一张速查表方便各位遇到问题快速定位。现象可能原因排查/解决思路读回来的数据全 0xFF 或全 0x00CS 没选对、从机没上电、MISO 线没接好、读操作没发空时钟用万用表测 CS 电压逻辑分析仪看 MISO 是否有波形数据错位逻辑上是对的但整体偏移一位或多位SPI 模式不对CPOL/CPHA 反了、位序不对、时钟过快导致采样点偏了抓波形对照时序图检查 CPOL/CPHA、位序配置数据大部分对偶尔出错SCLK 太快、信号完整性差、供电纹波大、中断干扰时序降低分频加串联匹配电阻检查供电屏蔽中断通信正常但多个从机互相干扰MISO 三态隔离没做好、CS 控制不严谨确认从机 MISO 是否高阻检查片选时序DMA 传输后最后一个字节丢失SPI 还在发送时 DMA 已报告完成CS 被提前拉高等待 SPI BSY 标志清除后再拉高 CS模拟 SPI 时好时坏延时不够、中断打断了位翻转加大延时关闭不相关中断或改用硬件 SPI这张表不是万能的但它能覆盖大约八成的 SPI 问题。剩下的属于玄学范畴比如某个器件只有在特定温度下才出错那大概率是信号完整性和器件工艺的问题这种只能靠加测试点、慢慢调参数来解决了。12. SPI 与 I2C、UART 的选型对比——为什么这个场景用 SPISPI、I2C、UART 三种协议各有适用场景选型时主要看速率、引脚数量、从机数量和协议复杂度。维度SPII2CUART线数4 N每个从机一条 CS22RX/TX速率很高几十 MHz低最高几 MHz中最高几 Mbps寻址方式片选7/10 位地址点对点全双工是否半双工是多从机可以但占引脚很方便需要额外扩展协议复杂度无内置帧/校验有 ACK/NACK无内置帧/校验嵌入式中最常见的搭配是板级高速数据传输用 SPI低速传感器多设备挂载用 I2C跨板点对点通信用 UART。比如一个典型的 IoT 节点主控通过 SPI 驱动显示屏和 Flash通过 I2C 读取温度和湿度传感器通过 UART 跟外部通信模块交互。这几乎是行业标准搭配。如果你在两种协议之间纠结我建议先问自己几个问题数据量多大、速率要求多高、从机数量多少、引脚有没有余量、有没有现成的驱动库。回答完这几个问题选型结论基本就出来了。以我的经验如果只是偶尔读几个字节的传感器数据I2C 足够了——省引脚、代码简单、抗干扰性能也不算差如果要持续刷新屏幕或者读写 FlashSPI DMA 是唯一靠谱的答案如果是两个板子之间单向传数UART 最省心。13. 从“能通”到“稳定”——SPI 驱动代码的几个进阶技巧SPI 通信“能通”和“稳定”之间还隔着不少距离。分享几个我常用的小技巧都是拿真实项目换来的经验。第一所有 SPI 操作都要有超时保护。阻塞式 HAL 发送如果从机芯片异常比如根本没上电、CS 悬空、MISO 短路SPI 外设可能一直等不到预期的状态位程序就卡死在 while 循环里。给每个 SPI 读写函数加一个超时计数或者使用 HAL 库的超时参数超时后返回错误码这样系统至少不会死锁。第二片选操作放在互斥锁里。如果 RTOS 环境下多个任务会访问同一个 SPI 从机必须用互斥锁保护整个“拉低 CS - 通信 - 拉高 CS”的过程否则两个任务交叉操作同一个从机CS 状态会被打乱通信数据混乱不堪。我在 FreeRTOS 项目里吃过这个亏后来给每个 SPI 设备加了一个 mutex问题就消失了。第三尽量把“读寄存器”和“写寄存器”封装成函数内部处理逻辑完全隐藏。比如w25q_read_id()、st7789_write_data()各个模块之间不要直接操作 SPI 底层函数这样后续更换 MCU 平台、更换从机芯片时只改底层驱动即可应用层代码几乎不用动。第四留意 SPI 总线的空闲电平。SPI 信号线在空闲状态下如果没有被拉低或者拉高到固定的电平容易受到外界电磁干扰而误触发从机。通常 MOSI 和 SCLK 在主机初始化后就会自动设置到空闲电平取决于 CPOL但如果你用的是软件模拟 SPI请务必在初始化时先把 MOSI 和 SCLK 引脚设置成确定的状态通常 SCLK 为低或高取决于 CPOLMOSI 设置为低或高都可以但要稳定。第五SPI Flash 类的从机有擦写周期限制和写入超时读没问题但写操作必须检查状态寄存器的忙标志。有些新手写完一个字节马上又去写下一个完全不管 Flash 内部是否还在擦写结果数据错乱。这个不是 SPI 协议本身的问题但是 SPI 驱动里最常见的实用坑所以提一下。14. 从 8 位 MCU 到 FPGA——SPI 在不同平台的移植思路SPI 驱动在不同平台上移植本质上做的是同一件事按照协议规定的时序去控制引脚或者操作 SPI 外设寄存器。最底层做的是“往移位寄存器塞字节”和“把移位寄存器里的字节取出来”中间层是命令组织、片选管理、DMA 控制和错误处理上层才是具体的设备操作读 Flash ID、写屏幕像素点等。移植的时候我一般分三步走第一步抽离硬件抽象层。把 SPI 初始化和收发函数封装成一组统一的接口比如spi_init()、spi_transfer_byte()、spi_transfer_buffer()。不同平台用不同的实现但接口保持一致。第二步抽象设备驱动层。每个从机写一个独立的驱动文件比如w25q.c、st7789.c内部调用底层 SPI 接口不直接操作寄存器。第三步校验移植结果。换平台后先跑一次简单的读写测试比如读 Flash ID、读传感器寄存器确认波形和时序正常再跑全量功能测试。我曾在 STM32F1、STM32H7、ESP32、GD32、高云 FPGA 上分别驱动过同一款 W25Q 系列 Flash流程上没有任何区别唯一要调整的就是底层接口和时钟分频参数。所以“学一次 SPI到处都能用”这句话是真的成立前提是你把协议本身搞懂而不是死记某个平台的配置流程。由于我是嵌入式老手这里再唠叨一句不要沉迷于某个平台的便捷配置工具比如 CubeMX 一键生成代码却不知道底层到底配了什么东西。某次手头一个项目从 STM32F1 换到 STM32H7CubeMX 生成的 SPI 初始化代码看起来差不多但 H7 的 SPI 外设实际支持的特性更多比如 FIFO、更高的时钟频率如果你只满足于“生成的代码能跑”可能永远发现不了自己的 SPI 还有提升空间。了解协议本身比背熟任何一份配置文件都重要。