3. DMA传输描述符

DMA 传输描述符是驱动与 DMA 引擎之间的核心契约。它封装了一次传输所需的所有参数:源地址、目的地址、传输长度、硬件链路指针、完成回调以及状态标志。理解描述符的结构与生命周期,是正确使用 DMA 引擎的前提。

3.1 描述符核心结构:struct dma_async_tx_descriptor

该结构体定义在 <linux/dmaengine.h> 中,是 DMA 事务的抽象表示。每个描述符对应一个或多个硬件描述符(取决于引擎是否支持 scatter-gather)。

字段 类型 说明
cookie dma_cookie_t 提交后由引擎分配的传输标识符,用于查询状态
flags unsigned long 控制位:DMA_PREP_INTERRUPT(使能完成中断)、DMA_CTRL_ACK 等
callback dma_async_tx_callback 传输完成后的回调函数指针(在 tasklet 或中断上下文中调用)
callback_param void * 回调函数的私有参数,通常指向请求者的上下文
tx_submit dma_cookie_t (*)(struct dma_async_tx_descriptor *) 将描述符提交到硬件待处理队列的方法
desc_free int (*)(struct dma_async_tx_descriptor *) 释放描述符内存的方法,由驱动在不再需要时调用
chan struct dma_chan * 该描述符所属的 DMA 通道
phys dma_addr_t 硬件描述符链表的物理地址(用于链式 DMA)
tx_list struct list_head 用于将多个描述符链接成软件链表(环形缓冲区或散列集合)
关键设计原则:描述符的 callback 和 callback_param 在 tx_submit 之前设置,提交后引擎拥有描述符的所有权。驱动不应在传输完成前修改描述符内容。

3.2 描述符生命周期

一个描述符从创建到释放经历四个明确阶段。环形缓冲区设计需要精确管理每个阶段的状态转换。

  1. 预备(Prepared):调用 device_prep_dma_* 系列函数(如 dmaengine_prep_slave_sg())获得描述符。此时描述符属于驱动,可以设置回调、参数和 flags。
  2. 提交(Submitted):调用 tx_submit 将描述符移交给 DMA 引擎的软件待处理队列。引擎返回一个 cookie,驱动用此 cookie 跟踪传输状态。
  3. 触发(Issued):调用 issue_pending 通知引擎将待处理队列中的描述符写入硬件寄存器或链表,启动实际 DMA 传输。
  4. 完成(Completed):硬件传输结束,引擎调用回调(如果设置了 DMA_PREP_INTERRUPT),驱动在回调中释放描述符或将其归还到空闲池。
描述符生命周期状态机 预备 Prepared 提交 Submitted 触发 Issued 完成 Completed tx_submit issue_pending 硬件传输结束 + 中断 desc_free / 归还空闲池
环形缓冲区中的生命周期管理:在环形缓冲区设计中,描述符通常从预分配的池中取出(预备),提交后进入软件环形队列,触发后硬件开始处理。完成回调将描述符归还到空闲池,避免频繁的 kmalloc/kfree。这种零分配策略对高吞吐量场景至关重要。

3.3 tx_submit:提交描述符

tx_submit 是描述符的方法,调用后将描述符加入 DMA 引擎的软件待处理队列。该函数是原子操作,不能在中断上下文之外睡眠。

struct dma_async_tx_descriptor *desc;
dma_cookie_t cookie;

desc = dmaengine_prep_slave_sg(chan, sgt, nents, dir, DMA_PREP_INTERRUPT);
if (!desc) {
    dev_err(dev, "Failed to prepare DMA descriptor\n");
    return -ENOMEM;
}

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

/* 提交描述符,获得 cookie */
cookie = desc->tx_submit(desc);
if (dma_submit_error(cookie)) {
    dev_err(dev, "Failed to submit DMA descriptor\n");
    desc->desc_free(desc);
    return -EIO;
}
行为 说明
原子提交 将描述符追加到通道的 pending 链表尾部,返回递增的 cookie
所有权转移 提交后驱动不应再修改描述符的地址/长度字段,引擎可能随时读取
错误处理 返回 -ENOMEM 或 -EINVAL 时,驱动必须手动调用 desc_free 释放描述符
中断使能 若需要完成回调,必须在预备时设置 DMA_PREP_INTERRUPT 标志

3.4 issue_pending:触发传输

issue_pending 是通道的方法,调用后 DMA 引擎将待处理队列中的所有描述符写入硬件,启动实际传输。该函数通常不阻塞,但具体行为取决于硬件实现。

/* 触发所有待处理的描述符 */
dma_async_issue_pending(chan);

/* 或者直接调用通道方法 */
chan->device->device_issue_pending(chan);
常见陷阱:部分 DMA 引擎(如某些 ARM PL08x)在 issue_pending 时只写入第一个描述符,后续描述符通过硬件链表自动加载。驱动必须确保描述符的 phys 字段指向有效的硬件描述符链表,否则传输会在第一个描述符完成后停止。

3.5 描述符状态查询

驱动通过 cookie 查询传输状态,常用函数为 dma_async_is_tx_complete() 或 dmaengine_tx_status()。

函数 返回值 使用场景
dma_async_is_tx_complete() DMA_COMPLETE / DMA_IN_PROGRESS / DMA_PAUSED / DMA_ERROR 轮询模式,非中断驱动
dmaengine_tx_status() 同上,同时返回剩余字节数 需要知道传输进度的场景
enum dma_status status;
status = dmaengine_tx_status(chan, cookie, NULL);
if (status == DMA_COMPLETE) {
    /* 传输完成,可以重用描述符 */
} else if (status == DMA_ERROR) {
    /* 传输出错,需要复位通道 */
}

3.6 环形缓冲区中的描述符管理策略

在环形缓冲区设计中,描述符的生命周期与缓冲区槽位绑定。典型策略如下:

  • 预备阶段:从空闲池获取描述符,设置源/目的地址指向环形缓冲区当前槽位。
  • 提交阶段:调用 tx_submit,将描述符加入软件待处理队列,同时将槽位标记为“提交中”。
  • 触发阶段:调用 issue_pending,硬件开始处理。此时槽位数据正在被 DMA 读取或写入。
  • 完成阶段:回调中将描述符归还空闲池,槽位标记为“空闲”,唤醒等待的生产者或消费者。
环形缓冲区与描述符生命周期映射 槽位 0 空闲 槽位 1 提交中 槽位 2 传输中 槽位 3 已完成 槽位 4 待回收 槽位 5 空闲 描述符空闲池(预分配,避免运行时分配) 每个槽位绑定一个描述符,完成回调后归还池中
性能建议:在环形缓冲区中,tx_submit 和 issue_pending 应尽量批量调用。每次提交多个描述符后统一触发一次 issue_pending,减少硬件寄存器访问次数,提高总线利用率。