4. dmaengine核心数据结构

dmaengine 是 Linux 内核中统一 DMA 控制器驱动的框架层。它把不同厂商、不同通道模型的 DMA 硬件抽象成一组稳定的数据结构与操作接口,使客户端驱动(如串口、SPI、音频、存储)无需关心底层寄存器差异。理解 dmaengine,本质上就是理解五个核心结构体之间的职责划分与引用关系:struct dma_device、struct dma_chan、struct dma_chan_percpu、struct dma_slave_config、struct dma_async_tx_descriptor。它们分别对应“控制器”“通道”“通道统计”“从设备配置”“传输描述符”五个层次。

dmaengine 五大核心数据结构关系图 struct dma_device 控制器抽象:能力、通道链表、操作函数集 device_prep_dma_memcpy / slave_sg / cyclic struct dma_chan 通道实例:设备指针、cookie、客户端引用 chan_id / chan->device / chan->cookie struct dma_chan_percpu 每 CPU 统计:提交/完成计数 bytes_transferred / memcpy_count struct dma_slave_config 从设备参数:方向、地址、宽度 src_addr / dst_addr / src_addr_width struct dma_async_tx_descriptor 传输描述符:回调、cookie、物理地址 tx_submit / callback / phys 包含多个 per-cpu 配置 提交生成

一、struct dma_device:控制器能力描述

struct dma_device 代表一个 DMA 控制器实例,是 dmaengine 注册到内核的顶层对象。它并不直接对应某个物理通道,而是描述“这个控制器能做什么”。其中最关键的是 struct list_head channels,它挂载该控制器下所有可用通道;以及一组 device_prep_* 函数指针,例如 device_prep_dma_memcpy、device_prep_slave_sg、device_prep_dma_cyclic。客户端驱动通过 dma_request_chan() 拿到通道后,最终调用的就是这些函数指针。

此外,dma_device 还包含 cap_mask 位图,用于声明控制器支持的能力(如 DMA_MEMCPY、DMA_SLAVE、DMA_CYCLIC)。客户端在申请通道前应检查该掩码,避免请求到不支持的模式。控制器驱动在 probe 阶段填充该结构,并调用 dma_async_device_register() 完成注册。

二、struct dma_chan 与 struct dma_chan_percpu:通道与统计

struct dma_chan 是客户端实际持有的句柄,代表一个可独立调度的 DMA 通道。它内部通过 chan->device 反向指向所属控制器,通过 chan->cookie 记录最近一次提交的传输序号,用于完成状态查询。客户端调用 dma_request_chan() 获得通道,使用完毕后必须 dma_release_channel() 释放,否则会造成通道泄漏。

struct dma_chan_percpu 是每 CPU 的统计结构,通常通过 this_cpu_ptr() 访问。它记录该通道在每个 CPU 上的提交次数与传输字节数,例如 memcpy_count 和 bytes_transferred。这些数据会汇总到 sysfs 的 /sys/class/dma/dma0chan0/ 下,是排查“通道是否被真正使用”“传输量是否异常”的重要依据。注意它属于统计辅助结构,不参与传输控制逻辑。

三、struct dma_slave_config:从设备传输参数

当 DMA 用于外设(如 UART、SPI、I2S)时,必须告诉控制器“从设备的地址、数据宽度、突发长度、方向”。这些信息由 struct dma_slave_config 承载,并通过 dmaengine_slave_config() 下发。典型字段包括:

  • direction:DMA_MEM_TO_DEV 或 DMA_DEV_TO_MEM;
  • src_addr / dst_addr:外设 FIFO 的物理地址;
  • src_addr_width / dst_addr_width:数据宽度,如 1、2、4 字节;
  • src_maxburst / dst_maxburst:最大突发长度;
  • slave_id:部分平台用于选择硬件请求线。
配置必须在提交传输之前完成。若外设地址或宽度变化,需要重新调用 dmaengine_slave_config(),否则描述符会按旧参数搬运,导致数据错位。

四、struct dma_async_tx_descriptor:传输描述符

struct dma_async_tx_descriptor 是“一次传输任务”的抽象。客户端通过 device_prep_* 系列函数创建描述符,填充源/目的地址、长度、回调函数等,然后调用 tx_submit() 将其提交到通道的待处理队列。描述符中的 cookie 是完成状态查询的凭据,callback 与 callback_param 用于传输完成后的异步通知。

描述符的生命周期由控制器驱动管理,客户端不应在提交后继续修改其内容。若需要等待完成,可使用 dma_sync_wait() 或 dma_wait_for_async_tx();若使用回调,则应在回调中释放上层资源,避免在中断上下文执行耗时操作。

五、结构体职责对比与常见误区

结构体 职责 典型获取方式 常见误区
dma_device 控制器能力与操作函数集 由控制器驱动注册 客户端直接访问,忽略 cap_mask 检查
dma_chan 通道句柄,承载 cookie 与设备指针 dma_request_chan() 忘记 release,导致通道泄漏
dma_chan_percpu 每 CPU 统计计数 内核内部维护 误当作传输控制结构使用
dma_slave_config 外设地址、宽度、方向、突发 dmaengine_slave_config() 提交后才配置,参数不生效
dma_async_tx_descriptor 单次传输任务描述 device_prep_* 提交后继续修改描述符字段

六、实战场景:一次 UART 接收 DMA 的完整链路

以 UART 接收为例,客户端驱动先调用 dma_request_chan() 获得 RX 通道,随后填充 dma_slave_config,设置 direction = DMA_DEV_TO_MEM、src_addr 为 UART FIFO 地址、src_addr_width = 1、src_maxburst = 16,并调用 dmaengine_slave_config() 下发。接着使用 device_prep_dma_cyclic() 创建环形缓冲区描述符,提交后由 DMA 控制器持续搬运数据。每完成一个周期,回调函数被触发,驱动更新读指针并唤醒上层。此时可通过 /sys/class/dma/dma0chan0/memcpy_count 观察统计值是否增长,验证通道确实在工作。

struct dma_slave_config cfg = {
    .direction = DMA_DEV_TO_MEM,
    .src_addr = uart_fifo_phys,
    .src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE,
    .src_maxburst = 16,
};
dmaengine_slave_config(rx_chan, &cfg);

desc = dmaengine_prep_dma_cyclic(rx_chan, buf_phys,
                                 buf_len, period_len,
                                 DMA_DEV_TO_MEM, flags);
desc->callback = uart_rx_dma_callback;
dmaengine_submit(desc);
dma_async_issue_pending(rx_chan);
复盘要点:若发现 DMA 不搬运,优先检查三处——dma_slave_config 是否在提交前下发、src_addr 是否为外设 FIFO 物理地址、cap_mask 是否声明了 DMA_SLAVE 或 DMA_CYCLIC。多数“通道申请成功但无数据”的问题,根源都在配置顺序或能力不匹配。

掌握这五个结构体的分工,就掌握了 dmaengine 的骨架:dma_device 管能力,dma_chan 管句柄,dma_chan_percpu 管统计,dma_slave_config 管外设参数,dma_async_tx_descriptor 管单次任务。后续环形缓冲区设计,正是围绕描述符的循环提交与回调节奏展开。