3. DMA 引擎驱动框架
Linux DMA Engine 子系统为设备驱动开发者提供了一套统一的、与硬件无关的 API,用于管理和使用 DMA 控制器。本章节将深入剖析该子系统的核心架构、关键数据结构以及数据准备接口,帮助您理解 DMA 传输的完整生命周期。
3.1 Linux DMA Engine 子系统架构
DMA Engine 子系统采用分层设计,将通用 API 与硬件特定实现解耦。其核心思想是:消费者(如网卡、声卡驱动)通过标准接口发起 DMA 请求,而提供者(DMA 控制器驱动)则负责将这些请求转化为硬件操作。
架构核心分层:
- 上层 API 层 (dmaengine API): 提供给消费者驱动使用的通用接口,如
dmaengine_prep_slave_sg()、dmaengine_submit()、dma_async_issue_pending()。 - 核心框架层 (dmaengine core): 负责管理 DMA 设备、通道、事务队列,以及提供注册/注销接口。核心数据结构包括
dma_device和dma_chan。 - 硬件驱动层 (DMA controller driver): 实现具体的 DMA 控制器操作,填充
dma_device结构体中的函数指针,处理中断,管理硬件描述符。
设计意图: 这种分层架构使得一个 DMA 控制器驱动可以被多个不同类型的消费者驱动复用,同时消费者驱动无需关心底层 DMA 控制器的具体寄存器细节,极大地提高了代码的可移植性和可维护性。
3.2 核心数据结构:dma_device 与 dma_chan
在 DMA Engine 框架中,dma_device 代表一个 DMA 控制器硬件设备,而 dma_chan 代表该控制器上的一个独立 DMA 通道。理解这两个结构体是掌握 DMA 驱动开发的关键。
3.2.1 dma_device 结构体
dma_device 是 DMA 控制器驱动的核心注册单元。驱动在探测(probe)阶段需要填充该结构体,并通过 dma_async_device_register() 将其注册到内核框架中。
struct dma_device {
unsigned int chancnt; /* 通道数量 */
unsigned int privatecnt; /* 私有通道数量 */
struct list_head channels; /* 该设备管理的通道链表头 */
/* 设备能力描述 */
enum dma_transaction_type cap_mask; /* 支持的事务类型位掩码 */
bool copy_align; /* 内存拷贝对齐要求 */
bool slave_align; /* 从设备传输对齐要求 */
/* 关键操作函数指针 */
int (*device_alloc_chan_resources)(struct dma_chan *chan);
void (*device_free_chan_resources)(struct dma_chan *chan);
struct dma_async_tx_descriptor *(*device_prep_dma_memcpy)(
struct dma_chan *chan, dma_addr_t dst, dma_addr_t src,
size_t len, unsigned long flags);
struct dma_async_tx_descriptor *(*device_prep_slave_sg)(
struct dma_chan *chan, struct scatterlist *sgl,
unsigned int sg_len, enum dma_transfer_direction direction,
unsigned long flags, void *context);
int (*device_issue_pending)(struct dma_chan *chan);
/* ... 其他回调函数 */
};
| 字段/回调 | 类型 | 说明 |
|---|---|---|
chancnt |
unsigned int | 该 DMA 控制器拥有的物理通道总数。 |
cap_mask |
位掩码 | 声明设备支持哪些 DMA 传输类型,如 DMA_MEMCPY、DMA_SLAVE、DMA_CYCLIC。 |
device_prep_slave_sg |
函数指针 | 准备一个从设备(如 SPI、I2C)的分散/聚集 DMA 传输。这是最常用的回调之一。 |
device_issue_pending |
函数指针 | 通知 DMA 控制器开始处理已提交(submitted)但尚未启动的事务。 |
3.2.2 dma_chan 结构体
dma_chan 代表一个具体的、可被消费者使用的 DMA 通道。消费者通过 dma_request_channel() 获取一个通道实例。
struct dma_chan {
struct dma_device *device; /* 所属的 DMA 设备 */
dma_cookie_t cookie; /* 事务 cookie,用于跟踪提交状态 */
dma_cookie_t completed_cookie; /* 最后一个完成的事务 cookie */
/* 通道属性 */
enum dma_transfer_type chan_type; /* 通道类型:MEMCPY, SLAVE, CYCLIC 等 */
struct dma_chan_dev *dev; /* 对应的设备文件,用于 sysfs */
/* 客户端私有数据 */
void *private; /* 消费者驱动可以在此存储私有上下文 */
/* 链表节点 */
struct list_head device_node; /* 用于挂入 dma_device->channels 链表 */
struct list_head global_node; /* 用于挂入全局通道链表 */
};
关键概念:
dma_chan 本身不包含硬件描述符或传输数据。它更像是一个“句柄”或“控制器”,用于管理 DMA 传输的提交、排队和完成状态。实际的传输参数(源地址、目的地址、长度等)封装在 dma_async_tx_descriptor 结构体中。
3.3 dmaengine_prep_* API:准备 DMA 事务
消费者驱动在发起 DMA 传输前,必须通过 dmaengine_prep_* 系列 API 来“准备”一个事务。这些 API 会返回一个 dma_async_tx_descriptor 结构体指针,该结构体描述了传输的所有细节。
3.3.1 常用 API 一览
| API 函数 | 适用场景 | 关键参数 |
|---|---|---|
dmaengine_prep_slave_sg() |
从设备(SPI, I2C, UART)与内存之间的分散/聚集传输 | chan, sgl (scatterlist 链表), sg_len, direction (DMA_MEM_TO_DEV / DMA_DEV_TO_MEM) |
dmaengine_prep_dma_memcpy() |
内存到内存的拷贝,常用于加速数据搬移 | chan, dst (目的 DMA 地址), src (源 DMA 地址), len |
dmaengine_prep_dma_cyclic() |
音频流等需要循环缓冲区的场景 | chan, buf_addr, buf_len, period_len, direction |
dmaengine_prep_interleaved_dma() |
交错(interleaved)数据传输,如图像处理中的非连续帧传输 | chan, xt (interleaved template 结构体), flags |
3.3.2 典型使用流程:从设备 DMA 传输
以下代码片段展示了消费者驱动如何使用 dmaengine_prep_slave_sg() 发起一次从设备到内存的 DMA 读取操作。
/* 1. 请求 DMA 通道 */
struct dma_chan *dma_chan = dma_request_slave_channel(dev, "rx");
if (!dma_chan) {
dev_err(dev, "Failed to request DMA channel\n");
return -ENODEV;
}
/* 2. 准备 DMA 描述符 */
struct scatterlist sg;
sg_init_one(&sg, buffer, BUFFER_SIZE);
dma_addr_t dma_addr = dma_map_single(dev, buffer, BUFFER_SIZE, DMA_FROM_DEVICE);
struct dma_async_tx_descriptor *tx_desc;
tx_desc = dmaengine_prep_slave_sg(dma_chan, &sg, 1, DMA_DEV_TO_MEM,
DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
if (!tx_desc) {
dev_err(dev, "Failed to prepare slave sg\n");
dma_unmap_single(dev, dma_addr, BUFFER_SIZE, DMA_FROM_DEVICE);
return -ENOMEM;
}
/* 3. 设置完成回调 */
tx_desc->callback = dma_rx_callback;
tx_desc->callback_param = my_private_data;
/* 4. 提交事务到硬件队列 */
dma_cookie_t cookie = dmaengine_submit(tx_desc);
if (dma_submit_error(cookie)) {
dev_err(dev, "Failed to submit DMA transaction\n");
return -EIO;
}
/* 5. 启动 DMA 传输 */
dma_async_issue_pending(dma_chan);
流程要点:
- 通道请求:
dma_request_slave_channel()根据设备树或 ACPI 中的 DMA 配置,获取一个合适的通道。 - 内存映射: 使用
dma_map_single()或dma_map_sg()确保 CPU 缓存与 DMA 设备之间的一致性(详见后续章节)。 - 准备描述符:
dmaengine_prep_slave_sg()将 scatterlist 转换为硬件可理解的描述符链。 - 提交与启动:
dmaengine_submit()将描述符放入通道的等待队列,dma_async_issue_pending()则通知硬件开始处理。
性能提示: 在高速数据流场景(如 10GbE 网络)中,应尽量复用
dma_async_tx_descriptor 和 DMA 缓冲区,避免在每次传输时都进行分配和映射操作。可以使用 DMA_CTRL_ACK 标志来指示描述符在完成后不被自动释放,以便后续重用。