3. Linux DMA子系统概览

在Linux内核中,DMA并不是“某个驱动自己写寄存器搬数据”这么简单。为了让不同厂商的DMA控制器、不同外设和不同使用场景能够以统一方式协作,内核抽象出了dmaengine子系统。它把“谁提供DMA能力”和“谁使用DMA能力”解耦:DMA控制器驱动负责实现底层通道操作,DMA客户端驱动只关心提交传输请求、等待完成并处理回调。理解这套分层,是后续描述符链管理、周期传输和实战调试的基础。

一、dmaengine的四层架构

从调用方向看,dmaengine可以划分为四个层次:客户端API层、dmaengine核心层、DMA控制器驱动层和硬件通道层。客户端驱动调用dmaengine_prep_slave_sg()、dmaengine_submit()、dmaengine_issue_pending()等接口;核心层负责通道查找、描述符封装、队列管理和回调调度;控制器驱动实现struct dma_device中的操作函数,最终把描述符写入DMA控制器的寄存器或内存链表;硬件通道层则真正产生总线读写和完成中断。

这种分层的价值在于:客户端不需要知道DMA控制器是挂在AMBA、PCIe还是平台总线上,也不需要知道描述符是链表式、寄存器式还是混合式。只要控制器驱动正确注册了能力位,客户端就能通过统一API完成传输。

Linux dmaengine 架构分层与通道映射 DMA 客户端驱动层 音频 / SPI / UART 存储 / 网络 / 加密 dma_request_chan() 回调 / 完成量 统一 API 调用 dmaengine 核心层 通道查找与能力匹配 描述符封装与队列 issue_pending 调度 完成回调分发 dma_device 操作集 DMA 控制器驱动层 probe / 时钟 / 复位 申请中断 / 注册设备 实现 device_prep / issue 通道能力位 寄存器 / 描述符链表 DMA 硬件通道层 通道 0 / 1 / 2 … 总线读写 / 突发传输 完成中断 / 错误状态 客户端只依赖 dmaengine API,控制器差异被核心层屏蔽

二、DMA控制器驱动:注册什么、实现什么

一个典型的DMA控制器驱动在probe()中完成资源获取后,会填充struct dma_device并调用dma_async_device_register()。其中最关键的是能力位和操作函数:

  • 能力位:directions、src_addr_widths、dst_addr_widths、max_burst、descriptor_types等,决定该控制器支持哪些传输形态。
  • 操作函数:device_prep_slave_sg、device_prep_dma_cyclic、device_prep_dma_memcpy、device_issue_pending、device_terminate_all、device_synchronize等。
  • 通道注册:通过dma_async_device_register()把控制器和通道暴露给核心层,供客户端按名称或能力查找。

控制器驱动还必须正确处理完成中断:在中断服务程序中读取状态寄存器,判断是哪个通道完成,然后调用vchan_cookie_complete()或自定义回调机制,通知核心层该描述符已完成。

三、DMA客户端API:从请求通道到提交传输

客户端驱动的标准流程可以概括为“请求通道 → 配置参数 → 准备描述符 → 提交 → 启动 → 等待完成”。下面是一段典型的从设备DMA传输代码骨架:

struct dma_chan *chan;
struct dma_slave_config cfg;
struct dma_async_tx_descriptor *desc;
dma_cookie_t cookie;

chan = dma_request_chan(dev, "rx");
if (IS_ERR(chan))
    return PTR_ERR(chan);

cfg.direction = DMA_DEV_TO_MEM;
cfg.src_addr = rx_fifo_phys;
cfg.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
cfg.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
cfg.src_maxburst = 8;
cfg.dst_maxburst = 8;
dmaengine_slave_config(chan, &cfg);

desc = dmaengine_prep_slave_sg(chan, sgl, sg_len,
                               DMA_DEV_TO_MEM,
                               DMA_PREP_INTERRUPT);
desc->callback = rx_done_callback;
desc->callback_param = priv;

cookie = dmaengine_submit(desc);
dma_async_issue_pending(chan);

这里有几个容易混淆的点:dmaengine_prep_slave_sg()只是“准备”描述符,并没有真正启动传输;dmaengine_submit()把描述符挂入待处理队列并返回cookie;dma_async_issue_pending()才通知控制器开始执行。回调函数在DMA完成中断上下文中被调用,因此不能睡眠,也不能做耗时操作。

四、设备树与DMA通道映射

设备树负责描述“哪个设备使用哪个DMA控制器、哪个通道、什么方向”。常见属性包括dmas和dma-names。例如:

uart0: serial@10000000 {
    compatible = "vendor,uart";
    reg = <0x10000000 0x1000>;
    interrupts = <10>;
    dmas = <&dma0 3 1>,
           <&dma0 4 2>;
    dma-names = "rx", "tx";
};

其中&dma0指向DMA控制器节点,后面的3 1和4 2是控制器特定的通道参数,通常表示通道号和方向/优先级。客户端调用dma_request_chan(dev, "rx")时,核心层会根据dma-names匹配到对应的dmas条目,再解析出通道。如果设备树写错,最常见的结果是dma_request_chan()返回-EPROBE_DEFER或-ENODEV。

层次 核心对象 关键接口/属性 常见问题
客户端驱动 dma_chan dma_request_chan、prep_slave_sg、submit 忘记issue_pending,传输不启动
dmaengine核心 dma_device、virt_dma_chan dma_async_device_register、cookie管理 能力位不匹配,prep返回NULL
控制器驱动 dma_device操作集 device_prep_*、device_issue_pending 中断未清,回调不触发
设备树 dmas、dma-names 通道号、方向、优先级 名称不匹配,请求通道失败
调试建议:打开CONFIG_DMA_ENGINE_DEBUG和CONFIG_DYNAMIC_DEBUG,在/sys/kernel/debug/dmaengine/下可以查看通道状态、已提交描述符和cookie进度。遇到“传输不完成”时,先确认issue_pending是否调用,再检查中断是否到达、状态寄存器是否清除。
常见误区:把dmaengine_prep_slave_sg()当成启动函数;在回调中调用可能睡眠的API;设备树中dma-names顺序与dmas顺序不一致;控制器驱动没有正确设置src_addr_widths导致客户端配置被拒绝。这些问题在实战中非常高频,建议在板级调试阶段逐一核对。

五、案例复盘:一次UART接收DMA不触发回调

某项目使用UART接收DMA,客户端驱动按标准流程请求通道、配置DMA_DEV_TO_MEM、准备SG描述符并提交。但发现数据能进入内存,回调却不执行。排查后发现:控制器驱动在中断处理中只清除了全局状态位,没有清除通道完成位,导致核心层认为描述符仍未完成。修复方式是在中断中按通道读取并写回完成寄存器,再调用vchan_cookie_complete()。这个案例说明:dmaengine分层虽然屏蔽了大部分差异,但控制器驱动的中断处理仍然是正确性的关键。

掌握dmaengine的四层结构、控制器驱动的注册与中断处理、客户端API的调用顺序,以及设备树中dmas/dma-names的映射关系,就具备了继续深入描述符链管理的基础。后续无论是做SG传输、周期音频DMA,还是排查通道冲突,都可以沿着“客户端→核心→控制器→硬件”这条链路逐层定位。