3. Linux DMA 子系统架构:dmaengine 框架概述
在嵌入式 Linux 系统中,DMA(Direct Memory Access)控制器是连接内存与外设、外设与外设之间高速数据搬运的核心硬件。为了统一管理不同厂商、不同型号的 DMA 控制器,内核提供了 dmaengine 子系统框架。本章将深入剖析 dmaengine 的架构设计、核心数据结构、通道模型以及引擎注册流程,帮助读者建立从驱动开发视角理解 DMA 子系统的完整知识体系。
核心认知: dmaengine 框架将 DMA 控制器抽象为“引擎(engine)”,将一次数据传输抽象为“描述符(descriptor)”,将传输通道抽象为“通道(channel)”。这种分层设计使得上层驱动(如音频、视频、存储)无需关心底层 DMA 控制器的具体寄存器操作,只需通过标准 API 发起传输请求即可。
3.1 dmaengine 框架整体架构
dmaengine 框架位于内核 drivers/dma/ 目录下,采用三层分层结构:
- 底层:DMA 控制器驱动 —— 实现具体硬件(如 PL330、EDMA、SDMA)的初始化、通道管理、描述符处理。
- 中间层:dmaengine 核心 —— 提供统一的注册、注销、通道申请、描述符提交等基础设施。
- 上层:DMA 引擎用户 —— 包括 ALSA、MMC、SPI、UART 等子系统,通过
dma_request_channel()等 API 使用 DMA 服务。
3.2 dma_device 结构体:DMA 控制器的抽象
每个 DMA 控制器在 dmaengine 框架中对应一个 struct dma_device 实例。该结构体定义在 include/linux/dmaengine.h 中,是 DMA 控制器驱动的核心描述符。它包含了控制器的能力、操作函数指针、通道列表等关键信息。
| 字段 | 类型 | 说明 |
|---|---|---|
dev |
struct device * |
指向 DMA 控制器的设备结构体 |
channels |
struct list_head |
该控制器管理的所有 DMA 通道链表头 |
cap_mask |
dma_cap_mask_t |
控制器能力位图(如 DMA_SLAVE、DMA_CYCLIC) |
device_alloc_chan_resources |
函数指针 | 分配通道资源(中断、描述符池等) |
device_free_chan_resources |
函数指针 | 释放通道资源 |
device_prep_slave_sg |
函数指针 | 准备从设备到内存的 SG 传输描述符 |
device_prep_dma_memcpy |
函数指针 | 准备内存到内存的拷贝描述符 |
device_issue_pending |
函数指针 | 触发硬件开始执行待处理的描述符 |
device_tx_status |
函数指针 | 查询传输状态(已完成字节数、错误等) |
开发要点: 在实现 DMA 控制器驱动时,
dma_device 中的函数指针并非全部必须实现。例如,如果控制器不支持内存拷贝,则 device_prep_dma_memcpy 可以设置为 NULL。框架通过 cap_mask 来暴露控制器实际支持的能力。
3.3 dma_chan 通道模型:数据传输的载体
struct dma_chan 是 dmaengine 框架中代表一个 DMA 通道的核心结构体。每个物理 DMA 通道对应一个 dma_chan 实例。通道模型的设计遵循“申请-配置-使用-释放”的生命周期管理。
3.3.1 dma_chan 结构体关键字段
| 字段 | 说明 |
|---|---|
device |
指向所属的 dma_device,即该通道属于哪个 DMA 控制器 |
chan_id |
通道 ID,在同一控制器内唯一 |
slave |
指向使用该通道的从设备(如 SPI 控制器) |
client_count |
当前使用该通道的客户端数量,用于引用计数 |
private |
驱动私有数据指针,用于存放硬件相关的上下文 |
3.3.2 通道状态机
一个 DMA 通道在其生命周期中会经历以下状态:
- 空闲(IDLE) —— 通道未被任何客户端使用,等待分配。
- 已分配(ALLOCATED) —— 通过
dma_request_channel()成功申请,但尚未配置传输参数。 - 已配置(CONFIGURED) —— 调用
dmaengine_slave_config()设置了源/目的地址、宽度、突发大小等参数。 - 传输中(IN_PROGRESS) —— 描述符已提交并下发到硬件,DMA 正在搬运数据。
- 已完成(COMPLETED) —— 传输完成,等待回调通知或轮询查询。
3.4 DMA 引擎注册流程
将一个新的 DMA 控制器驱动注册到 dmaengine 框架中,需要遵循以下标准流程。该流程通常在驱动的 probe() 函数中完成。
3.4.1 注册步骤
- 分配并初始化 dma_device —— 使用
kzalloc分配struct dma_device,并填充dev、cap_mask等字段。 - 初始化通道链表 —— 调用
INIT_LIST_HEAD(&dma_dev->channels)。 - 创建并初始化每个 dma_chan —— 为每个物理通道分配
struct dma_chan,设置device指针和chan_id,并添加到channels链表。 - 设置能力位图 —— 通过
dma_cap_set()宏设置控制器支持的能力(如DMA_SLAVE、DMA_MEMCPY)。 - 实现并赋值操作函数指针 —— 包括
device_alloc_chan_resources、device_prep_slave_sg、device_issue_pending等。 - 调用 dma_async_device_register() —— 将填充好的
dma_device注册到内核 dmaengine 核心。 - 错误处理与清理 —— 如果注册失败,释放已分配的资源。
3.4.2 注册代码模板
static int my_dma_probe(struct platform_device *pdev)
{
struct dma_device *dma_dev;
struct my_dma_chan *my_chan;
int ret, i;
// 1. 分配 dma_device
dma_dev = kzalloc(sizeof(*dma_dev), GFP_KERNEL);
if (!dma_dev)
return -ENOMEM;
// 2. 初始化通道链表
INIT_LIST_HEAD(&dma_dev->channels);
// 3. 创建并添加通道
for (i = 0; i < NR_CHANNELS; i++) {
my_chan = kzalloc(sizeof(*my_chan), GFP_KERNEL);
if (!my_chan) {
ret = -ENOMEM;
goto err_chan;
}
my_chan->chan.device = dma_dev;
my_chan->chan.chan_id = i;
// 将通道加入链表
list_add_tail(&my_chan->chan.device_node, &dma_dev->channels);
}
// 4. 设置能力位图
dma_cap_set(DMA_SLAVE, dma_dev->cap_mask);
dma_cap_set(DMA_CYCLIC, dma_dev->cap_mask);
// 5. 实现操作函数
dma_dev->device_alloc_chan_resources = my_alloc_chan_resources;
dma_dev->device_free_chan_resources = my_free_chan_resources;
dma_dev->device_prep_slave_sg = my_prep_slave_sg;
dma_dev->device_issue_pending = my_issue_pending;
dma_dev->device_tx_status = my_tx_status;
dma_dev->dev = &pdev->dev;
// 6. 注册到 dmaengine 核心
ret = dma_async_device_register(dma_dev);
if (ret) {
dev_err(&pdev->dev, "Failed to register DMA engine\n");
goto err_reg;
}
platform_set_drvdata(pdev, dma_dev);
dev_info(&pdev->dev, "DMA engine registered successfully\n");
return 0;
err_reg:
err_chan:
// 清理已分配的通道和 dma_device
// ... (省略具体清理代码)
kfree(dma_dev);
return ret;
}
常见错误: 在注册
dma_async_device_register() 之前,必须确保所有通道已经添加到 channels 链表,并且所有必要的操作函数指针已经赋值。否则,框架在注册时会因为缺少必要回调而返回错误,或者导致后续运行时崩溃。
3.5 关键数据结构关系总结
为了帮助读者建立整体认知,下表总结了 dma_device、dma_chan 和 dma_async_tx_descriptor 三者之间的关系:
| 数据结构 | 角色 | 包含关系 | 生命周期 |
|---|---|---|---|
dma_device |
DMA 控制器抽象 | 包含多个 dma_chan |
与设备驱动共存亡 |
dma_chan |
DMA 通道抽象 | 属于一个 dma_device |
从申请到释放 |
dma_async_tx_descriptor |
一次传输的描述符 | 由 dma_chan 创建 |
从 prep 到 callback 完成 |
架构设计思想: dmaengine 框架采用了典型的“工厂模式”——
dma_device 是工厂,dma_chan 是生产线,dma_async_tx_descriptor 是生产订单。上层驱动通过工厂获取生产线,然后下发订单,由生产线完成具体的数据搬运工作。这种设计极大地提高了代码复用性和可移植性。