嵌入式SPI通信实战:从时序原理到调试技巧

发布时间:2026/9/6 13:27:10
嵌入式SPI通信实战:从时序原理到调试技巧 1. 从一根时钟线说起SPI通信的整体设计思路做嵌入式这些年调试过液晶屏、Flash存储、传感器、SD卡几乎每个项目都绕不开SPI。SPI全称Serial Peripheral Interface串行外设接口是Motorola最早提出的一套同步串行通信标准。它解决的痛点很直接芯片之间要高速、大批量地交换数据但引脚资源又极其有限怎么用尽量少的线把数据可靠地搬过去。如果你第一次接触SPI可以把它想象成一条流水线。主设备是工头从设备是工人SCLK是节拍器MOSI是工头递给工人的料MISO是工人反馈给工头的成品CS片选线则决定了今天到底哪个工人该上岗。工头一敲节拍器数据就按位挪动一位高低电平轮流翻转数据就一位一位传出去了。整个通信过程完全同步不发时钟就没有数据传输这一点和UART那种异步靠波特率对齐的方式有本质区别。SPI最常见的形态是四根线SCLK串行时钟由主设备产生决定传输速率MOSI主设备输出、从设备输入主发从收MISO主设备输入、从设备输出从发主收CS/SS片选信号低电平有效选中哪个从设备就拉低哪个因为MOSI和MISO独立SPI天然支持全双工也就是主设备发送数据的同时能收到从设备回的数据。这一条在实际项目中非常关键比如读写W25Q256这类SPI Flash主机在发读命令和地址之后正是靠继续打时钟把Flash吐出来的数据从MISO上一路读回来。如果换成I2C半双工模式下同一根数据线又要发又要收效率和时序复杂度都会上升。SPI适合谁来用基本是这几类场景数据量中等偏大、要求速率高、从设备数量有限且引脚可接受。典型的是显示屏ST7789这类、Flash、ADC/DAC、SD卡、传感器如DHT11虽然它自己走单总线但很多温湿度传感器也有SPI版本以及FPGA和MCU之间的高速数据交换。STM32、ESP32、国产GD32、复旦微等主流MCU都带硬件SPI外设四根线就能跑几十MHz的时钟比I2C那几百K到几M的速率高一个量级。但SPI也有它的“死穴”没有应答机制。数据发出去了从设备到底收到没有、处理对没有主机是不知情的。想要确认只能靠从设备用MISO回读状态寄存器或者干脆用额外的GPIO做握手信号。这是SPI协议设计上的一大特性也是实际调试中无数坑的源头。网上经常有人把“SPI通信”和“I2C通信”放在一起比较其实两者的定位完全不同。I2C只需要两根线SCL、SDA带设备地址可以挂一堆从设备但速率慢半双工每传一个字节都要等ACKSPI四根线无地址概念靠片选区分设备全双工速率快但没有应答。后面我会专门用一节把这两个协议摆在同一张桌子上对比让你选型的时候心里有数。2. 时序和四种模式SPI能不能通就看这一关2.1 CPOL和CPHA一对决定采样时刻的时钟参数SPI被问得最多的一个问题是为什么我的SPI有时候能通有时候乱码换个从设备又不通答案十有八九出在时钟极性CPOL和时钟相位CPHA上。CPOL决定的是SCLK空闲时的电平CPOL0表示空闲时时钟线为低电平CPOL1表示空闲时为高电平。CPHA决定的是数据采样发生在时钟沿的哪个位置CPHA0表示在第一个边沿采样CPHA1表示在第二个边沿采样。把这两个参数组合起来就得到SPI的四种工作模式表格直接给出模式CPOLCPHA空闲时钟电平数据采样边沿常见应用Mode 000低第一个边沿上升沿大多数Flash、SD卡、部分传感器Mode 101低第二个边沿下降沿部分ADC、音频芯片Mode 210高第一个边沿下降沿少数外设如某些LCD驱动Mode 311高第二个边沿上升沿W25Q系列Flash、SD卡等实际项目里Mode 0 和 Mode 3 用得最多。Mode 0是绝大多数SPI从设备的默认模式很多国产传感器、Flash都默认支持它Mode 3的时序正好和Mode 0对称不少SD卡和W25Q系列Flash同时支持这两种模式。如果你不确定从设备支持哪种模式最稳妥的办法是翻数据手册里的时序图——那里通常会画出SCLK空闲电平、采样沿位置和数据的建立保持时间对着图选模式不要凭感觉猜。理解CPOL和CPHA还有一个关键点主设备和从设备的模式必须一致。否则主设备在上升沿发送数据从设备却偏偏在下降沿去采样采到的可能是上一个位、当前位或者空信号结果就是数据右移一位、丢位或者整个乱掉。这种问题在逻辑分析仪上非常直观MOSI上的波形明明是对的但MISO回来的就是不对多半就是采样沿错位。2.2 数据怎么“挪”进去的移位寄存器视角SPI传输的本质是主从设备各自有一个移位寄存器通过MOSI和MISO两根线把两个移位寄存器串联成一个环。每来一个时钟沿主设备的移位寄存器最高位移出去从设备的最低位移进来同时从设备的最高位移出去主设备的最低位移进来。这样经过8个时钟主设备发送的一个字节就到了从设备从设备的一个字节也到了主设备。这也是为什么SPI读操作通常要“先写后读”你想从Flash里读数据必须先在MOSI上把读命令和地址发出去Flash收到命令后才知道接下来要在MISO上回数据。而主机在读数据的同时MOSI上依然在打时钟只是多数情况下发送的是0x00这种无关字节。不理解这一层很容易写出“读数据却忘记发时钟”的bug。用生活类比解释SPI就像两人隔着墙用两根管子对传小纸条一根管子你传给我一根管子我传给你每敲一次钟时钟沿就传一个比特。两边节奏完全同步谁先谁后由钟声决定这就是“同步串行通信”的含义。2.3 内部移位原理与全双工数据交换刚才提到主从移位寄存器串成一个环这里再补充一点SPI的数据发送和接收是同时发生的所以主机在MISO上没有读到预期数据时先别急着怀疑硬件先确认自己到底发了多少个时钟、命令和地址对不对、从设备的CS有没有正确拉低。很多时候不是SPI坏了而是协议没对齐。举个例子W25Q256的读数据命令是0x03后面跟3字节地址然后才是连续的数据。主机如果只发了一个字节的命令就停下来那从设备自然一个数据字节都不会给你。同理如果命令发对了但地址字节的位序反了MSB first还是LSB first搞反读回来的也是乱七八糟的东西。3. 硬件片选与软件片选谁才是可靠的选择3.1 硬件片选STM32 NSS引脚的自动管理讲到片选这就涉及一个很多新手纠结的问题到底用硬件片选还是软件片选硬件片选是指MCU的SPI外设自带NSS引脚在SPI传输开始时由硬件自动拉低传输结束后自动拉高全程不需要CPU干预。看起来非常省心好像“硬件帮你把CS管了”实际用起来才发现坑不少。第一个坑是NSS引脚模式。STM32的NSS可以配置为硬件输出模式NSS Output由SPI外设控制也可以配置为硬件输入模式NSS Input此时片选由外部主设备拉低通常用于把STM32做成从设备的场景还有一种是软件模式NSS Soft片选信号完全由软件控制硬件NSS引脚释放出来当GPIO用。如果你做主设备希望硬件自动控制片选要正确配置NSS Output模式同时注意SPI初始化时序。很多人在CubeMX里勾选了“Hardware NSS Output Signal”但主函数里没有先调用HAL_SPI_EnableWriteProtection之类的流程或者片选信号在SPI初始化前就乱跳导致从设备被误选中。第二个坑是多从设备场景。硬件片选只能在初始化时绑定一个NSS引脚。如果你要挂三个SPI从设备怎么办要么切成软件片选用三个GPIO分别控制三个CS要么做外部逻辑选通。硬件NSS的“自动”只对单从设备友好多从设备时反而碍手碍脚。第三个坑是时序上的“自动拉高太早”。有些MCU的硬件NSS在SPI传输结束会立即拉高但某些从设备比如部分LCD驱动和Flash要求CS在高电平前必须保持一段时间的低电平或者要求CS在高电平之后保持一段时间的空闲。硬件NSS不会替你考虑这些建立保持时间尤其在高频传输时可能出现CS脉冲过窄导致从设备状态机复位异常。基于这些经验我的做法是大多数工程直接使用软件片选用一个普通的GPIO控制CS在每次传输前手动拉低传输完成后手动拉高。虽然看起来“多做了几步”但换来的是绝对可控的时序和灵活度。硬件NSS更多用在从设备模式或者极高速且严格要求自动片选的场合。3.2 软件片选的实现与注意事项软件片选的操作很简单核心就三步传输前指定CS引脚拉低执行SPI收发传输结束后拉高。用STM32 HAL库举例大概是这个流程#define CS_LOW() HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, GPIO_PIN_RESET) #define CS_HIGH() HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, GPIO_PIN_SET) CS_LOW(); HAL_SPI_Transmit(hspi1, tx_buf, len, 100); CS_HIGH();看起来简单但有几个细节容易被忽略。第一CS拉低之后、发送数据之前最好加一个极短的延时或者至少不要在同一条语句里“拉低CS后立刻发数据”。某些从设备的CS有效到接收第一个时钟之间有一段时间要求太快的时钟可能让从设备来不及准备。我实测过部分传感器CS拉低后立即给时钟也能工作但也有芯片会出现偶发错位稳妥起见加个几百纳秒到几微秒的延时并不亏尤其当SPI时钟本身在10MHz以下时这点延时完全不影响吞吐。第二传输结束后不要立刻拉高CS。多字节传输还好单字节传输尤其要注意最后一个字节的最后一个时钟沿之后从设备可能还需要一点时间锁存数据。很多情况下从设备本身不要求这个延时但也有器件会要求“CS拉高前数据线保持有效一段时间”否则最后一字节数据可能丢失。建议在CS_HIGH()前加一个小的延时比如1微秒左右。第三也是最隐蔽的坑不要让CS在多字节传输过程中被置高。如果你的SPI收发是一次性发一帧数据例如命令地址数据连续发出那CS需要在整帧期间保持低电平不能发完一个字节就拉高。这也是HAL_SPI_Transmit和HAL_SPI_TransmitReceive这类接口的优势——它可以一次传输任意长度期间CS始终有效。如果你分多次调用每次CS都重新拉低拉高很多从设备就会把每次传输当作独立命令导致协议错乱。这一点在操作W25Q256这种需要连续地址读的Flash时尤其致命。3.3 硬件片选和软件片选怎么选对比维度硬件片选软件片选CPU占用低硬件自动控制高一点但通常可忽略时序灵活性差CS拉高时机固定好可任意控制多从设备支持差需外部逻辑或切换好一个GPIO一个CS配置复杂度中需理解NSS模式低一个GPIO搞定出错风险高容易踩时序坑低时序完全自己控制实际项目中我的原则是单从设备、对时序要求不极端、希望代码简单时可以考虑硬件片选多从设备、协议复杂如需要中途把CS拉高再拉低、或者做过一次硬件片选踩坑之后果断软件片选。不要迷信“硬件”两个字软件控制并不丢人反而可控性更强。4. 实操从CubeMX配置到点亮一块ST7789屏幕4.1 CubeMX里SPI配置的几个关键参数现在MCU开发基本都靠CubeMX这类图形化配置工具。很多新手对着CubeMX的SPI配置界面发懵因为选项实在太多。其实真正需要关心的参数就那几个一个一个说清楚。以STM32F4系列为例打开SPI1的配置页你会看到参数选项说明ModeTransmit Only / Receive Only / Transmit Receive根据外设选Flash选前两个屏幕一般选Transmit OnlyHardware NSS SignalDisable / NSS Output / NSS Input默认选Disable用软件片选Data Size8 Bits / 16 Bits大多数器件是8位个别AD/DA用16位First BitMSB First / LSB First大多数协议要求MSB First个别器件例外Prescaler2/4/8/16/32/64/128/256决定SPI时钟频率CPOL / CPHA0/1组合见上文四种模式说明Prescaler分频系数是很多人忽视的一环。SPI时钟频率 外设时钟 / Prescaler。比如STM32F407的APB2时钟是84MHz选Prescaler4得到21MHz的SPI时钟选8得到10.5MHz。这里有个关键点SPI时钟频率的上限由从设备决定不是越高越好。我见过有人用32MHz的SPI时钟去读一个最大支持20MHz的Flash结果数据乱飞改成16MHz立刻稳定。CubeMX里还有个容易踩坑的选项叫“NSSP”NSS Pulse Detection这个选项只在某些系列上有用于检测CS脉冲。如果不清楚用途不要随意开启默认值即可。4.2 点亮ST7789屏幕全流程ST7789是一个很常见的SPI接口LCD驱动芯片用它当例子能把SPI配置的几个关键知识点串起来。屏幕分辨率常见240×240或者240×320很多小尺寸圆形屏用的就是它。这类屏通常是SPI透传驱动芯片本身不要求读回数据所以SPI配置可以选“Transmit Only”。第一步CubeMX配置SPI模式选Transmit Receive用Transmit Only也行但Transmit Receive更通用Hardware NSS Signal选DisableData Size选8 BitsFirst Bit选MSB FirstPrescaler根据APB2频率选让SPI时钟落在1MHz到20MHz之间CPOL0、CPHA0也就是Mode 0ST7789支持Mode 0和Mode 3片选用普通GPIO推挽输出默认高电平第二步初始化序列。ST7789上电后需要发一串初始化命令包括睡眠退出0x11、颜色格式设置0x3A、显示开0x29等。这块代码网上到处都是重点不是命令内容而是发送节奏有些命令之间需要延时有些命令必须连续发送不能随意穿插CS拉高拉低。我一般把初始化命令放在一个大数组里用同一个函数连续发送中间不释放CS直到整段命令结束。第三步数据发送。屏幕显示是连续往GRAM里写像素数据一次可能几K到几十K字节。这里有两个选择用HAL_SPI_Transmit逐次发送或者用SPI DMA发送。小屏幕、画面不频繁刷新时普通发送够用大屏或动画场景一定要用DMA否则CPU会被刷屏占满。给一个最简单的DMA发送流程参考CS_LOW(); HAL_SPI_Transmit_DMA(hspi1, (uint8_t *)frame_buffer, size); // 等传输完成后在回调里拉高CS这里有个细节DMA传输是异步的HAL_SPI_Transmit_DMA函数返回时传输并没有完成如果你立刻拉高CS或者修改buffer内容就会出问题。正确做法是在传输完成回调里做CS拉高操作或者使用信号量等待传输结束标志。4.3 实操中的其他注意点显示屏这类外设还有个特殊点它的数据/命令选择往往靠DC引脚D/CX而不是靠CLK或者CS。DC为高表示数据DC为低表示命令。这意味着每一次SPI发送前都要根据协议设置DC引脚的电平。很多人第一次用这类屏忘记在发送命令和数据之间切换DC结果屏幕完全黑屏或者乱码排查半天才发现是DC引脚没拉对。另外SPI时钟线SCLK和DC、CS之间如果有较长走线高速传输时可能出现波形振铃导致误采样。解决办法是降低SPI时钟频率或者在CLK线上串联一个22Ω到33Ω的电阻。我遇到过一块屏幕在20MHz SPI下不稳定降到16MHz就一切正常不是说20MHz一定不行而是高速率对走线、上拉电阻、电源稳定性都更敏感。5. 实战中绕不开的坑SPI调试常见问题与排查技巧5.1 数据全对但就是不通的十大排查方向SPI调试有一个特点信号看起来都是对的但就是死活不通。这种问题最磨人。根据我的经验把排查步骤梳理成一个清单按顺序检查能解决九成以上问题。引脚复用对不对——很多MCU的同一引脚有多个复用功能SPI的SCLK、MOSI、MISO是否映射到了正确的复用功能上。这点在SPI2、SPI3这些非默认引脚上尤其容易错。时钟极性相位的匹配——CPOL和CPHA是否和从设备一致用逻辑分析仪对比时序图。片选极性——多数从设备CS低有效但个别设备是高有效搞反了就是全不通。数据格式——MSB First还是LSB First多数时候是MSB First但总有例外。SPI时钟频率——是否超过从设备最大支持频率降低分频试试。MISO/MOSI接反——总有人把主设备的MOSI接到从设备的MOSI上那是完全错误的必须交叉连接主MOSI接从MOSI同名端相接注意这里是接从设备的MOSI输入脚。电源和地——从设备没供电或者地没共好SPI不可能通。上拉电阻——MISO线在从设备三态输出时需要上拉否则空闲电平不确定可能误触发。软件片选的时序——CS是否在整帧传输期间一直保持低电平有没有中途被拉高。从设备的复位——很多外设芯片有复位引脚上电后需要正确延时和复位否则芯片处于未知状态。在排查这十项时我强烈建议先上逻辑分析仪。不需要多高级十几块钱的USB逻辑分析仪就够用配合开源软件能看到SCLK、MOSI、MISO、CS的实际波形几乎能立刻判断出是哪一类问题。没有逻辑分析仪时用示波器也可以但要抓到多通道时序会很麻烦。5.2 极路由那类设备挂SPI Flash编程器固件为什么那么麻烦搜索热词里出现了“极路由4增强版 spi编程器固件”这样的词。这类设备调试的痛点其实很有代表性无拆机引导模式、没系统、Flash内容坏了必须用SPI编程器直接在Flash芯片上读写固件。SPI编程器的本质就是一个高度精简的SPI主设备它直接把Flash当成一个从设备用软件模拟或专用硬件发出读、写、擦除命令。这类操作最容易出的问题有几种芯片型号识别错误导致命令不匹配供电电压不对把芯片烧了或者读不出数据焊接或者夹具接触不良导致SPI信号不稳定读取固件校验不通过。如果你要玩这一类恢复建议先备份原固件用编程器读出的数据一定是完整的、校验过的再考虑修改写回。放个血泪教训给路由器Flash备份时编程器识别到芯片型号后最好先用“读出”功能完整读一遍至少读两次校验两次内容一致再动手动焊。哪怕只是改一个字节的MAC地址也要确认原固件备份OK。因为一旦写入错误路由可能彻底变砖而且没有软件手段能救回来。5.3 SPI乱码、丢字节、偶发失败的处理方法SPI乱码和偶发失败比完全不通更让人头疼。我自己遇到过几次总结下来主要是这几个原因。第一电源纹波大。SPI高速翻转时电流变化剧烈如果供电走线细、去耦电容不够容易在时钟沿产生毛刺导致从设备误采样。排查方法是给主控和外设分别加100nF和10μF去耦电容并尽量缩短电源走线。第二地线回路差。SPI是同步通信但地电位不一致时信号的电平判定就会出错。尤其是屏幕、传感器和主控板分开供电时一定要把地连好最好单点共地。第三中断干扰。如果在SPI传输过程中有高优先级中断频繁插入可能导致片选时序被拉长或者传输被打断。解决方法是SPI传输期间关闭不必要的中断或者用DMA传输、用状态机检查传输完成标志后再做下一步操作。第四时钟相位在高速时变化。有些MCU的SPI时钟在极速时输出的占空比不完美边缘抖动变大原本刚好满足建立保持时间的从设备就会偶发错位。处理办法很直接降速。从20MHz降到10MHz往往就解决了。第五从设备总线竞争。如果你把小电阻、长走线的MISO线接了多个从设备而某个从设备的片选没拉高、输出没有释放成高阻它就会持续驱动MISO线导致其他从设备数据被干扰。解决办法是软件片选下确认每个从设备的CS在非选中时保持高电平以及从设备的MISO在不被选中时确实处于高阻态。6. 一张图搞懂SPI和近亲们的区别I2C、UART、CAN各管哪摊6.1 SPI和I2C的对比速度与灵活性的取舍I2C和SPI经常被放在一起比较因为它们在MCU里承担着类似的任务——连接外设但设计哲学完全不同。I2C只有两根线SCL时钟和SDA数据数据线是双向的半双工通信每个从设备有7位或10位地址主机通过地址来选择设备每传输一个字节后从设备必须回一个ACK位所以它有应答机制。SPI则是四根线、全双工、无地址、靠片选区分设备、无应答。把两者摆在一起看对比维度SPII2C线数4根SCLK/MOSI/MISO/CS2根SCL/SDA通信方式全双工半双工速率可到数十MHz标准100K/快速400K/高速可达3.4M寻址无靠片选有地址应答无有ACK/NACK多主支持弱支持多主仲裁硬件复杂度低稍高开漏上拉典型应用Flash、屏、ADC、SD卡传感器、EEPROM、PMIC、RTC选型建议如果外设要求高速率、数据结构简单比如刷屏、读写Flash选SPI如果外设数量多、速率要求不高、希望一根总线挂一堆设备选I2C。我个人的习惯是能走SPI就走SPI因为时序直观、调试简单、速度上限高I2C留给EEPROM、RTC这类低速率外设。需要特别注意的是I2C因为是开漏结构必须接上拉电阻阻值通常在2.2K到10K之间取决于总线上设备数量和速率。上拉电阻太小会导致总线驱动能力不足太大则上升沿变缓影响时序。很多人I2C不通排查到最后发现就是上拉电阻值选错了。6.2 SPI和UART的对比同步和异步的思维差异UART是异步串口通信典型的是TX、RX两根线。它不需要时钟线靠双方约定波特率来对齐收发。每一帧有起始位、数据位、校验位和停止位接收方通过检测起始位下降沿来同步之后按波特率采数据位。Core difference一直在于UART是“异步点对点”SPI是“同步一主多从”。UART不需要时钟线连线最省但同一时刻只能一对通信SPI有专门时钟线连线多但主设备可以挂多个从设备。实际项目里UART常用于模块通信GPS、蓝牙串口透传、调试日志输出SPI则用于和高速外设的本地连接。两者定位差异非常明显不存在“谁替代谁”的问题。比如ESP32连接ST7789屏幕用SPI同时用UART输出调试日志两个并行不悖。要注意的是UART没有时钟线对波特率精度敏感通信双方波特率偏差不能太大否则会出现间歇性乱码。而SPI因为有时钟线只要时钟极性和相位摆对速率对双方是“天然同步”的不会因为主设备跑91.54MHz、从设备跑90MHz而出现速率漂移。6.3 SPI与CAN/USB/EtherCAT它们的战场完全不同CAN是汽车和工业领域的总线协议历史比较悠久两条差分线多主通信高可靠性抗干扰传输速率通常几百K到几M但单帧数据量只有8字节CAN FD可以更多。CAN不追求单个字节的搬运速度追求的是在恶劣电磁环境下可靠地把报文传到总线上所以它和SPI的关系不大两者甚至经常是同时出现MCU接CAN收发器用SPI配置寄存器外部设备用CAN总线连接。USB是另一种思路它把主机和设备的角色固定下来通过枚举、端点等机制管理数据传输速率从1.5Mbps到几十Gbps不等的都有远不是SPI能比的。但USB的协议栈复杂度极高需要专用控制器不能像SPI那样用GPIO模拟几下就完事。在MCU项目里USB通常用于和电脑、手机等上位机通信SPI则继续承担板级外设连接。EtherCAT是一种实时工业以太网协议定位是工业控制里的高速确定性通信。它帧结构和标准以太网相关但追求的是“纳秒级”的同步精度和很低的通信周期。这明显不是SPI的舞台。在工业设备里EtherCAT和SPI的关系是主站可能是PC或专用芯片从站芯片和MCU之间再用SPI交换数据。所以SPI永远是“局部总线”不会和这些高速网络协议正面竞争反而是它们的底层衔接手段。7. FPGA和Verilog如何实现SPI从机7.1 为什么要在FPGA里写SPI从机聊完MCU侧的SPI再聊聊FPGA侧。很多FPGA项目里SPI是一个高频出现的外设要么FPGA做主机去控制ADC/DAC要么FPGA做从机接收MCU的配置命令。在FPGA里写一个SPI从机是很多数字IC验证工程师和FPGA工程师的基本功。为什么要自己写SPI从机而不是像MCU那样直接用硬件外设因为FPGA没有固定的SPI外设一切都是用Verilog/VHDL在逻辑门层面实现。虽然你也可以在FPGA里例化一些IP核但很多时候自己写一个轻量级SPI从机更可控、更节省资源也更方便在仿真阶段观察波形。一个典型的SPI从机功能是接收主机发来的命令、地址和数据解析后写入寄存器或者把寄存器数据通过MISO返回给主机。这本质上是一个状态机再加上移位寄存器和输出逻辑。7.2 Verilog实现要点写一个SPI从机核心模块分为三块SPI时钟检测和片选管理、移位寄存器收发、协议状态机。代码不复杂但有三个关键点值得强调。第一SPI时钟是外部输入的SCLK它和FPGA内部系统时钟是异步的。直接拿SCLK去采样内部信号或者拿内部时钟去采样SCLK上的数据都可能出现亚稳态。稳妥的做法是用系统时钟对SCLK做两级同步再在SCLK的边沿上采样数据。如果对性能要求很高可以考虑用专用的时钟域交叉方案但很多场景下两级同步加边沿检测就够了。第二片选信号CS低有效它不仅决定整个传输是否有效也是状态机复位的依据。主机在传输过程中如果把CS拉高从机的移位寄存器和状态机应该立刻复位等待下一次CS有效。这个行为要和真实芯片一致否则后续命令解析会错乱。第三MISO输出在CS无效时要置为高阻。这是因为SPI总线上可能挂了多个从设备一个从设备没被选中时它的MISO必须释放总线否则多个设备同时驱动MISO就会打架。在Verilog里这个高阻输出通常用一个三态门实现assign MISO (cs_n 1b0) ? miso_reg[7] : 1bz;这里miso_reg是移位寄存器的最高位。在发送模式下每个SCLK沿把miso_reg左移一位MISO就按顺序吐出数据在接收模式下把MOSI采样值填入移位寄存器的最低位。我在FPGA项目里的经验是写SPI从机其实不难难的是在仿真里把所有边界情况都覆盖到。尤其要注意的是跨时钟域、CS异步复位、数据边沿采样和MISO高阻控制这几块。建议先用一段简单的验证代码模拟主机发送一个字节的命令和地址观察从机是否正确解析并返回数据把这条链路跑通了再集成到更大的系统里。7.3 SPI从机常见错误与仿真经验我见过很多新手写的SPI从机仿真时一切正常上板就失败。原因往往是仿真里没接好CS的时序或者SCLK边沿采样位置不对仿真环境里用手工激励没有覆盖真实主机的高速时钟。调试SPI从机最好的工具是逻辑分析仪和FPGA内部的在线逻辑分析仪比如Vivado的ILA、Quartus的SignalTap。把SCLK、MOSI、MISO、CS四根线都抓到对比波形就知道是哪个状态跳变出了问题。如果你的FPGA开发板把这几根线引出来了直接接逻辑分析仪最直观如果没引出来用ILA把信号抓回电脑看也行。另外从机的数据位宽和协议解析往往不匹配。比如主机发送的是16位数据但你的从机状态机只处理8位那数据就会错位。这类问题在仿真里很容易发现所以强烈建议在写状态机之前先画出协议的时序图把每个字节的含义、命令格式、CS的边界都明确写出来再做RTL实现。磨刀不误砍柴工。8. 共享SPI总线的设备选型与DMA优化8.1 ESP32的屏幕和SD卡能不能共享SPI热搜词里有一个很接地气的问题ESP32的屏幕和SD卡共享SPI哪个好。这个问题本质上是SPI总线上挂多个从设备的场景。ESP32的SPI外设可以挂多个从设备每增加一个从设备就需要增加一个CS引脚。屏幕和SD卡共享一条SPI总线硬件上没有任何问题只要把两个设备的CS分别接到两个GPIO即可。但性能上要考虑两个因素。第一是两个外设的工作模式是否兼容。ST7789屏幕是Mode 0或Mode 3SD卡通常也支持Mode 0或Mode 3两者基本兼容。第二是通信速率的匹配。屏幕刷新通常希望SPI时钟尽量高10MHz甚至更高SD卡读写通常不需要那么高20MHz也能接受。共享总线时可以把SPI外设配置成同一个时钟频率也可以切换时钟频率来适配不同设备。在实际使用中我更推荐把屏幕和SD卡放同一条SPI总线因为降低硬件成本且平时很少同时操作屏幕和SD卡总线冲突概率低。需要注意的一点是当其中一个设备正在通信时另一个设备的CS必须处于无效电平确保它不会误响应总线上的数据。如果你的驱动代码在切换设备时忘了把上一次的CS拉高就会出现两台设备同时选中、总线冲突、数据乱掉的问题。8.2 SPI DMA高速刷屏与波形优化的关键SPI数据传输的短板在CPU上而不是SPI本身。传统方式是CPU逐字节往数据寄存器里写每写一个字节都要等待传输完成标志CPU大量时间花在等待上。引入DMA后CPU只需要设置好起始地址和长度DMA控制器自动把数据从内存搬运到SPI发送寄存器CPU可以腾出来干别的事。DMA带来的性能提升在刷屏场景下非常直观。以一块320×240的RGB565屏幕为例一帧原始数据是320×240×2字节约153.6K字节。在25MHz SPI时钟下单纯SPI发送就要大约50ms如果用CPU逐字节写中间还会夹杂各种判断和等待实际耗时可能翻倍。而DMA模式下CPU只负责启动DMA之后的时间可以处理其他任务或者让MCU休眠省电。使用SPI DMA有几个关键点DMA通道配置要和SPI外设对应。STM32上每个SPI都有固定的DMA请求线配置错了DMA不会触发。数据长度要和DMA配置一致。8位数据用8位DMA16位数据用16位DMA混用会出现数据错位。传输完成回调里释放DMA资源并及时拉高片选。这是最容易写错的地方——DMA传输完成不等于SPI外设已经把所有数据移位出去。有些MCU需要等SPI外设的“传输完成”标志才能真正拉高CS否则最后一个字节可能没发完。如果DMA buffer是在RAM里动态分配的要注意缓存一致性。在带Cache的MCU比如STM32H7上CPU写了DMA buffer后必须先做Cache clean否则DMA可能读到旧的缓存数据。8.3 如何评估要不要上DMA不是所有SPI场景都需要DMA。判断标准很简单CPU在SPI传输期间还有没有其他重要任务要做如果只是偶尔读写一次Flash每字节之间CPU等待的时间也就微秒级完全可接受上DMA反而增加代码复杂度。如果是刷屏、录音、高速日志记录这种持续、大流量、不能中断CPU的场景DMA就是必须的。另外使用RTOS时DMA配合信号量尤其顺畅。可以在DMA完成中断里释放信号量等待该信号量的任务被唤醒后直接处理buffer不用担心CPU被SPI传输阻塞。9. 写在最后我调试SPI的一些个人体会SPI这个协议说简单确实简单四根线、一个时钟、两个数据线、一个片选配置起来十几分钟就能跑通。但它也是最容易出“玄学问题”的协议之一。我调试SPI这么多年最大的体会是一定要先把时序图看明白再动手写代码。那些看起来“偶尔不通”“换个板子就不行”的问题几乎都能在时序上找到原因。另一个体会是调试SPI不能只靠肉眼盯着代码一定要借助工具。哪怕是最便宜的逻辑分析仪也比在代码里瞎猜效率高十倍。我见过太多人把CS极性、CPOL/CPHA翻来覆去地改最后发现是MOSI接到了MISO上。这种问题逻辑分析仪一眼就能看出来。选型的时候也别只盯着“SPI快”这个优点。SPI没有应答、线多、不适合超远距离传输如果外设不需要高速率、又希望省线I2C可能是更好的选择。但从系统整体看SPI绝对是嵌入式开发者的好朋友掌握它的时序、配置、调试方法能让你在屏幕、Flash、传感器等各种外设之间游刃有余。最后分享一个小技巧每当拿到一款新的SPI从设备我不会直接写完整驱动而是先用最简单的循环把寄存器读出来打日志确认SPI物理层通了再叠加协议层逻辑。这个“物理层先行”的习惯帮我避开了无数“协议和时序混在一起分不清谁错”的问题。希望这篇SPI通信的实战笔记也能帮你在项目里少踩几个坑。