4、协同痛点分析:DMA传输被低功耗打断的典型场景与后果

好,咱们进入正题。前面几章我们把DMA和低功耗模式单独讲了一遍,看起来各自都挺美好。但把它们放在一起用,问题就来了。

说白了,DMA想要的是“总线时钟一直跑,我好搬运数据”。低功耗想要的是“能睡就睡,最好连时钟都停了”。这两个家伙的目标,天然就是冲突的。

我在项目中遇到过好几次这种“打架”的情况。有一次产品做功耗测试,明明进了Stop模式,电流却比预期高了两个数量级。查了半天,发现是DMA还没来得及传完数据,CPU就下令睡觉了。DMA被强行挂起,外设的FIFO直接溢出,数据全丢了。

这一章,我们就来扒一扒这些典型的“打架”场景,看看后果有多严重。

4.1 场景一:进入低功耗模式时,DMA传输尚未完成

这是最常见、也最容易踩的坑。

你的代码逻辑可能是这样的:先启动DMA传输,然后立即调用WFI或WFE指令进入睡眠模式。你想着“DMA是硬件自动工作的,我CPU先睡,它自己传完就行了”。

嗯,想法很好,但现实很骨感。

⚠️ 关键问题: 在STM32中,进入Stop或Standby模式前,系统会先检查总线状态。如果DMA还在传输,总线仲裁器不会让CPU睡下去——或者更糟,强行切断DMA时钟,导致传输中断。

我见过一个做音频采集的同事,用DMA把I2S数据搬到内存,循环缓冲。他在主循环里判断缓冲半满后,就进Stop模式等下一次唤醒。结果呢?每次唤醒后,音频数据都有几毫秒的“咔哒”声。原因就是DMA在进入Stop的瞬间被截停了,最后一个采样点没搬完。

后果清单:

  • 数据丢失:DMA当前正在搬运的那一笔数据,直接丢掉。
  • 指针错位:DMA的CNDTR寄存器停在非预期值,下次启动时地址对不齐。
  • 外设FIFO溢出:比如USART、SPI、I2S这些带FIFO的外设,DMA不搬了,FIFO满了就溢出。
  • 总线死锁:极少数情况下,DMA和CPU同时请求总线,进入低功耗时仲裁逻辑混乱,导致系统卡死。

4.2 场景二:低功耗定时器唤醒后,DMA配置被复位

这个坑,我估计踩过的人不少。

有些低功耗模式(比如Stop模式),会把大部分外设的寄存器内容都丢光。唤醒后,系统需要重新初始化。但很多人只重新配了GPIO和时钟,忘了DMA。

你想想看:DMA的CCR(配置寄存器)、CNDTR(传输数量寄存器)、CMAR(内存地址寄存器)、CPAR(外设地址寄存器),在Stop模式下可能全部复位成默认值。唤醒后,DMA通道处于禁用状态,地址指针全乱套了。

💡 我的习惯: 在低功耗唤醒后的初始化函数里,把DMA的配置也加进去。不要只想着“DMA是硬件自动的,不用管”。唤醒后,它就是个“失忆”的硬件。

我曾经在一个电池供电的传感器节点项目里,用LPTIM(低功耗定时器)定期唤醒STM32L4,采集ADC数据。每次唤醒后,DMA传输的数据都是错的。折腾了两天,最后用调试器一看——唤醒后DMA的CNDTR寄存器是0x0000,根本没配置传输长度。

后果清单:

  • 传输长度错误:CNDTR复位为0,DMA认为不需要传输任何数据。
  • 地址指针丢失:CMAR和CPAR复位,DMA不知道从哪里搬数据。
  • 通道禁用:CCR的EN位被清零,DMA通道根本不工作。
  • 中断标志混乱:唤醒后可能残留了虚假的中断标志,导致误触发。

4.3 场景三:DMA正在传输时,系统进入低功耗模式

这个场景和第一个有点像,但更隐蔽。它不是“进入前没检查”,而是“进入的时机刚好撞上DMA传输”。

举个例子:你的系统用DMA循环搬运ADC数据。主循环里,你判断某个条件满足后,就调用HAL_PWR_EnterSTOPMode()。但就在你调用这个函数的瞬间,DMA刚好在搬运最后一笔数据。CPU的指令执行和DMA的传输是并行的,你根本不知道谁先谁后。

为什么会这样?因为STM32的Cortex-M内核是哈佛架构,指令总线和数据总线分开。CPU执行WFI指令走的是指令总线,DMA搬运数据走的是数据总线。两者互不干扰,但进入低功耗的硬件逻辑会在下一个时钟周期检查总线状态。如果DMA还在占用总线,系统会等待——但等待多久?数据手册上写的是“直到当前总线传输完成”。这个“当前传输”可能是一个字节,也可能是一个突发传输(Burst Transfer),时间不确定。

🔧 避坑指南: 我曾经在代码里加了一个“DMA传输完成标志”的检查,确保所有DMA通道都空闲了,才允许进入低功耗。具体做法是:在DMA传输完成中断里置位一个全局标志,主循环检查这个标志。如果DMA还在忙,就延迟进入低功耗。

后果清单:

  • 进入延迟:系统无法立即进入低功耗,功耗比预期高。
  • 数据不完整:如果强行切断时钟,DMA的最后一笔数据只搬了一半。
  • 外设状态异常:比如DAC正在输出波形,DMA突然停了,输出引脚可能卡在中间电平。
  • 调试困难:这种问题不是每次都能复现,跟时序强相关,排查起来很头疼。

4.4 核心冲突:时钟域与总线仲裁

说了这么多场景,其实根源就一个:时钟域冲突。

DMA需要AHB/APB总线时钟才能工作。低功耗模式(尤其是Stop和Standby)会把系统时钟(HCLK、PCLK)关掉。DMA没了时钟,就像汽车没了油,动不了。

但外设呢?有些外设在低功耗模式下还能继续工作——比如LPTIM、RTC、LPUART。它们有自己的独立时钟(LSI、LSE)。这些外设如果配置了DMA请求,就会在唤醒后立即触发DMA。但DMA的时钟还没恢复,请求就丢了。

我画了一张图,帮你理清这个关系:

DMA与低功耗时钟域冲突示意图 正常模式(Run/Sleep) HCLK / PCLK 运行中 DMA 时钟:✅ 可用 外设时钟:✅ 可用 总线仲裁:正常 DMA 传输:✅ 正常完成 低功耗模式(Stop/Standby) HCLK / PCLK 已关闭 DMA 时钟:❌ 不可用 外设时钟:部分可用(LPTIM/RTC) 总线仲裁:暂停 DMA 传输:❌ 中断/丢失 WFI/WFE 核心冲突点 DMA 需要 HCLK 才能传输数据 低功耗模式关闭 HCLK → DMA 无法工作 → 数据丢失或传输中断 典型后果 1. 数据丢失:DMA 当前传输被截断 2. 指针错位:CNDTR/CMAR 寄存器值异常 3. 外设 FIFO 溢出:数据无法及时搬走 4. 总线死锁:时钟恢复后仲裁逻辑混乱

4.5 后果总结:一张表看清

我把上面提到的场景和后果整理了一下,方便你对照排查:

场景 触发条件 直接后果 排查难度
进入低功耗时DMA未完成 WFI/WFE前未检查DMA状态 数据丢失、指针错位 ⭐⭐⭐ 中等
唤醒后DMA配置复位 Stop模式未保存DMA寄存器 传输长度错误、通道禁用 ⭐⭐ 较低
DMA传输中进入低功耗 时序巧合,DMA和CPU并行 数据不完整、外设异常 ⭐⭐⭐⭐ 高
外设唤醒后DMA时钟未恢复 LPTIM/RTC触发DMA请求 请求丢失、传输失败 ⭐⭐⭐⭐⭐ 极高
📌 一句话总结: DMA和低功耗的协同,核心就是管理好“时钟域切换”和“传输完成状态”。不要假设DMA在低功耗模式下还能正常工作,也不要假设唤醒后DMA的配置还在。