DMA基础概念:DMA工作原理、传输模式与CPU协同

各位同学,今天我们来聊聊DMA——直接存储器访问。说实话,我刚入行那会儿,觉得DMA就是个“自动搬砖工”,没啥技术含量。直到有一次做高速数据采集,CPU被中断拖到崩溃,我才真正意识到:没有DMA,高性能嵌入式系统就是空中楼阁。

一、DMA工作原理:谁在搬数据?

DMA说白了,就是硬件级别的数据搬运工。它不需要CPU一条条指令去读、写,而是自己搞定源地址、目的地址和传输长度。

核心流程:

  1. CPU配置DMA控制器:源地址、目的地址、传输长度、触发源
  2. 外设发出请求(比如UART收到一个字节)
  3. DMA控制器接管总线,执行一次或多次传输
  4. 传输完成,DMA发中断通知CPU

我做过一个项目,用STM32的UART接收GPS数据,每秒输出10条NMEA语句,每条80字节。如果用中断方式,CPU每秒要处理800次中断。用了DMA后,CPU只在缓冲区满时处理一次——效率提升了不止一个数量级。

二、DMA传输模式:三种主流玩法

DMA不是只有一种工作方式。根据应用场景,我把它分成三种模式。你想想看,每种模式对应不同的“搬砖策略”。

模式 工作方式 典型场景 我踩过的坑
单次传输 一次配置,传输指定长度后停止 一次性数据块传输(如Flash读取) 忘记重新配置,第二次传输直接挂掉
循环传输 传输完成后自动从头开始,永不停止 连续数据流(如UART接收、ADC采样) 缓冲区溢出没检测,数据被覆盖
链表传输 多个传输描述符串联,DMA自动切换 复杂数据流、多缓冲区管理 链表指针配置错误,DMA跑飞

2.1 单次传输模式

这是最基础的模式。你告诉DMA:“从地址A搬N个字节到地址B,搬完就停。”
嗯,简单粗暴,但有个问题——每次传输完都要CPU重新配置。

// 伪代码示例:单次传输配置
DMA_Channel->CPAR = (uint32_t)&USART1->DR;  // 外设地址
DMA_Channel->CMAR = (uint32_t)buffer;        // 内存地址
DMA_Channel->CNDTR = 256;                     // 传输256字节
DMA_Channel->CCR |= DMA_CCR_EN;              // 使能DMA

我的习惯:单次模式适合“一次性任务”,比如从SPI Flash读一张图片。但如果是连续数据流,千万别用单次——你会被中断淹没。

2.2 循环传输模式

循环模式是我在UART项目中最常用的。DMA传输完指定长度后,自动把地址指针和计数器复位,继续下一轮传输。CPU只需要在“半满”或“全满”时处理数据。

为什么会这样?因为循环模式本质上是一个环形缓冲区。DMA写数据,CPU读数据,两者互不干扰。

我曾经踩过的坑:循环模式下,如果CPU读取速度跟不上DMA写入速度,数据会被覆盖。解决方案是:使用“半传输中断”+“全传输中断”双缓冲机制。CPU在半满时处理前半段,DMA继续写后半段。

2.3 链表传输模式

链表模式是DMA的“高阶玩法”。它维护一个传输描述符链表,每个节点包含源地址、目的地址、长度和下一个节点指针。DMA自动遍历链表,完成所有传输。

我个人觉得,链表模式最适合多缓冲区、非连续内存的场景。比如你要从三个不同的内存区域收集数据,然后拼成一个包发送出去。

// 链表节点结构(以STM32G4为例)
typedef struct {
  uint32_t src_addr;
  uint32_t dst_addr;
  uint32_t size;
  uint32_t next_node;  // 指向下一个节点
} DMA_LinkedListNode;

// 配置三个节点
DMA_LinkedListNode node1 = {buf1, &USART1->DR, 64, &node2};
DMA_LinkedListNode node2 = {buf2, &USART1->DR, 32, &node3};
DMA_LinkedListNode node3 = {buf3, &USART1->DR, 128, 0};  // 最后一个节点指向NULL

避坑指南:链表模式最怕“断链”。我曾经在调试时,第二个节点的next_node写错了地址,DMA直接跳转到非法内存区,系统硬fault。所以我的习惯是:每次配置完链表,先手动检查所有指针是否有效。

三、DMA与CPU的协同:谁说了算?

很多初学者以为DMA和CPU是“竞争关系”,其实不然。它们更像是老板和员工——CPU是老板,DMA是员工。老板分配任务,员工执行,执行完了汇报。

但这里有个关键问题:总线仲裁。DMA和CPU共享系统总线,谁先拿到总线谁就先干活。

场景 总线占用情况 对CPU的影响
DMA传输中 DMA占用总线 CPU等待(或执行Cache内指令)
DMA传输完成 释放总线 CPU继续执行
CPU访问外设 CPU占用总线 DMA等待

你想想看,如果DMA一直在搬数据,CPU是不是就卡死了?
其实不会。现代MCU有总线矩阵和多AHB/APB总线,DMA和CPU可以同时访问不同的外设。比如DMA在搬UART数据,CPU同时可以跑算法——只要它们不抢同一条总线。

我的经验:在调试DMA+CPU协同问题时,先看总线时钟配置。很多坑都是因为DMA和CPU共享同一条低速总线导致的。我习惯把DMA放在高速总线上,CPU核心跑在另一条总线上。

四、DMA传输的完整流程(SVG图解)

下面这张图是我自己总结的DMA传输全流程,从CPU配置到传输完成。你看一遍就能理解整个协作过程。

DMA传输完整流程图 DMA传输完整流程图 CPU配置DMA参数 外设发出DMA请求 DMA仲裁并获取总线 DMA执行数据传输 DMA发送完成中断 CPU响应中断处理数据 循环模式特殊处理 传输完成后自动复位 不触发完成中断 链表模式特殊处理 自动跳转下一节点 直到链表尾结束 单次模式 传输完即停止

五、DMA与CPU协同的实战要点

说了这么多理论,来点干货。我在实际项目中总结了几条DMA+CPU协同的黄金法则:

  • 数据一致性:DMA修改内存时,CPU的Cache可能还是旧数据。记得用__DSB()或SCB_CleanInvalidateDCache()同步。
  • 中断优先级:DMA完成中断的优先级要合理设置。我习惯设成中等优先级——太高会抢占关键任务,太低会导致数据溢出。
  • 缓冲区对齐:DMA传输的缓冲区最好4字节对齐。有些MCU的DMA不支持非对齐访问,一跑就挂。
  • 超时处理:DMA传输不是100%可靠的。我每个DMA通道都配一个硬件定时器做超时监控,超过预期时间没完成就复位DMA。

我曾经犯过的错:有一次做UART DMA接收,忘了配置DMA的传输完成中断。结果数据早就收完了,CPU还在傻等。从那以后,我每次配置DMA都会检查:中断使能了没?回调函数注册了没?

六、三种模式的选择建议

最后,我根据实际项目经验,给你一个选择指南:

场景 推荐模式 理由
UART接收不定长数据 循环模式 + IDLE中断 连续接收,空闲时触发处理
SPI Flash读取 单次模式 一次性读取,完成后释放总线
多通道ADC采集 循环模式 连续采样,双缓冲防溢出
复杂数据打包发送 链表模式 多缓冲区拼接,减少CPU干预
高速数据流(如摄像头) 链表模式 + 双缓冲 流水线作业,零拷贝