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控制器驱动可独立演进;第三,内核可统一处理通道申请、释放和错误恢复。
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_ENGINE | dmaengine核心支持 | y |
CONFIG_DMA_OF | 从设备树解析DMA通道 | y |
CONFIG_DMA_ACPI | 从ACPI解析DMA通道 | 按需 |
CONFIG_DMATEST | dmaengine自测模块 | 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每完成一个周期触发回调,驱动据此更新播放位置并填充新数据。
常见误区有三类:
- 通道号硬编码:直接写死通道号导致换板即失效,应始终通过设备树和名称申请。
- 忽略
-EPROBE_DEFER:provider未就绪时consumer直接失败,正确做法是返回-EPROBE_DEFER等待重试。 - 混淆memcpy与slave传输:内存到内存用
dmaengine_prep_dma_memcpy(),外设FIFO传输必须用dmaengine_prep_slave_sg()或cyclic接口,并先配置dma_slave_config。
dma_request_chan();也不要在传输未完成时释放缓冲区,否则DMA会读写已释放内存,造成难以定位的崩溃。
理解dmaengine的定位与provider/consumer模型,是后续设计环形缓冲区、处理传输完成回调以及排查DMA超时问题的基础。设备树描述和内核配置决定了框架能否正确匹配通道,而consumer驱动只需关注传输语义与缓冲区生命周期。