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。它们分别对应“控制器”“通道”“通道统计”“从设备配置”“传输描述符”五个层次。
一、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_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 管单次任务。后续环形缓冲区设计,正是围绕描述符的循环提交与回调节奏展开。