☰
SPI帧结束检测的实用指南:协议缺陷、DMA中断与工程方案
2026/10/13 6:06:16 网站建设 项目流程

如果你用SPI和从设备通信时遇到过“数据偶尔错位一个字节”“DMA收到的数据长度总是对不上”“最后一个字节读完了,从设备好像还没反应过来”这类问题,恭喜你,你踩中的正是SPI开发里最经典的一个坑——帧结束检测。SPI协议本身没有像UART那样的起始位和停止位,也没有像I2C那样的ACK和STOP条件,它只提供时钟、数据和片选,帧的边界必须由通信双方额外约定。一旦约定不明确,小到数据错位,大到整个通信链路跑飞,都是家常便饭。我早期做MEMS传感器驱动时,就为这个“何时才算读完一帧”的问题折腾了好几个通宵,后面花了大量时间整理出一套从协议层到DMA中断层的检测思路,这篇文章就是把那段经验完整拆开,讲清楚SPI帧结束检测的本质、方法以及真正能在工程里落地的方案。

1. 协议先天的漏洞:SPI为什么没有一个“帧结束”标志

1.1 对比UART和I2C,SPI把边界问题全部交给了人

先别急着上手改代码,我们要先把SPI协议本身的问题看清楚。

UART能够靠一个起始位加一个停止位来界定每个字节的边界,接收端一旦检测到起始位,就知道接下来要采样一个字节,采样完看到停止位,一个字节就算完整收下了。I2C更干脆,它用START条件表示总线事务开始,用STOP条件表示整个事务结束,中间每个字节都带ACK反馈,设备多了一拍就知道该停了。这两种协议都在协议层面内置了“边界信息”,收发双方不需要额外约定。

SPI呢?它只有四根线:SCLK、MOSI、MISO、CS。SCLK负责给数据打节拍,MOSI/MISO分别承载主机到从机和从机到主机的数据,CS用来选中从机。协议本身没有任何“这一帧到最后了”的显式标志。主机停止产生时钟,从机的移位寄存器就停在原地;SCLK不再翻转,数据线也进入高阻或维持状态。你无法仅仅通过观察SCLK自己看出传输是否真正完成,因为主机可能只是暂时停一下,下一拍又继续拉时钟。帧的边界,完全是靠通信双方在协议之外达成的默契,这种默契一旦定义不清楚,通信就出问题。

这也是“SPI master: reliably detecting end of frame”这个需求存在的根本原因。master要把一个字节流看作一帧,必须具备“外部约定+硬件信号+软件判断”的组合能力。真正可靠的帧结束检测,第一步就是承认:SPI协议本身不会告诉你帧在哪里结束,你必须自己设计一个可靠的结束信号。

1.2 时钟停了不等于帧结束了:主从双方对“结束”的理解差异

在SPI通信里,有一个非常容易误导人的直觉:SCLK停了,传输就应该结束了。这个直觉在单个字节的层面勉强成立,但在多字节帧的层面是完全错误的。

打个比方:SCLK就像一条传送带上持续转动的齿轮,齿轮停下来,货物只是暂时不往前走了,但“这箱货的包装在哪里封口”和“这箱货是不是已经完整发完”,齿轮本身是不管的。主机停止SCLK,从机的移位寄存器里可能只凑了半个字节,也可能是完整的一个字节,也可能正好凑满一帧数据。对从机来说,它判断一帧结束通常依赖CS信号拉高;对主机来说,它判断一帧结束通常依赖自己计划好的发送/接收字节数。这两者如果不一致,产生的问题非常隐蔽。

实际项目中我遇到过一种典型场景:用四线SPI读一个从设备的状态寄存器,从设备在CS下降沿之后准备数据,在CS上升沿锁存最后一个字节。软件上我先是发命令,然后连续接收4个字节,接收完最后一个字节后马上拉高CS。结果呢?偶尔MISO上读回来的数据错位一个字节,状态机完全对不上。抓波形才看清楚,从设备要等CS上升沿才会把最后一批数据送出来,而我在它送出来之前就已经把CS拉高了,等于把最后一个字节的时序窗口硬生生截断了。

这个案例说明一个核心问题:帧结束不是主机单方面宣布的,而是主机与从设备共同协作的结果。主机必须知道从设备期望的结束条件是什么,并严格按照从设备的时序要求去拉高CS或停止时钟。否则,SCLK停了,程序以为帧结束了,但从设备根本还没准备好。

1.3 帧结束不可靠时,软件里会看到哪些妖蛾子

帧结束检测不可靠,不会直接报错,而是以更隐蔽的方式暴露。我把实际调试中见过的问题整理了一下,你可以对照自己遇到的现象。

  • 数据整体错位:读回的寄存器值总是右移或左移一个字节,帧头出现在帧尾的位置。比如用SPI读W25Q128的JEDEC ID,标准响应是0xEF 0x40 0x18,结果读回来变成0x18 0xEF 0x40。这往往是因为CS拉高时机错误,或者接收缓冲区长度设置偏了一个字节。
  • DMA接收长度不稳定:配置好固定长度的DMA接收,数据总是时对时错。用调试器一看NDTR寄存器,剩余计数有时候是0,有时候是1,有时候是负数——DMA把上次的残留数据边界带到了下一次传输里。
  • 最后一个字节丢失或重复:从设备发送的数据长度是N字节,主机只收到N-1字节或者N+1字节。原因通常是DMA接收缓冲区的触发条件设置错误,或是传输完成中断跟CS上升沿的先后关系没处理好。
  • 偶发超时:通信链路大部分时间正常,偶尔出现一次超时,重启后又恢复。这种最难排查,往往是CS信号在高速传输下沿不够陡峭,或者从设备的片选响应时间刚好卡在时序边缘。

我把这些现象拿到一块,几乎都可以归因到一个点上:接收端没有一个明确、可重复、不受外部扰动影响的帧结束判定依据。要么依赖硬件CS,但CS管理得不干净;要么依赖软件计数,但计数和实际硬件时序不一致。所以接下来要做的,就是把帧结束判定的几个主流方案逐一拆解,看看各自的适用条件和坑在哪里。

2. 最正统的边界信号:片选CS,以及它的节奏感

2.1 硬件NSS和软件GPIO片选的本质区别

在所有可用的帧结束信号里,CS是最自然、最可靠的一个。CS拉低代表主机开始和某个从设备通信,CS拉高代表这一帧结束。从设备几乎都是这么设计的:CS低电平时把数据往总线上送,CS高电平时停止发送、锁存结果、释放MISO。所以只要CS波形管理好,帧边界就稳稳的。

但CS本身有两种实现方式,一种是用MCU的硬件NSS引脚,一种是用普通GPIO做软件片选。这两种方式在帧结束检测上的表现差别非常大。

硬件NSS引脚由SPI外设内部自动控制。在主机模式下,配置合适的控制位后,外设会在传输开始时自动把NSS拉低,传输结束时自动拉高。这个方式最省CPU,而且CS动作和SCLK动作之间是完全同步的,硬件电路保证时序不出错。但它的一个短板是,很多MCU的硬件NSS只在“单次发送/接收完成后”翻转一次,而一个协议帧往往需要“发送命令+连续接收多个字节”多步组合,硬件NSS很难精确地在这组多步传输中保持CS持续拉低,然后到最后一步结束才拉高。这时你就不得不退回到软件GPIO片选。

软件GPIO片选是嵌入式开发中最常见的做法。主机用任意一个GPIO引脚模拟CS,发命令之前拉低,整帧结束后拉高。这个方式的优点是灵活,CS拉高拉低的时机完全由软件控制,可以精准匹配从设备的时序要求。缺点是,CS动作和SCLK动作之间的同步关系完全依赖代码执行时序,一旦中间插入了中断或调度延迟,CS沿的位置就可能偏移,导致帧边界抖动。这也是软件片选最让人头疼的一点,尤其在高SPI时钟频率下,一个中断响应延迟就可能让CS的高低电平宽度变化几百纳秒到几微秒,从设备可能因此误判帧结束。

2.2 拉高片选的一个坑:到底应该在哪里拉高?

“拉高CS”听起来简单,实际操作中我发现至少有三个时机需要仔细考量。

第一个时机是“最后一个SCLK沿之后立即拉高”。这种方式适用于从设备在CS上升沿锁存数据的场景。比如标准的SPI Flash读操作,主机在读完最后一个字节后,通常要确保最后一个SCLK上升沿已经正确采样,然后拉高CS,Flash才会把读到的数据锁存到内部缓存。这里有个细节:很多从设备要求CS上升沿发生在最后一个SCLK沿之后,并且两者之间有一个最小的建立时间,数据手册上一般写作tCSH或者tCLCH。如果你在最后一个SCLK还没完整结束时就拉高CS,从设备可能采样不到最后一位数据。

第二个时机是“在接收传输完成中断里拉高CS”。这是最常见的做法,但里面有个隐患。DMA或外设的传输完成中断并不是在最后一个SCLK沿上触发的,而是在最后一个数据写入接收寄存器之后、由硬件置起标志,软件再等到中断优先级轮到它,可能已经在几十个时钟周期之后。对高速SPI来说,这几十个周期意味着CS比预期晚拉高了很久。好在大多数从设备对CS高电平的保持时间只要求很短的脉冲,晚一点拉高问题不大。真正要小心的是相反的场景:某些从设备要求CS必须在最后一个字节发送后的特定窗口内拉高,太晚拉高它会把下一帧的数据也当成当前帧的一部分。这时候你就要考虑在DMA传输完成事件后,立即通过硬件事件链接或定时器触发CS拉高,而不是等中断。

第三个时机是“提前拉高CS再收尾”。有些从设备例如某些编码器或ADC,内部有移位寄存器和转换逻辑,CS上升沿代表一个转换周期的结束。对这类设备,CS的拉高时机必须和SCLK严格同步,差一个时钟周期都不行。软件GPIO模拟在这种场景下几乎很难到达硬件时序的稳定度,所以必须考虑用硬件NSS、外部定时器比较输出或者FPGA来做片选控制。我的建议是:在选型阶段,如果从设备对CS时序要求特别严格(数据手册时序图里有tCSH这种参数且值很小),优先选支持硬件NSS+可编程帧长度的MCU,或者干脆用带DMA和定时器的组合方式来实现片选脉冲。

2.3 片选电平持续时间与从设备的响应速度

片选拉高之后,还有个容易被忽视的问题:CS高电平要保持多久,从设备才算完成了内部处理?很多从设备在CS上升沿之后还需要一段时间来做内部状态切换、刷新数据或者把MISO释放回高阻态。如果主机紧接着又发起下一帧通信,CS拉低间隔过短,从设备可能根本还没来得及处理上一帧的数据。

我处理过一颗MEMS加速度计,数据手册上写着CS高电平持续时间最少要1微秒。起初我用软件片选,每帧之间只间隔几百纳秒,结果从设备返回的数据经常夹杂着上一次读命令的残留值。当时逻辑分析仪显示CS波形是正常的,但放大看,两个CS低电平脉冲之间的高电平时间只有约300ns,离1微秒的要求差了很远。后来在每帧结束后加了一个微秒级延时,或者用定时器确保CS高电平时间满足要求,数据就正常了。

这个点对“可靠检测帧结束”来说算是最后一公里:帧结束信号本身已经正确产生,但接收方还需要一点时间消化。帧结束的判定不能只看信号沿,还要给设备和协议留足余量。在实际编码中,我通常会把CS拉高后的代码路径设计成带最小间隔检查的,尤其是DMA模式下连续发帧时,要确保上一帧的CS上升沿到下一帧CS下降沿之间的时间不小于从设备要求的CS高电平时间。

3. 没有CS可用或CS不可靠时,怎么判断一帧读完

3.1 固定长度帧:DMA传输完成中断的局限与正确用法

很多从设备是固定长度帧,比如每个转换结果固定占2字节,或者每次读寄存器固定返回4字节。这种场景下,DMA传输完成中断看起来是最直接的帧结束信号。

DMA配置成固定长度传输,指定内存缓冲区长度就是帧长,当DMA搬运完最后一个字节,硬件会置传输完成标志并触发中断。在中断里拉高CS,理论上就可以得到一个精确的帧结束点。

但实际用下来,DMA传输完成中断有它自身的问题:它比最后一个SCLK沿要晚不少。以STM32的SPI+DMA为例,DMA在普通模式下一个字节一个字节地把SPI_DR里的数据搬到内存,最后一个字节搬完时,DMA产生传输完成事件,但这个事件要经过总线仲裁、NVIC调度才能进入中断服务函数。如果此时系统里还有更高优先级的中断在跑,CS拉高时间可能会被推迟几十微秒。对于低速从设备,这个延迟不影响;但如果从设备要求CS上升沿必须在最后一个SCLK之后的某个窄时间窗内,这个延迟就是致命的。

解决思路有两个。第一个思路是,把CS拉高动作从CPU中断里摘出来,用硬件事件触发。比如某些MCU的DMA传输完成事件可以配置成直接触发另一个定时器或引脚翻转,这样CS拉高和DMA接收完成之间的硬件延迟只有几个时钟周期,而不是软件中断延迟。第二个思路是,如果从设备不要求CS严格同步,那DMA传输完成中断就足够可靠,只要保证中断响应及时即可。实际选哪种,取决于你手头从设备的具体时序要求。

另外,DMA固定长度接收时还有一个常见细节:DMA的CNDTR寄存器(或对应平台的剩余传输计数)在传输完成时归零。如果你在中断里读取的是“剩余数”,一定要意识到传输完成后它可能是0,而不是你期望的帧长。有些平台还支持“自动重载”模式,传输完成后自动把NDTR填回初值,这时候要用DMA_TC标志位判断当前传输周期是否完整。这类寄存器层面的细节,往往是数据错位的真正来源。

3.2 变长帧:空闲超时才是真正好用的方案

固定长度帧有清晰的DMA完成点,但变长帧就麻烦得多。很多SPI从设备的帧长不是固定的,它取决于主机的读写命令和寄存器地址。比如一个带FIFO的传感器,主机可能需要读取1字节的FIFO状态、也可能需要读取16字节的FIFO数据,帧长完全由主机决定。这种情况下,DMA的传输完成中断不再是一个固定的帧结束信号,因为DMA不知道这次该收多少字节是“对”的。

一个可靠的变长帧判定策略是“空闲超时”。具体做法是:SPI接收使能后,当最后一个字节到达且总线上不再有新的数据时,启动一个定时器计时,如果在设定的超时时间内没有新的字节到达,就认为当前帧结束。这个思路类似UART接收不定长数据时常用的“IDLE中断”或者“字符超时”处理,但SPI外设本身通常没有IDLE中断,所以需要额外通过定时器来实现。

实现方式有两种。第一种,如果你用的是带FIFO的SPI外设,比如STM32的部分系列,可以对SPI接收FIFO设置水位线,当接收数据低于阈值时触发接收事件,在事件里刷新看门狗/定时器。第二种更通用,用一个硬件定时器作为超时定时器,在每次接收完成事件(对DMA来说是半传输或部分传输中断,对普通接收来说是RXNE中断)里重新装载定时器计数值,当定时器溢出时就认为SPI总线空闲。要注意的是,这个定时器的超时时间必须大于SPI协议上相邻两个字节之间的最大间隔,否则一帧中间稍慢一点就被误判成一帧结束。

3.3 STM32上SPI+DMA+空闲超时的实测配置

这里我给一个可以在STM32F1/F4系列上实际跑通的思路和关键代码。平台不同寄存器名会有差异,但思路是通用的。

核心思想:SPI接收用DMA,设定一个较大的DMA缓冲区长度。每个字节到达后,DMA自动搬运,同时外设有接收事件。我再用一个定时器作为“空闲定时器”,每次SPI有接收事件就清零重装,定时器溢出就表示超时,此时从DMA缓冲区已接收的数据长度中解析帧。

// 假设定时器TIM3用于空闲超时,SPI1_DMA接收 // 设置空闲超时时间为字节间隔的8倍,例如SPI时钟18MHz,单字节间隔约0.44us,超时设3us void spi_idle_timeout_init(uint32_t timeout_ns) { // 使能TIM3时钟 RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; // 预分频器:把定时器时钟分频到1MHz,则1个tick = 1us TIM3->PSC = (SystemCoreClock / 1000000) - 1; // 自动重装载值:超时时间转换为tick数 TIM3->ARR = timeout_ns / 1000; // 使能更新中断(超时溢出触发) TIM3->DIER |= TIM_DIER_UIE; NVIC_EnableIRQ(TIM3_IRQn); // 初始关闭定时器,收到SPI数据后再启动 TIM3->CR1 = 0; } // SPI接收中断里刷新超时定时器 void SPI1_IRQHandler(void) { if (SPI1->SR & SPI_SR_RXNE) { // 读DR清标志,或者由DMA处理时这里不需要 TIM3->CNT = 0; // 清空计数 TIM3->CR1 |= TIM_CR1_CEN; // 启动/继续定时 } } // 定时器溢出中断:表示SPI总线空闲,帧结束 void TIM3_IRQHandler(void) { if (TIM3->SR & TIM_SR_UIF) { TIM3->SR = 0; // 清标志 TIM3->CR1 &= ~TIM_CR1_CEN; // 停掉定时器,等待下一帧 // 此时DMA已经接收了若干字节,从NDTR算出已接收长度 uint16_t received = DMA_BUFFER_SIZE - DMA1_Channel2->CNDTR; if (received > 0) { frame_process(rx_buffer, received); // 处理完整帧 } } }

这段代码有几个细节值得注意。第一,DMA缓冲区长度必须比最大帧长大,否则数据可能溢出。第二,DMA在普通模式下传输完成后,如果没有及时处理,CNDTR可能停在某个残留值,所以最好是每次帧处理完之后重新初始化DMA缓冲区,或者在DMA传输完成中断里也做一次收尾。第三,TIM3的CNT清零时机很重要,一定要在SPI接收事件里清,不能放在主循环里懒洋洋地清,否则高负载下定时器没有及时刷新,会把一帧数据硬生生切成两半。

实测下来,用这套配置处理一个变长帧的SPI从设备,只要超时时间设置合理(一般是单字节在最坏情况下间隔的3~5倍),帧判定的可靠性很高。而且因为空闲定时器可以跨帧复用,连续多帧传输时不需要重新配置,只需要每帧处理完重置DMA缓冲区和定时器状态,实现起来也不复杂。

4. 从协议层面根治:让每一帧自带“结束”信息

4.1 帧头、长度字段、校验:三段式的价值

靠硬件信号检测帧结束,永远是在“信号层面”解决问题。到了更复杂的系统里,如果总线噪声、芯片时序偏差、电压波动都会扰乱CS波形,那无论你的检测逻辑写得多严密,帧边界依然可能被干扰。这时候就应该把一部分可靠性搬到协议层去,让帧本身带上“结束”信息,软件在解析时不再完全依赖信号沿。

一个典型的可靠SPI帧格式可以这样设计:

  • 帧头:固定值,比如0xA5或0x5A,用于识别帧起始。帧头可以让接收端排掉噪声和错位数据,一旦在数据流中找不到帧头,就知道帧同步丢了。
  • 命令/长度字段:用一两个字节指明接下来的数据长度。不管是主机发给从机还是从机返回给主机,长度信息都是帧结束判断的关键依据。有了长度字段,接收端在收到长度之后就能精确计算还要收多少字节,不需要猜。
  • 数据区:实际携带的数据。
  • 校验字段:简单的CRC8或累加和,用于校验数据完整性。校验通过,才能确认这一帧真正收发正确。

我在一个8位MCU上的SPI多设备通信项目里,把简单的裸SPI读写改成“帧头+长度+CRC”的协议后,之前偶尔出现的错位问题基本清零。原因很简单:长度字段给了接收方一个精确的字节计数,接收方把收到的字节数累加,等到和长度字段一致时,这一帧就算收齐了。即使CS信号偶尔多抖了一下,或者DMA多搬了一个字节,CRC校验也能把坏帧筛掉,不会让坏数据进入业务流程。

4.2 背靠背传输时,如何防止帧粘连

“帧粘连”是背靠背传输场景下最常见的坑。主机连续发送两帧数据,时间间隔很小,从一方看,上一帧的结束和下一帧的开始几乎重叠。如果接收方的帧结束判断只依赖CS上升沿,而CS上升沿被干扰或延迟,那么接收方可能把第一帧的尾部数据误认为是第二帧的头部数据,整条数据链就乱了。

解决帧粘连的办法,从我实际经验看有三个方向。第一个方向是在每帧之间插入一个“强制空闲间隔”,也就是上一帧CS拉高之后,保证总线保持空置状态一段时间(比如几个字节周期的长度)再开始下一帧。这个间隔让从设备和主机的接收状态机都有时间复位。第二个方向是上面说到的发送长度字段,接收端收到长度字段之后只按长度收数据,其余多出来的字节全部丢弃或等待下一个帧头。第三个方向是在帧尾加一个特定的结束标记,比如双字节的0x0D 0x0A,接收端看到结束标记才认为帧结束。这三个方向可以组合使用,最稳妥的工程方案是“帧头+长度+N字节数据+校验”,因为结束标记在数据内容随机时会误判,而长度字段配合帧头更普适。

我还见过一种“伪帧粘连”,实际不是粘连,而是接收缓冲区没有清干净。上一帧的数据还剩一点没处理完,下一帧的DMA传输就已经覆盖过来了。这时如果不是用循环DMA,而是普通DMA模式,最好在每帧处理完之后把DMA缓冲区清空或重置,避免旧数据残留在缓冲区里被当成新帧的头部。

4.3 多字节命令与自动连续读的场景怎么处理

SPI从设备里有一类比较特殊,它们支持“自动连续读”。意思是主机只要在CS低电平期间读取,从机会自动滚动输出后续的数据,读多少个字节由主机决定。典型的就是各类Flash芯片的快速读操作,以及部分ADC芯片的连续转换读取模式。

这类设备的帧结束检测有个特点:帧长度完全取决于主机想读多少字节,从机并不会主动结束。此时作为master,你要在发出读命令之后立刻确定读长度,并且把这个长度纳入帧结束判定条件。有两种常见做法,一种是主机固定读N个字节,N就是你需要的寄存器数据量,用DMA配合固定长度N,传输完成就是帧结束;另一种是主机提前声明“读长度”,比如先写一个命令字节+地址+长度,从设备返回长度对应的数据,主机用长度值作为接收计数。

我在一个雷达传感器项目里就遇到过这种自动连续读的设备,一开始想偷懒,每次读固定32字节的原始数据,但帧率高了之后偶尔会丢数据。后来把读操作改成“先读2字节的数据头,从数据头里解析出本次有效数据长度,再根据长度发起第二轮DMA读取”,虽然多了一次事务交互,但数据完整性大幅提升,每一帧都干净利落,调试也容易得多。工程上很多时候宁可多一次握手,也不能让帧边界模糊。

5. 经验复盘:调试帧结束问题的一线实操

5.1 先看波形,再写代码

我调试SPI问题时有一个朴素但高效的原则:先相信示波器,再相信逻辑分析仪,最后才相信源代码。因为帧结束问题本质上是电平时序问题,代码逻辑再正确,只要信号波形的边沿不对,数据就是错的。

抓波形时,把CS、SCLK、MOSI、MISO四根线同时接上逻辑分析仪,采样率尽量高一点,然后触发条件设置为CS下降沿,把一帧完整抓下来。接下来重点看三个地方:CS拉低的位置距离第一个SCLK是否满足从设备的tCSS(CS建立时间);最后一个SCLK和CS上升沿之间的距离是否满足tCSH;CS高电平时是否有毛刺。我之前排查一个SPI Flash读数据错位的问题,就是通过逻辑分析仪发现CS上升沿比最后一个SCLK晚了差不多一个字节的时间,而这颗Flash要求CS必须在最后一个SCLK之后的短时间内拉高,否则内部状态机就会出现偏移。代码逐行看半天看不出来的问题,波形一抓就明白了。

5.2 两个真实故障的定位全过程

先说第一个故障。项目里用STM32F103主控读一颗SPI接口的数字温度传感器,现象是每读10次左右会出现一次温度值跳变,偏差特别大。刚开始怀疑是电路干扰,加了滤波电容也没用。后来让DMA在接收模式下共接收5个字节,CS用GPIO软件控制,在接收到第5个字节后触发DMA传输完成中断,中断里拉高CS。逻辑分析仪抓到一次异常波形,发现某次中断响应被一个高优先级的串口中断插队了,CS拉高时间比正常情况晚了约20微秒。对于这颗传感器来说,它的SPI状态机在主机的CS上升沿时开始内部处理,但20微秒的延迟导致它多采了一个无效字节,温度数据就跳到错误值了。解决办法是把GPIO拉高CS的操作从DMA中断服务函数中拆出来,改用DMA传输完成事件直接触发一个定时器比较翻转引脚,硬件延迟降低到几微秒以内,问题消失。

再说第二个故障。一个STM32G4项目用SPI+DMA循环模式接收来自ADC的数据流,DMA缓冲区设成64字节,ADC每一帧是8字节。现象是每8帧里有一帧数据和前一帧重复。抓波形看不出SPI时序异常,CS也正常。后来定位到问题出在DMA循环模式下缓冲区边界处理上:DMA在缓冲区地址回绕的那一帧,由于时序竞争,覆盖了上一帧的末尾,导致数据错位。解决办法是给DMA缓冲区增加一帧的余量,并且在软件解析时,用时间戳对齐每8字节一帧的边界,避免在循环地址回绕处跨帧读取。这类问题在DMA循环模式下非常隐蔽,因为波形上看不到任何异常,纯粹是内存缓冲区管理的问题。

5.3 我在这个坑里积累的三条原则

多年下来,我总结出三条处理SPI帧结束问题的原则,基本每次都能用上。第一条,能靠硬件CS就不靠软件数时钟,能靠事件/定时器就不用中断服务程序。硬件能确定的时序,不要让软件去抢,这是可靠性的第一来源。第二条,帧格式里一定要有长度信息或校验信息,哪怕只是最简单的累加和,它能在信号层面漏判的时候兜底,把你从“查半天找不到原因”的困境里解放出来。第三条,调试时先抓波形再改代码,把CS、SCLK和最后几个字节的时序波形拿到手里,再开始怀疑驱动,这会节省大量时间。SPI的帧结束问题,大部分时候不是芯片坏了,而是时序没有对齐。把这三点记在心里,你可以少熬很多个夜。

最后分享一个我一直在用的习惯:新项目拿到一颗SPI从设备,我会先建一个最小测试工程,只做一件事——用固定的帧长读回一个固定寄存器,然后在示波器上看CS的上升沿是否落在预期位置。这一步通过之后,我才会在驱动里加入DMA、中断、协议封装等复杂逻辑。先确认最基本的帧结束信号可靠,后面所有复杂的判定才有意义。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询