4. DMA描述符链:链式DMA的概念、描述符结构、链表构建、硬件自动加载机制
各位做嵌入式开发的同行,咱们今天聊一个硬核话题——DMA描述符链。说白了,就是让DMA自己学会“接活儿”,不用CPU一次次去喂指令。
我记得刚入行那会儿,做一款视频采集卡。数据量一大,CPU光忙着配置DMA寄存器就占用了30%的负载。后来用了描述符链,CPU负载直接降到5%以下。嗯,这就是链式DMA的魅力。
4.1 为什么需要链式DMA?
先看一个场景:你要从ADC采集1000个数据块,每个块送到内存的不同位置。用普通DMA怎么做?
- 传输完一块 → 触发中断 → CPU重新配置下一块 → 启动DMA
- 1000块就要进1000次中断
- 中断响应+寄存器配置,每次至少几微秒
你想想看,这效率能高吗?
链式DMA的思路完全不同:提前把1000个传输任务做成一个链表,告诉DMA“链表头在哪”,然后DMA自己顺着链子一个个执行。CPU?去干更重要的事吧。
核心收益:
- 中断次数从N次降到1次(甚至0次)
- CPU负载大幅降低
- 传输延迟更稳定,没有中断抖动
4.2 描述符结构——链表的“节点”长什么样?
每个描述符就是一个数据结构,告诉DMA“这次搬什么、搬多少、搬完去哪”。不同厂商的DMA控制器细节不同,但核心字段大同小异。
| 字段 | 说明 | 我常用的值/建议 |
|---|---|---|
| 源地址 | 数据从哪来 | 外设地址或内存地址 |
| 目的地址 | 数据搬到哪 | 内存缓冲区地址 |
| 传输长度 | 本次传输的字节数 | 注意对齐要求 |
| 控制标志 | 中断使能、传输方向、地址增减等 | 最后一块才开中断 |
| 下一个描述符指针 | 指向链表中的下一个节点 | NULL表示链尾 |
以STM32的DMA描述符为例,实际代码中是这样定义的:
typedef struct {
uint32_t SADDR; // 源地址
uint32_t DADDR; // 目的地址
uint32_t LLI; // 下一个描述符指针 (Linked List Item)
uint32_t Control; // 控制寄存器值
} DMA_Descriptor_t;
这里有个坑——描述符必须放在内存中特定的对齐地址上。我曾经在一个项目里,因为描述符没按32字节对齐,DMA死活不认链。查了两天手册才发现,嗯,教训深刻。
避坑指南:描述符的内存对齐要求因芯片而异。STM32要求32字节对齐,有些DSP要求64字节对齐。建议用 __attribute__((aligned(32))) 强制对齐。
4.3 链表构建——手把手搭一条链
构建链表其实不复杂,核心就是三步:
- 分配描述符数组:按最大传输块数分配
- 填充每个描述符:设置地址、长度、控制标志
- 链接成链:把每个描述符的
LLI字段指向下一个
看代码更直观:
#define NUM_DESCRIPTORS 10
DMA_Descriptor_t desc[NUM_DESCRIPTORS] __attribute__((aligned(32)));
uint8_t buffer[NUM_DESCRIPTORS][1024];
void build_dma_chain(void) {
for (int i = 0; i < NUM_DESCRIPTORS; i++) {
desc[i].SADDR = (uint32_t)&ADC_DATA_REG;
desc[i].DADDR = (uint32_t)buffer[i];
desc[i].Control = DMA_CCR_MSIZE_16BIT | DMA_CCR_PSIZE_16BIT;
// 最后一块才产生中断
if (i == NUM_DESCRIPTORS - 1) {
desc[i].Control |= DMA_CCR_TCIE; // 传输完成中断
desc[i].LLI = 0; // 链尾
} else {
desc[i].LLI = (uint32_t)&desc[i + 1];
}
}
}
我个人习惯把描述符数组放在DMA专用的内存区(如SRAM2或紧耦合内存),这样DMA访问描述符时不会和CPU争总线。
4.4 硬件自动加载机制——DMA自己“读链”
这才是链式DMA的精髓。你只需要告诉DMA控制器:
- 链表头地址(第一个描述符的地址)
- 启动传输
剩下的,DMA自己搞定:
- 从链表头加载第一个描述符
- 按描述符配置执行传输
- 传输完成后,读取
LLI字段 - 如果
LLI != NULL,自动加载下一个描述符 - 重复步骤2-4,直到链尾
整个过程,CPU完全不用参与。这就是所谓的“硬件自动加载”。
小技巧:有些DMA支持“乒乓缓冲”模式——用两个描述符链交替工作。一个链在传输时,CPU可以修改另一个链的描述符。这样就能实现“无中断连续传输”。
4.5 链式DMA的典型应用场景
| 场景 | 描述 | 链式DMA的优势 |
|---|---|---|
| 高速数据采集 | ADC连续采样,数据存入循环缓冲区 | 无需中断,采样率更高 |
| 显示驱动 | 从多个图层合成一帧图像 | 一次启动,自动搬运所有图层 |
| 网络数据包处理 | 将分散的数据包描述符链式传输 | 减少CPU拷贝,提升吞吐量 |
| 音频流处理 | 多通道音频数据交错传输 | 精确控制每个通道的传输时机 |
4.6 链式DMA的核心流程
下面这张图展示了链式DMA的完整工作流程,从CPU初始化到硬件自动加载:
4.7 实际项目中的避坑经验
做了这么多年嵌入式,链式DMA的坑我踩过不少。分享几个印象深刻的:
- 描述符被Cache污染:CPU写描述符时只写了Cache,DMA从内存读到的却是旧数据。解决方案是使用非Cacheable内存区域,或者在修改描述符后手动Clean Cache。
- 链尾处理不当:如果最后一个描述符的LLI指向了无效地址,DMA会跑飞。我习惯在链尾放一个“哨兵描述符”,LLI指向自身,控制标志设为只中断不传输。
- 描述符更新时机:DMA正在执行的描述符不能修改。我曾经在调试时,DMA刚加载完描述符#3,我改了描述符#3的地址,结果传输数据全乱了。正确做法是只修改“尚未被加载”的描述符。
重要提醒:链式DMA虽然强大,但调试起来比普通DMA复杂得多。建议先用逻辑分析仪或DMA调试寄存器观察描述符加载过程,确认链表构建正确后再跑实际数据。
好了,链式DMA的核心内容就这些。说白了就是让DMA自己管理自己的任务队列,CPU只负责“搭链”和“收尾”。这个机制用好了,系统性能能上一个台阶。