有人第一次接触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到底在管什么
CPOL(Clock Polarity)决定时钟空闲时的电平:CPOL=0是空闲低电平,CPOL=1是空闲高电平。CPHA(Clock Phase)决定数据是在时钟的第一个跳变沿采样还是第二个跳变沿采样。这两个参数组合出四种模式:Mode 0(CPOL=0, CPHA=0)、Mode 1、Mode 2、Mode 3。
说得更直白一点:CPOL管的是“不干活的时候时钟线停在哪个电平”,CPHA管的是“干活的时候哪半边脸用来采样数据”。如果CPHA=0,数据在时钟的第一个跳变沿被采样,第二个跳变沿切换数据;CPHA=1则反过来,第二个沿采样,第一个沿切换。
这看起来就是个小配置项,但实际影响非常大,尤其是在全双工传输里。因为SPI在全双工模式下,MOSI上的数据是由主机在某个时钟沿“推”出来的,MISO上的数据是由从机在另一个沿“推”出来的,采样方必须在数据稳定之后再采样,如果CPOL和CPHA配错了,采样点刚好落在数据切换的瞬间,采回来的就是半稳定状态,轻则偶尔出错,重则一个字节都读不对。
2.2 四种模式怎么选:看从机数据手册的哪一页
选模式的依据只有一个:从机数据手册里的时序图。大部分芯片数据手册都会画一张类似“图N:SPI读取时序图”的图,图上会标明“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那样等到数据再发ACK,SPI天然适合流水线式和突发式传输。在主机端,你只要设计好时序状态机,把“发命令→丢弃垃圾数据→收集有效数据”这个流程无缝衔接起来,就能做到很高的传输效率。
4. 提升全双工吞吐量的关键:DMA与片选策略
4.1 SPI+DMA实现真正无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。下面这张表是我积累的排查路线,按出现频率排列。
| 现象特征 | 最可能原因 | 排查手段 |
|---|---|---|
| 读回全0xFF | MISO没接好、从机供电异常、从机未进入工作模式 | 万用表测MISO电压,检查从机电源和复位 |
| 读回全0x00 | MISO被拉死到地、从机没驱动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短接,发什么就收什么,确认基本时序没问题了再去接真实器件。这一步能帮你隔离掉一半以上的硬件接线问题,剩下的才是真正的协议问题。