DMA传输四要素:源、目的、长度、控制

做DMA驱动这么多年,我见过最多的坑就是——寄存器配了,中断也开了,数据就是传不对。排查一圈,发现不是源地址打错了,就是长度少写了一个0。说白了,DMA传输就是个“搬砖”过程,你总得告诉它:从哪搬、搬到哪、搬多少、怎么搬。这四个要素,一个都不能少。

源地址(Source Address)

源地址就是数据从哪来。可能是外设的FIFO,也可能是内存的一块区域。嗯,这里有两个细节你一定要注意:

  • 地址对齐:很多DMA控制器对源地址有对齐要求,比如32位总线必须4字节对齐。我在一个项目里遇到过,源地址配了个奇数地址,结果DMA搬了一堆乱码。
  • 地址递增模式:从内存读数据通常要递增地址;从外设FIFO读数据,地址固定(不递增)。搞反了,你就等着丢数据吧。
配置源地址时,先看外设的地址寄存器是否支持自动递增。如果不支持,必须将递增模式关闭,否则DMA会逐个地址去读外设,外设只会返回同一个寄存器值。

代码里一般这样写(以某常见DMA控制器为例):

// 配置源地址为外设数据寄存器
dma_channel->SAR = (uint32_t)&UART->DR;
// 关闭源地址递增
dma_channel->CR &= ~DMA_CR_SINC;

目的地址(Destination Address)

目的地址就是数据搬到哪。和源地址很像,也有对齐和递增问题。不过有个经典场景:把外设数据搬到内存缓冲区,目的地址递增,源地址固定。

我曾经调试过一个音频采集驱动,声音总是断断续续。查了一天发现目的地址递增模式没配错,但缓冲区基地址写错了——每次DMA完成中断后,我手动改了目的地址,但DMA描述符链里的地址没更新。这个教训告诉我:描述符链模式下,每个描述符的源、目的地址都要独立且正确。

传输长度(Transfer Length / Count)

长度决定这次DMA搬多少数据。通常用“传输次数 × 数据宽度”来计算。但注意:有些控制器配置的是“字节数”,有些是“传输次数”(比如一次传输4字节,长度写10表示40字节)。

控制器类型长度单位常见宽度
STM32 DMA传输次数8/16/32位
Xilinx AXI DMA字节数固定32位
Synopsys DW DMA传输次数可配置
我最头疼的就是长度溢出问题。如果DMA描述符链中某个描述符传输长度设得太大,导致超过缓冲区末尾,DMA会继续写后面的内存——轻则数据错乱,重则系统死机。所以长度计算时,务必做边界检查。

配置长度时,我习惯先在初始化阶段把缓冲区总大小算好,然后按描述符个数均分:

// 每个描述符传输512字节,数据宽度32位
dma_desc->NDT = 512 / 4;  // 128次传输

控制(Control / Configuration)

控制部分是DMA的“大脑”。它决定了传输方向、是否产生中断、是否开启循环模式、优先级高低等等。我把常用的控制位总结成一张表:

控制位作用典型值
方向内存→外设,外设→内存,内存→内存0/1/2
中断使能传输完成、错误时触发中断1(启用)
循环模式完成后自动重载,持续传输1(循环)
优先级低/中/高/最高根据系统负载选择
突发传输一次请求传输多个宽度单位4/8/16拍

为什么我把循环模式单独拎出来?因为它在描述符链中特别关键。循环模式下,DMA完成最后一个描述符后会跳回第一个,形成一个闭环。但有些控制器要求描述符链本身必须配置成环形(最后一个描述符的next指针指向第一个)。否则中断释放后,DMA就停在原地了。

关键经验:控制寄存器的配置顺序也很重要。 我的做法是先清零整个控制寄存器,再按位设置。这样能避免之前残留的配置项污染新传输。

四要素在描述符链中的协同

你可能要问:这四个要素和描述符链有什么关系?我举个实际例子:一个双缓冲音频播放流程。

  1. 描述符0:源地址 = 音频缓冲区A,目的地址 = DAC数据寄存器,长度 = 1024字节,控制 = 外设→内存方向,完成中断使能。
  2. 描述符1:源地址 = 音频缓冲区B,目的地址相同,长度相同,控制相同。
  3. 将描述符0和1通过next指针连成链,并配置循环模式。
  4. 启动DMA后,它会自动在A和B之间来回传输,每次完成后触发中断,你只需要在中断里填好另一块缓冲区的数据。

这套模式在数据采集、音视频流、网络包处理中非常常见。但有个细节:每个描述符的源/目的地址和长度必须独立配置,不能共用。我见过新手把描述符0的长度改大了,结果DMA运行到描述符1时还沿用大长度,直接踩坏了后面内存。

设计描述符链时,最好在初始化阶段就把所有描述符的四要素统一定义成结构体数组,然后在启动DMA时统一写入。这样代码清晰,也方便后期调试。
DMA传输四要素及描述符链关系 DMA传输四要素 × 描述符链 源地址 (SAR) 目的地址 (DAR) 传输长度 (NDT) 控制 (CR) 描述符链(Descriptor Chain) 每个描述符包含一组 SAR/DAR/NDT/CR 通过 next 指针形成环或列表 ⚠️ 关键检查点 地址对齐 | 长度边界 控制位冲突 | 循环配置

这张图把四要素和描述符链的关系画清楚了。你仔细看:每个描述符里都包着SAR、DAR、NDT、CR四个寄存器配置。DMA控制器在传输过程中,每完成一个描述符,就自动加载下一个描述符的这四个值。所以,优化描述符链循环,本质就是优化这四个要素的切换效率。

举个例子:如果你需要频繁修改源地址(比如从不同的外设FIFO读数据),那就在描述符链里把每个源地址提前配好,DMA跑起来后不需要软件干预。我曾经在高速数据采集卡上这么干过,八个通道轮流采集,CPU零干预,带宽直接拉满。