ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SPI全双工机制详解:从物理层时序到DMA与片选策略

2026/9/13 15:55:53 拓冰建站 浏览量
SPI全双工机制详解:从物理层时序到DMA与片选策略 有人第一次接触SPI时看协议框图发现主机发送和接收是同时进行的两条独立数据线一进一出以为这就是全双工的全部秘密。真上手调了一两次问题就来了主机发一个字节出去为什么同时收到的字节不是你想要的从机明明已经把数据准备好了为什么状态寄存器还是空的今天这篇就把SPI全双工这层窗户纸捅破从物理层收发机制、时序模式到实际工程里的DMA和片选策略一次讲透。这个内容适合所有跟嵌入式硬件打交道的人单片机开发、FPGA逻辑设计、Linux驱动开发都适用。不需要你有很深的硬件基础只要写过几次数码管、点过几次OLED屏就能跟着捋清楚。读完你会明白SPI为什么能全双工、全双工实际用起来有什么坑、以及怎么把它真正跑出高性能。1. 全双工的物理基础SPI为什么天生就能双向跑1.1 独立收发通道 vs 分时复用通道首先要明确一个概念全双工不是说“能同时收发”这么简单而是物理上存在两条互不干扰的数据通路。SPI有MOSI主机输出/从机输入和MISO主机输入/从机输出两根独立线主机往MOSI上推数据的同时从机往MISO上推数据两个方向的信号互不占用对方的引脚。对比一下就清楚了。I2C只有一根SDA线收发必须分时复用同一时刻只能有一个方向的数据在线上跑所以I2C是半双工。UART虽然有独立的TX和RX两根线但它的时序是异步的没有统一时钟约束数据必须自己约定波特率而且收发双方各自控制自己的移位节奏。SPI不一样它的收发节奏完全由主机产生的时钟统一带动从机没有自己的时钟源发多快、什么时候采样全部听主机的。这个“统一时钟”就是SPI全双工能高效工作的命根子。主机拉高SCK、拉低SCK每跳变一次MOSI和MISO上就各推进一位数据。一根时钟线同时驱动两个方向的移位寄存器天然就是同步的不需要额外的握手、应答机制。1.2 移位寄存器全双工的核心引擎SPI的收发核心是两个移位寄存器——主机一个从机一个。不过这两个寄存器在逻辑上是串在一起的数据先在主机移位寄存器里准备好时钟到来时从MOSI移出到从机的移位寄存器同时从机的移位寄存器也把一位数据从MISO移回主机的移位寄存器。一个时钟周期两个方向各移动一位8个时钟周期后主机发出一个字节同时收到一个字节。这就是SPI全双工最奇妙的地方你发出去一个字节必然同时收回一个字节你不可能只发不收也不可能只收不发。所以很多工程师习惯说“SPI没有读操作只有交换操作”这句话虽然不严谨但从机制上讲完全正确。正因为如此SPI读数据的时候主机必须“假装”在发数据——通常发0x00或者0xFF——才能把时钟转起来把从机移位寄存器里的数据“踢”回来。写数据的时候从机那边也会往MISO上推东西但那些数据多半是垃圾主机直接丢弃就行。1.3 主时钟统率全双工不需要握手SPI没有ACK机制没有流量控制没有帧结束符全靠时钟脉冲数来界定一次传输的长度。主机想让传输停止只要停止产生时钟、拉高片选信号就行。这种设计让SPI的全双工极其干净利落开销极小但同时也意味着通信双方必须严格按照约定好的时序来配合任何一方动作慢了半拍数据就错了而且错得毫无痕迹。很多项目里SPI全双工出问题都不是信号质量的问题而是双方对“什么时候该采样、什么时候该切换数据”的理解不一致也就是接下来要说的CPOL和CPHA。2. 四模式与主从握手全双工能稳定工作的前提2.1 CPOL和CPHA到底在管什么CPOLClock Polarity决定时钟空闲时的电平CPOL0是空闲低电平CPOL1是空闲高电平。CPHAClock Phase决定数据是在时钟的第一个跳变沿采样还是第二个跳变沿采样。这两个参数组合出四种模式Mode 0CPOL0, CPHA0、Mode 1、Mode 2、Mode 3。说得更直白一点CPOL管的是“不干活的时候时钟线停在哪个电平”CPHA管的是“干活的时候哪半边脸用来采样数据”。如果CPHA0数据在时钟的第一个跳变沿被采样第二个跳变沿切换数据CPHA1则反过来第二个沿采样第一个沿切换。这看起来就是个小配置项但实际影响非常大尤其是在全双工传输里。因为SPI在全双工模式下MOSI上的数据是由主机在某个时钟沿“推”出来的MISO上的数据是由从机在另一个沿“推”出来的采样方必须在数据稳定之后再采样如果CPOL和CPHA配错了采样点刚好落在数据切换的瞬间采回来的就是半稳定状态轻则偶尔出错重则一个字节都读不对。2.2 四种模式怎么选看从机数据手册的哪一页选模式的依据只有一个从机数据手册里的时序图。大部分芯片数据手册都会画一张类似“图NSPI读取时序图”的图图上会标明“DATA SAMPLED ON RISING/ FALLING EDGE”或者“DATA TOGGLE ON FALLING/RISING EDGE”。我平时判断的步骤是这样的先看空闲时SCK是高还是低确定CPOL再看数据手册里写的采样沿上升沿采样或者下降沿采样结合CPOL推算出CPHA。直接看表格也行很多芯片会在“SPI Features”一节直接写“Supports SPI Mode 0 and Mode 3”那就省事了配置成对应模式直接跑。提示如果手头没有数据手册可以用示波器抓从机MISO上的波形。MOSI上有数据跳动但MISO一直是高阻或恒定电平多半是模式配错了或者CS没拉低、从机压根没进入通信状态。2.3 时钟空闲状态对全双工的影响这里有个容易被忽略的细节在纯物理层面全双工数据传输并不依赖时钟空闲电平因为数据切换和采样都发生在跳变沿上。但很多从机芯片的片内逻辑会在CS拉低后、第一个有效沿到来之前对输入的指令做预处理如果时钟空闲电平和从机预期不一致从机可能把第一个边沿误判成“真正的传输开始”导致后续所有数据错位。举个实际例子。某款射频收发芯片要求SPI工作在Mode 0如果你用Mode 3去驱动它它可能也能通信——因为很多从机对两种模式都做了兼容——但某些对时序敏感的芯片就会间歇性读回错误寄存器值。我调过一颗ADC芯片Mode 3下读取转换结果时好时坏切到Mode 0后一次错都没出过。所以最好还是严格按照说明书来不要用“好像也能跑”去赌长期稳定性。3. 实战拆解一次完整的SPI全双工收发3.1 你以为的“收发”不是真正的“同时收发”新手最容易理解错的点是认为全双工就是“我在发一帧的同时还能收另一帧”就像打电话两个人同时说话。实际上SPI的全双工是逐位交替的一个时钟周期内主机发送1位同时接收1位但这一位发送和这一位接收发生在同一个时钟周期里整体是“边发边收”的流水线模式。以读Flash存储器为例。你跟Flash说“我要从地址0x000000开始读数据”操作流程是主机把CS拉低 → 主机发送读命令0x03 → 主机发送三个字节的地址 → 主机开始连续发送0x00作为虚拟时钟 → 从机从MISO上返回Flash内容。在这个过程中主机发送命令和地址的同时MISO上会收到Flash返回的垃圾数据此时Flash还没进入数据输出状态你必须把它们丢弃。等到地址发送完毕再往MOSI上送0x00的时候MISO上才有真正的有效数据。3.2 从机必须先准备好数据——全双工里最容易被忽略的规则这句话请务必记住在SPI全双工中从机要在时钟沿到来之前就把待发送的数据放到MISO线上。因为从机没有自己的时钟它不能“等收到命令再准备数据”——时钟是主机启动的一旦时钟开始跳MISO上的数据就要同步跟着跳没有延迟的余地。这就引出了全双工通信中一个著名的现象——“死字节”dummy byte。从机在收到完整命令之前根本不知道你要它干什么它只能先把MISO置成高阻或者输出无效电平。等它解析完命令、准备好响应数据通常已经是几个字节之后的事了。所以SPI读操作总是带有“前导无效数据”真正有用的数据往往从第2、第3个字节才开始。我做FPGA从机的时候被这个坑过。逻辑里为了省事MISO在CS拉低之前是低电平CS拉低后、命令解析完成前也是低电平结果主机端读到的第一个有效数据前面总是多了一个0x00排查了半天才发现是MISO没有在三态和高低电平之间正确切换把从机默认输出改成了高阻态才解决。3.3 伪全双工的常见场景命令-响应式器件很多SPI器件设计上并不是真正意义上的同时双向数据流而是“命令-响应”式交互主机发命令从机在后续时钟周期里返回数据。这类器件包括绝大多数Flash、ADC、射频芯片、传感器。你以为你在全双工地跟传感器聊天实际上协议栈层面是半双工对话主机问一句从机答一句。但物理层面它依然是全双工因为命令下发的同时MISO上确实有数据流在走——只是那些数据是无效的。这个特性带来的好处是你不需要像I2C那样切换总线方向也不需要像UART那样等到数据再发ACKSPI天然适合流水线式和突发式传输。在主机端你只要设计好时序状态机把“发命令→丢弃垃圾数据→收集有效数据”这个流程无缝衔接起来就能做到很高的传输效率。4. 提升全双工吞吐量的关键DMA与片选策略4.1 SPIDMA实现真正无CPU干预的双向传输SPI全双工模式下每收发一个字节CPU都要处理一个中断除非速率很低否则CPU很快就被吃满了。以1MHz SPI速率计算每秒钟要产生125000次中断每次中断哪怕只花10微秒CPU占用率也超过100%了这会卡死整个系统。解法是DMA。把SPI的接收数据寄存器地址和发送数据寄存器地址配置成DMA搬运的源和目的让DMA自动把内存里的数据搬到发送寄存器同时把接收寄存器里的数据搬到内存缓冲区。CPU只需要配置好DMA通道、设置好传输长度然后就可以去干别的事了等DMA传输完成中断到来后再处理数据。这里有一个非常关键的细节SPI全双工模式下DMA要同时启动发送和接收。接收DMA和发送DMA的传输长度必须一致不然接收缓冲区要么读不满要么溢出覆盖。我用STM32的HAL库时常用的做法是同时调用HAL_SPI_Transmit_DMA和HAL_SPI_Receive_DMA然后在DMA完成回调里等待两个DMA都完成后才认为一次传输结束。4.2 硬件片选 vs 软件片选全双工时序的分水岭片选信号在SPI全双工里的作用被绝大多数人低估了。CS拉低代表“这次传输开始”CS拉高代表“这次传输结束”全双工的数据流完全是在CS有效窗口内进行的CS边沿的timing直接决定从机是否能正确识别一帧数据的边界。硬件片选硬件NSS由SPI外设自动控制时序精确但有个要注意的地方很多MCU的硬件NSS在传输开始时是自动拉低的但传输结束后恢复高电平的时机跟SPI时钟不是严格同步的如果从机对CS释放边沿有严格要求可能出问题。软件片选GPIO控制最大的优势是灵活你想什么时候拉低就什么时候拉低想保持多久就保持多久某些需要“CS保持低电平连续操作多个寄存器”的场景必须用软件片选。但它有个致命弱点CS拉低到第一个时钟沿之间、或者最后一个时钟沿到CS拉高之间可能因为GPIO操作延迟而出现不确定的间隙。在全双工高速传输中这个间隙可能被从机误解为传输开始或结束。我个人的建议是能用硬件片选就用硬件片选除非你确定需要自定义CS时序。尤其是Linux下用SPIDEV驱动外部器件时硬件片选由SPI控制器统一管理配合DMA可以做到非常稳定的全双工批量传输。4.3 高负载场景的传输优化真正跑高吞吐量的SPI全双工比如驱动TFT屏幕、高速ADC采集、FPGA和MCU之间批量交换数据有几个优化思路很实用。第一尽量把多次短传输合并成一次长传输。每次CS拉低拉高都有开销而SPI全双工传输的数据有效性跟传输长度成正比把写命令、写地址、读数据放在同一次CS低电平窗口内完成能省掉大量帧间隙开销。第二用环形缓冲区配合DMA。如果数据是持续不断产生的比如ADC连续采样接收DMA指向一个环形缓冲区DMA满了就触发中断搬数据然后重新武装下一次DMA这样可以做到无缝全双工采集。第三SPI时钟速率不是越高越好。全双工工作模式下MOSI和MISO线路上的信号同时翻转串扰和反射会更明显。如果PCB走线没有做好阻抗匹配速率上去反而会出错这个跟协议本身没关系但却是实际项目里最常见的性能瓶颈。5. 进阶从机侧的全双工与多字节流水线5.1 从机如何保证“数据已准备好”前面讲了主机端怎么理解全双工换个视角看从机端如果你用FPGA或者另一颗MCU做SPI从机怎么保证在全双工模式下及时响应主机核心只有一条从机逻辑必须在主机时钟到来之前就计算出MISO上要输出的位。这意味着从机要先接收到完整命令解析完再把后续要返回的数据准备好这天然引入了几字节的延迟。很多从机芯片的做法是在CS拉低后的第一个字节MISO不输出任何有效数据整个字节时间用来解析命令第二个字节才真正开始返回数据。作为从机设计者你要做的就是把MISO的输出逻辑做成“打拍子”的形式所有数据位提前一个时钟周期算好等主机时钟沿一到来就把算好的那位数据推出。实际工程里我习惯用移位寄存器预装载寄存器配合主时钟的第一个沿用来切换数据位第二个沿用来采样输入位这样既保证建立时间又保证保持时间。5.2 全双工通信中的半双工“错觉”调试SPI全双工的时候经常有一种错觉明明MISO上有波形示波器抓下来也有数据变化但逻辑分析仪解出来的数据就是不对。这时候十有八九是主机的MISO采样时序和从机的MISO输出时序对不上说白了就是CPHA配置的问题。举例来说如果主机按Mode 0工作上升沿采样、下降沿切换从机却按“上升沿切换数据”的逻辑来生成MISO那主机在上升沿采到的就是从机刚刚切换出来的毛刺信号采10次能对5次都算运气好。对这种问题用示波器同时抓SCK和MISO对照采样沿检查数据是否稳定是最高效的判断方法。5.3 多字节连续传输时的流水线处理真正的全双工高性能场景往往是连续多字节交换主机一次CS低电平窗口内完成几十上百字节的收发。这时候从机侧不能每个字节都重新解析一次命令而是第一次收到命令后进入连续传输模式后续每个时钟周期内从机移位寄存器自动把下一个字节推到MISO上。对于这种连续模式从机的设计要特别注意“字节间隙”。有些主机在连续传输的字节与字节之间会插入一个极短的时钟停顿如果从机对这个停顿处理不当比如把停顿当成CS释放就会导致数据移位错位。解决办法是让从机的移位寄存器始终跟随SCK的跳变工作不依赖字节边界只在CS拉高时才复位。6. 常见问题与排查技巧实录6.1 全双工通信异常的定位思路SPI全双工通信出问题跟普通串口通信不太一样——它的错误往往不是某一个字节错了而是整体错位、丢字节、或者数据全是0xFF/0x00。下面这张表是我积累的排查路线按出现频率排列。现象特征最可能原因排查手段读回全0xFFMISO没接好、从机供电异常、从机未进入工作模式万用表测MISO电压检查从机电源和复位读回全0x00MISO被拉死到地、从机没驱动MISO高阻冲突检查MISO引脚的推挽/开漏配置断开其他外设数据有值但错位时钟模式CPOL/CPHA不匹配示波器抓SCK与MISO关系对照数据手册修改模式偶发性一个字节错误从机数据准备好时间不足、DMA配置错位检查从机是否需等待、DMA传输长度是否匹配高波特率下频繁出错PCB走线过长、串扰、上拉/下拉电阻不当降低SPI时钟验证检查走线长度与信号完整性CS释放时机不对导致丢尾帧片选时序不满足从机要求用软件片选延长CS高电平保持时间6.2 逻辑分析仪和示波器的正确用法调试SPI全双工时逻辑分析仪比示波器好用得多。SPI是同步协议只要抓到SCK和MOSI、MISO就可以在软件里自动解码而且一次能抓几十万帧。示波器更适合观察信号质量比如上升沿是否过缓、MISO上有没有毛刺、建立时间是否足够。实际常用的组合是先用逻辑分析仪确认协议层时序没错再用示波器复查物理层信号。如果逻辑分析仪解出的数据是乱的先别急着怀疑信号质量九成是时钟极性和相位配置不对或者片选时序没满足从机要求。提示很多逻辑分析仪的SPI解码器默认按Mode 0解码。如果你实际用的是Mode 3需要在解码设置里手动改CPOL和CPHA不然解码结果必然错乱。这个坑我踩过不止一次。6.3 一个典型的全双工读错误实战案例说一个我之前调NRF24L01的案例很有代表性。NRF24L01是SPI接口的2.4G射频芯片我一开始用的是STM32硬件SPI配置成Mode 0。现象是写配置寄存器成功率很高但读状态寄存器的值时不时不对。排查过程是这样的先用逻辑分析仪抓了MOSI和MISO发现MISO上确实有数据返回但返回的数据跟命令完全不对应比如发送读状态命令0x07返回的却是0x05、0x03这种乱码。再看时序发现MISO上的数据跳变沿跟SCK上升沿几乎重合——这说明从机是在上升沿切换数据而主机也在上升沿采样采样点落在数据切换的毛刺上。查了NRF24L01的数据手册发现它要求的是“数据在SCK下降沿切换、上升沿稳定”严格来说是Mode 0但外部状态导致切换沿有延迟。最后把SPI时钟降到了2MHz问题才解决。这个案例的启发是即便数据手册写了某种模式实际情况中从机的内部逻辑延迟、PCB走线寄生电容都可能导致切换沿偏移低速调试、高速优化才是正道。7. 片选策略的深层影响与DMA配合技巧7.1 软件片选的时序补偿软件片选最大的坑就是CS拉低到第一个SCK沿之间的建立时间以及最后一个SCK沿到CS拉高之间的保持时间。GPIO操作本身是纳秒级的延迟高速SPI下可能不够从机进入状态。解决办法是写驱动时在CS拉低之后、开启SPI传输之前插入一小段延时。具体延多长时间取决于从机的数据手册大部分器件要求CS拉低后至少几百纳秒才能接收数据。如果你用的是裸机程序用几条NOP指令就够如果你跑的是Linux用usleep会有微秒级误差实测下来没问题但需要验证边界工况。7.2 DMA与软件片选的组合优化软件片选和DMA配合是有讲究的。DMA传输是硬件行为启动后CPU不再干预但CS的拉低和拉高需要CPU控制所以必须在DMA启动之前把CS拉低、在DMA传输完成中断回调里把CS拉高。问题在于从CS拉低到DMA真正把第一个字节送到移位寄存器中间有一段启动延迟。如果这段延迟不够稳定从机可能还没准备好第一个字节就发飞了。比较稳妥的做法是用硬件的“片选跟随自动控制”功能——很多MCU的SPI外设支持NSS输出自动跟随传输启动——把CS交给硬件管DMA传输也由硬件触发这样CS拉低和第一个SCK沿之间的间隔是固定的时序最可靠。7.3 硬件片选的多从机扩展当总线上挂了多个SPI从机时硬件片选方案会更清晰。每个从机一根CS线互不干扰主机可以随时切换目标从机。但要注意硬件片选模式下如果两个从机的MISO都配置成推挽输出且没有正确片选管理空闲从机就会跟工作从机抢总线。解决办法是所有从机的MISO都必须支持三态门CS为高时输出高阻。如果某个从机芯片不支持三态MISO那就必须外接隔离逻辑不然硬件片选方案没法可靠工作这种情况只能退回到软件片选靠上下拉电阻和从机的片选逻辑来隔离。8. 半双工场景的对比为什么SPI比I2C快8.1 不需要反转总线方向I2C做一次读操作主机要先发从机地址写位再发寄存器地址然后重新发开始条件从机地址读位等从机释放SDA总线主机才能开始读数据。每一次方向切换都要经过总线仲裁、ACK应答、释放总线等操作开销非常大。SPI全双工没有这些流程。CS一拉低主机就可以连续地发命令、收数据数据方向是物理隔离的不需要任何总线切换流程。这就是SPI读操作能比I2C快好几倍的核心原因——不是SPI时钟跑得高而是协议层面的开销小。8.2 时钟频率上限对比I2C标准模式只有100kHz快速模式400kHz高速模式也才3.4MHz。SPI轻轻松松就能跑几十MHz嵌入式系统里20MHz、30MHz的SPI时钟很常见。这背后是因为SPI是全同步协议只要信号完整性允许时钟想多快就多快不需要像I2C那样通过开漏结构和上拉电阻来保证时序。当然跑高速全双工时MOSI和MISO两根线的串扰、地回路噪声、连接线长度都会直接影响可靠性。我的经验是开发板上飞线调试时SPI超过10MHz就要小心了正规PCB设计、走线控制好30MHz以上没压力。8.3 什么时候SPI全双工反而是劣势SPI全双工也不是万能的。如果通信数据本来就是对半交互的“一问一答”而且每次交互的数据量很小SPI全双工的优势发挥不出来反而因为四根线占用了更多IO、接线更复杂。这时候I2C只有两根线成本更低接线更方便。更极端的场景是从机本身设计成半双工交互模式主机发送期间MISO无数据主机接收期间MOSI必须置为特定电平。这种情况下SPI虽然物理上是全双工但实际利用率不高全双工的带宽优势被白白浪费了。所以选协议还是要看具体器件和场景不要为了全双工而全双工。说到最后我个人的体会是SPI全双工这套机制核心魅力不在于“双向同时传输”这个噱头而在于它用一条时钟线、两条数据线就搭建出了一个开销极低、速率极高、时序确定性极强的数据传输通道。只要你理解了移位寄存器“边发边收”的本质理解了从机必须预置数据这个牺牲理解了CPOL和CPHA的主从配合关系再回头去看那些调不通的时序问题基本一眼就能瞄出病灶。如果你也在调试SPI全双工建议第一步先别急着接外设把主机和从机用杜邦线直连先跑个最简单的回环测试——把MOSI和MISO短接发什么就收什么确认基本时序没问题了再去接真实器件。这一步能帮你隔离掉一半以上的硬件接线问题剩下的才是真正的协议问题。