3. Linux DMA子系统概览

在 Linux 内核中,DMA 并不是由每个驱动各自“裸操作寄存器”来完成,而是被抽象成了一套通用子系统——dmaengine。它的核心价值在于:把“谁提供 DMA 能力”和“谁使用 DMA 能力”解耦。SoC 厂商实现 DMA 控制器驱动,外设驱动只通过统一的 DMAEngine API 申请通道、准备描述符、提交传输并等待完成。这样,同一份 SPI、UART、音频或存储驱动,可以在不同平台上复用,而不必关心底层是哪种 DMA 控制器。

一、dmaengine 框架的分层结构

dmaengine 框架自上而下可分为四层:客户端驱动层、DMAEngine 核心层、DMA 控制器驱动层和硬件层。客户端驱动调用 dma_request_chan() 等接口;核心层负责通道分配、描述符生命周期管理和回调调度;控制器驱动实现 struct dma_device 与 struct dma_chan 的具体操作;硬件层则是真实的 DMA 引擎与总线。

Linux dmaengine 框架分层与数据流 客户端驱动层(Client Driver) SPI / UART / I2S / MMC / 网络 / 自定义外设驱动 dma_request_chan() · dmaengine_prep_*() · dmaengine_submit() DMAEngine 核心层(dmaengine.c) 通道分配 · 描述符队列 · 回调调度 · 虚拟通道 · 残量查询 struct dma_chan · struct dma_desc · dma_cookie_t DMA 控制器驱动层(Controller Driver) 实现 struct dma_device 操作集:device_prep_dma_memcpy / slave_sg / cyclic device_alloc_chan_resources · device_issue_pending · device_tx_status 硬件层(DMA 控制器 + 总线) 通道寄存器 · 描述符链表 · 中断控制器 · AXI/AHB 总线 完成回调 tasklet / 中断 callback()

二、DMAEngine API 的核心调用流程

一次典型的 DMA 传输遵循“申请通道 → 配置参数 → 准备描述符 → 提交 → 等待完成 → 释放通道”的流程。以内存到内存的 memcpy 为例:

struct dma_chan *chan;
struct dma_device *dev;
struct dma_async_tx_descriptor *tx;
dma_cookie_t cookie;
struct completion done;

chan = dma_request_chan(&pdev->dev, "tx");
dev  = chan->device;

tx = dev->device_prep_dma_memcpy(chan, dst, src, len,
                                 DMA_CTRL_ACK | DMA_PREP_INTERRUPT);
tx->callback = my_dma_callback;
tx->callback_param = &done;

cookie = tx->tx_submit(tx);
dma_async_issue_pending(chan);
wait_for_completion(&done);

dma_release_channel(chan);

其中 dma_cookie_t 是描述符的序号,用于追踪传输状态;dma_async_issue_pending() 才是真正“踢”硬件开始工作的动作。很多初学者只调用 tx_submit() 就等待完成,结果永远等不到回调,原因就是漏掉了 issue_pending。

三、通道管理与设备树 DMA 配置

DMA 通道是稀缺资源,核心层通过 dma_request_chan() 按名称或按 phandle 申请。设备树中通常用 dmas 和 dma-names 描述外设与 DMA 控制器的连接关系:

uart0: serial@10000000 {
    compatible = "vendor,uart";
    reg = <0x10000000 0x1000>;
    dmas = <&dma0 5 0>, <&dma0 6 0>;
    dma-names = "tx", "rx";
};

dma0: dma-controller@20000000 {
    compatible = "vendor,dma-v2";
    reg = <0x20000000 0x10000>;
    interrupts = <0 30 4>;
    #dma-cells = <3>;
    dma-channels = <16>;
};

驱动中通过 dma_request_chan(&pdev->dev, "tx") 即可拿到对应通道,无需硬编码通道号。这种“名字匹配”机制让同一驱动在不同板级配置下都能正确工作。

API作用常见误区
dma_request_chan()按名称申请 DMA 通道忘记检查 ERR_PTR 返回值
dmaengine_prep_slave_sg()准备外设散列表传输sg 数量超过控制器上限
dmaengine_submit()提交描述符到待处理队列误以为已启动硬件
dma_async_issue_pending()触发硬件执行漏调用导致无回调
dmaengine_tx_status()查询传输残量与状态在回调外轮询浪费 CPU
dma_release_channel()释放通道资源在回调中释放造成死锁

四、典型场景与常见误区复盘

以音频播放为例:I2S 控制器通过 dma-names = "tx" 申请播放通道,使用 dmaengine_prep_dma_cyclic() 准备环形缓冲区描述符,实现循环播放。当周期完成时,DMA 控制器产生中断,核心层调度回调,驱动在回调中填充下一段音频数据。这里的关键是周期长度与回调延迟的匹配:周期太短会导致中断风暴,周期太长则会出现音频断续。

常见误区:一是在中断上下文里调用可能睡眠的 dma_request_chan();二是多个外设共用同一通道导致传输互相踩踏;三是忘记在 remove() 中释放通道,造成通道泄漏,后续设备申请失败。排查时可用 cat /sys/kernel/debug/dmaengine/summary 查看通道占用与传输统计。

实践建议:优先使用 devm_dma_request_chan() 这类托管接口,让内核自动处理释放;在设备树中为每个方向单独命名通道;对高带宽场景,尽量使用 SG 或 cyclic 模式,减少描述符提交次数,从而降低 CPU 占用。

理解 dmaengine 的分层与 API 调用顺序,是后续设计 DMA 描述符管理、实现高带宽传输的基础。只有把通道申请、描述符准备、提交与回调这条链路走通,才能在具体外设驱动中稳定地发挥 DMA 的吞吐能力。