DMA传输四要素:源、目的、长度、控制
做DMA驱动这么多年,我见过最多的坑就是——寄存器配了,中断也开了,数据就是传不对。排查一圈,发现不是源地址打错了,就是长度少写了一个0。说白了,DMA传输就是个“搬砖”过程,你总得告诉它:从哪搬、搬到哪、搬多少、怎么搬。这四个要素,一个都不能少。
源地址(Source Address)
源地址就是数据从哪来。可能是外设的FIFO,也可能是内存的一块区域。嗯,这里有两个细节你一定要注意:
- 地址对齐:很多DMA控制器对源地址有对齐要求,比如32位总线必须4字节对齐。我在一个项目里遇到过,源地址配了个奇数地址,结果DMA搬了一堆乱码。
- 地址递增模式:从内存读数据通常要递增地址;从外设FIFO读数据,地址固定(不递增)。搞反了,你就等着丢数据吧。
代码里一般这样写(以某常见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 | 传输次数 | 可配置 |
配置长度时,我习惯先在初始化阶段把缓冲区总大小算好,然后按描述符个数均分:
// 每个描述符传输512字节,数据宽度32位
dma_desc->NDT = 512 / 4; // 128次传输
控制(Control / Configuration)
控制部分是DMA的“大脑”。它决定了传输方向、是否产生中断、是否开启循环模式、优先级高低等等。我把常用的控制位总结成一张表:
| 控制位 | 作用 | 典型值 |
|---|---|---|
| 方向 | 内存→外设,外设→内存,内存→内存 | 0/1/2 |
| 中断使能 | 传输完成、错误时触发中断 | 1(启用) |
| 循环模式 | 完成后自动重载,持续传输 | 1(循环) |
| 优先级 | 低/中/高/最高 | 根据系统负载选择 |
| 突发传输 | 一次请求传输多个宽度单位 | 4/8/16拍 |
为什么我把循环模式单独拎出来?因为它在描述符链中特别关键。循环模式下,DMA完成最后一个描述符后会跳回第一个,形成一个闭环。但有些控制器要求描述符链本身必须配置成环形(最后一个描述符的next指针指向第一个)。否则中断释放后,DMA就停在原地了。
四要素在描述符链中的协同
你可能要问:这四个要素和描述符链有什么关系?我举个实际例子:一个双缓冲音频播放流程。
- 描述符0:源地址 = 音频缓冲区A,目的地址 = DAC数据寄存器,长度 = 1024字节,控制 = 外设→内存方向,完成中断使能。
- 描述符1:源地址 = 音频缓冲区B,目的地址相同,长度相同,控制相同。
- 将描述符0和1通过next指针连成链,并配置循环模式。
- 启动DMA后,它会自动在A和B之间来回传输,每次完成后触发中断,你只需要在中断里填好另一块缓冲区的数据。
这套模式在数据采集、音视频流、网络包处理中非常常见。但有个细节:每个描述符的源/目的地址和长度必须独立配置,不能共用。我见过新手把描述符0的长度改大了,结果DMA运行到描述符1时还沿用大长度,直接踩坏了后面内存。
这张图把四要素和描述符链的关系画清楚了。你仔细看:每个描述符里都包着SAR、DAR、NDT、CR四个寄存器配置。DMA控制器在传输过程中,每完成一个描述符,就自动加载下一个描述符的这四个值。所以,优化描述符链循环,本质就是优化这四个要素的切换效率。
举个例子:如果你需要频繁修改源地址(比如从不同的外设FIFO读数据),那就在描述符链里把每个源地址提前配好,DMA跑起来后不需要软件干预。我曾经在高速数据采集卡上这么干过,八个通道轮流采集,CPU零干预,带宽直接拉满。