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 服务。
dmaengine 框架三层架构 上层 DMA 用户 ALSA | MMC | SPI | UART | 自定义驱动 dmaengine 核心层 dma_request_channel() | dmaengine_prep_slave_sg() | dmaengine_submit() DMA 控制器驱动 PL330 | EDMA | SDMA | 自定义 DMA 控制器 说明:上层驱动通过标准 API 调用,无需感知底层 DMA 控制器的寄存器级差异 底层驱动只需实现 dma_device 结构体中的回调函数即可接入框架

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 来暴露控制器实际支持的能力。
dma_device 结构体关键字段关系 struct dma_device dev (设备指针) cap_mask (能力位图) channels (通道链表) device_alloc_chan_resources device_prep_slave_sg device_issue_pending 左侧为属性字段,右侧为操作函数指针,共同定义了一个 DMA 控制器的完整行为

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 通道在其生命周期中会经历以下状态:

  1. 空闲(IDLE) —— 通道未被任何客户端使用,等待分配。
  2. 已分配(ALLOCATED) —— 通过 dma_request_channel() 成功申请,但尚未配置传输参数。
  3. 已配置(CONFIGURED) —— 调用 dmaengine_slave_config() 设置了源/目的地址、宽度、突发大小等参数。
  4. 传输中(IN_PROGRESS) —— 描述符已提交并下发到硬件,DMA 正在搬运数据。
  5. 已完成(COMPLETED) —— 传输完成,等待回调通知或轮询查询。
dma_chan 通道状态机 空闲 已分配 已配置 传输中 已完成 dma_request_channel slave_config submit + issue_pending 传输完成 dma_release_channel 通道状态转换由 dmaengine API 驱动,驱动开发者需确保状态机正确流转

3.4 DMA 引擎注册流程

将一个新的 DMA 控制器驱动注册到 dmaengine 框架中,需要遵循以下标准流程。该流程通常在驱动的 probe() 函数中完成。

3.4.1 注册步骤

  1. 分配并初始化 dma_device —— 使用 kzalloc 分配 struct dma_device,并填充 dev、cap_mask 等字段。
  2. 初始化通道链表 —— 调用 INIT_LIST_HEAD(&dma_dev->channels)。
  3. 创建并初始化每个 dma_chan —— 为每个物理通道分配 struct dma_chan,设置 device 指针和 chan_id,并添加到 channels 链表。
  4. 设置能力位图 —— 通过 dma_cap_set() 宏设置控制器支持的能力(如 DMA_SLAVE、DMA_MEMCPY)。
  5. 实现并赋值操作函数指针 —— 包括 device_alloc_chan_resources、device_prep_slave_sg、device_issue_pending 等。
  6. 调用 dma_async_device_register() —— 将填充好的 dma_device 注册到内核 dmaengine 核心。
  7. 错误处理与清理 —— 如果注册失败,释放已分配的资源。

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 链表,并且所有必要的操作函数指针已经赋值。否则,框架在注册时会因为缺少必要回调而返回错误,或者导致后续运行时崩溃。
DMA 引擎注册流程 1. 分配 dma_device 2. 初始化通道链表 3. 创建并添加 dma_chan 4. 设置能力位图 5. 实现操作函数指针 6. dma_async_device_register 7. 注册成功,设置 drvdata 步骤 1-3 为资源准备阶段,步骤 4-5 为能力声明阶段,步骤 6-7 为注册生效阶段

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 是生产订单。上层驱动通过工厂获取生产线,然后下发订单,由生产线完成具体的数据搬运工作。这种设计极大地提高了代码复用性和可移植性。