4、DMA描述符链:描述符结构设计、链表构建方法、硬件自动加载机制

DMA描述符链,说白了就是给DMA控制器准备的一份「任务清单」。我刚开始接触这个的时候,觉得不就是个链表嘛,有啥好讲的?后来踩了几个坑才明白——描述符结构设计得好不好,直接决定了你的DMA传输效率,甚至决定了系统会不会莫名其妙地卡死。

嗯,咱们今天就把这块掰开揉碎了讲清楚。

4.1 描述符结构设计——别小看这几个字段

描述符是什么?它就是DMA硬件能读懂的一个数据结构。每个描述符描述了一次DMA传输任务:从哪里搬数据,搬到哪里,搬多少,搬完了怎么办。

我个人习惯把描述符分成三个区域:控制区、地址区、状态区。你想想看,这样设计,硬件解析起来也快,软件维护起来也清晰。

偏移地址 字段名 位宽 说明
0x00 NEXT_DESC_PTR 32位 下一个描述符的物理地址
0x04 SRC_ADDR 32位 源地址(外设或内存)
0x08 DST_ADDR 32位 目的地址
0x0C CTRL 32位 控制字:传输大小、中断使能、传输类型等
0x10 STATUS 32位 状态字:完成标志、错误标志

这里有个关键点——描述符必须对齐到8字节边界。为什么?因为DMA硬件在读取描述符时是一次性读多个字的,不对齐的话,轻则性能下降,重则直接触发总线错误。我在项目中遇到过一回,描述符地址没对齐,结果DMA每次读描述符都要多花好几个时钟周期,整个传输速率掉了30%。

核心原则:描述符结构中的每个字段,硬件必须能在一个总线突发传输中读完。所以字段顺序、对齐方式,都得按硬件手册来,别自己瞎排。

4.2 链表构建方法——环形还是链式?

描述符链有两种常见形态:单向链表环形链表。我建议你根据应用场景来选。

  • 单向链表:最后一个描述符的NEXT_DESC_PTR填NULL。适合一次性的批量传输,比如从摄像头抓一帧图像。
  • 环形链表:最后一个描述符指向第一个。适合持续的数据流,比如音频播放、网络收发。

构建链表时,我习惯用这样的流程:

// 伪代码:构建DMA描述符链
struct dma_desc *desc_pool;
int num_desc = 16;

// 1. 分配连续物理内存(关键!)
desc_pool = dma_alloc_coherent(dev, 
                num_desc * sizeof(struct dma_desc),
                &phys_addr, GFP_KERNEL);

// 2. 初始化每个描述符
for (i = 0; i < num_desc; i++) {
    desc_pool[i].src_addr = src_buf + i * BLOCK_SIZE;
    desc_pool[i].dst_addr = dst_buf + i * BLOCK_SIZE;
    desc_pool[i].ctrl = BLOCK_SIZE | INT_ENABLE;
    desc_pool[i].status = 0;
    
    // 3. 链接:指向下一个
    if (i < num_desc - 1)
        desc_pool[i].next = phys_addr + (i+1) * sizeof(struct dma_desc);
    else
        desc_pool[i].next = phys_addr; // 环形:指向第一个
}

// 4. 把链表头地址告诉DMA硬件
dma_engine_start(chan, phys_addr);

避坑指南:我曾经在环形链表中犯过一个低级错误——最后一个描述符的NEXT指针忘了回指到第一个。结果DMA跑完一轮就停了,音频输出直接断流。排查了半天才发现是链表没闭环。嗯,从那以后我每次构建完链表都会手动遍历一遍,确认闭环。

4.3 硬件自动加载机制——DMA是怎么「自动」的?

很多人以为DMA描述符链是软件一条一条喂给硬件的。其实不是。硬件自己会顺着链表往下读。

流程是这样的:

  1. 软件把链表头地址写入DMA控制器的描述符基地址寄存器
  2. DMA硬件自动从该地址读取第一个描述符。
  3. 解析描述符中的SRC_ADDR、DST_ADDR、CTRL,开始传输数据。
  4. 传输完成后,硬件读取描述符中的NEXT_DESC_PTR。
  5. 如果NEXT_DESC_PTR非空,硬件自动跳转到下一个描述符,重复步骤2-4。
  6. 如果NEXT_DESC_PTR为空(或回到链表头),硬件停止或产生中断。

你想想看,这个过程中CPU几乎不用干预。只有两种情况下CPU需要介入:

  • 链表跑完了,产生完成中断。
  • 传输过程中出错了,产生错误中断。

这就是为什么DMA描述符链能实现「零CPU干预」的数据搬运。我做过一个网络加速的项目,用环形描述符链配合硬件自动加载,CPU占用率从原来的60%降到了5%以下。

注意:硬件自动加载描述符时,描述符所在的内存不能被CPU缓存。否则硬件读到的是缓存中的旧数据,而CPU更新的是内存中的新数据,两边就不同步了。解决办法:用dma_alloc_coherent分配内存,或者手动做cache一致性维护。

4.4 实战中的几个坑

讲几个我实际踩过的坑,希望能帮你省点时间:

  • 描述符池大小要留余量:我建议至少分配两倍于理论需求的描述符。因为有时候外设会突发大量数据,描述符不够用就只能丢数据了。
  • 中断处理要快:描述符完成中断里,别做复杂操作。我一般只做两件事:更新软件维护的链表尾指针,然后唤醒等待队列。其他事情放到下半部去做。
  • 小心描述符被覆盖:环形链表中,如果软件更新描述符的速度跟不上硬件消费的速度,就会出现「硬件正在读的描述符被软件改了」的情况。解决办法:用双缓冲或者增加描述符数量。

好了,关于DMA描述符链的核心内容就这些。说白了,描述符结构设计是基础,链表构建是手段,硬件自动加载是目的。把这三点吃透了,你的DMA驱动就能跑得又快又稳。