SPI通信协议详解:从四线时序到STM32实战与DMA优化
发布时间:2026/9/8 16:07:14来源:尧图网络
搞嵌入式这么多年要说哪几个通信接口用得最多SPI绝对排在前三。不管你是玩STM32、ESP32还是刚入门单片机准备点亮一块屏幕、挂一个Flash存储芯片SPI几乎是绕不开的。这块协议本身不复杂但实际调起来时钟极性、片选时序、DMA配合哪个环节没搞对都能卡你半天。这篇就把SPI从原理到实操完整捋一遍全是实际项目里验证过的经验。先说清楚这篇文章适合谁刚接触单片机通信、准备用SPI驱动外设的初学者已经在用SPI但经常遇到时序和片选问题、想搞明白底层原理的进阶开发者以及做FPGA、Linux驱动需要快速回顾SPI细节的同行。无论哪种情况理解SPI背后的设计逻辑比死记硬背时序图更有用。1. 先搞清SPI的四根线和主从关系SPI全称Serial Peripheral Interface串行外设接口由Motorola在80年代提出。它在嵌入式领域能活这么多年核心原因就一个字简单。没有复杂的地址帧、应答帧就是纯粹的“谁提供时钟谁说了算”的主从结构。整个通信只依赖四根线任何MCU上的SPI外设都是围绕这四根信号线展开的。1.1 四根线各自到底干什么SCLKSerial Clock串行时钟由主机产生并驱动整个通信节拍。它决定了数据传输速率比如1MHz的SCLK就是每秒传输1M个bit。MOSIMaster Out Slave In主机输出、从机输入。主机发送数据到从机走这根线。MISOMaster In Slave Out从机输出、主机输入。从机回传数据走这根线。CS/SSChip Select / Slave Select片选信号低电平有效。主机拉低某个从机的CS等于告诉这个从机“准备接收数据”。其中CS是最容易被忽略但又最关键的信号。SPI总线上可以挂多个从机它们共享SCLK、MOSI、MISO三根线唯独CS必须“一线一从机”。主机想和哪个设备通信就把哪个设备的CS拉低。这样设计的好处是省引脚坏处是从机一多、CS引脚数量就上去了。1.2 主从模式和数据流向的底层逻辑SPI是同步全双工通信意思是主机给从机发数据的同时从机也能给主机回数据。你可能会问这不就是两根独立的数据线吗有什么稀奇的关键在于SPI的收发是同一个时钟驱动的每一个SCLK边沿MOSI线上发送一位数据同时MISO线上采样一位数据。主机发出1个字节同时收到1个字节两者是绑定发生的。举个例子读Flash芯片的JEDEC ID指令第一步发送0x9F指令字节这一步主机其实“顺带”收到了从机返回的1个字节第二步到第四步继续发3个空字节从机才把真正的3字节ID数据返回来。所以SPI驱动里常见的“读操作先写指令再写空字节收数据”本质上就是因为收发同拍、必须靠写空字节制造时钟来实现读。SPI的结构决定了它没有I2C那种应答机制ACK/NACK主机也不关心从机是否真的“听懂”了指令。从机是否就绪、数据是否有效完全依赖协议层自行处理。这对设计者的要求反而更高你需要熟读外设手册搞清楚“要发几条指令”“主时钟要跑多久从机才准备好”。很多新手第一次调SPI读数据读出一堆0xFF大概率就是没给从机足够的响应时间。2. 时钟极性和相位四种模式别傻傻分不清SPI最让人头大的就是四种工作模式SPI Mode 0到Mode 3。很多MCU的SPI外设初始化代码里都有CPOL和CPHA两个参数搞不明白这两个参数配置就全靠猜。这里把它们的本质讲透。2.1 CPOL和CPHA的四种组合CPOLClock Polarity时钟极性决定空闲状态下SCLK是高电平还是低电平CPOL 0空闲时SCLK为低电平。CPOL 1空闲时SCLK为高电平。CPHAClock Phase时钟相位决定数据在哪个边沿被采样CPHA 0第一个边沿采样数据也就是“前沿采样”。CPHA 1第二个边沿采样数据也就是“后沿采样”。组合起来就是下表模式CPOLCPHA空闲电平数据采样边沿Mode 000低第一个边沿上升沿Mode 101低第二个边沿下降沿Mode 210高第一个边沿下降沿Mode 311高第二个边沿上升沿为了记起来方便业内有个经验口诀Mode 0和Mode 3用得多Mode 0对应CPOL0/CPHA0Mode 3对应CPOL1/CPHA1。倒不是因为这两个模式“更高级”而是很多常见外设比如W25Q系列Flash、ST7789屏幕、MAX31865等默认就支持这两种。剩下两个模式不是没人用只是相对来说少。2.2 实际项目里怎么判断该用哪个模式判断方法其实很简单看从机数据手册里的时序图。手册会明确标出SCLK空闲电平、数据建立时间、采样边沿。比如看到图里SCLK默认低电平、数据在上升沿被锁存那就是Mode 0看到SCLK默认高电平、数据在上升沿被锁存那就是Mode 3。假如手头没有手册、只有一颗未知芯片也有笨办法先用逻辑分析仪抓取示波器波形把SCLK空闲电平、数据变化边沿和SCLK边沿对照基本一眼就确定模式。没有逻辑分析仪的话就逐个模式试哪个模式能读出正常数据就用哪个。我在调试某些国产传感器时就这么“暴力”试过三个模式读出来都是垃圾数据唯独Mode 1是正常的。所以不要迷信网上默认Mode 0的说法一切以实际芯片手册为准。提醒一点SPI模式必须主机和从机完全一致。MCU配置成Mode 0、从机却是Mode 2轻则数据错位重则通信完全失败。而且这种问题不报错、不中断只输出一堆看似随机但实际有规律的错误数据排查起来非常费劲。3. CubeMX配置SPI从参数到实操一次说清STM32用户绝大多数都是先开CubeMX生成初始化代码再填业务逻辑。CubeMX里的SPI配置页面看着复杂其实每一项都对应于前面讲的原理。把参数和硬件机制对应起来配置就是水到渠成的事。3.1 参数逐项解释Prescaler预分频器决定SPI时钟频率。例如STM32F103的APB2总线时钟为72MHzSPI1挂在这个总线上预分频为16时SPI时钟就是72/164.5MHz。具体分频多少取决于从机支持的最高频率。Data Size数据大小选8位或16位。绝大多数SPI外设Flash、SD卡、屏幕控制器默认8位。改成16位之前务必确认从机支持。First BitMSB First还是LSB First。一般选MSB First最高位先发因为大多数SPI协议都是从高字节开始定义指令位顺序。CPOL和CPHA结合上文选择正确的模式。NSS片选模式这个是软件片选和硬件片选的分岔路后面单独细说。RX Only / Transmit Only / Full-Duplex收发模式选择。读Flash、读传感器一般用Full-Duplex只写不读的LCD、LED驱动芯片可以选Transmit Only省掉MISO引脚。3.2 代码层面如何收发数据CubeMX生成的HAL库代码最常用的收发接口就几个// 发送接收全双工 HAL_SPI_TransmitReceive(hspi1, tx_buffer, rx_buffer, size, timeout); // 只发送 HAL_SPI_Transmit(hspi1, tx_buffer, size, timeout); // 只接收配合手动控制CS HAL_SPI_Receive(hspi1, rx_buffer, size, timeout);这里有个很多新手都会踩的坑HAL_SPI_Receive并不是“只接收不发送”它内部照样会发送垃圾数据。因为SPI收发同拍主机不输出时钟就没有办法让数据“流”过来。所以HAL库内部的HAL_SPI_Receive调用的是同一个全双工底层函数发送缓冲区填的是0xFF之类的默认值。要读芯片内部数据最干净的方式是全部用HAL_SPI_TransmitReceive发送的字节根据协议来该发指令发指令该发空字节发空字节。再看一个具体例子读W25Q256 Flash的Manufacturer IDuint8_t cmd 0x90; // 读 Manufacturer/Device ID 指令 uint8_t dummy 0x00; // 空字节 uint8_t rx_data[2] {0}; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); // 拉低CS选中从机 HAL_SPI_Transmit(hspi1, cmd, 1, 100); // 发送指令 HAL_SPI_Transmit(hspi1, dummy, 1, 100); // 发送地址高字节多为0x00 HAL_SPI_Transmit(hspi1, dummy, 1, 100); // 发送地址低字节多为0x00 HAL_SPI_Receive(hspi1, rx_data, 2, 100); // 接收2字节ID HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); // 拉高CS释放从机这个代码没问题但请你同时注意几点每次通信结束必须拉高CS否则Flash认为总线还占着后续指令全部忽略如果从机对时序要求严格CS拉低和第一个SCLK之间需要一定的建立时间硬件上可能没问题但有些严格的从机对CS建立时间有明确要求需要查询数据手册确认没有低于最小值。4. 硬件片选和软件片选怎么选才是最优解片选信号是SPI最容易出问题的地方因为它有两种控制方式各有利弊硬件片选NSS Hardware和软件片选NSS Software。这个概念经常在CubeMX里直接出现对新手来说特别容易纠结。4.1 两类片选的工作机制硬件片选由SPI外设硬件控制NSS引脚。配置成硬件输出时只要调用HAL_SPI_Transmit外设会自动拉低NSS通信结束后自动拉高。听上去很省心但实际里坑不少在多从机总线中硬件NSS可能会产生多余的片选脉冲有些从机受不了这种毛刺而且硬件控制的是固定引脚想换GPIO就要改外设配置灵活性差很多。软件片选就是GPIO软件控制在通信开始前手动拉低CS通信结束后手动拉高CS。这相当于把片选的控制权完全交到你自己手里灵活度最大。项目里95%以上的情况我推荐用软件片选。4.2 什么时候用硬件片选更稳硬件片选适合单从机、从机对CS时序要求特别高的场景。比如某些高速SD卡在SPI模式下如果CS拉高时机不对或者拉低期间有抖动可能直接进入错误状态。这时候用硬件NSS自动控制可以减少软件时序误差。此外在SPI DMA配合下硬件NSS能避免软件中途干预导致CS时序不准确因为DMA传输的结束中断可能存在延迟如果等中断里再软件拉高CS从机早就因为空闲超时而误判了。但即使使用硬件片选也建议在外部IO上增加微秒级的延时确保CS建立时间或者干脆在硬件设计上留出软件控制的冗余方案。很多工程师最终都趋向于软件片选因为绝大多数从机标准协议并不那么挑剔CS抖动只要保证正常的时序范围即可。我的习惯做法一旦发现通信偶发异常第一步不是动SCLK频率而是检查CS时序。用逻辑分析仪抓下拉低到第一个时钟的间隔以及最后一个时钟到拉高的间隔很多时候问题就出在这里。5. 进阶玩法DMA、中断和多从机共享SPI的进阶用法主要集中在性能和架构层面。数据量小、对实时性要求不高的时候阻塞式收发足够。一旦遇到屏幕刷新、Flash读写大量数据、SD卡日志这类场景DMA和中断就派上用场了。5.1 SPI DMA到底快在哪阻塞收发在收发期间CPU全程参与比如发1024字节到FlashCPU就干等着SPI慢慢挪数据。而DMA模式下CPU只需配置好DMA通道、告诉它“数据从哪来、要去哪、传多少”然后SPI硬件就接管搬运工作数据搬完后DMA再触发中断通知CPU。CPU空出来的时间可以去跑算法、刷UI、处理其他外设。以STM32为例在CubeMX里配置SPI DMA很简单勾选SPI的TX和RX两个DMA请求选择Normal模式即可。但实践中有几个容易踩的坑在HAL库中使用HAL_SPI_Transmit_DMA之后必须等传输完成后再进行下一步操作因为在DMA传输过程中再次操作SPI寄存器可能导致总线冲突。DMA中断优先级要合理设置。如果中断优先级过低高频通信时可能丢失完成标志数据对不上。使用FreeRTOS时DMA中断回调里不要直接调用osDelay之类的阻塞函数否则系统调度会被打乱。读Flash多字节数据是DMA最常见的场景。比如读1MB数据阻塞式可能要几百毫秒DMA模式只需要启动传输完成后中断通知时间开销能大幅降低。实际测试中STM32F4在40MHz SPI时钟下DMA读W25Q256的读取速度能跑到约4MB/s而阻塞式一般只有2MB/s左右差距非常明显。5.2 ESP32屏幕和SD卡共享SPI总线的处理思路热搜词里出现了“ESP32屏幕与SD卡共享SPI哪个好”这是个实战性很强的问题。很多ESP32项目要用TFT屏幕显示图片同时要把抓拍的数据存到SD卡两者都接SPI共享总线几乎是必然选择。共享SPI总线的要点是分时复用不能同时进行两个传输。外设SCLKMOSIMISOCSTFT屏幕ST7789共用共用通常不用/共用GPIO 15SD卡SPI模式共用共用共用GPIO 4ESP32上挂两个SPI从机软件上唯一要杜绝的是“交叉传输”。在切换外设时最好先等当前DMA/SPI传输完全停止再操作另一个外设的CS。实际项目中我见过因为SD卡写入还没完成就拉低屏幕CS导致SD卡写入失败、文件系统损坏的情况数据直接报废。至于“哪个好”共享SPI肯定是更省引脚、更省线的方案缺点是吞吐率受限于总线频率而且两个外设不能并行刷新。如果项目对显示帧率和SD卡写入速度同时要求很高更稳妥的是给屏幕和SD卡分配独立的SPI总线ESP32通常有SPI2和SPI3两组牺牲引脚换性能。凡是追求稳定性的产品级设计我都建议“能分就分”只有在引脚极度紧张时才考虑共享。另外提一嘴SD卡SPI模式有初始化时序陷阱上电后需要至少74个时钟周期的延时且片选应保持高电平如果没满足这个条件SD卡会拒绝进入SPI模式。很多共享总线项目里SD卡初始化失败就是因为屏幕或Flash先干活占用了SCLK导致SD卡上电信号没吃够足够的时钟脉冲。6. 我踩过的坑与排查方法SPI接口本身不难难的是那些“看起来一切正常但就是不出数据”的玄学问题。我把自己和身边朋友踩过的高频问题整理成一个速查表这些东西在数据手册里未必写得那么直白但实践中太有用了。6.1 典型问题和排查路径现象可能原因排查方法读出来的数据全是0xFF从机没被正确选中、MISO线没接好、从机供电问题检查CS是否正常拉低万用表量MISO通断确认从机电源电压读出来的数据和预期不符且规律性偏移SPI模式不匹配逻辑分析仪抓时序对照手册确认CPOL/CPHA有时候能通有时候不通时钟频率太高从机跟不上降低SPI预分频比如从8MHz降到1MHz试一下数据第一个字节丢失从机在收到CS下降沿后准备时间不足在CS拉低和SCLK启动之间插入小延时如1~10usDMA模式数据只更新一半DMA传输被中断或配置成Circular模式确认DMA工作在Normal模式检查传输完成回调是否真正触发屏幕花屏但初始化正常MISO/MOSI接反了或者尝试了共享总线冲突仔细核对硬件接线断开SD卡再刷新屏幕验证最让我印象深刻的是一次调试国产Flash芯片初始化完全正常读厂商ID也对但连续读数据时前几百字节对、后续全是错的。排查到最后发现是SPI时钟频率6MHz时在布线上损耗太大下降沿不够陡从机采样出错。换成2MHz一切正常。这个案例说明SPI频率不是越高越好高频状态下链路阻抗、走线长度、上下拉电阻都会影响信号完整性。PCB设计时SCLK走线尽量短而直避免过孔和直角必要时串33Ω电阻减小振铃。6.2 几个随时能用的小技巧调试SPI别急着看协议先准备逻辑分析仪或示波器。花几百块买一个8通道的24MHz逻辑分析仪足以应对绝大多数SPI调试。抓完时序图模式对不对、CS有没有拉低、字节边界清不清晰一目了然。用SPI直接驱动ST7789这类屏幕时刷新率不是越高越好。SPI时钟频率再高还要受屏幕控制器的扫描时间、像素时钟限制。很多情况下跑到40~60MHz已经是极限继续超频只会出现花屏。用DMA加帧缓冲比单纯超频靠谱得多。自己写SPI驱动初始化寄存器务必养成“先写配置再读回校验”的习惯。很多外设都有状态寄存器或ID寄存器上电后先读一遍确认通信通路正常再继续初始化能省下大量调试时间。碰到SPI与I2C的区别问题记住一句话SPI靠CS选人、全双工、速度快、无应答I2C靠地址选人、半双工、速度相对慢、有应答。选型时追求速率和全双工选SPI追求引脚少、多从机总线简洁选I2C。7. 关于SPI的FPGA和Verilog实现再多说一嘴SPI的热搜词里总有“SPI Verilog”“FPGA SPI”这类关键词。FPGA做SPI从机或主机本质和单片机没有区别核心就是按照时序图产生和采样信号。对FPGA工程师来说最大的难点不是写代码而是如何保证跨时钟域的稳定性。单片机上有现成的SPI外设而FPGA上所有时序逻辑都得手工设计。以SPI从机为例核心模块无非是检测SCLK边沿、在正确边沿锁存MOSI数据、在正确时刻更新MISO输出、处理CS信号复位内部逻辑。一个基础框架思路是状态机检测CS下降沿进入“接收模式”CS上升沿回到“空闲模式”。移位寄存器每次SCLK有效沿把MOSI输入移入寄存器如果需要回发数据把移位寄存器内容在另一沿输出到MISO。位计数统计接收位数通常是8位一个字节凑满一个字节后产生中断或置位标志通知上层逻辑取数据。有一点务必注意FPGA的时钟和外部SCLK往往是异步的直接用外部SCLK驱动内部逻辑会产生亚稳态风险。常见的做法是用系统时钟对SCLK打两拍同步再做边沿检测或者用内部高速时钟对数据进行oversampling采样。如果SCLK本身比系统时钟还快那就得老老实实用SCLK域逻辑配合双口RAM或异步FIFO做跨时钟域数据交换。网上有很多现成的SPI slave Verilog代码但大多数只适合教学演示真正用于工业环境还是得结合CDC跨时钟域设计规范重写。FPGA还有一个热点应用是“基于SPI接口的远程升级”和“SPI Flash存储”本质上就是FPGA通过SPI访问外部Flash。这里FPGA一般扮演主机主动读取Flash中的启动镜像。要注意的除了时序外还有一个启动控制策略FPGA上电后要先等待电源稳定再操作Flash如果Flash芯片支持Quad SPI还可以通过配置寄存器切换成四线模式提高读取速度。但四线模式的IO方向控制和时序比标准SPI复杂不少建议先从标准SPI跑通再考虑优化。最后分享一个个人习惯所有涉及SPI的工程我都会在硬件设计阶段预留测试点把SCLK、MOSI、MISO、CS四根关键信号引出来。这样做的好处是后续不管用示波器还是逻辑分析仪都能快速接入、快速定位。尤其是产品量产之后现场反馈通信故障时测试点能省去大量拆机排查时间。SPI这个东西只要你把原理吃透、时序摸清、工具配齐基本就是一次通关的事。
网站建设高端定制企业官网