2. Linux DMA 子系统架构:dmaengine 框架、dma_device 结构体、dma_chan 通道管理
在 Linux 内核中,DMA 操作并非由各设备驱动直接操作硬件寄存器,而是通过一套统一的 dmaengine 框架 进行抽象。该框架位于 drivers/dma/ 目录下,向上为各设备驱动提供标准 API,向下对接不同厂商的 DMA 控制器硬件。理解这一分层架构是进行 DMA 性能优化与缓存一致性管理的基础。
核心认知: dmaengine 框架将 DMA 控制器抽象为
dma_device,将传输通道抽象为 dma_chan。设备驱动通过 dma_request_channel() 获取通道,通过 dmaengine_prep_slave_sg() 等接口准备传输描述符,最终由 dmaengine_submit() 提交并启动传输。
2.1 dmaengine 框架分层与核心数据结构
dmaengine 框架采用经典的三层架构:设备层、通道层 和 传输描述符层。每一层都有对应的核心数据结构,它们通过指针相互关联,形成一个完整的 DMA 传输对象模型。
架构要点: 框架的核心设计思想是“通道即资源”。每个 DMA 控制器可以拥有多个物理或逻辑通道,每个通道在同一时刻只能服务于一个传输请求。通道的分配与释放由框架统一管理,驱动开发者无需关心底层硬件细节。
2.2 struct dma_device:DMA 控制器的抽象
struct dma_device 是 dmaengine 框架中最顶层的结构体,它完整描述了一个 DMA 控制器的能力、资源与操作接口。每个 DMA 控制器驱动在初始化时,需要填充该结构体并调用 dma_async_device_register() 将其注册到框架中。
| 字段/操作 | 类型 | 说明 |
|---|---|---|
dev |
struct device * |
指向 DMA 控制器的通用设备结构体,用于电源管理、设备树绑定等 |
channels |
struct list_head |
该设备下所有可用通道的链表头,通过 list_add_tail() 添加通道 |
cap_mask |
dma_cap_mask_t |
能力位图,标记该设备支持的传输类型(如 DMA_SLAVE、DMA_CYCLIC、DMA_INTERLEAVE) |
device_alloc_chan_resources |
函数指针 | 分配通道资源,通常在 dma_request_channel() 时被调用 |
device_free_chan_resources |
函数指针 | 释放通道资源,在通道被释放时调用 |
device_prep_slave_sg |
函数指针 | 准备一次 slave 模式的 SG(Scatter-Gather)传输描述符 |
device_issue_pending |
函数指针 | 通知 DMA 控制器开始处理已提交的传输请求 |
device_tx_status |
函数指针 | 查询指定传输描述符的完成状态,返回已传输的字节数或 cookie 状态 |
性能陷阱: 在注册
dma_device 时,cap_mask 的设置必须精确匹配硬件能力。如果错误地声明了不支持的传输类型,框架在运行时不会进行二次校验,可能导致 DMA 传输静默失败或数据损坏。建议在 probe 函数中通过 dma_cap_set() 逐位设置,并配合硬件寄存器确认。
2.3 struct dma_chan:通道管理与生命周期
struct dma_chan 是设备驱动与 DMA 引擎交互的直接对象。每个 dma_chan 实例代表一个可独立调度的传输通道,它维护了通道的运行时状态、所属设备、客户端绑定关系以及传输完成情况。
struct dma_chan {
struct dma_device *device; /* 指向所属的 DMA 控制器设备 */
dma_cookie_t cookie; /* 当前正在传输的描述符 cookie */
dma_cookie_t completed_cookie; /* 最后一个完成的描述符 cookie */
int chan_id; /* 通道 ID,用于调试和日志 */
struct dma_chan_dev *dev; /* 通道对应的字符设备,用于 sysfs 导出 */
struct list_head device_node; /* 用于挂入 dma_device.channels 链表 */
struct dma_chan *slave; /* 指向配对的 slave 通道(memcpy 模式使用) */
struct dma_client *client; /* 当前占用该通道的客户端 */
void *private; /* 驱动私有数据,通常指向硬件通道寄存器基址 */
};
通道的生命周期管理遵循“申请 → 配置 → 使用 → 释放”的严格流程:
- 通道申请: 设备驱动调用
dma_request_channel(),传入能力掩码和过滤函数。框架遍历所有已注册的dma_device,找到第一个满足条件的空闲通道。 - 通道配置: 申请成功后,驱动通过
dmaengine_slave_config()设置传输参数,包括源/目的地址宽度、突发大小、传输方向等。 - 描述符准备与提交: 调用
dmaengine_prep_slave_sg()准备描述符,然后通过dmaengine_submit()提交到通道的 pending 队列。 - 启动传输: 调用
dma_async_issue_pending()通知 DMA 控制器开始处理。 - 通道释放: 传输完成后,调用
dma_release_channel()将通道归还给框架。
2.4 通道管理与性能优化要点
在实际的嵌入式系统中,DMA 通道是稀缺资源。一个高性能的 DMA 驱动设计需要关注以下通道管理策略:
| 优化方向 | 具体策略 | 预期收益 |
|---|---|---|
| 通道复用 | 对于非实时性传输,多个外设共享同一个 DMA 通道,通过软件调度分时使用 | 减少通道占用数量,降低硬件资源竞争 |
| 描述符预分配 | 在通道初始化时预分配一批描述符内存池,避免运行时动态分配 | 消除分配延迟,提升传输启动速度 |
| 批量提交 | 将多个小传输合并为一个 SG 描述符,一次性提交 | 减少通道切换开销,提高总线利用率 |
| 中断聚合 | 设置 DMA 控制器在完成多个描述符后才触发一次中断 | 降低 CPU 中断频率,减少上下文切换 |
| 通道优先级 | 为实时性要求高的外设(如音频、视频)分配高优先级通道 | 保证关键数据的传输延迟可控 |
实战建议: 在调试 DMA 通道问题时,可以通过
/sys/class/dma/ 目录下的 sysfs 接口查看每个通道的状态。例如 cat /sys/class/dma/dma0chan0/bytes_transferred 可以获取该通道累计传输的字节数,用于验证传输是否正常进行。
2.5 传输描述符与 cookie 机制
dmaengine 框架使用 cookie 机制来跟踪每个传输描述符的状态。每次调用 dmaengine_submit() 时,框架会为该描述符分配一个递增的 cookie 值。驱动可以通过 dma_async_is_tx_complete() 或 dmaengine_tx_status() 查询指定 cookie 对应的传输是否完成。
/* 典型的 DMA 传输提交与完成查询流程 */
struct dma_async_tx_descriptor *tx;
dma_cookie_t cookie;
enum dma_status status;
/* 准备描述符 */
tx = dmaengine_prep_slave_sg(chan, sgt->sgl, sgt->nents, dir, DMA_PREP_INTERRUPT);
if (!tx) {
dev_err(dev, "Failed to prepare DMA descriptor\n");
return -ENOMEM;
}
/* 设置完成回调 */
tx->callback = my_dma_callback;
tx->callback_param = my_data;
/* 提交传输,获取 cookie */
cookie = dmaengine_submit(tx);
if (dma_submit_error(cookie)) {
dev_err(dev, "Failed to submit DMA transaction\n");
return -EIO;
}
/* 启动传输 */
dma_async_issue_pending(chan);
/* 轮询等待完成(在中断回调中通常不这样做) */
do {
status = dmaengine_tx_status(chan, cookie, NULL);
} while (status == DMA_IN_PROGRESS);
if (status == DMA_COMPLETE) {
dev_info(dev, "DMA transfer completed successfully\n");
} else {
dev_err(dev, "DMA transfer failed with status: %d\n", status);
}
关键注意事项: cookie 值在通道生命周期内单调递增,但可能会回绕(wrap around)。驱动在比较 cookie 时,应使用
dma_async_is_complete() 辅助函数,该函数内部处理了回绕逻辑。直接使用 == 比较 cookie 在回绕时会导致错误判断。