ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

SPI通信协议详解:从时序原理到STM32实战与DMA优化

SPI通信协议详解:从时序原理到STM32实战与DMA优化 经常有朋友问我SPI这么老的协议为什么还在到处用我的回答通常是你看看自己手边的开发板、屏幕、Flash芯片、传感器十个里头至少有八个在跟MCU用SPI说话。SPI通信协议之所以在嵌入式世界里几十年屹立不倒靠的就是四个字简单、够快。它不需要地址概念不需要复杂的应答机制一根时钟线带着数据双向流动主从双方各司其职就能把数据从A点搬到B点。这篇东西主要写给正在学嵌入式、做驱动开发或者被SPI时序折腾到失眠的工程师朋友们我会把协议原理、时序细节、硬件软件片选的坑、DMA优化、以及屏幕和Flash这类典型外设的实战配置一次说透。1. 重新认识SPI它到底是怎么工作的1.1 SPI的核心通信模型与四根线很多人把SPI理解成四根线这个说法对但不够准确。严格来说SPI总线在标准模式下有四根信号线但这四根线在不同场景下分工完全不同搞清楚它们的角色比死记名字重要得多。SCKSerial Clock时钟线由主设备生成并输出是所有数据同步的基准节拍。所有从设备的数据移位都发生在SCK的上升沿或下降沿。MOSIMaster Out Slave In主设备输出从设备输入。主设备往从设备写数据的通道。MISOMaster In Slave Out从设备输出主设备输入。从设备往主设备回传数据的通道。CS/SSChip 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的时序之所以让人头大是因为它没有在协议里规定死上升沿采样还是下降沿采样而是把选择权交给了主设备和从设备双方。这就引出了两个核心参数CPOLClock Polarity时钟极性决定空闲时SCK的电平。CPOL0空闲时SCK为低电平。CPOL1空闲时SCK为高电平。CPHAClock Phase时钟相位决定数据在哪个边沿被采样。CPHA0在第一个边沿即从空闲切换到激活状态的边沿采样。CPHA1在第二个边沿即从激活状态切换回空闲的边沿采样。两两组合就成了四个标准模式模式CPOLCPHA采样边沿数据输出边沿常见外设Mode 000上升沿下降沿W25Q系列Flash、ST7789屏幕Mode 101下降沿上升沿部分传感器Mode 210下降沿上升沿部分SD卡模式Mode 311上升沿下降沿部分音频芯片、ENC28J60总结一句话CPOL决定空闲电平CPHA决定数据是在第一个边沿还是第二个边沿被锁存。这两个参数如果和从设备不匹配通信结果就是第一个字节对了后面全错或者偶尔对偶尔错这类极其诡异的现场。2.2 从时序图看懂一次完整的数据传输画一张在一拍SCK内数据变化与采样关系的简化逻辑假设CPOL0CPHA0Mode 0主设备先把CS拉低表示占用总线。在第一个SCK上升沿到来之前主设备已经把MOSI的数据位准备好即数据在SCK上升沿之前就稳定。SCK上升沿到达时主设备和从设备同时在这个边沿采样数据线上的电平完成一位数据的锁存。随后SCK下降沿到来双方在这个边沿切换下一位数据到线上。如此重复8次或16次、32次取决于外设字长完成一个字节/字的传输。最后主设备把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外设内部的NSSNegated 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驱动屏幕和Flash4.1 CubeMX中的关键参数配置逐项拆解以STM32F407为例使用SPI1驱动一块ST7789屏幕和一块W25Q128 Flash。我打开CubeMX按下面的方式设置参数ModeFull-Duplex Master全双工主模式。如果你只是写屏幕半双工也能跑但我建议直接用全双工因为有些屏幕初始化时需要读寄存器返回值。Hardware NSS SignalDisable。这里我明确关闭硬件片选改用普通GPIO做软件片选原因上面已经说了。Parameter Settings - Clock ParametersPrescaler分频系数对于ST7789屏幕SPI时钟从APB284MHz分频我选8分频得到10.5MHz的SCK屏幕参数手册允许。对于W25Q128官方支持更高时钟但为了跟屏幕共用总线统一跑10.5MHz完全够用稳定优先。CPOLLow即CPOL0。CPHA1 Edge即CPHA0。也就是经典的Mode 0。ST7789和W25Q128都支持Mode 0和Mode 3我统一选Mode 0简单省心。Data Size8 Bits。有些屏幕支持9bit或16bit命令/数据模式但那是给特定显示控制芯片用的驱动SPI本身保持8bit把命令/数据区分交给DC引脚实现。First BitMSB First。几乎所有SPI外设默认都是MSB先发除非外设手册特别说明要用LSB。CRCDisable。标准SPI通信不需要CRC除非你用的从设备支持并强制要求。CubeMX配置完成后生成代码再把CS引脚初始化为GPIO Output默认输出高电平。这样初始化阶段从设备就是未选中状态不会误收数据。4.2 DMA传输如何让CPU从搬运工的岗位上解放当你的屏幕分辨率是240x320每像素用RGB5652字节一帧全屏刷新的数据量是240×320×2153600字节。就算SPI跑10.5MHz如果一字节一字节靠CPU写寄存器发送即使SPI外设自带发送缓冲CPU仍然要频繁处理TXE发送寄存器空中断或轮询标志位大量计算周期被白白耗掉。DMA的基本工作方式我把要发送的数据放在内存缓冲区配置DMA把这块内存的内容自动搬到SPI的发送数据寄存器每搬一次SPI硬件就发一位数据直到整块数据发送完毕DMA触发传输完成中断。在STM32上的配置要点在CubeMX里把SPI1的TX和RX请求都设置成DMA请求。DMA方向内存到外设Memory To Peripheral用于发送。DMA模式Normal模式每次传输完成后自动停止而不是循环搬运同一块数据。Data WidthByte。如果你在代码里用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核心是一个移位寄存器加上边沿检测逻辑。以CPOL0、CPHA0为例SCK的上升沿是采样点。FPGA在SCK上升沿把MOSI上的数据移入移位寄存器同时把移位寄存器中的数据推送到MISO输出。每8个SCK上升沿后一个字节的就位产生一个接收完成脉冲。CS拉高时所有内部状态复位等待下一次传输。代码上用always块边沿检测SCK是常规做法但这里有一个隐患如果SCK是外部异步信号直接用它做时钟驱动寄存器很容易产生亚稳态。稳妥的做法是先把SCK打两拍同步再用同步后的信号做边沿检测或者用系统时钟对SCK采样根据采样序列判断边沿。5.2 跨时钟域和数据对齐一个实战案例我的一个项目里MCU通过SPI向FPGA发送命令和参数FPGA内部跑的是100MHz系统时钟而SPI时钟只有10MHz。两个时钟域之间交换数据如果处理不好就会出现命令第一个字节偶尔错乱这种问题。解决办法是在FPGA内部建立一个小FIFOSPI接收逻辑把收到的字节写入FIFO系统时钟域的控制逻辑从FIFO读取数据并解析。FIFO天然解决了跨时钟域的数据同步问题也起到了缓冲作用。另一个容易踩的点是半字节错位。当MCU一次只发8位数据而FPGA内部按16位字来处理时如果不注意接收完成的时序很可能把前一个字节的低4位和后一个字节的高4位拼成一个错误数据。解决方法是在接收完成脉冲到来时按字节写入FIFO解析时按固定协议比如第一字节是命令第二字节是高字节第三字节是低字节拼接绝不能依赖时序猜边界。FPGA作为从机时MISO数据的驱动时刻也值得注意。在CPOL0、CPHA0模式下从机应该在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不会辜负有耐心的人把时序图看懂了它就是你手里最可靠的一把利器。
返回列表