三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

深入解析SPI通信:从基础时序到DMA优化与实战调试

深入解析SPI通信:从基础时序到DMA优化与实战调试

1. 从“串行”到“全双工”:为什么SPI依然是嵌入式通信的基石

在嵌入式开发的世界里,通信协议就像设备之间交流的语言。当我们需要在微控制器(MCU)和传感器、存储器、显示屏之间快速、可靠地交换数据时,UART、I2C和SPI是绕不开的“三巨头”。今天,我们不聊异步的UART,也不聊需要地址寻址的I2C,而是聚焦于那个看似简单、实则充满细节的同步串行王者——SPI。你可能已经用它驱动过W25Q64闪存,或者点亮了ST7789屏幕,甚至用模拟SPI读取了MAX31855的温度数据。但你是否真正理解,为什么在需要高速、全双工数据流的场景下,SPI依然是工程师的首选?这篇文章,我将结合十多年的踩坑经验,从协议本质、模式选择、硬件软件博弈,到DMA优化和实战排错,为你彻底拆解SPI,让你不仅能“用”,更能“懂”和“用好”。

SPI,全称Serial Peripheral Interface,由摩托罗拉公司推出。它的核心设计哲学是“简单”和“快速”。与I2C不同,SPI没有复杂的起始、停止、应答信号,也没有统一的地址广播机制。它采用主从架构,一主多从,通过四根线(有时三根)实现全双工同步通信。这种设计让它天生适合传输数据流,比如刷新LCD屏、读写大容量Flash,或者与高速ADC/DAC通信。然而,正是这种“简单”,把许多复杂性留给了软件和硬件工程师去协调——从四种模式(CPOL, CPHA)的理解,到片选信号的管理,再到DMA传输的稳定性,每一个环节都可能成为项目进度的“拦路虎”。接下来,我们就深入这些细节。

2. 四线舞者:深入拆解SPI的物理层与时序逻辑

要驾驭SPI,首先得看清它的舞台和舞步。SPI通信至少需要四根信号线,这构成了其物理层的基础。

2.1 核心信号线定义与角色

  • SCLK (Serial Clock, 串行时钟):由主机产生并输出给所有从机。这是整个通信的节拍器,所有数据位的采样和输出都严格跟随它的边沿。时钟频率决定了通信速度,这也是SPI高速优势的来源。
  • MOSI (Master Out Slave In, 主机输出从机输入):数据输出线。主机通过这根线向从机发送数据。
  • MISO (Master In Slave Out, 主机输入从机输出):数据输入线。主机通过这根线接收从机发回的数据。注意,在单主单从或特定配置下,这根线才是有效的。如果主机只写不读,或者从机是只发送设备(如某些DAC),这根线可能悬空。
  • CS/SS (Chip Select / Slave Select, 片选/从机选择):这是SPI多从机管理的关键。每个从机都有自己独立的片选线,由主机控制。只有当某个从机的片选信号被主机拉低(通常是低电平有效)时,该从机才会被“激活”,响应SCLK并参与通信。片选信号在通信开始前有效,在通信结束后无效,它框定了一次完整的数据传输过程。

注意:这就是“SPI硬件片选”的典型用法。它稳定可靠,但会占用主机大量的GPIO引脚。当从机数量多时,就需要使用译码器或“SPI软件片选”(用一根GPIO模拟片选时序)来节省引脚,但这会引入额外的软件开销和时序风险。

2.2 理解时序的核心:CPOL与CPHA

这是SPI最令人困惑,也最容易出错的地方。SPI的四种模式,本质是由时钟极性(CPOL)和时钟相位(CPHA)两个参数组合而成。它们定义了数据在时钟的哪个边沿被采样(捕获),以及在哪个边沿被改变(输出)。

  • CPOL (Clock Polarity, 时钟极性)
    • CPOL=0:时钟空闲状态为低电平。
    • CPOL=1:时钟空闲状态为高电平。
  • CPHA (Clock Phase, 时钟相位)
    • CPHA=0:数据在时钟的第一个边沿(对于CPOL=0是上升沿,对于CPOL=1是下降沿)被采样,在下一个边沿被改变。
    • CPHA=1:数据在时钟的第二个边沿被采样,在第一个边沿被改变。

为了直观理解,我们结合一个具体器件来分析。以常见的SPI Flash芯片W25Q64为例,其数据手册通常会明确要求SPI模式为Mode 0或Mode 3。我们假设它要求Mode 0 (CPOL=0, CPHA=0)。

  1. 空闲状态:SCLK线保持低电平(CPOL=0)。
  2. 片选有效:主机拉低对应从机的CS引脚。
  3. 第一个时钟边沿(上升沿):由于CPHA=0,主机和从机在这个上升沿对数据线进行采样。这意味着,在上升沿到来之前,发送方(无论是主机还是从机)必须已经将第一位数据稳定地放在MOSI或MISO线上。因此,数据输出实际上发生在时钟上升沿之前
  4. 第二个时钟边沿(下降沿):发送方利用这个下降沿来切换(改变)到下一位数据,为下一个采样边沿(下一个上升沿)做准备。
  5. 重复:如此循环8次或16次(取决于数据帧长度),完成一个字节或字的传输。
  6. 通信结束:主机拉高CS引脚。

如果你用逻辑分析仪抓取Mode 0的时序,你会清晰地看到,数据线(MOSI/MISO)在SCLK上升沿时是稳定的,而在下降沿期间发生变化。这就是“在第一个边沿采样”的直观体现。

2.3 四种模式对比与选型陷阱

模式CPOLCPHA时钟空闲电平采样边沿数据变化边沿常见应用
Mode 000低电平上升沿下降沿最常用。如W25Qxx Flash, ST7789 LCD, 很多传感器
Mode 101低电平下降沿上升沿部分ADC, 少数特定器件
Mode 210高电平下降沿上升沿相对少见
Mode 311高电平上升沿下降沿某些RFID读卡器芯片, 部分音频Codec

选型心得永远以从机设备的数据手册为准!这是铁律。主机的SPI模式必须严格匹配从机的要求。配置错误是导致“通信无数据”或“数据错乱”的最常见原因。例如,你用STM32的HAL库,默认可能是Mode 0,但如果你的DRV8711电机驱动芯片要求Mode 1,而你未修改,那么SPI通信就会完全失败。在调试时,第一件事就是用逻辑分析仪确认SCLK、MOSI、MISO、CS的波形,核对时序是否符合从机手册的描述。

3. 超越基础:SPI的高级配置与实战中的“坑”

当你理解了基本时序,真正将SPI用在实际项目中时,才会遇到那些数据手册不会写的“坑”。这部分我们结合热词中的具体问题展开。

3.1 硬件片选 vs. 软件片选:不仅仅是省引脚

  • 硬件片选:使用MCU硬件SPI外设自带的NSS(Negated Slave Select)引脚。优点是硬件自动管理,在数据传输开始前自动拉低,结束后自动拉高,时序精准,不占用CPU资源。缺点是通常一个SPI外设只有一个硬件NSS输出,难以直接驱动多个从机(除非使用菊花链)。
  • 软件片选:使用任意一个GPIO引脚来模拟片选信号。在通信前手动拉低,通信后手动拉高。优点是灵活,一个SPI总线可以挂载多个从机(每个从机独占一个GPIO)。缺点是需要程序员严格保证时序,特别是在高速通信下,软件操作的延迟可能导致片选信号边沿与数据时钟边沿过于接近,违反从机的建立/保持时间要求,造成通信不稳定。

实战建议:对于低速设备(如低于1MHz),软件片选足够且灵活。对于高速设备(如读写W25Q64的Fast Read模式可能到几十MHz),强烈建议使用硬件片选,或者如果必须用软件片选,务必在拉低片选后,插入几个NOP指令延时,再启动SPI传输;在传输结束后,等待SPI总线真正空闲(查询状态寄存器或使用中断/DMA回调),再拉高片选。

3.2 数据帧格式与大小端:不只是8位

大多数SPI通信默认以8位为一个数据帧。但很多现代SPI外设支持可配置的数据帧长度,如4位到16位。例如,某些OLED屏驱动芯片可能接受16位色彩数据(565格式)的一次性传输。配置时需确保主机和从机理解一致。 另一个隐藏细节是数据位的传输顺序,即MSB(最高位)先行还是LSB(最低位)先行。绝大多数SPI器件采用MSB先行,但总有例外(例如一些老旧的芯片)。这同样需要在从机数据手册中确认,并在主机SPI配置中设置正确。

3.3 全双工、半双工与单工:理解MISO和MOSI的角色

标准SPI是全双工的:MOSI和MISO同时工作,主机在发送一个字节的同时,也会接收一个字节。这个接收到的字节可能是从机返回的有效数据,也可能是无意义的“哑元”(Dummy Byte)。 但在很多实际应用中,通信是半双工甚至单工的:

  • 只写不读:例如驱动WS2812B(虽然它严格来说不是SPI,但常用SPI模拟时序)或某些DAC。此时MISO线可以忽略。
  • 先写后读:例如读取W25Q64。主机先发送一个8位的指令(如0x03 Read Data),再发送24位地址,然后主机需要继续产生时钟,同时从机通过MISO线返回数据。在这个过程中,主机在“读阶段”发送的数据通常是0xFF(哑元),只为产生时钟。
  • 菊花链(Daisy Chain):这是一种特殊的连接方式,多个从机的MOSI和MISO首尾相连,形成一个环。主机发送一个很长的数据流,依次通过所有从机。每个从机在接收数据的同时,将自身要发送的数据推到链中。这种方式可以用一个片选控制多个从机,但协议需要专门设计,通用性不强。

3.4 关于“HAL库SPI Lock”的原因

在使用STM32的HAL库时,你可能会遇到HAL_SPI_TransmitHAL_SPI_TransmitReceive函数返回HAL_BUSY,或者函数内部调用SPI_WaitFlagStateUntilTimeout等待超时。这通常是因为前一次传输尚未完成(TXE或RXNE标志未就绪),而你又发起了新的传输请求。 HAL库通过一个Lock机制(hspi->Lock)来防止对同一SPI外设的并发访问。如果你在中断服务程序(ISR)和主循环中同时调用SPI函数,或者在没有等待上一次传输完成的情况下就发起下一次传输,就容易触发这个问题。解决方案

  1. 顺序执行:确保“发送-等待完成-再发送”的流程。
  2. 使用DMA:这是解决此问题、提升效率的最佳实践。配置好DMA通道,传输过程由硬件完成,CPU只需等待DMA传输完成中断或回调,在此期间可以处理其他任务,不存在“Busy”等待。
  3. 检查中断优先级:如果使用了SPI中断,确保它的优先级安排合理,避免被高优先级中断长时间阻塞,导致状态无法及时处理。

4. 性能飞跃:利用DMA释放CPU,实现高效SPI通信

当SPI用于传输大量数据时,例如从SPI Flash读取固件、向LCD屏刷新一帧图像,如果使用查询或中断方式,CPU会被频繁占用,效率低下。此时,直接存储器访问(DMA)就是你的救星。

4.1 DMA与SPI配合的工作原理

DMA控制器可以在不打扰CPU的情况下,在外设(如SPI数据寄存器)和存储器(如SRAM中的数组)之间直接搬运数据。对于SPI发送:

  1. 你配置DMA:源地址是内存中的发送缓冲区地址,目标地址是SPI的DR(数据寄存器)地址。
  2. 设置传输数据量(如1024字节)。
  3. 启动DMA和SPI(使能SPI的TX DMA请求)。
  4. DMA会自动将发送缓冲区的数据,一个一个地搬运到SPI->DR寄存器中,SPI硬件则自动将这些数据串行发出。
  5. 传输完成后,DMA产生传输完成中断,你可以在中断回调函数中处理后续事宜(如关闭片选、通知任务完成)。

对于接收,原理类似,DMA将SPI->DR寄存器中的数据搬运到内存的接收缓冲区。

4.2 配置DMA-SPI的关键细节

以STM32向LCD屏发送一帧数据为例:

  • 内存与外设的数据宽度:必须匹配。SPI数据寄存器通常是8位或16位,你的内存缓冲区也应定义为uint8_tuint16_t数组。如果SPI配置为16位数据帧,DMA的外设数据宽度也应设为半字(16位)。
  • 外设地址是否递增:对于SPI->DR这个固定寄存器地址,DMA的外设地址应设为不递增
  • 内存地址是否递增:对于连续的内存缓冲区,DMA的内存地址应设为递增
  • 循环模式:对于连续不断的流数据(如音频),可以开启DMA循环模式。对于单次帧传输,使用普通模式。
  • 中断使能:使能DMA传输完成中断(TC)和半传输完成中断(HT,如果用到)。在半传输中断中,你可以填充后半部分缓冲区(双缓冲区技术),实现无缝数据流。

4.3 DMA实战避坑指南

  1. 数据对齐问题:如果你用16位SPI模式(数据帧16位)发送一个8位数组,需要特别注意。DMA以16位为单位取数据,如果数组首地址是奇数,可能引发硬件错误。确保缓冲区地址对齐。
  2. 缓存一致性(Cache Coherence):在带有D-Cache(数据缓存)的高性能MCU(如STM32H7)上,CPU写入缓冲区的数据可能还在Cache里,并未真正写入SRAM。如果此时DMA直接从SRAM取数据,会取到旧数据。解决方法是在启动DMA前,执行缓存清理(Clean)操作(如SCB_CleanDCache_by_Addr)。
  3. SPI时钟与DMA速度的匹配:确保DMA的传输速度能跟上SPI的发送速度。如果SPI时钟很快(如50MHz),而DMA总线访问内存较慢,可能导致SPI等待数据,出现欠载(Underrun)错误。通常需要开启DMA的FIFO并合理设置突发(Burst)传输模式来优化。
  4. 传输完成判定:不要仅仅依赖DMA传输完成标志。对于SPI接收,在DMA完成后,最好再等待一下SPI本身的RXNE标志,或者查询SPI总线空闲标志(BUSY位为0),确保最后一个字节也已完全移出移位寄存器。

5. 当硬件SPI不够用:模拟SPI的灵活性与代价

不是所有MCU都有足够多的硬件SPI外设,或者你的硬件SPI引脚可能被其他功能占用。这时,用普通GPIO口通过软件“模拟”SPI时序,就成了一个可行的方案。热词中提到的“STM32模拟SPI程序”和“模拟SPI获取MAX31855数据”就是典型场景。

5.1 模拟SPI的实现要点

模拟SPI的核心就是用代码控制GPIO的高低电平变化,严格按照目标模式(如Mode 0)的时序来产生SCLK,并在正确的时刻设置MOSI和读取MISO。

// 以Mode 0为例的模拟SPI发送一个字节函数(简化版) void SPI_Soft_SendByte(uint8_t byte) { for(int i = 7; i >= 0; i--) { // MSB first // 在时钟第一个边沿(上升沿)前,准备好数据位 if(byte & (1 << i)) { MOSI_GPIO_Port->BSRR = MOSI_Pin; // 拉高MOSI } else { MOSI_GPIO_Port->BRR = MOSI_Pin; // 拉低MOSI } // 产生时钟上升沿(CPOL=0, 所以从低到高) SCLK_GPIO_Port->BSRR = SCLK_Pin; delay_ns(50); // 短暂延时,模拟时钟高电平时间 // 产生时钟下降沿,数据位将在此时改变(为下一位准备) SCLK_GPIO_Port->BRR = SCLK_Pin; delay_ns(50); // 模拟时钟低电平时间 } }

5.2 模拟SPI的优缺点与适用场景

  • 优点
    • 引脚完全自由:可以使用任意GPIO,极大提高了布线灵活性。
    • 不受硬件限制:可以模拟非标准的SPI时序(如特殊时钟占空比),或者驱动那些时序要求古怪的器件。
    • 调试直观:每一步都由代码控制,便于单步调试和逻辑分析。
  • 缺点
    • 速度慢:受限于CPU执行指令和软件延时循环的速度,通常最高只能达到几百KHz到几MHz,远低于硬件SPI(可达数十MHz)。
    • CPU占用率高:通信期间CPU被完全占用,无法执行其他任务。
    • 时序精度差:软件延时受中断、系统负载影响,时序抖动(Jitter)大,对时序严格的器件不友好。

5.3 模拟SPI的实战技巧

  1. 使用硬件定时器替代delay_ns:为了获得更精确的延时,可以使用一个基本定时器(如TIM)产生精确的微秒或纳秒级中断,在中断里翻转时钟和数据。这能大幅提升时序精度和最大速度。
  2. 针对读取优化:对于像MAX31855这种需要先发送命令再读取数据的芯片,模拟SPI时,可以在发送完命令后,立即将MOSI引脚配置为浮空输入(或上拉),避免总线冲突,然后继续产生时钟并读取MISO。
  3. 注意GPIO速度:将用于模拟SPI的GPIO配置为最高输出速度(如Very High),以减少信号边沿的上升/下降时间。

6. 调试艺术:从“没有数据”到稳定通信的完整排错链路

遇到SPI通信失败(如热词中的“drv8711 spi 没有数据”)是家常便饭。建立一个系统性的排查思路,远比盲目尝试有效。

6.1 第一阶段:硬件与基础配置检查

  1. 物理连接:用万用表检查所有线路(SCLK, MOSI, MISO, CS, GND, VCC)是否连通,有无短路、虚焊。这是最基本也最常被忽略的一步。
  2. 电源与地:确保从机供电电压在额定范围内,电流充足。测量从机VCC引脚的实际电压。共地必须良好。
  3. 引脚复用:确认MCU的SPI引脚已正确映射到AF(复用功能),并且GPIO模式设置正确(SPI SCK/MOSI通常为复用推挽输出,MISO为浮空输入或上拉输入)。
  4. 时钟与模式反复核对从机数据手册要求的SPI模式(CPOL/CPHA)、时钟极性和相位、数据位顺序(MSB/LSB)、数据帧长度(8/16位)。这是错误重灾区。
  5. 片选信号:用逻辑分析仪或示波器观察CS信号。是否在通信前被拉低?通信结束后是否被拉高?拉低和拉高的时机是否与SCLK有足够的建立/保持时间?

6.2 第二阶段:信号完整性分析(必须使用逻辑分析仪或示波器)

没有仪器,调试SPI如同盲人摸象。连接逻辑分析仪,抓取SCLK, MOSI, MISO, CS四路信号。

  • 看波形:SCLK频率是否与配置一致?波形是否干净(无过冲、振铃)?MOSI上是否有主机发送的数据?MISO线是始终为高/低,还是完全没有变化?
  • 分析数据:解码SPI数据。主机发送的指令和地址是否正确?从机在应该回复的时钟周期内,MISO上是否有数据变化?
  • 查时序参数:测量从CS有效到第一个SCLK边沿的时间(CS setup time),以及最后一个SCLK边沿到CS无效的时间(CS hold time)。测量数据在SCLK采样边沿前后的稳定时间(Data setup/hold time)。这些参数必须满足从机数据手册的要求。

6.3 第三阶段:软件逻辑与交互流程排查

  1. 初始化顺序:确保先完成GPIO、SPI外设、DMA(如果使用)的初始化,最后再使能从机(如拉低CS)。
  2. 指令序列:许多SPI器件有复杂的命令序列。例如,W25Q64在写操作前必须先执行“写使能”命令(0x06)。是否遗漏了必要的命令?命令、地址、数据的字节顺序是否正确?
  3. 从机状态:很多器件有状态寄存器。通信失败后,尝试读取状态寄存器,可能能发现“写保护使能”、“忙标志”等错误状态。
  4. 延时:从机执行某些操作(如Flash擦除、写入)需要时间。操作后是否等待了足够的时间(查询状态寄存器或简单延时)再进行下一次通信?
  5. 中断与DMA:如果使用了中断或DMA,检查中断服务函数或回调函数是否被正确触发和执行。检查DMA传输完成标志和SPI错误标志(如OVR溢出错误)。

以“DRV8711 SPI无数据”为例的专项排查

  1. 确认DRV8711的电源(VM, VCP)和逻辑电源(DVDD)是否正常。
  2. 检查SPI模式。DRV8711数据手册明确要求CPHA = 0, CPOL = 0或1均可(通常用0)。重点检查CPHA
  3. 检查数据帧格式:DRV8711是16位数据帧(高8位为地址/指令,低8位为数据),且MSB先行。确保主机SPI配置为16位数据大小。
  4. 使用逻辑分析仪抓取波形。看主机是否发出了正确的16位数据(例如,写CTRL寄存器:地址0x01,数据0x01,则发出的16位应为0x0101)。看MISO线在主机发送后的下一个16位时钟周期内,是否有数据返回(读操作时)。
  5. 检查DRV8711的nSLEEP引脚是否被拉高(退出睡眠模式),nFAULT引脚状态是否正常。

7. 跨界通信:FPGA与MCU的SPI协同设计

热词中提到了“FPGA与32单片机的SPI通信”和“FPGA SPI Slave”。当MCU作为主机,FPGA作为从机时,这要求我们在FPGA端用硬件描述语言(如Verilog)实现一个SPI Slave接口。

7.1 FPGA SPI Slave设计核心

FPGA端的SPI Slave需要精确捕捉主机的时序。

  1. 同步设计:使用主机的SCLK作为FPGA内部相关逻辑的时钟源或采样时钟,确保同步。
  2. 边沿检测:检测SCLK的边沿(根据CPOL/CPHA)。例如,对于Mode 0,在SCLK的上升沿采样MOSI数据,在下降沿更新MISO数据。
  3. 片选同步:CS信号作为使能。当CS有效时,才启动接收和发送逻辑。
  4. 移位寄存器:实现一个移位寄存器,在SCLK的每个有效边沿,将MOSI数据移入,同时将待发送数据移出到MISO。
  5. 字节/帧边界处理:当移位寄存器移满8位或16位(根据约定)后,将接收到的数据锁存到内部寄存器,并准备好下一个要发送的数据。

7.2 通信协议设计建议

单纯的SPI接口只定义了物理层和数据链路层。上层应用协议需要双方约定。

  • 指令-响应式:类似Flash。MCU先发一个命令字节,FPGA解析后执行相应操作(如读取内部某个寄存器),并在后续时钟周期返回数据。
  • 流式:MCU持续发送数据流,FPGA持续接收并处理(如视频像素数据);或FPGA持续产生数据流(如ADC采样值),MCU持续读取。
  • 双工交换:每个SPI时钟周期,双方同时交换一位数据。可以设计成每帧数据都包含双方的信息。

7.3 调试要点

  1. 仿真先行:在Quartus/Vivado中编写Testbench,模拟MCU的SPI时序,对FPGA的SPI Slave模块进行充分仿真,验证其在不同模式下的行为。
  2. 信号抓取:在实际硬件上,使用FPGA内部的逻辑分析仪(如Xilinx的ILA, Intel的SignalTap)同时抓取SCLK, CS, MOSI, MISO以及FPGA内部的关键状态信号(如移位寄存器值、状态机状态)。这是定位FPGA端问题的利器。
  3. 时钟域处理:如果FPGA内部处理时钟与SPI的SCLK不同源,那么接收到的数据需要经过跨时钟域同步(如两级触发器)才能送入FPGA的其他逻辑部分,避免亚稳态。

SPI协议的精髓在于其简洁和灵活。它没有过多的规则束缚,把效率和控制权很大程度上交给了工程师。正是这种特点,使得深入理解其每一个细节变得至关重要。从模式匹配到片选管理,从DMA优化到模拟实现,再到复杂的系统级调试,每一步都需要清晰的逻辑和细致的验证。我个人的体会是,调试SPI问题,逻辑分析仪是你的最佳伙伴,而数据手册是你的最高法律。不要相信任何“默认”或“猜想”,一切以波形和手册为准。当你成功驯服一个棘手的SPI设备,看到数据稳定流畅地传输时,那种成就感,正是嵌入式开发的乐趣所在。希望这篇总结,能帮你少走些弯路,更自信地应对下一个SPI挑战。

← 返回列表