3. Linux DMA子系统

在 Linux 中,DMA 并不是“某个驱动直接操作寄存器搬数据”这么简单。内核提供了一套通用抽象层 dmaengine,把不同厂商、不同总线、不同通道能力的 DMA 控制器统一成标准接口。驱动开发者只需要面向“通道、描述符、传输请求”编程,而不必为每个 SoC 重写一套 DMA 逻辑。理解 dmaengine 的层次结构,是后续做 DMA 吞吐分析和性能调优的基础。

一、dmaengine 框架的分层结构

dmaengine 位于内核 drivers/dma/ 目录,核心文件是 dmaengine.c 与 dmaengine.h。它把系统划分为三层:最上层是客户端驱动(如 SPI、I2C、音频、存储、网络驱动),中间是dmaengine 核心,最下层是DMA 控制器驱动。客户端不直接接触硬件,控制器驱动也不关心谁在用它,二者通过 dmaengine 核心完成解耦。

核心抽象对象主要有四个:struct dma_device 表示一个 DMA 控制器;struct dma_chan 表示一条可用通道;struct dma_slave_config 描述从设备方向、地址、宽度、突发长度等参数;struct dma_async_tx_descriptor 表示一次异步传输任务。客户端拿到通道后,配置 slave 参数,提交描述符,最后通过回调或 dma_sync_wait() 等待完成。

dmaengine 三层结构与通道生命周期 客户端驱动层(Client Driver) SPI / I2C 音频 / 存储 网络 / 串口 自定义 mem2dev / dev2mem 驱动 dmaengine 核心(dmaengine.c / dmaengine.h) dma_request_chan() 申请通道 dma_slave_config() 配置从设备参数 dmaengine_submit() 提交描述符 dma_release_channel() 释放通道 DMA 控制器驱动层(Controller Driver) dma_device 注册 device_alloc / add 通道初始化 chan->device / cookie 传输回调与中断 tasklet / irq handler 硬件寄存器 DMA 引擎 通道生命周期: request_chan → slave_config → prep_dma → submit → issue_pending → 回调完成 → release_channel

二、DMA 控制器驱动如何注册能力

控制器驱动在 probe 阶段完成三件事:申请硬件资源、初始化 dma_device、调用 dma_async_device_register() 注册到核心。注册时最关键的是填写能力字段,例如 directions(支持的方向)、copy_align(对齐要求)、max_burst(最大突发)、src_addr_widths 与 dst_addr_widths(地址宽度)。这些字段决定了客户端能否成功申请到匹配的通道。

static int my_dma_probe(struct platform_device *pdev)
{
    struct my_dma *d = devm_kzalloc(&pdev->dev, sizeof(*d), GFP_KERNEL);
    d->dev = &pdev->dev;

    INIT_LIST_HEAD(&d->dma_device.channels);
    d->dma_device.dev = &pdev->dev;
    d->dma_device.copy_align = DMAENGINE_ALIGN_4_BYTES;
    d->dma_device.directions = BIT(DMA_MEM_TO_DEV) | BIT(DMA_DEV_TO_MEM);
    d->dma_device.device_prep_slave_sg = my_prep_slave_sg;
    d->dma_device.device_issue_pending = my_issue_pending;
    d->dma_device.device_tx_status = my_tx_status;

    for (i = 0; i < d->nr_channels; i++)
        my_dma_chan_init(d, i);

    return dma_async_device_register(&d->dma_device);
}

注意 device_prep_slave_sg、device_issue_pending、device_tx_status 是控制器必须实现的核心回调。缺少任何一个,客户端提交传输时都会失败或无法获知完成状态。

三、客户端 API 与通道申请释放流程

客户端驱动的典型流程是“申请—配置—准备—提交—触发—等待—释放”。每一步都有对应 API,顺序不能颠倒。下面给出一个从设备接收方向的完整示例:

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

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

memset(&cfg, 0, sizeof(cfg));
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 = 16;
cfg.dst_maxburst = 16;
dma_slave_config(chan, &cfg);

tx = dmaengine_prep_slave_sg(chan, sgl, sg_len,
                             DMA_DEV_TO_MEM,
                             DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
tx->callback = my_dma_callback;
tx->callback_param = priv;

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

/* 传输完成后在回调中处理,或使用 dma_sync_wait(chan, cookie) 同步等待 */

dma_release_channel(chan);

其中 dma_request_chan() 会依据设备树中的 dmas 与 dma-names 属性查找通道;dmaengine_submit() 返回 cookie,用于后续状态查询;dma_async_issue_pending() 才是真正“启动”传输的调用,很多初学者漏掉它,导致传输永远不开始。

通道申请失败时,优先检查设备树 dma-names 是否与代码中的名称一致,以及控制器 directions 是否包含所需方向。使用 dma_request_slave_channel() 的旧接口已不推荐,新代码应使用 dma_request_chan() 并处理 -EPROBE_DEFER。

四、常见误区与复盘清单

误区 现象 正确做法
只调用 submit 不调用 issue_pending 传输永不启动,回调不触发 submit 后必须 issue_pending
在原子上下文调用 dma_request_chan 睡眠报错或申请失败 在 probe 或进程上下文申请通道
忽略 -EPROBE_DEFER 控制器未就绪时驱动直接失败 返回 -EPROBE_DEFER 等待重试
释放通道后仍使用 cookie use-after-free 或状态查询异常 确保传输完成后再 release
slave_config 参数与硬件不匹配 数据错位、丢包或总线错误 核对地址宽度、突发长度与 FIFO 深度
通道是有限资源。若客户端在每次传输时都申请、释放通道,会带来锁竞争和调度开销,在高吞吐场景下应尽量复用通道,仅在驱动卸载或长时间空闲时释放。