4. DMA描述符与链表:描述符结构、链表模式、循环模式、硬件自动跳转

好,咱们今天来聊聊DMA描述符和链表。说实话,这块内容在嵌入式开发里特别重要,但也是很多人容易踩坑的地方。我当年刚接触DMA时,就被描述符的配置折腾了好几个通宵。

4.1 描述符结构:DMA的大脑

描述符是什么?说白了,它就是DMA控制器的一个“任务单”。每个描述符告诉DMA:你要从哪里搬数据,搬到哪去,搬多少,搬完了怎么办。

一个典型的DMA描述符结构大概长这样:

struct dma_descriptor {
    uint32_t src_addr;      // 源地址
    uint32_t dst_addr;      // 目标地址
    uint32_t transfer_size; // 传输大小(字节)
    uint32_t ctrl;          // 控制字段
    uint32_t next_desc;     // 下一个描述符指针
    uint32_t status;        // 状态字段
};

每个字段都有讲究。源地址和目标地址不用多说,但要注意对齐问题。我在项目中遇到过,如果源地址不是4字节对齐,某些DMA控制器会直接罢工。

控制字段(ctrl)是个大杂烩,里面包含了:

  • 传输方向(内存到外设,还是外设到内存)
  • 数据宽度(8位、16位、32位)
  • 中断使能(传输完要不要通知CPU)
  • 链式使能(要不要跳转到下一个描述符)

关键点:描述符本身必须放在内存中,而且通常要求地址对齐到缓存行大小(一般是32字节或64字节)。否则,DMA从内存读取描述符时可能会读到脏数据。

4.2 链表模式:串起来的任务

单个描述符只能做一次传输。但实际项目中,我们往往需要连续传输多个数据块。这时候,链表模式就派上用场了。

链表模式的核心思想是:每个描述符里都有一个next_desc字段,指向下一个描述符的地址。DMA完成当前传输后,自动去读取下一个描述符,继续干活。

我习惯把描述符链表画成这样的结构:

描述符1 --> 描述符2 --> 描述符3 --> ... --> 描述符N --> NULL

最后一个描述符的next_desc设为NULL,表示传输结束。DMA看到NULL后,就会停止,并触发一个完成中断。

链表模式的好处很明显:

  • 可以处理不连续的内存块
  • CPU只需要配置一次,DMA自己跑完整个链表
  • 适合大数据量传输,比如网络数据包、音频流

我的经验:配置链表时,一定要确保所有描述符都在DMA可访问的内存区域。我曾经把描述符放在DDR里,但DMA只能访问SRAM,结果DMA读到的全是0xDEADBEEF,排查了好久才发现是内存区域的问题。

4.3 循环模式:永不停歇的搬运工

有些场景下,我们希望DMA一直工作,比如音频播放、视频采集。这时候,循环模式就比链表模式更合适。

循环模式其实就是把链表的最后一个描述符的next_desc指回第一个描述符,形成一个环:

描述符1 --> 描述符2 --> 描述符3 --> ... --> 描述符N
   ^                                                |
   |________________________________________________|

DMA跑完一圈后,自动回到开头,继续下一轮传输。CPU只需要在中断里更新描述符里的数据缓冲区,就能实现不间断的数据流。

循环模式有几个关键点要注意:

  • 描述符数量要足够,避免CPU来不及更新缓冲区
  • 每个描述符的缓冲区大小要合理,太小会导致中断频繁,太大则延迟增加
  • 要处理好“生产者-消费者”关系,CPU和DMA不能同时操作同一个描述符

避坑指南:我曾经在音频项目中用过循环模式,结果声音断断续续。后来发现是描述符数量太少(只有2个),CPU更新缓冲区的速度跟不上DMA的消耗速度。解决方案很简单:把描述符增加到8个,问题就解决了。你想想看,缓冲区数量至少要是CPU处理延迟的2倍以上,才能保证不丢数据。

4.4 硬件自动跳转:DMA自己找活干

硬件自动跳转,是DMA控制器最酷的特性之一。它让DMA不需要CPU干预,就能自动从链表或循环中获取下一个描述符。

实现原理其实不复杂:

  1. DMA完成当前描述符的传输
  2. 检查当前描述符的next_desc字段
  3. 如果next_desc非空,DMA自动去内存读取下一个描述符
  4. 加载新描述符到内部寄存器
  5. 开始新的传输

整个过程完全由硬件完成,CPU可以安心睡觉或者干别的活。这也是DMA被称为“后台搬运工”的原因。

不同厂家的DMA控制器,自动跳转的实现略有差异:

厂家 自动跳转方式 特点
TI(德州仪器) EDMA3 支持三维传输,描述符可嵌套
NXP(恩智浦) eDMA 支持TCD(传输控制描述符),可配置优先级
Xilinx(赛灵思) AXI DMA 支持分散-聚集(Scatter-Gather)模式
ST(意法半导体) DMA2D 专门用于图形加速,支持像素格式转换

嗯,这里要注意:硬件自动跳转虽然方便,但也有一些限制。比如,描述符更新后,DMA可能不会立即感知到。我记得在某个项目中,CPU更新了描述符的缓冲区地址,但DMA还在用旧的地址,导致数据写到了错误的位置。

解决办法是:在更新描述符后,执行一个内存屏障指令(如DSB),确保CPU的写操作已经到达内存,然后再通知DMA重新加载描述符。

总结一下:描述符是DMA的任务单,链表模式适合一次性传输多个数据块,循环模式适合持续的数据流,硬件自动跳转让DMA能自己找活干。理解这些概念,你就能写出高效、可靠的DMA驱动。

我个人建议,刚开始学习时,先从单描述符开始,跑通后再尝试链表模式,最后再上循环模式。一步一个脚印,别想着一步登天。我在带新人时,经常看到他们一上来就搞复杂的循环模式,结果出了问题根本不知道从哪排查。

好了,这一章的内容就到这里。下一章我们会聊聊DMA中断处理与性能优化,到时候我会分享一些实际项目中的调优经验。