STM32H743 SPI发送时SCK为何空闲一半?从FIFO到DMA优化全解析
2026/8/31 22:57:58 网站建设 项目流程

1. 先还原现场:SCK为什么一半时间在“沉默”

如果你用示波器或逻辑分析仪去抓STM32H743在SPI发送数据时的波形,大概率会看到这么一幅画面:CS已经拉低了,MOSI线上也确实有数据在变化,但SCK并不是想象中那样“均匀地、连续地”翻转,而是翻一阵、停一阵,整个传输周期拉长到理论时间的两倍左右。标题里说的“idles half of the time”,指的就是这个现象——SCK在有效传输窗口内出现了大量静止间隙。

有人看到这个波形第一反应是“SPI时钟极性配置错了”,或者“分频配得不对”。其实不然,如果极性配错,SCK的空闲电平会相反,但时钟翻转的频率和连续性不会变。真正的问题在于:数据没有及时喂进SPI的发送寄存器,导致移位寄存器在发送完一个字节之后,需要停下来等MCU把下一个字节写进来。SCK是跟着移位寄存器走的,发送寄存器空了,SCK自然就暂停翻转。

这个“停下来等”的过程,恰恰是所有MCU的SPI外设在使用中断方式、甚至某些不当的轮询方式时最容易踩的坑。STM32H743这颗芯片主频最高能跑到480MHz,CPU算力并不弱,但SPI外设本身是一条独立的硬件链路,它跟CPU之间隔着总线、FIFO、中断控制器。CPU处理完一个字节的中断、把数据写进SPI数据寄存器,这个响应链路中的每一环都有延迟。SPI时钟一旦配得比较高(比如20MHz以上),一个字节8个bit只需要不到0.4微秒就移完了,而CPU从中断触发到把下一个字节写入寄存器,随便就要一两微秒甚至更久。于是SCK就只能等。

我最早遇到这个现象是在用H743驱动一颗SPI接口的ADC转换芯片,SCLK配置到40MHz,理论上采样256个点只需要几十微秒,但实际抓波形发现传输时间翻倍还不止。当时第一反应是HAL库的API调用开销太大,后来换成寄存器操作、调整中断优先级,问题依然存在。最后把发送方式从逐字节中断改成DMA,波形立刻变成了干净利落的连续时钟。所以这篇文章咱们不绕弯子,直接说清楚:SCK出现间隙,本质上是数据供给速度跟不上SPI移位速度,而解决思路就围绕着如何让数据连续不断地流进发送寄存器。

2. 从SPI时序到底层机制:SCK间隙是怎么产生的

2.1 SPI的“连续传输”和“字节间隙”是两码事

要理解这个现象,先把SPI的基本传输模型拆开。SPI主机的移位寄存器在SCK的每个上升沿把一位数据从MOSI推出去,同时从MISO采一位数据进来。一个字节8位,也就是8个SCK周期。关键在于:SCK不是自由振荡的,它是被“有没有数据要发送”控制的。如果SPI外设的发送缓冲区里没数据,SCK就不会继续翻转。

协议层面的“连续传输”通常指CS保持拉低、多个字节之间没有CS释放;但硬件层面的“连续时钟”要求的是发送数据寄存器始终保持非空状态。这中间有个容易被忽略的细节:很多SPI外设内部有一个发送FIFO,软件往数据寄存器写一个字节,实际上进的是FIFO,移位寄存器从FIFO头部取数据。只要FIFO里有数据,SCK就持续翻转;FIFO空了,移位寄存器发完当前字节后就得停下等待。

STM32H743的SPI外设也不例外,而且它的FIFO深度比F103老内核的SPI深不少——H7系列SPI的TX/RX FIFO都是32位宽、深度8个字节(具体以参考手册为准)。也就是说,理论上你可以一口气往FIFO里塞多个字节,然后让SPI自己慢慢发。但这里有个实际问题:绝大多数人第一次写代码,都是用“发送一个字节 -> 等待TXE标志 -> 再发送下一个字节”这种最朴素的方式。在SPI时钟不高、中断响应够快的场景下这没问题,可一旦SPI时钟跑到25MHz以上,TXE置位到下一次写DR之间的时间窗口就变得非常短促,总线延迟、中断压栈、HAL库函数调用的开销叠加起来,SCK间隙就不可避免。

2.2 中断发送为什么必然出现间隙

我做了个简单测试:H743主频480MHz,SPI1挂在APB2上,APB2时钟配到120MHz,SPI预分频4分频得到30MHz SCK。用HAL_SPI_Transmit_IT发送512字节,示波器抓CS和SCK,你会看到每一两个字节后面就有一段明显的SCK停止期。停止期大概在几百纳秒到几微秒不等,平均下来有效吞吐只有理论值的一半左右,刚好对得上“idles half of the time”。

为什么用中断发送就必然有间隙?拆一下时间线:

  1. SPI发送FIFO里的字节被移位寄存器取走,TXE(TX FIFO empty)标志置位,触发中断;
  2. CPU响应中断,需要经历中断控制器仲裁、压栈、跳转到中断服务函数,这个时间在Cortex-M7上一般几十到一两百个时钟周期;
  3. 中断服务函数通过HAL库的回调机制找到对应SPI句柄,检查状态、调用发送函数把下一个字节写进DR;
  4. 写DR之后,FIFO可用的中断标志可能还需要额外周期才会重新置位。

整个过程顺利的话三四百个时钟周期,换算到480MHz主频下大概是0.7~1微秒。而30MHz SCK下发一个字节只要不到0.3微秒。也就是说,SPI已经把当前字节发完了,下个字节还没到位,SCK只能空转等待。这就是间隙的根源。

2.3 轮询方式为什么也可能翻车

有人会说“那我不用中断,直接用while循环等TXE再写下一个字节,总该没问题吧?”实测下来,在SPI时钟低的时候确实够用,但在高速场景下依然有隐患。while (__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_TXE) == RESET);这个循环看似紧凑,但每次循环都要执行标志位读取、判断、跳转,这些指令本身需要周期,而且在缓存未命中时访问外设寄存器还要等总线延迟。更微妙的是,如果你在循环里加了一句调试打印、或者编译器优化等级没开,这段等待时间会被进一步拉长。

轮询方式最大的问题不是单次响应慢,而是它让CPU全程死等,白白浪费了M7核心的算力。而且当系统里同时跑着RTOS任务、定时器中断、DMA传输,轮询SPI的这段代码会被其他高优先级中断打断,一打断就是几十上百微秒,SCK间隙只会更严重。

2.4 别把SCK占空比和传输间隙混为一谈

顺便澄清一个容易混淆的点:示波器上看到的SCK“高电平一半、低电平一半”是正常现象,SPI的SCK本来就是对称方波,占空比接近50%。标题里的“idles half of the time”指的是“整个有效传输期间,SCK有一半时间没翻转”,这是传输效率问题,不是占空比问题。这两个概念的排查方向完全不同:占空比异常要去查SPI时钟极性和相位配置(CPOL/CPHA),而SCK停顿要去查数据供给链路。

判断方法很简单:用示波器看一段完整的CS拉低时间,数一下SCK有效上升沿的数量。如果传输512字节,CS低电平期间SCK上升沿数量远小于4096个,说明SCK有大量“空洞”,这就是数据供给瓶颈。如果上升沿数量对得上,只是SCK波形肉眼看着不对称,那才需要去查时钟极性和相位。

3. STM32H743 SPI外设的关键机制:FIFO、TXFT和EOT

3.1 正确认识H7的SPI发送FIFO

STM32H743的SPI外设跟F1/F4系列相比,最大的变化就是引入了8级深度的发送/接收FIFO,以及对应的阈值中断机制。这是解决SCK间隙问题的第一个抓手。

TX FIFO是一个32位宽的缓冲区,写入DR寄存器时,如果写入的是8位数据,它会占用FIFO的一个槽位;如果写入16位或32位数据,占用的槽位对应增加。FIFO的主要作用是解耦CPU写入和硬件移位这两个过程。CPU只要保证FIFO不空,移位寄存器就能连续工作,SCK就不会停。

但FIFO本身只是缓冲区,如果CPU写入速度跟不上移位速度,FIFO依然会被耗尽。所以提高“连续供给”能力的关键,不是单纯等待TXE标志,而是要尽量多、尽量快地把数据预存到FIFO里,让FIFO始终处于半满甚至全满状态。

H7 SPI的TXFT(TX FIFO threshold)标志就是干这个的:你可以配置一个阈值等级,当FIFO中的剩余空间达到这个阈值时,硬件置位TXFT标志并触发中断。比如你设置阈值为FIFO半空(HF),那么FIFO里被取走了一半字节后,TXFT就会置位。借着这个标志,你在中断里可以一次性把后续一批数据补进去,而不是一次只补一个字节。这种方式能显著降低中断频率,从而减少间隙。

3.2 TXFT阈值怎么配

H7的SPI_CR1寄存器里有TXFTHF位,用于选择TX FIFO阈值是半空还是四分之一空。如果开启TXFT中断,当FIFO空余量达到设定阈值时会产生中断。实际操作中:

  • 数据量小、中断频率不影响大局时,阈值可以设得保守一点,比如四分之一空,这样FIFO更不容易耗尽;
  • 数据量大、希望尽可能压榨SCK连续时间时,阈值可以设成半空,中断触发后一次补进几个字节,减少中断次数;
  • 如果要追求极致,就不要用中断,改用DMA,让DMA在后台持续搬运数据,CPU完全不用管。

顺带提一句,H7 SPI还有一个EOT(End Of Transfer)中断事件。EOT在SPI完成了一次完整的、由MSTSTART/MSTEND控制的传输序列后触发,这个在需要精确控制CS释放时间的时候特别有用。比如你用DMA发一批数据,DMA的传输完成中断可能比SPI实际发完最后一个bit更早,因为DMA只是把数据搬进了FIFO,FIFO里的数据还没被移位寄存器发完。如果这个时候直接把CS拉高,会把最后一个字节截断。正确做法是等EOT事件,确认SPI真正把FIFO里的数据全部移出了再操作CS。

3.3 硬件片选和软件片选对连续传输的影响

另一个容易踩坑的地方是NSS片选管理。H7的SPI支持硬件片选管理,也支持软件控制NSS输出。如果你的应用要求CS在整包数据发送期间保持低电平、中间不能有抖动,建议把NSS设置为软件控制(HAL_SPI_Init里的NSS字段设为SPI_NSS_SOFT),然后在发送前手动拉低CS、发送结束后再拉高。

硬件片选的好处是省一个GPIO的控制逻辑,但它的释放时机有时候很尴尬。尤其在HAL库的DMA发送流程里,SPI在发生EOT事件时硬件片选会不会自动释放,取决于MasterKeepIOStateSSOE等位的配置。用不好就会出现CS提前释放、或者CS在字节间闪断的情况。我之前调试一款TFT屏幕就是这样,用硬件片选时屏幕偶尔花屏,抓波形发现CS在数据帧中间出现了一个极窄的高脉冲,后来改成软件片选彻底解决。

3.4 轮询、中断、DMA三种方式的上限对比

用数据说话。H743主频480MHz,APB2配120MHz,SPI1时钟分频到30MHz,向外部SPI Flash发送512字节,三种方式实测:

发送方式有效SCK翻转比例实际搬运512字节耗时CPU占用适用场景
逐字节轮询约40% ~ 60%约70 ~ 100us100%,全程等待低速调试、SPI时钟低时可用
逐字节中断(TXE)约50% ~ 70%约50 ~ 80us中,但频繁进入中断中低速、CPU还有别的事但不多
中断 + 合理阈值(TXFT)约80%左右约40 ~ 50us较低中高速、不想引入DMA
DMA + EOT等待接近100%约27us(理论值)极低,仅启动时占用高速大批量传输的首选

表格里“有效SCK翻转比例”是我用逻辑分析仪抓CS低电平时间除以理论最短传输时间的估算值。理论最短传输时间 = 512字节 × 8bit ÷ 30MHz ≈ 27us。DMA方式实测非常接近这个值,SCK几乎没有明显停顿。而轮询和逐字节中断,因为每发一个字节都要停下来等CPU填充,实际耗时普遍在40us以上,算下来SCK有一半甚至更多时间处于空闲状态,标题描述的现象就完全对上了。

4. 完整的优化实操:从配置到代码一步步把SCK“填满”

4.1 前提准备:把SPI时钟和GPIO速度配到位

不管用哪种发送方式,先把底层配置做对。拿H743最常用的SPI1举例,它挂载在APB2总线上。为了让SPI时钟尽量高,APB2分频不能太大。在SystemClock_Config里,通常把RCC_HCLK_DIV4配成HCLK的四分之一,也就是480MHz/4=120MHz。此时SPI1的外设时钟就是120MHz。

SPI时钟分频(BaudRatePrescaler)和最终SCK频率的关系:SCK = PCLK / 分频系数。分频系数可选2、4、8、16、32、64、128、256。

  • 想要30MHz SCK:120MHz / 4 = 30MHz;
  • 想要60MHz SCK:120MHz / 2 = 60MHz,但前提是外设本身的最高时钟能扛住,且对端从机也支持;
  • 想要15MHz SCK:120MHz / 8 = 15MHz。

GPIO的速度等级也要跟着SCK频率走,否则高速翻转时信号质量会很差。PA5(SCK)、PA6(MISO)、PA7(MOSI)这些引脚建议配置成GPIO_SPEED_FREQ_VERY_HIGH,也就是GPIO速度等级设为极高速。如果用的CubeMX生成代码,在GPIO设置里把Maximum output speed改成Very High即可。低频应用用High也能跑,但在高速SPI下非常容易看到波形边沿变缓、过冲,甚至通信误码。

4.2 方案一:中断方式下如何压榨间隙

如果你不想为了SPI单独引入DMA,先把中断发送优化到极限,关键就是两点:降低中断响应延迟、减少中断次数。

降低中断响应延迟

SPI中断优先级一定要配高。在NVIC里把SPI1的IRQ优先级设到抢占优先级0或1,同时保证其他中断不要频繁抢占它。M7内核的中断响应本身很快,但如果SPI中断被一个长耗时的高优先级中断堵住,FIFO一样会被耗尽。实测中我遇到过SPI中断被一个每100us触发一次、执行40us的定时器中断抢占,结果SPI SCK间隙直接翻了倍。

减少中断次数:用TXFT半空阈值

初始化时,在SPI的CR1寄存器里设置SPI_CR1_TXFTHF,让TXFT标志在FIFO剩余半空时置位。然后使能TXFT中断而不是单纯使能TXE中断。这样每触发一次中断,可以往FIFO里补多个字节,而不是一次补一个。

HAL库对TXFT的支持比较绕,HAL_SPI_Transmit_IT内部其实还是按照TXE来处理的。想要完全发挥TXFT特性,直接操作寄存器更直观。下面给一段发送函数的伪代码思路:

// SPI发送FIFO半空中断处理 void SPI1_IRQHandler(void) { if (__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_TXFT)) { // FIFO剩余空间达到半空,说明至少还能写好几个字节 if (tx_index < tx_length) { // 一次性把剩余数据尽量填进FIFO while ((tx_index < tx_length) && (__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_TXE))) { *((__IO uint8_t *)&hspi1.Instance->DR) = tx_buffer[tx_index++]; } } else { // 数据发完,关闭TXFT中断,等待EOT __HAL_SPI_DISABLE_IT(&hspi1, SPI_IT_TXFT); __HAL_SPI_ENABLE_IT(&hspi1, SPI_IT_EOT); } } // 处理EOT中断,确认所有数据已经移出FIFO if (__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_EOT)) { __HAL_SPI_CLEAR_FLAG(&hspi1, SPI_FLAG_EOT); // 此时可以安全拉高CS,或者通知任务发送完成 } }

这段代码的核心思想是:在TXFT中断里尽量多写几个字节,榨干FIFO的缓冲能力。配合高优先级中断,能把SCK间隙压缩到15%以内。但要注意,如果发送缓冲区很大,中断服务函数里循环写FIFO的时间也会变长,此时要考虑是否会让其他系统任务饿死。这就是DMA方案的价值所在——它连CPU都不占。

4.3 方案二:DMA发送的正确姿势

DMA是解决“SPI空闲一半时间”的最直接方案。H743的DMA1/DMA2支持从内存到外设的搬运,配置好之后,DMA会在后台按字节流把数据搬进SPI的DR寄存器,SPI移位寄存器几乎总是有数据可取,SCK于是能保持接近100%的连续翻转。

HAL库的DMA发送接口很简单:

// 先初始化SPI,开启DMA时钟,配置DMA通道,最后调用发送接口 HAL_SPI_Transmit_DMA(&hspi1, tx_buffer, length);

但要注意,HAL库这个接口完成回调的触发点是DMA传输完成。DMA传输完成不等于SPI已经把数据全部移出。从场景上看,DMA把最后一个字节写进SPI的DR后,可能还有几个字节在FIFO里排队没发完。如果你在DMA完成中断里立刻操作CS或释放缓冲区,就可能出问题。

正确做法是:在DMA传输完成中断里,不急着处理业务,而是等SPI的EOT事件,确认硬件移位寄存器把FIFO全部清空后再收尾。

用HAL库的写法,在HAL_SPI_TxCpltCallback里不要再直接操作CS,改成使能EOT中断,在EOT中断里再拉高CS:

void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { // DMA搬完了,但SPI可能还有数据在FIFO里 // 使能EOT中断,等真正发完 __HAL_SPI_ENABLE_IT(hspi, SPI_IT_EOT); } } void SPI1_IRQHandler(void) { // 注意HAL库在DMA模式下也会进入这个中断 HAL_SPI_IRQHandler(&hspi1); // 处理EOT标志 if (__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_EOT)) { __HAL_SPI_CLEAR_FLAG(&hspi1, SPI_FLAG_EOT); __HAL_SPI_DISABLE_IT(&hspi1, SPI_IT_EOT); // 此时FIFO和移位寄存器都空了,可以安全拉高CS HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_SET); // 通知应用层发送完成 spi_send_complete = 1; } }

这样处理之后,CS的释放时机是“SPI硬件真正发完最后一个bit”的那一刻,不会早也不能晚。

4.4 方案三:批量突发传输时怎么安排CS和缓冲

如果你的应用是一次性要发几百上千字节的大块数据,DMA方案已经很理想,但还有几个细节可以进一步优化:

第一,发送缓冲区尽量放在DMA可访问的内存区域。H743的DMA1/DMA2都可以访问DTCM以外的SRAM区域,但核心的DTCM RAM如果被DMA访问会有限制。通常把DMA缓冲区定义在普通SRAM(比如__attribute__((section(".ARM.__at_0x24000000"))))里,避开DTCM即可。用CubeMX生成工程时,默认的DMA缓冲区放置位置一般没问题。如果你把缓冲区定义在DTCM且DMA传输异常,大概率就是这个原因。

第二,大块数据可以拆分发送,但CS要保持稳定。比如你要发一整帧画面数据给TFT屏,可以分成多次DMA发送,但每次之间CS别释放。这要求软件只控制一次CS拉低,然后连续启用多次DMA,或者用“DMA循环链表”把多个缓冲串起来。H743的DMA支持DMAMUX和链表模式,可以把多个传输描述符串成一个链表,DMA会在后台自动搬运完一个再搬下一个,真正做到零CPU干预。

第三,如果数据量极大且SPI时钟很高,可以考虑双缓冲(乒乓缓冲)。一块缓冲在DMA搬运,另一块由CPU填充数据,两块轮流切换。这样CPU填充数据的时间不会影响SPI发送的连续性,也不再需要在中断里反复启动DMA。

4.5 现场实测:优化前后波形对比

我把同一个512字节发送任务分别在“逐字节中断”和“DMA + EOT等待”两种方式下跑了一遍,逻辑分析仪采样率1GHz,观察CS低电平时间:

发送方式CS拉低总耗时有效SCK边沿数平均SCK频率(按总时长算)
逐字节TXE中断48.7us约3800个约13MHz
TXFT半空中断34.2us约4010个约23.5MHz
DMA + EOT27.4us4096个约30MHz

可以看到,逐字节中断方式的平均SCK频率只有13MHz,连配置值30MHz的一半都不到,这就是“idles half of the time”的量化结果。TXFT半空中断缓解了一些,但依然达不到满速。DMA方式则基本拉满,4096个边沿全部出现,总耗时接近理论值。

如果你是要求严苛的实时性任务,比如驱动高速ADC采样、刷新高分辨率显示屏,建议直接上DMA。如果你的发送数据比较短(几个字节),DMA启动配置的开销反而比中断方式更大,这时候保持中断发送也完全够用,CS拉低时间只有几微秒,SCK间隙的影响可以忽略。

5. 常见问题与排查技巧实录

5.1 怎么快速判断SCK间隙的严重程度

不要靠肉眼看示波器上的波形疏密来猜测,直接数边沿或者测量总耗时最靠谱。具体操作:

  1. 示波器探头接CS和SCK,触发方式设为CS下降沿;
  2. 调整时基,确保一帧完整传输过程都在屏幕上;
  3. 用示波器的频率测量功能,测量CS低电平期间SCK的平均频率;
  4. 拿这个平均频率除以SPI配置的理论SCK频率,就得到有效时钟利用率。

如果利用率低于80%,基本可以判定数据供给有问题,优先检查是不是还在用逐字节中断或轮询。

另一个快捷办法是看HAL库发送接口的耗时。调用HAL_SPI_Transmit_ITHAL_SPI_Transmit_DMA,在同一缓冲区同一长度下比较函数返回时间和最终发送完成回调的时间。通常Transmit_DMA完成回调的时间会比Transmit_IT短一倍左右,差值大的就说明中断发送SCK间隙严重。

5.2 常见问题速查表

问题现象可能原因排查/解决方向
SCK波形密度不均匀,时密时疏中断响应延迟不稳提高SPI中断优先级;关闭无关中断抢占
SCK持续时间翻倍,边沿数明显不足逐字节发送,CPU供给太慢改用TXFT阈值中断或DMA
DMA发送完成回调提前触发,CS提前拉高把DMA完成当成SPI发送完成等待EOT事件后再拉高CS
DMA搬运数据错乱或丢失缓冲区放在了DTCM改用普通SRAM区;检查DMA通道配置
SPI时钟很高时MISO采样不稳PCB走线太长、GPIO速度等级不够GPIO配置成Very High;缩短走线;降低时钟
使用硬件片选时CS在字节间抖动NSS释放时机不受控改为软件控制NSS,手动管理CS
发送长数据期间CPU被拖死发送方式仍为轮询换成DMA是首选,至少也要用中断

5.3 我踩过的几个坑

第一个坑是TXFT中断的误触发。刚开始我配置了TXFT中断,但没注意在初始化阶段FIFO是空的,TXFT标志会立即置位,导致进入中断时根本没有可发送的数据,逻辑混乱。解决办法是在发送开始后才使能TXFT中断,并且在发送完成或FIFO写入足够的字节后及时屏蔽该中断。

第二个坑是MPU配置。H743带Cache,如果DMA缓冲区在可缓存的地址区域,且你没有做Cache维护,会出现CPU写好的数据DMA搬的是“旧数据”的情况,表现为发送内容错乱。解决方案是把DMA缓冲区放到非Cacheable区域,或者在启动DMA前调用SCB_CleanDCache刷新Cache、在接收完调用SCB_InvalidateDCache失效Cache。STM32H7的DMA和外设访问默认是不走Cache的,但CPU写SRAM的数据会先进Cache,所以Clean/Invalidate这步不能省。

第三个坑是APB时钟配置和SPI分频的关系。H743的SPI1/4/5/6挂在APB2上,APB2时钟可能是120MHz,也可能是240MHz,取决于RCC分频配置。有个同事在CubeMX里把APB2的Prescaler从4改成2,SPI时钟选项没动,结果SCK实际频率翻倍,CS期间SCK边沿增多导致从机通信异常。排查的时候务必先把外设时钟频率确认清楚,再谈SCK有效利用率。

第四个坑跟电路设计有关,但影响的是时序。SPI时钟线SCK上如果串了磁珠或者过大的电阻(比如100欧以上),高速翻转时边沿变缓,从机采样点会发生偏移。在20MHz以下可能没事,到40MHz以上就会偶发误码。我之前在SCK上串了一个22欧的电阻,30MHz下工作没问题,换成50欧就开始丢数据。不是说SCK不能串电阻,而是阻值要根据信号完整性实测来选,不能盲目加大。

第五个坑就是标题这个问题的隐蔽版本:你以为只有中断发送才会出现SCK间隙,其实在某些配置下DMA也会出现。比如你用了HAL_SPI_Transmit_DMA,但DMA传输数据宽度和外设数据宽度没配对——SPI配置成8位数据,DMA也配成8位,这是匹配的;但如果你把DMA配成16位搬运,FIFO里的数据错位,可能会导致SPI需要额外等待处理,SCK同样出现间隙。所以每次改动DMA配置,记得回头确认传输宽度和SPI帧格式保持一致。

5.4 其他值得留意的高阶细节

H7的SPI有几个高级特性,在特定场景下能进一步优化传输效率:

一个是SPI FiFo在接收方向也有用。如果你做的是全双工通信,接收数据同样走FIFO,阈值中断可以减少CPU读取频率。在双向高速传输场景,把RX FIFO阈值配到半满或全满,配合DMA双缓冲,可以把CPU开销降到极低。

另一个是MasterKeepIOState配置。在H7的SPI配置里,把NSS控制设成软件之后,MasterKeepIOState决定SPI在传输结束后是否保持SCK和MOSI的输出状态。如果这个位配置不对,CS拉高之后SCK可能还会闪两下,下游设备可能因此误判帧结束。

还有就是SPI时钟极性和相位的组合。很多外设只支持SPI模式0(CPOL=0,CPHA=0)或模式3(CPOL=1,CPHA=3),如果你的采样点正好落在数据变化沿上,就会出现偶发读错。这个和标题的“SCK间隙”不是一回事,但排查时容易混淆。用示波器抓一下MOSI和SCK的相对位置,确认数据在每个SCK采样边沿时是稳定的,就能排除这个问题。

最后说一句关于中断优先级的题外话。H743使用NVIC,SPI中断优先级设成0(最高)确实能减少SCK间隙,但前提是你的系统里没有更紧急的事件,比如电机控制里的PWM刹车、电源故障检测。如果这种硬实时事件和SPI数据供给冲突,建议还是把SPI发送交给DMA,彻底把CPU解放出来。不要为了一个SCK间隙,把整个中断系统优先级搞乱,那会埋下更大的隐患。

6. 回到标题:你该怎么看待“SPI空闲一半时间”

说句实在话,这个问题本身并不冷门,几乎每个从F1/F4老平台切到H7的人都会遇到。它在老平台上没那么刺眼,是因为F103的SPI外设没有深FIFO,中断方式和DMA方式的性能差距没那么明显,而且SPI时钟普遍配得不高。H7的SPI时钟可以拉很高,外设越强,软件供给链路的短板就越暴露。所以“SPI on STM32H743 idles half of the time during active data transmission”这个标题,本质上不是硬件缺陷,而是软件没跟上硬件的表现。

我个人在实际调试中体会最深的一点是:不要迷信一个API调用。HAL_SPI_Transmit_IT看似是“异步、不阻塞”的高级接口,但如果你不了解它底层每发一个字节都要进一次中断,在高速SPI下就必然吃大亏。反而花点时间把DMA和EOT的配合理清楚,一套代码能同时解决传输效率、CPU占用、CS时序三个问题。

如果你现在手里的项目也出现了类似现象,建议按这个顺序排查:先确认SPI时钟频率和有效SCK利用率,再用逻辑分析仪看是不是每发一两个字节就有一段停止期,最后根据数据量选择TXFT阈值中断或DMA发送。大多数情况下,DMA方案能直接把有效吞吐拉满,剩下那点SCK间隙基本可以忽略不计。

最后再分享一个小技巧:调试SPI时,逻辑分析仪比示波器好用得多,尤其是采样率够高的时候。示波器适合看信号质量和毛刺,逻辑分析仪适合看协议时序和SCK边沿数量。买一个几百块钱的USB逻辑分析仪,配合免费的软件,就能把CS、SCK、MOSI三路信号同时抓下来,边沿数、总时长、字节间隔一目了然。很多时候问题在波形上一眼就能看出来,根本不用瞎猜寄存器配置。

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

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

立即咨询