SPI通信协议详解:从时序原理到STM32实战与DMA优化
2026/9/6 12:16:56 网站建设 项目流程

经常有朋友问我:"SPI这么老的协议,为什么还在到处用?"我的回答通常是:你看看自己手边的开发板、屏幕、Flash芯片、传感器,十个里头至少有八个在跟MCU用SPI说话。SPI通信协议之所以在嵌入式世界里几十年屹立不倒,靠的就是四个字:简单、够快。它不需要地址概念,不需要复杂的应答机制,一根时钟线带着数据双向流动,主从双方各司其职,就能把数据从A点搬到B点。这篇东西主要写给正在学嵌入式、做驱动开发,或者被SPI时序折腾到失眠的工程师朋友们,我会把协议原理、时序细节、硬件软件片选的坑、DMA优化、以及屏幕和Flash这类典型外设的实战配置一次说透。

1. 重新认识SPI:它到底是怎么工作的

1.1 SPI的核心通信模型与四根线

很多人把SPI理解成"四根线",这个说法对,但不够准确。严格来说,SPI总线在标准模式下有四根信号线,但这四根线在不同场景下分工完全不同,搞清楚它们的角色,比死记名字重要得多。

  • SCK(Serial Clock):时钟线,由主设备生成并输出,是所有数据同步的基准节拍。所有从设备的数据移位都发生在SCK的上升沿或下降沿。
  • MOSI(Master Out Slave In):主设备输出,从设备输入。主设备往从设备写数据的通道。
  • MISO(Master In Slave Out):从设备输出,主设备输入。从设备往主设备回传数据的通道。
  • CS/SS(Chip Select / Slave Select):片选线,低电平有效。主设备把某个从设备的CS拉低,表示"我现在要跟你说话"。

这个模型最大的特点是"全双工":在同一个时钟周期内,主设备可以从MOSI发一位,同时从MISO收一位。所以SPI本质上是一个"移位寄存器对"的交互,主设备的移位寄存器移出一位,从设备移位寄存器也移出一位,时钟打一拍,两边寄存器同步滑动。

我见过不少人把SPI当成"半双工"来用,一次只发不收,或者一次只收不发,这其实浪费了SPI一半的带宽。很多外设协议(比如W25Q系列Flash)就利用了全双工特性,比如读数据时,主设备在MOSI上发送命令字和地址,从设备同时在MISO上返回数据。如果你在写驱动时没注意到这个特性,很可能就会在"发送命令之后还要单独等接收"这种思路上绕远路。

1.2 SPI与IIC、UART的差异和选型逻辑

讨论SPI绕不开"它跟IIC到底有什么区别"。我做了个表,把三者放在一起比,结论就很清楚了:

特性SPIIICUART
线数4根(SCK/MOSI/MISO/CS),可省MISO或MOSI变3根2根(SCL/SDA),靠地址区分设备2根(TX/RX)
通信方式全双工同步半双工同步全双工异步
寻址方式硬件片选(或软件GPIO模拟片选)软件地址(7位/10位地址)无地址概念,点对点
速率通常10MHz~100MHz+标准100kbps,快速400kbps,高速3.4Mbps常见9600~4Mbps
复杂程度协议本身极简单,无应答位有ACK/NACK、起始停止条件、总线仲裁有帧格式、波特率匹配
典型用途Flash、屏幕、ADC、SD卡、FPGA配置传感器、EEPROM、PMIC调试日志、GPS模块、蓝牙模块

怎么选?我的经验是:

  • 如果外设需要高速、大数据量传输,比如TFT屏幕刷新、Flash固件升级,优先SPI。在同等主频下,SPI能跑出比IIC高一个数量级的吞吐。
  • 如果只是读个温度、湿度、姿态传感器,几十上百字节每秒就足够,IIC更省引脚、更方便挂多个设备。
  • UART没有时钟线,主从设备各自用自己的时钟,所以它最大的价值是"能互联的万物都是串口"——调试、模块通信、老旧设备对接,都靠它。

有朋友问"SPI和IIC谁更先进",这个问法本身就不严谨。两个协议的设计目标不同:IIC为了少走线、多设备挂载而生,SPI为了高速传输而生。它们没有谁取代谁的必然性,在复杂的嵌入式系统里,两个经常同时存在,各司其职。

2. SPI时序与四种工作模式:绝大多数问题的根源

2.1 CPOL和CPHA:相位与极性一次讲透

SPI的时序之所以让人头大,是因为它没有在协议里规定死"上升沿采样还是下降沿采样",而是把选择权交给了主设备和从设备双方。这就引出了两个核心参数:

  • CPOL(Clock Polarity,时钟极性):决定空闲时SCK的电平。
    • CPOL=0:空闲时SCK为低电平。
    • CPOL=1:空闲时SCK为高电平。
  • CPHA(Clock Phase,时钟相位):决定数据在哪个边沿被采样。
    • CPHA=0:在第一个边沿(即从空闲切换到激活状态的边沿)采样。
    • CPHA=1:在第二个边沿(即从激活状态切换回空闲的边沿)采样。

两两组合就成了四个标准模式:

模式CPOLCPHA采样边沿数据输出边沿常见外设
Mode 000上升沿下降沿W25Q系列Flash、ST7789屏幕
Mode 101下降沿上升沿部分传感器
Mode 210下降沿上升沿部分SD卡模式
Mode 311上升沿下降沿部分音频芯片、ENC28J60

总结一句话:CPOL决定空闲电平,CPHA决定数据是在第一个边沿还是第二个边沿被锁存。这两个参数如果和从设备不匹配,通信结果就是"第一个字节对了,后面全错"或者"偶尔对偶尔错"这类极其诡异的现场。

2.2 从时序图看懂一次完整的数据传输

画一张在一拍SCK内数据变化与采样关系的简化逻辑:

假设CPOL=0,CPHA=0(Mode 0):

  1. 主设备先把CS拉低,表示占用总线。
  2. 在第一个SCK上升沿到来之前,主设备已经把MOSI的数据位准备好(即数据在SCK上升沿之前就稳定)。
  3. SCK上升沿到达时,主设备和从设备同时在这个边沿采样数据线上的电平,完成一位数据的锁存。
  4. 随后SCK下降沿到来,双方在这个边沿切换下一位数据到线上。
  5. 如此重复8次(或16次、32次,取决于外设字长),完成一个字节/字的传输。
  6. 最后主设备把CS拉高,释放总线。

关键点在于"数据线的切换"和"采样点"是错开的。数据在非采样边沿变化,在采样边沿被捕获,这样能保证电平稳定后再采样,避免亚稳态问题。这也是为什么SPI的时序设计天然具备较高的抗干扰能力——数据的变化点落后于采样点半个周期。

实际调试时,如果你有示波器或逻辑分析仪,我强烈建议你把SCK、MOSI、CS三根线挂上去观察。光靠猜是猜不出来问题在哪里的。我见过太多人拿着代码反复改寄存器,最后接上逻辑分析仪一看,哦,原来是CPOL和CPHA与从设备要求相反,波形上所有数据位都偏移了半个周期。

2.3 时钟频率怎么选,速率上限怎么估算

SPI能跑多快,不是主设备说了算,也不是协议本身说了算,而是"主设备和从设备双方当前条件下能跑多快"的最小值。主要约束有三个:

  • 从设备手册上的最高SCK频率。这是硬约束,超了就可能采错数据或损坏芯片。比如W25Q128的普通读模式最高支持50MHz,但某些老型号Flash只支持33MHz,必须降速。
  • 主设备SPI外设的时钟分频能力。STM32上SPI时钟来自APB总线,分频比有2/4/8/16等选项,具体取决于主频和寄存器配置。
  • PCB走线长度和质量。这个很容易被忽略,但其实很要命。SPI在短距离(几个厘米)内跑几十MHz没压力,但如果飞线超过10厘米,速率又很高,信号反射、串扰就会导致偶发性通信失败。

一个实用建议:从设备标称50MHz,你实际跑40MHz是合理的;如果你用杜邦线连接,速率最好不要超过20MHz,否则出问题你大概率会怀疑是代码问题,实际上却是物理链路问题。

另外说一句SPI和DMA的关系。很多人以为开了DMA就能跑得更快,其实DMA解决的是"CPU等待"问题,而不是"总线速率"问题。总线上SCK的速率由波特率寄存器决定,DMA只是让数据搬运不占用CPU。当你需要在屏幕刷新或Flash读写时同时处理其他任务,DMA才真正发挥价值。关于DMA的实操,下面专门开一节讲。

3. 片选机制:硬件片选和软件片选,谁更好用

3.1 硬件片选:自动操控的"隐藏管家"

硬件片选是指由MCU的SPI外设内部的NSS(Negated Slave Select)引脚来管理CS信号。主设备启动传输时,硬件自动把NSS拉低;传输结束,硬件自动拉高。整个过程不需要CPU干预,也不需要GPIO操作。

看上去很美好,但硬件片选有几个必须注意的坑:

  • NSS引脚的映射:在STM32上,同一个SPI外设的NSS可能有多个可选引脚(通过AFIO或GPIO_AF配置),而且不是所有封装都引出了所有复用功能,芯片选型时必须确认封装引脚支持硬件NSS功能。
  • 半双工和全双工模式下的NSS行为不同:做从机时,NSS是输入信号,必须由外部主设备控制;做主设备时,如果配置成NSS输出,硬件会在每次传输期间自动输出有效电平。
  • 多从设备挂载:硬件NSS引脚通常只有一个,如果你的总线上挂了多个从设备,你就需要额外用GPIO来分别控制每个从设备的CS。这种情况下,纯粹依靠硬件片选就捉襟见肘了。

3.2 软件片选:把主动权握在自己手里的"手动挡"

软件片选就是把CS接到一个普通GPIO上,在每次传输之前手动拉低,传输结束后手动拉高。代码上多两行操作,但灵活性远高于硬件片选。

软件片选最大的优势是时序完全可控。你可以控制CS拉低后延迟多久再打时钟,也可以在最后一个数据位发送完毕后再保持CS低电平一段时间再拉高。这对某些从设备是硬性要求。举个例子,很多Flash芯片在写寄存器或执行写操作时,要求CS低电平期间连续发送完所有指令和数据,然后CS拉高的瞬间芯片才开始执行内部操作。如果你CS拉高的时机不对,指令序列再正确也白搭。

还有一个非常容易被忽视的场景:片选线的毛刺。硬件片选在某些MCU上,SPI外设初始化的瞬间,NSS引脚会出现短暂的意外电平抖动,这个抖动可能让从设备误以为"被选中",进而进入异常状态。软件GPIO片选配合上拉电阻,可以有效避免这种情况。

3.3 实测场景中的选择建议

场景推荐方案理由
单从设备固定连接软件片选逻辑简单,时序完全可控
多从设备共享SPI总线软件片选每个设备独立GPIO控制,不受NSS映射限制
需要极致降低CPU占用硬件片选+DMA整轮传输无需CPU参与
从设备对CS时序有苛刻要求软件片选可精细控制CS拉低到首个时钟的间隔
初始化阶段软件片选(或GPIO高电平保持)避免外设初始化过程中CS毛刺

实操中我的习惯是:除非极特殊情况,否则一律用软件片选。连一个GPIO的成本几乎为零,但换来的是排查问题时极大的灵活性。有些工程师喜欢"更自动化的硬件片选",等到被毛刺坑了一次,就明白为什么老手都爱手动拉GPIO了。

需要注意的是,用软件片选时,CS拉低和拉高之间的整个事务不能被打断。如果你在中途开了中断,中断服务函数里又对同一个SPI外设做了操作,就会把正在进行的传输数据搅乱。所以使用软件片选时,要么在事务开始前关中断,要么确保中断里不会争用同一个SPI外设,要么用互斥标志包住整个事务。

4. 实战配置:STM32CubeMX + SPI DMA驱动屏幕和Flash

4.1 CubeMX中的关键参数配置,逐项拆解

以STM32F407为例,使用SPI1驱动一块ST7789屏幕和一块W25Q128 Flash。我打开CubeMX,按下面的方式设置参数:

  • Mode:Full-Duplex Master(全双工主模式)。如果你只是写屏幕,半双工也能跑,但我建议直接用全双工,因为有些屏幕初始化时需要读寄存器返回值。
  • Hardware NSS Signal:Disable。这里我明确关闭硬件片选,改用普通GPIO做软件片选,原因上面已经说了。
  • Parameter Settings -> Clock Parameters:
    • Prescaler(分频系数):对于ST7789屏幕,SPI时钟从APB2(84MHz)分频,我选8分频,得到10.5MHz的SCK,屏幕参数手册允许。对于W25Q128,官方支持更高时钟,但为了跟屏幕共用总线,统一跑10.5MHz完全够用,稳定优先。
    • CPOL:Low(即CPOL=0)。
    • CPHA:1 Edge(即CPHA=0)。
    • 也就是经典的Mode 0。ST7789和W25Q128都支持Mode 0和Mode 3,我统一选Mode 0,简单省心。
  • Data Size:8 Bits。有些屏幕支持9bit或16bit命令/数据模式,但那是给特定显示控制芯片用的,驱动SPI本身保持8bit,把"命令/数据区分"交给DC引脚实现。
  • First Bit:MSB First。几乎所有SPI外设默认都是MSB先发,除非外设手册特别说明要用LSB。
  • CRC:Disable。标准SPI通信不需要CRC,除非你用的从设备支持并强制要求。

CubeMX配置完成后,生成代码,再把CS引脚初始化为GPIO Output,默认输出高电平。这样初始化阶段从设备就是未选中状态,不会误收数据。

4.2 DMA传输:如何让CPU从搬运工的岗位上解放

当你的屏幕分辨率是240x320,每像素用RGB565(2字节),一帧全屏刷新的数据量是240×320×2=153600字节。就算SPI跑10.5MHz,如果一字节一字节靠CPU写寄存器发送,即使SPI外设自带发送缓冲,CPU仍然要频繁处理TXE(发送寄存器空)中断或轮询标志位,大量计算周期被白白耗掉。

DMA的基本工作方式:我把要发送的数据放在内存缓冲区,配置DMA把这块内存的内容自动搬到SPI的发送数据寄存器,每搬一次,SPI硬件就发一位数据,直到整块数据发送完毕,DMA触发传输完成中断。

在STM32上的配置要点:

  1. 在CubeMX里把SPI1的TX和RX请求都设置成DMA请求。
  2. DMA方向:内存到外设(Memory To Peripheral)用于发送。
  3. DMA模式:Normal模式,每次传输完成后自动停止,而不是循环搬运同一块数据。
  4. Data Width:Byte。如果你在代码里用uint16_t数组存RGB565颜色值,半字(Half Word)宽度传输效率更高,但要求内存对齐,实际用的时候看数据缓冲区的定义方式。

代码调用上,用HAL_SPI_Transmit_DMA函数发起传输,然后注册DMA的传输完成回调。屏幕刷新时,我填充缓冲区,启动DMA传输,主循环立刻去处理别的任务。DMA传完一帧数据后触发中断,我再更新下一帧缓冲区。

这里踩过的坑是:DMA传输期间绝对不能修改正在传输的缓冲区内容。我遇到过屏幕花屏,排查到最后发现就是我在DMA还没传完的时候就往同一个缓冲区写了新数据,导致DMA搬运的数据一半是旧的一半是新的。解决办法就是准备双缓冲,一个缓冲区在传输,另一个在填充,传输完成中断里交换角色。

4.3 共享总线场景:屏幕和Flash / SD卡如何和平共处

很多项目的屏幕、Flash、SD卡都挂在一根SPI总线上。这时候你面对的核心矛盾是:不同外设的速率需求和时序容忍度不一样。

我的建议是按"最慢设备"设置总线默认速率,或者干脆"变速率切换"。具体来说:

  • 静态配置:所有外设跑同一个速率,简单稳定,但牺牲了高速设备的性能。
  • 动态切换:每次操作不同外设时,重新修改SPI分频系数。速度快的外设跑高速,速度慢的外设降速跑。代价是每次切换要多几行寄存器操作,代码稍微复杂一点。

以ESP32为例,如果你用SPI2挂屏幕和SD卡,官方驱动建议把屏幕和SD卡放在不同的SPI总线上,或者使用不同的片选。实际应用里,如果必须共享,我建议优先保证屏幕的刷新体验,把较高优先级和较高时钟给屏幕,SD卡操作降速并放到后台。因为人眼对屏幕闪烁非常敏感,而SD卡读写晚几十毫秒用户感知不到。

总线仲裁的时间点也很讲究。两个外设的操作不能交叠,每次操作一个外设时必须把另一个外设的CS保持在高电平。一旦CS被误拉低,就相当于两个从设备同时响应主设备的指令,MOSI上的数据会被"双份接收",MISO上的数据则可能互相冲突。

5. 进阶场景:FPGA做SPI Slave时,最容易忽略的细节

5.1 从机侧的移位与采样:Verilog实现要点

SPI不只是MCU和外设之间用,FPGA与MCU通信、高速数据采集也大量使用SPI。我参与过几个FPGA项目,最典型的场景是:ADC采样数据通过FPGA处理后,由FPGA做SPI从机,把结果送回MCU。

FPGA做SPI Slave,核心是一个移位寄存器加上边沿检测逻辑。以CPOL=0、CPHA=0为例:

  • SCK的上升沿是采样点。FPGA在SCK上升沿把MOSI上的数据移入移位寄存器,同时把移位寄存器中的数据推送到MISO输出。
  • 每8个SCK上升沿后,一个字节的就位,产生一个接收完成脉冲。
  • CS拉高时,所有内部状态复位,等待下一次传输。

代码上用always块边沿检测SCK是常规做法,但这里有一个隐患:如果SCK是外部异步信号,直接用它做时钟驱动寄存器,很容易产生亚稳态。稳妥的做法是先把SCK打两拍同步,再用同步后的信号做边沿检测,或者用系统时钟对SCK采样,根据采样序列判断边沿。

5.2 跨时钟域和数据对齐:一个实战案例

我的一个项目里,MCU通过SPI向FPGA发送命令和参数,FPGA内部跑的是100MHz系统时钟,而SPI时钟只有10MHz。两个时钟域之间交换数据,如果处理不好,就会出现"命令第一个字节偶尔错乱"这种问题。

解决办法是在FPGA内部建立一个小FIFO:SPI接收逻辑把收到的字节写入FIFO,系统时钟域的控制逻辑从FIFO读取数据并解析。FIFO天然解决了跨时钟域的数据同步问题,也起到了缓冲作用。

另一个容易踩的点是"半字节错位"。当MCU一次只发8位数据,而FPGA内部按16位字来处理时,如果不注意接收完成的时序,很可能把前一个字节的低4位和后一个字节的高4位拼成一个错误数据。解决方法是在接收完成脉冲到来时按字节写入FIFO,解析时按固定协议(比如第一字节是命令,第二字节是高字节,第三字节是低字节)拼接,绝不能依赖时序猜边界。

FPGA作为从机时,MISO数据的驱动时刻也值得注意。在CPOL=0、CPHA=0模式下,从机应该在SCK的下降沿更新MISO数据,这样主设备在下一个上升沿采样时数据早就稳定了。如果你在上升沿才去更新MISO,主设备同一时刻采样,采到的很可能是旧数据,通信必然失败。

6. 高频问题排查:SPI通信不工作时的"急救手册"

6.1 常见现象、原因与对策速查表

现象可能原因排查与解决
完全没有响应,MISO恒为高/低CS没拉低、从设备供电异常或复位悬空先用万用表量CS电平,再查从设备供电和复位脚
第一个字节正确,后面全错数据/命令模式切换出错,或DC脚没切换检查屏幕类外设的DC引脚控制逻辑
随机性错误,时好时坏速率过高、线太长、供电纹波大、CPOL/CPHA配置不稳定降速到原来的1/4测试,检查信号质量
数据整体偏移一位或几位CPOL/CPHA与从设备不匹配用逻辑分析仪观察采样点和数据变化点
DMA传输后缓冲区内容被破坏DMA传输期间修改了源缓冲区使用双缓冲,传输完成后再填充新数据
多从设备挂载时互相干扰未选中的从设备CS没有保持高电平检查所有CS控制逻辑,确保同一时刻只有一个低电平
Flash写操作无效CS拉高时机不对,芯片没有捕捉到指令边界严格按手册时序操作,CS拉高前完成所有指令字节发送
FPGA作为从机时首字节偶尔错亚稳态或跨时钟域处理不当对SCK做同步,接收数据通过FIFO跨域

6.2 几个容易被忽视的细节

第一,不要忽略从设备的电平兼容性。如果你的MCU是3.3V,而某些外设是5V容忍的,直接互连问题不大;反过来,5V主控接3.3V从设备,就一定要做电平转换,否则长期使用可能烧坏从设备。用MOSI、MISO、SCK、CS四根线都要在同一个电平域下工作。

第二,SPI从设备的初始化时序往往比通信时序本身更苛刻。比如ST7789屏幕,上电后要先延时几十毫秒,再发一堆初始化命令序列。如果我在这段初始化命令里有一两条命令的时序不对,屏幕可能不亮、花屏或者白屏。我排查屏幕问题时很少一上来就怀疑SPI通信,大多数时候是初始化序列的问题。

第三,在调试早期就保留一个"慢速模式"。先把SPI时钟降到几百kHz,确认通信链路完全正常后再逐步提速。很多新手的错误就是上来就按最高速率跑,结果链路不稳定,代码和数据都看不出问题在哪。先慢后快,能帮你把"链路问题"和"逻辑问题"分开定位。

第四,关于片选和时钟线的上拉。CS线建议加上拉电阻,这样主设备初始化前CS不会意外拉低;SCK也可以加上拉,尤其在有干扰的环境里,能够显著减少时钟线上毛刺引发的误采样。实际量产项目中,线束较长时这个细节能省下很多现场排查成本。

7. 写在最后的一点个人体会

做了这么多年嵌入式,我愈发觉得SPI是一种"上限很高、下限也很低"的协议。它简单到你可以用GPIO模拟,又能复杂到细节里藏着无数个坑。很多问题——花屏、Flash写不进、传感器读数跳变——最终都能回溯到SPI的某个时序参数、某根信号线的电平状态或者某次不恰当的DMA配置。

如果你的项目正被SPI问题卡住,我个人建议你按这个顺序排查:先确认物理连接和电平,再用逻辑分析仪抓一份波形,对照从设备数据手册的时序图逐位比对,最后才去怀疑代码逻辑。这样看起来慢,其实是最快的路。SPI不会辜负有耐心的人,把时序图看懂了,它就是你手里最可靠的一把利器。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询