SPI 这个接口年纪比很多做嵌入式的新人还大但到现在依然是板级通信的常青树。我这些年调过屏幕、Flash、SD 卡、传感器甚至 FPGA 和 MCU 之间的高速数据交换十次里有八次都在跟 SPI 打交道。说它“优雅”不是因为协议复杂高深恰恰相反SPI 是那种你想让它干什么、它就干什么的接口只要把时序和片选弄清楚剩下的活儿基本就是照着数据手册填寄存器。这篇就把我平时折腾 SPI 的思路、踩过的坑、以及几种“很优雅”的玩法一次性倒出来希望对正在跟 SPI 缠斗的朋友有帮助。文章面向的读者是已经会点亮一颗 LED、能跑通串口打印、但想真正把外设驱动写明白的嵌入式开发者。不管你是用 STM32、ESP32还是想自己在 FPGA 里用 Verilog 撸一个 SPI 控制器这篇都能给你一套可以直接上手的方法论。1. 先搞清楚 SPI 在传输什么四根线背后的通信逻辑1.1 四根线各司其职别把片选当摆设SPI 全称 Serial Peripheral Interface中文叫串行外设接口。它最经典的样子就是四根线SCLK时钟、MOSI主出从入、MISO主入从出、CS/SS片选。很多人刚接触 SPI 时眼睛只盯着 SCLK、MOSI、MISO 这三根觉得片选就是一根“选中芯片”的线拉低就开始通信拉高就结束。这么理解没错但不够。片选这根线其实是 SPI 优雅与否的第一道分水岭。在一个 SPI 总线上挂多个设备时所有设备的 SCLK、MOSI、MISO 是可以并在一起的唯独片选必须各走各的。主机想跟谁说话就把谁的 CS 拉低把其他人的 CS 保持高电平。这样做的本质是“用一根线解决设备寻址”比 I2C 用地址字节来选设备要省事得多代价就是多占一根 IO。我见过不少新手把两个设备的 CS 并在一起结果两个设备同时响应数据全乱排查半天才发现是片选没分开。另外要注意异步串口UART是双方约定波特率I2C 是在 SCL 上带时钟而 SPI 是主机主动产生时钟从机完全被动。从机什么时候采样、什么时候输出全看主机拉出来的时钟边沿。这个“主从节奏”的差异决定了 SPI 天生适合做主从一对多的高速通信但不适合做多主机互连。你要想让两个单片机用 SPI 互相对发就得自己设计主从切换机制这属于“进阶玩法”里的内容了。1.2 时序里的坑四种模式选不对就白调SPI 有四种工作模式由 CPOL时钟极性和 CPHA时钟相位两个参数决定。CPOL 决定空闲时时钟是高电平还是低电平CPHA 决定数据是在时钟的第一个边沿采样还是第二个边沿采样。这样一组合就有了 Mode 0CPOL0CPHA0、Mode 1、Mode 2、Mode 3 四种。很多外设手册里会写“支持 SPI Mode 0 和 Mode 3”比如最常见的 W25Q64 Flash它两种模式都能用。但有些传感器或者屏幕控制器可能只支持某一种模式你配置错了读回来的数据全是 0xFF 或者乱码。我自己的习惯是拿到一个新外设先去看它的时序图数一数数据在 SCLK 上升沿还是下降沿被采样以此判断 CPHA再看空闲电平是高是低以此判断 CPOL。关于模式选择有两条实际经验大多数情况下首选 Mode 0CPOL0CPHA0。因为多数芯片默认支持 Mode 0SPI 设备在空闲电平为低时不容易产生误触发。如果 Mode 0 读数据不对不要急着改代码里的 GPIO 翻转逻辑先试试 Mode 3。Mode 3 和 Mode 0 的波形形状一样都是空闲低电平、第二个边沿采样错了Mode 3 空闲是高电平我说的是 Mode 0 和 Mode 3 是“最常用”的两个模式很多 Flash 和 SD 卡都同时支持切换成本低。这里还要提醒一个细节时钟极性配置错误并不会导致硬件损坏但会导致通信数据全部移位、错位。你要是用逻辑分析仪抓波形会发现 MOSI 上数据是对的但 MISO 上回来的数据总是少了半拍或者多了一位十有八九就是 CPHA 搞反了。1.3 传输速率与“多远能传”SPI 的物理边界SPI 没有像 I2C 那样协议规定的最高速率你能跑多快取决于主机的时钟源、从机的最大支持频率、PCB 走线长度和信号完整性。以我常用的 STM32F4 系列为例硬件 SPI 最高可以跑到 42 MHz 甚至更高而 W25Q64 这类 SPI Flash 手册上写的是支持 104 MHz 的时钟但实际在开发板上跑超过 40 MHz 后如果杜邦线稍微长一点数据就会出错。我把这个现象总结成一句话SPI 的速度上限不是芯片决定的是你的物理连接决定的。开发板上用短的 PCB 走线跑几十兆没问题你拿十几厘米的杜邦线去飞线能稳定跑到 10 MHz 就已经不错了。所以调试阶段建议把 SPI 时钟先降下来比如 1 MHz先把功能调通再逐步拉高频率。直接一上来就最大频率出了问题很难定位是逻辑错还是信号完整性问题。从电平标准看绝大多数 MCU 的 SPI 是 3.3V TTL/CMOS 电平如果和 5V 设备对接要确认从机是否兼容 3.3V 输入以及 MISO 输出会不会把 3.3V 主机电平拉偏。保险的做法是加电平转换芯片或者串电阻分压。别以为“逻辑电平不一样也能凑合工作”长期运行会有概率性死机、数据偶发错误排查起来极其痛苦。2. 一个 SPI 挂一大堆总线架构与设备共享设计2.1 多设备共享 SPI 总线的两种典型接法一个 SPI 主机带多个从机常见的接法有两种独立片选和菊花链。独立片选最直观所有从机的 SCLK、MOSI、MISO 并联每个从机单独一个 CS 接到主机不同 IO。这种接法好处是每个设备可以被单独访问互不干扰坏处是主机要消耗多根 IO 做片选设备多了 IO 可能不够。我做 ESP32 屏幕与 SD 卡共享 SPI 时就是这种接法LCD 用一个 CSSD 卡用另一个 CS两条 CS 绝对不要短接。菊花链则是把数据像串糖葫芦一样串过去主机的 MOSI 接第一个从机的 MOSI第一个从机的 MISO 接到第二个从机的 MOSI这样依次串下去最后一个从机的 MISO 回到主机。这种接法适合需要“把配置数据同时广播给多个从机”、或者从机支持菊花链模式的场景比如某些 LED 驱动芯片、移位寄存器。它的好处是省片选坏处是延时累积数据经过每一级都会多一个时钟周期的延迟如果你对实时性要求高就不太合适。我实际中用到菊花链不多更多是独立片选。这里给你一个判断标准如果你的从机之间没有数据依赖关系那就用独立片选如果有级联关系比如 A 的输出要作为 B 的输入再考虑菊花链。2.2 硬件片选与软件片选什么时候必须用硬件 CS聊聊热搜里特别多人关心的“硬件片选与软件片选”问题。软件片选就是你把 CS 接到任意一个 GPIO在通信前用GPIO_WriteLow拉低通信结束后拉高。硬件片选硬件 NSS则是使用 MCU SPI 外设自带的 NSS 引脚由外设硬件自动控制。STM32 的 SPI 外设里的 NSS 有几种工作模式硬件 NSS 输出、硬件 NSS 输入、软件 NSS 管理。使用硬件片选最大的好处是省一次 GPIO 中断响应的时间同时时序更精确、更不容易受中断干扰。比如你在一个高优先级中断里调用 SPI 传输如果用软件片选可能会出现“CS 刚拉低还没发数据就被另一个中断打断导致从机误以为一次通信开始了”的情况。硬件片选由外设自己控制能够保证 CS 的拉低和时钟的产生严格同步。但硬件片选也有坑很多 MCU 的硬件 NSS 引脚是固定的你在画板子的时候就得规划好比如 STM32F1 的 SPI1_NSS 固定在 PA4如果你这块引脚被复用了那就只能用软件片选。另外如果 SPI 总线上挂了多个设备硬件片选往往只能管一个 NSS 引脚其他设备还是得用普通 GPIO 做软件片选。我的建议很佛系单从机、追求极致速率、对时序敏感的用硬件片选多从机、IO 灵活、想省事就用软件片选但记得在片选拉低后加一点延时比如几个空指令让从机稳定下来。别小看这微秒级的延时很多入门者通信不稳定就是因为在 CS 拉低后立刻发时钟从机还没来得及准备好。2.3 实操案例ESP32 屏幕与 SD 卡共享 SPI 怎么设计“ESP32 屏幕与 SD 卡共享 SPI 哪个好”这个问题我理解的是问在同一个 ESP32 上需要同时驱动 TFT 屏幕和 SD 卡它们是共用一路 SPI 好还是分别用两路 SPI 好先给结论可以共用一路 SPI只要你满足两个条件硬件上 CS 分开软件上不同时访问。我在一个项目里用 ESP32 的 SPI2VSPI同时挂了 1.8 寸 TFT 屏幕和 SD 卡模块。屏幕的 DC数据/命令脚用 GPIOCS 用 GPIO5SD 卡的 CS 用 GPIO4SCLK、MOSI、MISO 三根线并联到两个设备。共用一路 SPI 的好处是省引脚、省外设资源坏处是屏幕刷新和 SD 卡读写不能同时进行。如果你的屏幕刷新率要求很高比如做 UI 动画同时又要频繁读写 SD 卡那你大概率会遇到刷新卡顿。这时候就该用两路独立 SPI一路专供屏幕一路专供 SD 卡互不干扰。ESP32 有 SPI2 和 SPI3 两个控制器分配好以后是很从容的。我给一个更具体的建议屏幕刷新率跟 SPI 时钟和像素格式有关。比如一个 240x320 的 RGB565 屏幕一帧数据量是 240x320x2 153600 字节。如果 SPI 时钟是 40 MHz理论最快刷新率大约是 40M / 153600 ≈ 260 帧/秒但实际还要扣除命令开销、DC 翻转、延时能跑到 30 到 60 帧已经不错。如果你用 1 MHz 的时钟去刷屏那刷新率就只能用惨不忍睹来形容了所以屏幕刷新率低先别怀疑屏先看时钟和代码效率。3. 从零手写一个软件模拟 SPI原理比想象中更值钱3.1 为什么还要软件模拟 SPI看到这里你可能疑惑MCU 都有硬件 SPI直接调库不就行了为什么还要手写模拟我总结下来有四个场景绕不开硬件 SPI 引脚不够用比如你要挂 6 个 SPI 传感器硬件 CS 不够或者 SCLK/MISO/MOSI 引脚被别的功能占用。IO 电平或时序不匹配比如某些低速传感器只需要几百 kHz 的时钟但硬件 SPI 的最低分频还是太快此时软件模拟更好控制。跨平台复用代码你希望同一份驱动代码在 STM32、ESP32、Arduino 上都能编译运行直接操作 GPIO 是兼容性最好的实现方式。调试和教学软件模拟能让你真正理解 SPI 时序的每一个细节。一句话硬件 SPI 是“高速公路”软件模拟是“乡间小路”但你得先会走路再开车。把软件模拟写明白硬件 SPI 用起来也就没有黑盒了。3.2 三线 SPI 与双向数据线用一根线完成读写热搜里提到的“io口模拟三线spi”其实就是只有 SCLK 和一根双向数据线比如某些 LCD 的 SDA 既发命令又收数据或者某些触控芯片用一根 SDA 完成读写。这种三线 SPI 实际是“半双工”模式发数据和收数据分时共用一根线。软件模拟三线 SPI 的关键在于切换 IO 方向。在主机发数据时这根线要配置为推挽输出发完最后一个位之后如果要等从机回复就要立刻把该引脚切换为输入模式或者开漏并外加上拉电阻。很多初学者在这里翻车因为忘记切换方向导致读到的一直是自己刚发出去的电平。以 STM32 的 GPIO 为例切换方向就是改 MODER 寄存器或者用 HAL 库的HAL_GPIO_Init。在频繁切换的场景下建议直接操作寄存器来切换输入输出模式因为HAL_GPIO_Init每次都要重新配置整个 GPIO 引脚开销大、时序也不好看。我自己写了一个小的位操作宏来切换方向实测在几十 kHz 下跑得很稳。3.3 核心代码实现GPIO 翻转的每一拍都不能错下面给一个通用的软件模拟 SPIMode 0MSB First示例语言用 CGPIO 操作抽象成宏方便移植到不同平台。// 假设你已经定义了以下几个宏/函数 #define SPI_SCLK_H() // 拉高 SCLK #define SPI_SCLK_L() // 拉低 SCLK #define SPI_MOSI_H() // 拉高 MOSI #define SPI_MOSI_L() // 拉低 MOSI #define SPI_MISO_READ() // 读取 MISO 电平返回 0 或 1 #define SPI_CS_L() // 拉低片选 #define SPI_CS_H() // 拉高片选 static void spi_delay(void) { // 用于调节时钟频率实际用时可以改成空循环或 __NOP() for (volatile int i 0; i 8; i); } void spi_write_byte(uint8_t data) { for (int i 7; i 0; i--) { SPI_SCLK_L(); if (data (1 i)) SPI_MOSI_H(); else SPI_MOSI_L(); spi_delay(); SPI_SCLK_H(); // Mode 0: 上升沿采样数据 spi_delay(); } // 最后保持时钟低电平也可以根据从机要求拉低 CS SPI_SCLK_L(); } uint8_t spi_read_byte(void) { uint8_t data 0; for (int i 7; i 0; i--) { SPI_SCLK_L(); spi_delay(); SPI_SCLK_H(); // 上升沿让从机准备数据注意这里要根据模式调整 spi_delay(); if (SPI_MISO_READ()) data | (1 i); } SPI_SCLK_L(); return data; }注意spi_read_byte里我对采样点的注释写得比较谨慎因为 Mode 0 和 Mode 3 的采样边沿不一样。Mode 0 是“空闲低电平第一个边沿采样”实际上 SCLK 从低变高的上升沿采样。所以读数据时要在 SCLK 拉高之后、保持高电平期间去读 MISO如果 SCLK 刚拉高立刻去读可能读到的是从机还没来得及更新的旧值。稳妥做法是把读取动作放在延时之后。实际操作中很多人用软件模拟 SPI 速率上不去是因为spi_delay太“厚”了。其实可以用空指令__NOP()替代循环或者直接调整循环次数让每个位周期在 1 到 10 微秒之间。别追求极致的速度稳定第一。4. 实战用硬件 SPI 把 W25Q64 和 LCD 玩明白4.1 用 STM32CubeMX 快速配置 SPI现在做 STM32 开发我几乎离不开 STM32CubeMX。不是因为它多智能而是它能快速把 SPI 外设的时钟、引脚、DMA 关系捋清楚省去对照数据手册算寄存器的功夫。在 CubeMX 里配置 SPI你要关心的几个参数Mode选择 Transmit Only只发、Receive Only只收、或者 Full-Duplex Master全双工主机。全双工主机是最常用的。Hardware NSS Signal如果你要用硬件片选就在这配置一般我习惯配成 Disable然后用软件 GPIO 管理 CS灵活。Parameter Settings重点设数据大小8 bit、时钟极性 CPOL、时钟相位 CPHA、时钟分频 Prescaler、MSB First。分频系数直接影响 SPI 速率比如 APB2 时钟 84 MHz预分频 2 就是 42 MHz预分频 64 就是 1.3125 MHz。调试的时候先用大分频稳定后再缩小。这里提醒一句CubeMX 生成的默认配置通常能用但它默认开启的 FIFO 和 DMA 在某些场景下反而会添乱。我习惯把 FIFO 关闭用最基础的中断或轮询方式先把数据通路打通再做优化。4.2 读写 W25Q64 核心代码读 ID、擦除、写页W25Q64 是华邦Winbond出品的一款 8 MB SPI NOR Flash几乎成了嵌入式开发板的标配。用它练手 SPI比用 LCD 更纯粹因为 Flash 的操作命令非常规范方便你验证时序对不对。W25Q64 的读 ID 流程如下拉低 CS发送命令 0x9FJEDEC ID 命令然后连续读三个字节分别是 Manufacturer ID0xEF、Memory Type0x40、Capacity0x17。如果这三个字节能读对说明你的 SPI 基本配置没问题。// 使用 HAL 库的硬件 SPI 读 W25Q64 ID uint8_t w25q_read_id(void) { uint8_t cmd 0x9F; uint8_t id_data[3] {0}; SPI_CS_L(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); HAL_SPI_Receive(hspi1, id_data, 3, 100); SPI_CS_H(); printf(Manufacturer ID: 0x%02X\r\n, id_data[0]); printf(Memory Type: 0x%02X\r\n, id_data[1]); printf(Capacity: 0x%02X\r\n, id_data[2]); return id_data[0]; }读对了 ID再试写读。写入 Flash 之前必须先发“写使能”命令0x06然后发“页编程”命令0x02、地址和数据。页大小是 256 字节跨页写入要分成多次。写完之后一定要等 BUSY 位清零查询命令是 0x05读回来的状态寄存器最低位为 1 表示正在忙。很多人写 Flash 失败不是 SPI 时序的问题而是忘了先擦除。W25Q64 的存储单元只能把 1 写成 0不能直接把 0 写成 1所以写之前必须先“扇区擦除”0x20擦除 4 KB或者“块擦除”0xD8擦除 64 KB。这里有一个重要的实操心得读 Flash 可以用任意速度但写 Flash 和擦除 Flash 的“等待完成”过程一定不要用 DWT 延时去猜。正确做法是发完写命令后反复发读状态寄存器命令直到 BUSY 位为 0。如果你用固定延时等待一旦环境温度、电压波动导致擦除时间变长你的程序就可能会在 Flash 还没写好时去读数据然后读出 0xFF。4.3 用 SPI 驱动 LCD屏幕刷新率的瓶颈在哪LCD 屏幕通过 SPI 驱动核心就是把“像素数据”按格式刷到显存里。常见的 1.8 寸 TFT 屏用的是 ST7735 控制器240x240 的屏用 ST7789它们都支持 4 线 SPI。驱动方式分为命令通道和数据通道通常有一根 DC数据/命令选择引脚来区分。驱动 LCD 的基本流程是初始化屏幕控制器发送一大堆初始化命令设置显示窗口然后连续发送像素数据。如果你想来一个真正的 “优雅”玩法用 DMA SPI 发送屏幕数据比用HAL_SPI_Transmit阻塞式发送高效得多。阻塞式发送时CPU 全程等在 SPI 外设旁边无法做别的DMA 发送时CPU 把数据地址和长度告诉 DMA然后就可以去处理别的任务DMA 会在后台帮你把数据搬完再触发传输完成中断。屏幕刷新率的瓶颈我归纳成三个SPI 时钟前面算过240x320 的 RGB565 一帧数据是 153600 字节SPI 时钟越高理论刷新越快。像素格式RGB565 是两个字节一个像素如果屏幕支持 RGB444 甚至 1 位色深数据量大幅下降但显示效果也会下降。MCU 到屏幕控制器的写时序很多屏幕控制器内部有 FIFO但如果你每发一个字节还要拉一次 DC 线、等一次状态刷新率就会被拉低。建议把要刷的整行数据放进一个 bufferDMA 一次性发完中间不要频繁操作 DC。5. 进阶玩法FPGA 里用 Verilog 实现 SPI 控制器5.1 为什么 FPGA 也要 SPI很多做 MCU 开发的工程师第一次接触 FPGA 时都会想我 MCU 自带 SPIFPGA 凭啥还要自己写 SPI 控制器答案是 FPGA 和 MCU 的 SPI 需求不一样。MCU 的 SPI 是“外设”用库函数调用就行FPGA 里的 SPI 是“逻辑”你跟外部 Flash、ADC、DAC 通信时没有现成的 SPI 模块只能自己写。而且 FPGA 经常要做“高速数据采集”或者“实时信号处理”对 SPI 时序的精确性要求极高。比如你要在 FPGA 里配置一颗高速 ADCSPI 时序必须在几十纳秒内稳定这时候用纯逻辑实现 SPI 反而更可控。另外FPGA 里写 SPI Master 也是学习状态机和串行协议的好素材。状态机天然适合描述“一个 bit 一个 bit 地把数据打出去”的过程。5.2 一个简洁可用的 SPI Master 状态机下面给一个最基本的 SPI Master Verilog 实现支持 Mode 08 位数据。核心思路是“分频产生 SCLK用状态机控制每一位的发送和接收”。module spi_master #( parameter CLK_DIV 8 // 系统时钟分频实际 SCLK sys_clk / (2*CLK_DIV) )( input wire clk, input wire rst_n, input wire start, input wire [7:0] tx_data, output reg [7:0] rx_data, output reg busy, output reg sclk, output reg mosi, input wire miso, output reg cs_n ); reg [3:0] bit_cnt; reg [7:0] tx_shift; reg [7:0] rx_shift; reg [15:0] clk_cnt; localparam IDLE 2b00; localparam RUN 2b01; localparam DONE 2b10; reg [1:0] state; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; cs_n 1b1; busy 1b0; sclk 1b0; mosi 1b0; bit_cnt 4d0; clk_cnt 16d0; end else begin case (state) IDLE: begin cs_n 1b1; busy 1b0; if (start) begin state RUN; cs_n 1b0; busy 1b1; tx_shift tx_data; bit_cnt 4d0; clk_cnt 16d0; end end RUN: begin if (clk_cnt CLK_DIV - 1) begin clk_cnt 0; sclk ~sclk; if (sclk 1b0) begin // sclk 从低到高发送 MOSI mosi tx_shift[7]; tx_shift {tx_shift[6:0], 1b0}; end else begin // sclk 从高到低采样 MISO rx_shift {rx_shift[6:0], miso}; bit_cnt bit_cnt 4d1; if (bit_cnt 4d7) begin state DONE; rx_data {rx_shift[6:0], miso}; end end end else begin clk_cnt clk_cnt 16d1; end end DONE: begin cs_n 1b1; busy 1b0; state IDLE; end endcase end end endmodule这段代码的关键在于“在 SCLK 的哪个边沿做事”Mode 0 要求数据在上升沿被主机送出、在下降沿被主机采样。上面的代码里当sclk从低变高sclk 1b0分支时更新 MOSI当sclk从高变低sclk 1b1分支时采样 MISO这样写是符合 Mode 0 习惯的但你要自己看着时序图再验证一遍。实际项目里如果你在 FPGA 和 MCU 之间联调时钟相位差半个周期很正常可以手动调整采样沿来适配。5.3 与 MCU 实现的关键差异并行世界的串行开销FPGA 世界是并行的SPI 却是一根一根地传所以“串行开销”是绕不开的话题。你在 MCU 里可能感觉不到因为 CPU 本身是串行执行的但 FPGA 里你的 SPI 模块要和 ADC 采样、FIFO 存储、DMA 搬运这些模块同时工作就需要考虑跨时钟域和缓冲区设计。一个最常见的做法是用 FIFO 做缓冲。SPI Master 模块负责把 FIFO 里的数据发出去同时把收到的数据写进另一个 FIFO上层逻辑比如内置的 CPU 软核或者状态机只管读写 FIFO不用关心 SPI 时序细节。这样做的好处是把“串行慢速”的外设和“并行快速”的内部逻辑解耦系统吞吐率会大幅提升。还有个经验是在 FPGA 里写 SPI不要一上来就追求高性能。先用 1 MHz 的慢速时钟把通路跑通再用逻辑分析仪抓波形确认数据和时钟的边沿关系正确再逐步提高分频。FPGA 的信号是并行的一改分频参数就全变了没有 MCU 里的“库函数帮你封转好一切”。6. 常见问题实测中踩过的坑做 SPI 通信遇到问题我建议你先别急着怀疑代码先怀疑“物理层”。我整理了一张常见问题速查表都是自己实际调试中遇到过的。现象可能原因解决方案读回来的数据全是 0xFFMISO 没接对或从机没被正确选中或从机供电异常检查接线、量 CS 电平、检查从机电源MOSI 输出正确但 MISO 采到错位数据CPOL/CPHA 配置错误切换 SPI Mode 0/1/2/3用逻辑分析仪看采样点第一次通信正常后续乱码软件片选没有在开始前拉低或者通信结束没有拉高检查 CS 拉低和拉高的时序位置高速率下偶发错误线太长、信号反射、供电不稳降低 SPI 时钟缩短杜邦线或用示波器看波形Flash 写入后读出来是 0xFF没有先擦除、或者写使能没发、或者 BUSY 没等先发 0x06 写使能再擦除扇区然后写页最后等待 BUSY 清零屏幕花屏或滚动条像素格式错误、窗口设置错误、数据发送丢字节检查初始化命令、发送整行数据时用 DMA 减少延迟SD 卡初始化失败SPI 时钟太快、上电时序不对、CS 被其他设备干扰初始化阶段把 SPI 时钟降到 400 kHz 以下再切换高速模式MISO 引脚在共享总线上互相干扰多个从机的 MISO 都不输出高阻态确保未选中的从机 MISO 为高阻或者加外部上拉/串联电阻中断里调用 SPI 传输导致死机SPI 传输是阻塞的中断里耗时太长不要在中断里做完整 SPI 传输只置标志位由主循环处理还有一个特别容易被忽略的问题IO 口复用冲突。比如你用了 STM32 的 PA5 做 SPI1_SCK但同时又初始化了一个外部中断把 PA5 配成了输入模式两段代码一冲突SPI 时钟就出不来。这种问题用逻辑分析仪看波形时往往看不到 SCLK 翻转卡半天才发现是初始化顺序的问题。如果你手头没有逻辑分析仪我强烈建议你买一个哪怕是最便宜的 8 通道 24 MHz 采样率的对调试 SPI 的帮助也远大于示波器。SPI 调试的核心就是“看波形、看边沿、看数据时序”没有一个靠谱的抓波工具全靠眼睛瞪代码效率会低很多。最后分享两个锦囊工具与习惯我用了很多年 SPI最重要的两条建议一条关于工具一条关于习惯。工具方面除了逻辑分析仪我还习惯在调试阶段打印所有收发数据的十六进制内容。很多人嫌打印拖慢速度但调试 SPI 时“快速看到数据对不对”比“通信速度”重要得多。等数据通路完全正确了再去掉打印切换 DMA 高速模式这样做能省一大半调 bug 的时间。习惯方面我强烈建议你在新项目里画一张 SPI 设备连接图标注好每个设备的 CS 引脚、Mode、最大时钟、MISO/MOSI 方向。这张图不用多精美但能让你在几个月后回头改代码时不用重新翻一堆数据手册。我以前吃过亏项目搁置两个月后再捡起来看着陌生的 SPI 初始化代码花了一天才想起当时为什么选择 Mode 3。SPI 这东西说难不难说简单也不简单。它的优雅之处在于只要把时钟、数据、片选这三件事的关系理清楚几乎所有 SPI 设备都能用同一套思路去驱动。希望这篇分享能帮你少走一些弯路在调试 SPI 的时候多一点从容少一点抓狂。