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完成传输。
二、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,还是排查通道冲突,都可以沿着“客户端→核心→控制器→硬件”这条链路逐层定位。