4. DMA 传输描述符:struct dma_async_tx_descriptor

DMA 传输描述符是 DMA 引擎驱动中最重要的数据结构之一。它抽象了一次 DMA 传输任务的所有属性:源地址、目的地址、传输长度、传输方向、硬件链路信息以及完成回调。理解描述符的结构和生命周期,是编写正确、高效 DMA 驱动的核心前提。

4.1 struct dma_async_tx_descriptor 结构体详解

该结构体定义在 include/linux/dmaengine.h 中,是 DMA 事务的通用表示。每个描述符对应一个硬件能够执行的 DMA 传输块(或链中的一个节点)。

核心字段速览: 描述符将软件请求(回调、标志、元数据)与硬件执行单元(phys、tx_list)绑定在一起。
struct dma_async_tx_descriptor {
    dma_cookie_t       cookie;          /* 传输的唯一标识 cookie */
    enum dma_status    status;          /* 传输状态:DMA_IN_PROGRESS / DMA_COMPLETE / DMA_ERROR */
    struct list_head   tx_list;         /* 用于硬件链表(DMA 链)的节点 */
    struct dma_chan    *chan;           /* 所属的 DMA 通道 */
    dma_addr_t         phys;            /* 描述符自身的物理地址(硬件链表使用) */
    struct dmaengine_unmap_data *unmap; /* 用于 unmap 和 sync 的辅助数据 */

    /* 回调机制 */
    dma_async_tx_callback      callback;        /* 传输完成回调函数指针 */
    dma_async_tx_callback      callback_result; /* 带结果(残差)的回调 */
    void                      *callback_param;  /* 回调参数 */

    unsigned long              flags;           /* 传输标志:DMA_PREP_INTERRUPT 等 */
    struct dma_descriptor_metadata_ops *metadata_ops; /* 元数据操作(用于 IN 方向) */

    /* 驱动私有数据 */
    struct list_head           node;            /* 用于挂入提交队列 */
    struct list_head           desc_node;       /* 用于挂入通道的已提交/进行中链表 */
};
字段类型说明
cookiedma_cookie_t每次 tx_submit() 时分配的唯一序号,用于查询传输状态。
statusenum dma_status驱动更新此字段反映硬件执行进度。
tx_liststruct list_head硬件链表节点,多个描述符可组成 DMA 链(如环形或链表模式)。
physdma_addr_t描述符本身的物理地址,硬件通过该地址获取下一个描述符。
callbackdma_async_tx_callback传输完成后的回调函数,在 tasklet 或中断上下文中调用。
callback_resultdma_async_tx_callback带结果信息的回调,可获取残差(residue)。
flagsunsigned long控制描述符行为,如是否产生中断、是否使用硬件链。

4.2 描述符的生命周期

一个 DMA 描述符从创建到释放,经历 5 个明确阶段。驱动开发者必须理解每个阶段的职责和约束。

DMA 描述符生命周期 ① 分配 alloc_chan_resources ② 准备 device_prep_dma_* ③ 提交 tx_submit ④ 执行 dma_async_issue_pending ⑤ 完成 / 释放 回调 → desc_free 描述符池预分配 避免运行时 kmalloc 硬件执行完成后 调用回调并回收
  1. 分配(Allocation): 在 alloc_chan_resources 回调中,驱动预分配一批描述符放入空闲池。避免在数据路径中动态分配内存。
  2. 准备(Preparation): 调用 device_prep_dma_memcpy、device_prep_slave_sg 等回调,填充描述符的源、目的、长度、标志。此时描述符处于“已初始化但未提交”状态。
  3. 提交(Submission): 调用 tx_submit 将描述符放入通道的提交队列,分配 cookie。描述符进入“等待调度”状态。
  4. 执行(Execution): 调用 dma_async_issue_pending 后,驱动将提交队列中的描述符写入硬件寄存器或链表指针,硬件开始传输。
  5. 完成与释放(Completion & Free): 硬件产生中断后,驱动在 tasklet 中更新状态、调用回调,然后将描述符归还空闲池。
常见错误: 在回调中再次调用 tx_submit 提交同一个描述符,会导致描述符被重复使用而引发硬件竞争。正确的做法是在回调中从空闲池获取新描述符,或使用描述符链(tx_list)一次性提交多个。

4.3 回调函数机制

回调函数是 DMA 引擎向消费者(如块设备层、音频驱动)通知传输完成的异步机制。回调在 tasklet 上下文 中执行,不能睡眠,不能持有锁。

4.3.1 回调注册与触发流程

回调触发流程 消费者驱动 描述符 callback = func DMA 硬件 中断处理 1. 消费者设置 callback 2. 提交描述符 3. 硬件完成传输 4. 触发中断 5. tasklet 中调用 callback(param)

4.3.2 回调类型与使用场景

回调类型函数原型典型场景
callbackvoid (*callback)(void *param)简单通知,不关心传输结果细节。
callback_resultvoid (*callback)(void *param, const struct dmaengine_result *result)需要获取残差(residue)或错误码的场景,如音频帧精确位置。

4.3.3 驱动中回调的实现要点

/* 消费者设置回调示例 */
struct dma_async_tx_descriptor *desc;
desc = dmaengine_prep_slave_single(chan, buf_dma, len, direction, flags);
if (!desc)
    return -ENOMEM;

desc->callback = my_dma_callback;
desc->callback_param = my_private_data;

/* 提交并启动 */
cookie = dmaengine_submit(desc);
dma_async_issue_pending(chan);

/* 回调函数实现(在 tasklet 上下文中执行) */
static void my_dma_callback(void *param)
{
    struct my_priv *priv = param;

    /* 注意:不能睡眠,不能调用 dmaengine_submit 提交同一个 desc */
    complete(&priv->done);  /* 或 wake_up 等轻量同步 */
}
最佳实践: 如果需要在回调中再次启动 DMA,请从空闲池获取新的描述符,而不是重用当前正在回调的描述符。使用描述符链(DMA_PREP_FENCE 标志)可以减少中断次数,提高吞吐量。

4.4 描述符标志与硬件链

flags 字段控制描述符的行为,常用标志如下:

标志值含义
DMA_PREP_INTERRUPT1该描述符完成后产生中断(用于链中最后一个描述符)。
DMA_CTRL_ACK2消费者承诺在回调后立即释放描述符,驱动可跳过 ACK 检查。
DMA_PREP_FENCE4该描述符之前的传输必须全部完成,才能开始本描述符(用于链排序)。

硬件链(DMA 链表)通过描述符的 tx_list 字段将多个描述符串联。驱动在 device_issue_pending 中将链表首地址写入硬件寄存器,硬件自动遍历执行。

/* 构建 DMA 链示例 */
struct dma_async_tx_descriptor *prev = NULL, *last = NULL;

for (i = 0; i < num_segments; i++) {
    desc = dmaengine_prep_slave_single(chan, sg_dma_address(&sg[i]),
                                       sg_dma_len(&sg[i]), direction,
                                       DMA_PREP_INTERRUPT);
    if (!desc)
        goto err;

    if (prev)
        prev->tx_list.next = &desc->tx_list;  /* 链接 */
    prev = desc;
    last = desc;
}

/* 只有最后一个描述符产生中断 */
last->flags |= DMA_PREP_INTERRUPT;

cookie = dmaengine_submit(last);  /* 提交链尾 */
dma_async_issue_pending(chan);
硬件链注意事项: 并非所有 DMA 控制器都支持硬件链表。不支持时,驱动需要将 tx_list 作为软件队列,在中断中逐个提交下一个描述符。检查 dma_chan 的 private 字段或设备树属性来判断硬件能力。

4.5 描述符管理与内存屏障

描述符的 phys 字段存储描述符自身的物理地址,供硬件通过 DMA 读取。驱动在填充描述符后,必须使用 dma_wmb() 确保描述符内容对硬件可见,然后再将物理地址写入硬件寄存器。

/* 典型硬件链表提交序列 */
desc->src = src_phys;
desc->dst = dst_phys;
desc->len = length;
desc->next = next_desc_phys;

/* 确保描述符内容写入内存 */
dma_wmb();

/* 将描述符物理地址写入硬件链表指针寄存器 */
writel(desc->phys, chan->base + DMA_NEXT_DESC_REG);
关键总结: 描述符是软件与硬件之间的契约。软件负责分配、填充、提交;硬件负责读取、执行、中断;回调是异步通知的桥梁。理解这个契约,才能写出稳定高效的 DMA 驱动。