3. Linux DMA子系统概览

在Linux内核中,DMA(Direct Memory Access)子系统并不是一个单一驱动,而是一套分层的通用框架,核心就是dmaengine。它的设计目标是把“DMA控制器硬件差异”和“外设如何使用DMA”解耦:底层由SoC厂商实现DMA控制器驱动,上层由各类外设驱动(如UART、SPI、I2C、音频、存储、网络)通过统一API发起传输。理解dmaengine的组成、provider与client的角色划分,以及设备树绑定方式,是后续做DMA驱动调优和性能测试的基础。

一、dmaengine框架的整体组成

dmaengine框架大致可分为四层:client驱动层、dmaengine核心层、provider驱动层和硬件层。client驱动通过dma_request_chan()等接口申请通道,提交dma_async_tx_descriptor描述符;核心层负责通道管理、描述符队列、回调调度和生命周期管理;provider驱动则把具体DMA控制器的寄存器操作、通道仲裁、中断处理封装成struct dma_device和struct dma_chan。

从数据结构看,几个关键对象必须分清:

  • struct dma_device:描述一个DMA控制器实例,包含能力位图、通道列表和操作函数集。
  • struct dma_chan:表示一条可用DMA通道,是client申请和释放的对象。
  • struct dma_async_tx_descriptor:一次传输任务的描述符,包含回调、cookie和提交接口。
  • struct dma_slave_config:用于配置外设侧地址、方向、总线宽度和握手信号。
核心思想:client只关心“我要搬多少数据、从哪到哪、完成后通知谁”,provider只关心“如何用我的硬件完成这次搬运”。dmaengine核心层负责把两者对接起来。

二、provider与client的角色划分

Provider是DMA控制器的驱动,通常由SoC原厂或内核社区维护,例如dw-axi-dmac、pl330、imx-sdma、ti-edma等。Provider负责注册dma_device,实现device_prep_slave_sg、device_prep_dma_memcpy、device_issue_pending、device_terminate_all等回调,并在中断中推进传输状态。

Client是使用DMA的外设驱动,例如SPI控制器驱动、音频ASoC驱动、UART驱动或网络驱动。Client不直接操作DMA寄存器,而是通过dmaengine API申请通道、配置从设备参数、准备传输描述符并提交。典型调用链如下:

chan = dma_request_chan(dev, "tx");
dmaengine_slave_config(chan, &cfg);
desc = dmaengine_prep_slave_sg(chan, sgl, nents,
                               DMA_MEM_TO_DEV,
                               DMA_PREP_INTERRUPT);
desc->callback = my_dma_callback;
dmaengine_submit(desc);
dma_async_issue_pending(chan);

这里dma_request_chan()会依据设备树中的dmas和dma-names属性找到对应provider和通道。client与provider之间通过struct device和phandle建立关联,而不是硬编码通道号。

三、设备树绑定:把provider和client连起来

设备树是dmaengine框架中provider与client建立关系的桥梁。Provider节点通常包含#dma-cells属性,声明每个通道需要几个参数;client节点通过dmas和dma-names引用provider。例如:

dma0: dma-controller@40000000 {
    compatible = "vendor,dma-ctrl";
    reg = <0x40000000 0x1000>;
    interrupts = <GIC_SPI 20 IRQ_TYPE_LEVEL_HIGH>;
    #dma-cells = <2>;
    dma-channels = <8>;
};

spi0: spi@40010000 {
    compatible = "vendor,spi";
    reg = <0x40010000 0x1000>;
    dmas = <&dma0 3 0>,
           <&dma0 4 0>;
    dma-names = "tx", "rx";
};

其中&dma0 3 0表示使用dma0控制器的第3号通道,后面的0是provider自定义的请求线或优先级参数。dma-names必须与client驱动中dma_request_chan(dev, "tx")的字符串一致,否则会返回-EPROBE_DEFER或-ENODEV。

调试时优先检查三点:provider是否已probe成功、#dma-cells与dmas参数个数是否匹配、dma-names是否与驱动请求名一致。很多“DMA申请失败”其实不是硬件问题,而是设备树绑定错误。

四、典型场景与常见误区

以音频播放为例,I2S控制器作为client,通过dmas = <&dma0 5 0>申请一条TX通道,把内存中的PCM数据搬到I2S FIFO。provider在每次搬运完成后触发中断,client回调再提交下一段buffer,形成环形缓冲。若dma-names写成"playback"而驱动请求的是"tx",就会在probe阶段报错。

对象 角色 关键接口/属性 常见问题
DMA控制器驱动 provider dma_device、#dma-cells 通道注册失败、能力位缺失
外设驱动 client dma_request_chan、dma-names 名称不匹配、方向配置错误
设备树 绑定描述 dmas、dma-names phandle错误、参数个数不符
dmaengine核心 中间层 dma_async_tx_descriptor 回调未触发、cookie丢失

复盘时建议按“provider是否就绪 → 设备树绑定是否正确 → client请求名是否匹配 → 描述符是否提交成功 → 中断与回调是否执行”的顺序排查。只有把dmaengine的框架组成、provider/client角色和设备树绑定关系理清,后续做DMA传输性能测试时,才能准确判断瓶颈是在硬件通道、驱动配置还是上层数据组织方式。

Linux dmaengine 框架组成与 provider / client 关系 Client 驱动层(外设驱动) SPI / I2C / UART 音频 ASoC 网络 / 存储 dma_request_chan() / prep_slave_sg() dmaengine 核心层 通道管理 / 仲裁 描述符队列 / cookie 回调调度 / 生命周期 Provider 驱动层(DMA控制器驱动) dma_device 注册 device_prep_* 回调 中断处理 / issue_pending 硬件层:DMA控制器 + 外设FIFO + 内存总线 设备树绑定 dmas = <&dma0 3 0> dma-names = "tx"