2、Linux DMA子系统架构:DMA Engine框架、dma_device与dma_chan结构体、DMA控制器抽象层
Linux内核的DMA Engine框架是驱动开发者与DMA控制器硬件之间的标准桥梁。它通过三层抽象——DMA Engine核心层、DMA控制器驱动层和DMA用户层(客户端)——实现了统一的API接口,使得不同厂商的DMA控制器(如Intel、TI、NXP、Xilinx等)能够以一致的方式被上层驱动调用。
核心设计思想:将硬件差异封装在控制器驱动内部,客户端驱动只需通过标准接口请求通道、提交传输描述符、等待完成回调,无需关心底层寄存器操作。
2.1 DMA Engine三层架构总览
下图展示了DMA Engine框架在Linux内核中的层次关系与数据流路径:
架构优势:这种分层设计使得DMA控制器驱动只需实现
dma_device 结构体中的回调函数,而客户端驱动则通过 dma_chan 句柄操作,两者完全解耦。更换DMA控制器硬件时,客户端驱动代码无需修改。
2.2 dma_device 结构体:控制器的能力描述
dma_device 是DMA控制器驱动向核心层注册的核心数据结构,它描述了控制器的硬件能力、支持的传输类型和操作函数指针。每个DMA控制器对应一个 dma_device 实例。
| 字段/成员 | 类型 | 说明 |
|---|---|---|
dev |
struct device * |
指向DMA控制器的Linux设备结构体 |
channels |
struct list_head |
该控制器管理的所有DMA通道链表头 |
cap_mask |
dma_cap_mask_t |
位掩码,标识支持的DMA传输能力(如DMA_SLAVE、DMA_CYCLIC) |
directions |
u32 |
位掩码,标识支持的传输方向(DMA_MEM_TO_MEM、DMA_MEM_TO_DEV等) |
src_addr_widths |
u32 |
支持的源端数据宽度(1/2/4/8字节) |
dst_addr_widths |
u32 |
支持的目的端数据宽度 |
device_alloc_chan_resources |
函数指针 | 分配通道资源,初始化硬件通道 |
device_free_chan_resources |
函数指针 | 释放通道资源 |
device_prep_slave_sg |
函数指针 | 准备从设备到内存(或反向)的SG传输描述符 |
device_prep_dma_memcpy |
函数指针 | 准备内存到内存的拷贝传输描述符 |
device_prep_dma_cyclic |
函数指针 | 准备循环传输描述符(常用于音频流) |
device_issue_pending |
函数指针 | 触发硬件开始执行挂起的传输描述符 |
device_tx_status |
函数指针 | 查询传输状态(已完成字节数、错误状态) |
关键注册流程:控制器驱动在 probe 函数中填充
dma_device 结构体,调用 dma_async_device_register() 向核心层注册。核心层随后会遍历 channels 链表,为每个通道创建 dma_chan 实例并加入全局通道池。
2.3 dma_chan 结构体:传输通道的抽象
dma_chan 是客户端驱动与DMA控制器之间的逻辑连接句柄。每个 dma_chan 对应硬件上的一个DMA通道,但通过抽象层隐藏了硬件细节。
struct dma_chan {
struct dma_device *device; /* 所属的DMA控制器 */
dma_cookie_t cookie; /* 当前传输的cookie标识 */
dma_cookie_t completed_cookie; /* 最后完成的传输cookie */
void *private; /* 控制器驱动的私有数据 */
struct list_head device_node; /* 挂入dma_device.channels链表 */
struct kref refcount; /* 引用计数 */
struct dma_chan_dev *dev; /* 对应的字符设备,用于用户空间访问 */
const struct dma_chan_ops *ops; /* 通道操作函数集 */
int chan_id; /* 通道ID */
struct dma_slave_config cfg; /* 从设备配置(地址、宽度、突发大小) */
};
客户端驱动通过 dma_request_channel() 获取一个满足特定能力要求的通道:
struct dma_chan *chan;
dma_cap_mask_t mask;
dma_cap_zero(mask);
dma_cap_set(DMA_SLAVE, mask); // 要求支持从设备传输
chan = dma_request_channel(mask, filter_fn, filter_param);
if (IS_ERR(chan)) {
dev_err(dev, "Failed to request DMA channel\n");
return PTR_ERR(chan);
}
通道生命周期:客户端驱动在初始化时请求通道,在卸载时通过
dma_release_channel() 释放。通道的分配策略由核心层根据 dma_cap_mask 和可选的过滤函数决定,支持按能力匹配和按设备匹配两种模式。
2.4 DMA控制器抽象层:从硬件到框架的映射
DMA控制器抽象层是连接具体硬件与DMA Engine框架的关键桥梁。不同的硬件实现通过填充 dma_device 中的函数指针,将硬件操作封装为标准接口。下表对比了三种常见DMA控制器的抽象实现差异:
| 控制器类型 | 典型硬件 | 抽象层实现特点 | 关键回调差异 |
|---|---|---|---|
| PL330(ARM) | ARM PrimeCell PL330 | 微码引擎,描述符链由硬件自动加载 | device_prep_slave_sg 构建微码指令序列 |
| EDMA(TI) | TI OMAP/Davinci | 参数RAM表,支持链接传输和PaRAM集 | device_prep_slave_sg 配置PaRAM条目 |
| SDMA(NXP i.MX) | NXP i.MX系列 | 脚本引擎,支持复杂传输模式 | device_prep_slave_sg 加载BD表 |
抽象层的核心工作包括:
- 描述符管理:将上层提交的传输请求转换为硬件可执行的描述符(Descriptor),并管理描述符链表的入队、出队和回收。
- 中断处理:注册硬件中断处理函数,在传输完成或出错时调用核心层的回调机制,通知客户端驱动。
- 通道状态维护:跟踪每个通道的忙/闲状态、当前传输进度、错误计数等。
- 电源管理:在系统休眠时保存DMA控制器状态,唤醒时恢复。
2.5 描述符管理与传输生命周期
DMA传输的核心是描述符(Descriptor),它封装了一次传输所需的所有参数。DMA Engine框架使用 dma_async_tx_descriptor 结构体表示描述符:
struct dma_async_tx_descriptor {
dma_cookie_t cookie; /* 传输cookie */
enum dma_ctrl_flags flags; /* 控制标志 */
dma_addr_t src; /* 源地址(物理地址) */
dma_addr_t dst; /* 目的地址(物理地址) */
size_t len; /* 传输长度 */
struct dma_chan *chan; /* 所属通道 */
dma_cookie_t (*tx_submit)(struct dma_async_tx_descriptor *tx);
dma_async_tx_callback callback; /* 传输完成回调 */
void *callback_param; /* 回调参数 */
struct list_head tx_list; /* 用于链式传输 */
};
一次典型的DMA传输生命周期包含以下步骤:
- 准备描述符:客户端调用
dmaengine_prep_slave_sg()或dmaengine_prep_dma_memcpy(),控制器驱动分配并初始化描述符。 - 提交描述符:调用
dmaengine_submit()将描述符加入通道的传输队列,返回一个cookie用于后续状态查询。 - 触发传输:调用
dma_async_issue_pending()通知硬件开始处理队列中的描述符。 - 等待完成:通过
dma_async_is_tx_complete()轮询或注册回调函数等待传输完成。 - 清理资源:传输完成后,控制器驱动回收描述符内存,调用回调通知客户端。
常见陷阱:描述符的
tx_submit 函数必须在 dma_async_issue_pending 之前调用,否则描述符不会被硬件感知。此外,回调函数在中断上下文中执行,不能调用可能导致睡眠的函数(如 kmalloc(GFP_KERNEL))。
2.6 通道能力匹配与过滤机制
当多个DMA控制器共存于系统中时,客户端驱动需要通过过滤函数精确匹配所需的通道。过滤函数的原型为:
bool (*dma_filter_fn)(struct dma_chan *chan, void *filter_param);
典型的过滤实现示例:
static bool my_dma_filter(struct dma_chan *chan, void *param)
{
struct device *dev = param;
struct dma_device *dma_dev = chan->device;
// 检查DMA控制器是否与我们的设备匹配
if (dma_dev->dev->parent == dev) {
// 进一步检查通道是否支持所需方向
if (chan->private) {
struct my_dma_chan_data *data = chan->private;
return data->direction == DMA_MEM_TO_DEV;
}
}
return false;
}
// 使用过滤函数请求通道
chan = dma_request_channel(mask, my_dma_filter, my_device);
最佳实践:在嵌入式系统中,通常使用设备树(Device Tree)中的
dmas 和 dma-names 属性来静态绑定DMA通道。内核提供了 dma_request_slave_channel() 函数,可直接通过设备树名称获取通道,无需手动编写过滤函数。