DMA为什么能降低CPU占用率?STM32DMA工作原理详解
时间:2026-09-24 来源:华清远见
导读:『用 DMA 可以降低 CPU 占用率』几乎成了口头禅,但 DMA 又不是协处理器,它凭什么让 CPU 变闲?本文先做一笔『周期账』量化 CPU 搬运的真实成本,再从总线主设备、总线矩阵仲裁的角度讲清 DMA 的硬件原理,然后给出 STM32 上串口+DMA 收不定长包、ADC+DMA 连续采样两个完整实操案例,最后总结 8 个最常见的 DMA 翻车现场。
一、先算账:CPU 搬一个字节到底贵在哪
看一段最朴素的串口收字节代码(中断方式):
▍CPU 中断搬运:每字节一次中断

每收 1 个字节,CPU 要经历:硬件入栈 8 字(约 12 周期)→ 编译器生成的 push {r4-r11, lr} → 读 DR(外设总线一次访存)→ 写内存 → 清标志 → 出栈返回(又 12 周期)。粗算每次 ISR 200 个周期并不夸张。把它乘以速率:

图 1 72MHz 内核上,不同串口速率下『CPU 逐字节中断』与『DMA 循环搬运』的占用率对比(估算)
115200bps 时 CPU 约 3% 被吃掉,似乎不多?但注意三个隐藏成本:
打断成本:每次中断都会冲刷流水线、污染 I-Cache/D-Cache,主循环里正在跑的FFT、滤波算法被反复『腰斩』,实测主任务性能下降远超 3%;
峰值成本:中断是突发的,一帧数据背靠背到达时,主任务可能连续几十微秒得不到执行,实时性(最坏情况延迟)才是嵌入式的第一指标;
扩展成本:速率上到 921600、3Mbps(蓝牙、数传电台常见),或者 ADC 1MSPS 连续采样,CPU 方式直接 30%~100% 占满,系统崩盘。
而 DMA 方式下 CPU 只在『配置』和『半传输/传输完成』两个时间点介入,搬运本身由 DMA 控制器在总线上独立完成。这就是『降低 CPU 占用率』的数学本质:把每字节 O(1) 的 CPU 开销,摊薄成每帧 O(1)。
二、DMA 的硬件本质:第二个『总线主设备』

图 2 两种搬运方式对比:CPU 方式每字节执行上百条指令;DMA 方式搬运期间 CPU 全速跑主程序
要理解 DMA,先破除一个直觉误区:内存和外设之间传数据『必须经过 CPU』是错的。在 STM32 内部,CPU 内核和 DMA 控制器都是总线矩阵(Bus Matrix)上的主设备(Master),SRAM、Flash、外设寄存器都是从设备(Slave)。CPU 之所以能搬数据,只是因为它恰好是一个『会执行 LDR/STR 指令的主设备』;而 DMA 是另一个『只会做固定搬运套路的主设备』,它有自己的地址发生器、计数器与请求仲裁器。

图 3 STM32 数据通路与循环模式缓冲区模型:DMA 写满半区触发 HT 中断,写满整区触发 TC 中断
于是『DMA 为什么快』就有了硬件答案:
零指令开销:一次搬运在 DMA 内部是『请求→仲裁→读→写→计数』的固定状态机,每字节约 2 个总线周期,而不是 CPU 的上百条指令;
并行性:总线矩阵有多层,DMA 搬外设→SRAM 的同时,CPU 可以从另一层访问Flash 取指、访问另一块 SRAM,两者真正并行;
冲突只付小代价:当 CPU 与 DMA 抢同一条总线/同一块内存时,仲裁器让一方等1~2 个周期——CPU 被『挂起』而不是被『打断』,没有上下文切换、没有 Cache 污染。
【一句话原理】DMA 降低 CPU 占用率,不是因为它『搬得魔法』,而是因为它把『每字节一次的软件循环』变成了『每字节一次的硬件总线事务』,并把 CPU 的介入频率从 N 次降到 2 次(HT/TC 中断)。
三、STM32 的 DMA 长什么样:通道、寄存器与工作模式
3.1 通道与请求映射
F1 系列有 DMA1(7 通道)+ DMA2(5 通道),外设到通道的映射是硬件固定的,查错表是日常(见表 1)。F4 系列升级为 8 条 Stream × 8 个 Channel 的交叉开关,每条 Stream 可用 Channel 选择寄存器连接多个外设之一,灵活但要查『Stream/Channel对照表』。

表 1 常用外设的 DMA 请求映射(以参考手册 DMA 章节表格为准)
3.2 一条通道运行时在做什么
CPAR/CMAR:外设地址、内存地址(源或目的由方向位 DIR 决定);
CNDTR:剩余传输计数,每完成一次搬运硬件减 1,减到 0 触发传输完成(TC);
CCR:方向、循环模式(CIRC)、外设/内存位宽(PSIZE/MSIZE)、地址自增(PINC/MINC)、内存到内存(M2M)、通道优先级、三个中断使能(TCIE/HTIE/TEIE);
运行时序:外设置位请求(如 RXNE)→ DMA 向总线矩阵申请总线 → 读外设 DR → 写内存 → CNDTR-- → 到半值触发 HT 中断 → 到 0 触发 TC 中断(循环模式则重载CNDTR 继续)。
位宽不一致时硬件会自动做打包/解包(如 PSIZE=16bit、MSIZE=8bit,一次外设读产生两次内存写),这既是便利也是 Bug 7 的温床。F4 还有 4 字 FIFO 与双缓冲(M0AR/M1AR + CT 位切换),适合高速连续流。
四、实操案例一:USART1 + 循环 DMA + IDLE 中断收不定长包
这是工程里最经典的 DMA 用法:DMA 负责『不停地把字节填入环形缓冲区』,IDLE 中断负责『一帧结束时通知我』,两者配合实现零丢包、零轮询的串口接收。
▍USART1 + DMA 循环接收 + IDLE 判帧尾

4.1 循环模式下的『回卷』问题
循环模式下 DMA 写到缓冲区尾部会自动回到 0 继续写。若一帧数据恰好跨过尾部,rx_buf[0..pos-1] 里其实是『上一帧尾巴 + 本帧开头』的拼接。工程上有两种稳妥做法:
半区法(推荐):只消费『刚被写满的半区』——HT 中断消费前半、TC 中断消费后半,把缓冲区当双缓冲用,彻底避开跨边界的帧(适合定长/流式数据,如 ADC、I2S);
软件环形法:记录 last_pos,IDLE 里若 pos<last_pos 则分两段 memcpy 拼帧(适合不定长协议包)。
无论哪种,都要保证『消费速度 ≥ 生产速度』,否则 DMA 会追上来覆盖未读数据(overrun)。缓冲区大小的经验值:≥ 2 × 最大帧长 × 安全系数。
五、实操案例二:ADC1 多通道连续扫描 + DMA
▍ADC + DMA 循环采样骨架

要点:ADC 转换完成自动触发 DMA 请求,无需 CPU;多通道扫描时 DMA 按转换顺序依次填入数组,天然对齐。若用 F7/H7(带 D-Cache),请务必看 Bug 6。
六、常用 Bug 与排查
Bug 1:DMA 完全不动——忘了开 DMA 时钟
现象:HAL_UART_Receive_DMA 返回 HAL_OK,但 CNDTR 永远不变。DMA 挂在 AHB 上,必须 __HAL_RCC_DMA1_CLK_ENABLE();UART 本体在 APB、GPIO 在 APB2,三路时钟一个都不能少。自检:读 DMA1_Channel5->CNDTR 是否在递减。
Bug 2:通道/Stream 映射张冠李戴
现象:配置看着都对,就是没数据。F1 的映射是硬件固定的(USART1_RX 只能是 DMA1_Channel5),用错通道 DMA 根本收不到请求;F4 用错 Stream 或 Channel 选择位同理。解决:永远以参考手册『DMA request mapping』表为准,CubeMX 生成的代码可以信,手写必须查表。
Bug 3:HT/TC 中断各触发了一次『多余』的进入
现象:回调函数莫名其妙多进一次,或进一次后再也不进。DMA 的 HT/TC/TE 标志是『置位后需软件清除』的;HAL 的 IRQHandler 会帮你清,但裸机写法若只清了 TC 没清 HT,HT 中断会一直挂起。裸机模板:进 ISR 先读 ISR 寄存器判断是哪个标志,处理完写 IFCR 对应位清掉。
Bug 4:循环缓冲被覆盖(overrun 丢数)
现象:偶发数据错乱,重现象消失。根因是消费太慢:DMA 写完半区后,你在 HT 回调里做了耗时操作,等回来时 DMA 已经开始写这半区了。对策:回调里只做 memcpy 到第二份缓冲或置标志;用半区法时确保单半区容量 × 2 的余量;高速流(I2S/ADC 1MSPS)直接上F4 的双缓冲模式,硬件切换 M0AR/M1AR。
Bug 5:位宽不匹配导致数据『错位』
现象:数组里隔一个错一个。外设 16 位(如 ADC)配了 MSIZE=BYTE,DMA 会做解包,每 16 位拆成 2 字节,你的 uint16_t 数组立刻错位。规则:PSIZE 跟随外设 DR 宽度,MSIZE 跟随内存数组元素宽度,两者一致最省心。
Bug 6:F7/H7 上 DMA 数据『看不见』——D-Cache 一致性
现象:M7 内核开了 D-Cache 后,CPU 读到的始终是旧数据(或外设收到旧数据)。根因:DMA 写内存不经过 CPU Cache,CPU 从 Cache 读到的是过期副本。三选一解决:
MPU 把 DMA 缓冲区配置为 Non-cacheable(最稳,推荐);
接收前 SCB_InvalidateDCache_by_Addr(),发送前 SCB_CleanDCache_by_Addr();
缓冲区 32 字节对齐且不与他人共享同一 Cache 行,避免 invalidate 误伤。
Bug 7:运行中改配置 / 重启动 DMA 后死机
DMA 通道的 EN 位必须先清零才能改配置,且硬件需要若干周期真正关掉——写 0 后要轮询确认 EN==0 再写其它寄存器;传输错误标志 TE 未清就重开也会导致立即报错。HAL 用户请走 HAL_DMA_Abort() 而不是直接戳寄存器。
Bug 8:缓冲区放在栈上 / 未对齐
函数内局部数组传进 HAL_UART_Receive_DMA,函数返回后栈帧作废,DMA 仍在往那里写——轻则数据乱、重则 HardFault(写到关键栈区)。同理 __attribute__((aligned(4))) 可避免非对齐访问在某些配置下产生的 BusFault。

七、总结
DMA 是与 CPU 并列的总线主设备,搬运走硬件状态机,每字节约 2 个总线周期;
『降低 CPU 占用率』的本质:介入次数从 N 次摊薄到 2 次,且不打断主任务流水线;
循环模式 + 半区/双缓冲是流式数据的标准姿势;IDLE 中断补上『帧尾』语义;
时钟、映射、清标志、位宽、Cache、对齐——DMA 的坑集中在『配置期』,运行期几乎零维护,这正是硬件方案的魅力。

