4. DMA传输方向与类型

DMA 传输的核心在于根据数据流向和传输模式选择正确的配置。Linux DMA Engine 框架抽象了四种标准传输类型,驱动开发者必须理解每种类型的语义、适用场景以及硬件约束,才能正确初始化描述符并处理完成回调。

4.1 四种传输类型概述

下表对比了四种 DMA 传输类型的核心特征、典型应用场景以及关键注意事项:

传输类型 数据流向 典型场景 关键约束
MEMCPY 内存 → 内存 数据拷贝、内存搬运、帧缓冲复制 源和目标必须都是内存地址;硬件可能要求对齐
DEV_TO_MEM 设备 → 内存 网卡接收、音频采集、摄像头数据捕获 源地址为设备 FIFO/寄存器,目标为内存缓冲区
MEM_TO_DEV 内存 → 设备 网卡发送、音频播放、LCD 显示刷新 源为内存缓冲区,目标为设备 FIFO/寄存器
CYCLIC 循环(设备 ↔ 内存) 音频流、连续采样、无限循环播放 描述符链表形成环形;硬件必须支持自动重载
关键概念:MEMCPY 是唯一不涉及外设的纯内存传输类型,常用于 CPU 卸载。CYCLIC 是 MEM_TO_DEV 或 DEV_TO_MEM 的循环变体,描述符链表首尾相连,硬件自动重复执行,无需软件重新提交。

4.2 内存到内存 (MEMCPY)

MEMCPY 传输将数据从一块内存区域拷贝到另一块内存区域,完全由 DMA 控制器完成,不涉及任何外设。这是最简单的 DMA 传输类型,也是验证 DMA 控制器基本功能的首选测试用例。

在 Linux DMA Engine 中,MEMCPY 传输通过 dmaengine_prep_dma_memcpy() 接口创建描述符:

struct dma_async_tx_descriptor *desc;
dma_addr_t src_dma, dst_dma;

/* 将源和目标缓冲区映射到 DMA 地址 */
src_dma = dma_map_single(dev, src_buf, size, DMA_TO_DEVICE);
dst_dma = dma_map_single(dev, dst_buf, size, DMA_FROM_DEVICE);

/* 创建 MEMCPY 描述符 */
desc = dmaengine_prep_dma_memcpy(chan, dst_dma, src_dma, size, flags);
if (!desc) {
    dev_err(dev, "Failed to prepare MEMCPY descriptor\n");
    goto err_unmap;
}

desc->callback = memcpy_done_callback;
desc->callback_param = &cb_data;

/* 提交并启动传输 */
dmaengine_submit(desc);
dma_async_issue_pending(chan);
注意事项:MEMCPY 传输的源和目标地址必须使用 dma_map_single() 映射,且方向参数需根据实际读写操作选择。部分 DMA 控制器要求源和目标地址按 4 字节或 8 字节对齐,未对齐地址可能导致传输失败或性能下降。

4.3 内存到设备 (MEM_TO_DEV) 与设备到内存 (DEV_TO_MEM)

这两种类型是 DMA 传输中最常用的模式,分别对应数据从内存发送到外设(如网卡发送、音频播放)和从外设接收数据到内存(如网卡接收、音频采集)。

它们的核心区别在于数据流向和地址映射方向:

  • MEM_TO_DEV:使用 dmaengine_prep_slave_sg() 创建描述符,方向参数为 DMA_MEM_TO_DEV。源地址是内存中的散列表(SG),目标地址是设备 FIFO 或寄存器。
  • DEV_TO_MEM:同样使用 dmaengine_prep_slave_sg(),方向参数为 DMA_DEV_TO_MEM。源地址是设备 FIFO,目标地址是内存中的散列表。

典型配置流程如下:

struct dma_slave_config config;
struct dma_async_tx_descriptor *desc;

/* 配置从设备参数 */
memset(&config, 0, sizeof(config));
config.direction = DMA_DEV_TO_MEM;  /* 或 DMA_MEM_TO_DEV */
config.src_addr = pdev->phys_base + FIFO_REG;  /* 设备 FIFO 物理地址 */
config.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
config.src_maxburst = 4;  /* 每次突发传输 4 个数据单元 */

/* 应用配置到 DMA 通道 */
dmaengine_slave_config(chan, &config);

/* 创建 SG 描述符 */
desc = dmaengine_prep_slave_sg(chan, sgl, sg_len, config.direction, flags);
if (!desc) {
    dev_err(dev, "Failed to prepare slave SG descriptor\n");
    return -ENOMEM;
}

desc->callback = slave_done_callback;
dmaengine_submit(desc);
dma_async_issue_pending(chan);

4.4 循环传输 (CYCLIC)

循环传输是音频、连续采样等实时数据流场景的核心需求。它通过将多个描述符链接成环形链表,让 DMA 控制器自动循环执行,无需软件在每次传输完成后重新提交描述符。

循环传输的关键特征:

  • 描述符链表形成环形:最后一个描述符的 next 指针指向第一个描述符。
  • 硬件自动重载:传输完成后自动跳转到链表头,继续下一轮传输。
  • 周期性回调:每个周期(period)完成后触发中断,软件在回调中处理数据。
  • 无终止条件:除非显式调用 dmaengine_terminate_sync(),否则传输永不停止。

创建循环传输描述符使用 dmaengine_prep_dma_cyclic() 接口:

struct dma_async_tx_descriptor *desc;

/* 创建循环传输描述符 */
desc = dmaengine_prep_dma_cyclic(chan, dma_buf, buf_size, period_size,
                                 direction, flags);
if (!desc) {
    dev_err(dev, "Failed to prepare cyclic descriptor\n");
    return -ENOMEM;
}

/* 注册周期完成回调 */
desc->callback = cyclic_period_done;
desc->callback_param = &audio_data;

/* 启动循环传输 */
dmaengine_submit(desc);
dma_async_issue_pending(chan);
周期与缓冲区:buf_size 是总缓冲区大小,period_size 是每个周期的大小。硬件将总缓冲区划分为多个周期,每个周期完成后触发一次中断。例如,音频场景中 buf_size = 4096,period_size = 1024,则每 1024 字节传输完成触发一次回调。

4.5 传输类型选择与硬件约束

并非所有 DMA 控制器都支持全部四种传输类型。驱动开发者在初始化阶段必须查询通道能力,避免使用不支持的传输类型:

struct dma_device *dma_dev = chan->device;
dma_cap_mask_t mask;

dma_cap_zero(mask);
dma_cap_set(DMA_MEMCPY, mask);   /* 检查 MEMCPY 支持 */
dma_cap_set(DMA_CYCLIC, mask);   /* 检查 CYCLIC 支持 */

if (!dma_has_cap(DMA_MEMCPY, dma_dev->cap_mask)) {
    dev_warn(dev, "DMA controller does not support MEMCPY\n");
    /* 回退到 CPU 拷贝 */
}

if (!dma_has_cap(DMA_CYCLIC, dma_dev->cap_mask)) {
    dev_warn(dev, "DMA controller does not support CYCLIC\n");
    /* 回退到手动重新提交 SG 描述符 */
}

下表总结了不同传输类型对硬件特性的依赖:

传输类型 必需硬件特性 可选硬件特性
MEMCPY 内存到内存传输引擎 对齐要求、最大传输长度
DEV_TO_MEM 外设接口、SG 支持 突发传输、地址宽度配置
MEM_TO_DEV 外设接口、SG 支持 突发传输、地址宽度配置
CYCLIC 描述符链表循环、自动重载 周期中断、硬件指针回绕
调试建议:在开发初期,先使用 MEMCPY 验证 DMA 控制器的基本功能,再逐步切换到 DEV_TO_MEM/MEM_TO_DEV 和 CYCLIC。MEMCPY 不依赖外设,可以独立测试 DMA 通道、描述符提交和中断回调的完整性。

4.6 传输方向与中断协同

不同传输类型的中断行为差异显著,驱动必须根据传输类型配置中断处理策略:

  • MEMCPY:通常只需一次完成中断,回调中释放 DMA 映射并通知等待队列。
  • DEV_TO_MEM / MEM_TO_DEV:每次 SG 列表传输完成触发中断,回调中处理数据并提交下一批描述符。
  • CYCLIC:每个周期触发中断,回调中处理周期数据(如音频缓冲区填充),不能停止传输。

下图展示了 CYCLIC 传输的中断与数据流协同关系:

循环传输 (CYCLIC) 中断与数据流协同 描述符 0 (周期 0) 描述符 1 (周期 1) 描述符 2 (周期 2) 描述符链表首尾相连,硬件自动循环 周期 0 完成中断 周期 1 完成中断 周期 2 完成中断 回调:处理周期 0 数据 填充新数据到缓冲区 回调:处理周期 1 数据 填充新数据到缓冲区 回调:处理周期 2 数据 填充新数据到缓冲区 DMA 硬件继续执行下一轮循环,无需软件重新提交描述符
中断协同要点:在 CYCLIC 传输中,回调函数必须快速执行,避免阻塞后续中断。通常采用双缓冲或环形缓冲区机制:回调中仅更新缓冲区指针并触发数据消费,不进行耗时操作。对于音频场景,回调中通过 snd_pcm_period_elapsed() 通知 ALSA 框架,由框架在独立线程中处理数据。