3. Linux DMA子系统概览

在Linux内核中,DMA控制器种类繁多:SoC内部通用DMA、网络加速器、音频专用DMA、存储控制器内置DMA等。如果每个驱动都直接操作寄存器,代码将高度重复且难以移植。为此内核抽象出dmaengine子系统,把“谁提供DMA能力”和“谁使用DMA能力”彻底解耦,形成provider与consumer模型。

一、dmaengine框架的定位

dmaengine位于内核驱动层与具体DMA控制器之间,向上为消费者驱动提供统一的传输描述接口,向下为DMA控制器驱动提供注册与回调框架。它不负责具体寄存器操作,而是负责通道管理、传输描述符调度、完成通知和资源生命周期。

核心对象包括:

  • struct dma_device:一个DMA控制器实例,描述其支持的传输类型与能力。
  • struct dma_chan:控制器上的一个物理或虚拟通道,是消费者申请和使用的句柄。
  • struct dma_slave_config:面向设备从属传输的配置,如方向、地址、宽度、突发长度。
  • struct dma_async_tx_descriptor:一次异步传输的描述符,提交后由控制器执行。
  • struct dma_tx_state:传输进度与剩余量,用于查询和同步。

消费者驱动通常不直接接触dma_device,而是通过dma_request_chan()获得dma_chan,再调用dmaengine_prep_*系列接口准备描述符,最后用dmaengine_submit()和dma_async_issue_pending()启动传输。

二、provider与consumer模型

provider是DMA控制器驱动,负责实现dma_device的device_prep_dma_memcpy、device_prep_slave_sg等回调,并向dmaengine注册。consumer是使用DMA的外设驱动,如UART、SPI、I2S、MMC等,它只关心“把数据从A搬到B”或“按周期搬运音频块”,不关心底层是哪个控制器。

这种分层带来三个直接好处:第一,同一外设驱动可在不同SoC上复用;第二,DMA控制器驱动可独立演进;第三,内核可统一处理通道申请、释放和错误恢复。

典型调用链:consumer调用dma_request_chan() → dmaengine根据设备树匹配provider → 返回dma_chan → consumer配置dma_slave_config → 准备并提交描述符 → provider执行传输 → 回调通知consumer。

三、设备树中的DMA描述

设备树是provider与consumer建立连接的桥梁。DMA控制器节点通常带#dma-cells属性,声明每个通道引用需要几个参数。消费者节点通过dmas和dma-names引用通道。

dma0: dma-controller@1f00000 {
    compatible = "vendor,dma-v2";
    reg = <0x1f00000 0x1000>;
    interrupts = <GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;
    #dma-cells = <2>;
    dma-channels = <8>;
};

uart1: serial@1f10000 {
    compatible = "vendor,uart";
    reg = <0x1f10000 0x100>;
    dmas = <&dma0 3 1>, <&dma0 4 2>;
    dma-names = "tx", "rx";
};

其中<&dma0 3 1>表示使用dma0的通道3,第二个参数1通常表示传输方向或请求线编号,具体含义由provider的#dma-cells和解析函数决定。consumer驱动通过dma_request_chan(dev, "tx")按名称申请,避免硬编码通道号。

设备树中的dma-names必须与驱动中dma_request_chan()使用的名称一致,否则会返回-ENODEV或-EPROBE_DEFER。若provider尚未probe,consumer应允许延迟探测。

四、常用内核配置选项

使用dmaengine前需确认内核配置。以下为常见选项:

配置项作用典型值
CONFIG_DMADEVICES启用dmaengine子系统y
CONFIG_DMA_ENGINEdmaengine核心支持y
CONFIG_DMA_OF从设备树解析DMA通道y
CONFIG_DMA_ACPI从ACPI解析DMA通道按需
CONFIG_DMATESTdmaengine自测模块m
CONFIG_DEBUG_FS查看dmaengine调试信息y

调试时可通过debugfs查看通道占用与传输状态:

mount -t debugfs none /sys/kernel/debug
cat /sys/kernel/debug/dmaengine/summary
ls /sys/class/dma/

五、案例与常见误区

以I2S音频播放为例:I2S驱动作为consumer,在设备树中声明dmas = <&dma0 5 1>; dma-names = "tx";。驱动probe时申请tx通道,配置dma_slave_config的源地址为内存缓冲区、目的地址为I2S FIFO、宽度为32位、突发长度为8。随后用dmaengine_prep_dma_cyclic()准备环形缓冲区描述符,提交后启动。DMA每完成一个周期触发回调,驱动据此更新播放位置并填充新数据。

常见误区有三类:

  1. 通道号硬编码:直接写死通道号导致换板即失效,应始终通过设备树和名称申请。
  2. 忽略-EPROBE_DEFER:provider未就绪时consumer直接失败,正确做法是返回-EPROBE_DEFER等待重试。
  3. 混淆memcpy与slave传输:内存到内存用dmaengine_prep_dma_memcpy(),外设FIFO传输必须用dmaengine_prep_slave_sg()或cyclic接口,并先配置dma_slave_config。
不要在中断上下文中调用可能睡眠的dma_request_chan();也不要在传输未完成时释放缓冲区,否则DMA会读写已释放内存,造成难以定位的崩溃。
dmaengine 框架:provider 与 consumer 模型 Consumer 层(外设驱动) UART 驱动 I2S 音频驱动 SPI 驱动 MMC 驱动 dmaengine 核心 dma_request_chan() 通道申请/释放 prep / submit 描述符调度 回调与完成通知 tasklet / 中断 Provider 层(DMA 控制器驱动) dma_device 注册 memcpy / slave_sg / cyclic 通道与请求线管理 dma_chan / irq 硬件寄存器操作 描述符执行 设备树:dmas = <&dma0 3 1>, <&dma0 4 2>; dma-names = "tx", "rx"; #dma-cells = <2>

理解dmaengine的定位与provider/consumer模型,是后续设计环形缓冲区、处理传输完成回调以及排查DMA超时问题的基础。设备树描述和内核配置决定了框架能否正确匹配通道,而consumer驱动只需关注传输语义与缓冲区生命周期。