Linux DMA Engine 驱动开发深度实战:从 dmaengine API 到 Slave DMA 传输工程实践

DMA(Direct Memory Access)是所有现代计算机系统中绕过 CPU 进行数据搬运的基础设施。Linux 内核自 2.6 时代引入 dmaengine 框架以来,已经发展出一套完整的 DMA 控制器驱动和客户端 API 体系。然而在开源社区中,关于 dmaengine 驱动端(即控制器端)开发的系统性文章极为稀缺——绝大多数文档聚焦于如何使用 DMA 做 Memcpy 或 PCIe BMDMA,而忽略了如何为一个自定义 SoC DMA 控制器编写内核驱动本身。本文将从 dmaengine 框架的核心数据结构出发,系统性地讲解驱动端开发的完整流程,并给出可落地的工程实践。


一、dmaengine 框架架构总览

在深入驱动代码之前,先理解 dmaengine 的三层结构:


┌─────────────────────────────────────────────────────────┐
│                Client Driver (SPI/I2S/UART)              │
│         调用 dmaengine_prep_slave_sg / dmaengine_submit  │
├─────────────────────────────────────────────────────────┤
│               dmaengine Core (drivers/dma/dmaengine.c)    │
│       资源分配、channel 管理、_descriptor 生命周期       │
├─────────────────────────────────────────────────────────┤
│             DMA Controller Driver (Vendor-specific)        │
│     实现 struct dma_device_ops / struct dma_chan 操作 │
│        描述符构建、硬件寄存器编程、中断处理               │
└─────────────────────────────────────────────────────────┘

核心数据结构的关系:

  • struct dma_device —— 代表整个 DMA 控制器,一个驱动实例对应一个
  • struct dma_chan —— 代表一个 DMA 通道,是客户端申请的基本单位
  • struct virt_dma_desc / struct virt_dma_chan —— dmaengine 提供的虚拟通道抽象,帮助驱动管理 descriptor 的 pending/issued/active 队列
  • struct dma_async_tx_descriptor —— 描述一次异步传输的元数据

对于驱动开发者而言,核心工作是实现一组硬件操作回调,并将这些回调注册到 dma_device 中。


二、驱动骨架:注册一个 DMA 控制器

最简的驱动骨架如下(基于 kernel 6.x):


#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>
#include <linux/dma-map-ops.h>

struct my_dma_chan {
    struct virt_dma_chan    vc;
    void __iomem           *regs;
    struct my_dma_desc     *current;
    bool                    paused;
    spinlock_t              lock;
};

struct my_dma_dev {
    struct dma_device       dma_dev;
    void __iomem           *base;
    struct my_dma_chan      channels[MAX_CHANNELS];
};

static const struct dma_chan_ops my_dma_chan_ops;

static int my_dma_probe(struct platform_device *pdev)
{
    struct my_dma_dev *mdev;
    struct dma_device *ddev;
    int i, ret;

    mdev = devm_kzalloc(&pdev->dev, sizeof(*mdev), GFP_KERNEL);
    if (!mdev)
        return -ENOMEM;

    ddev = &mdev->dma_dev;
    INIT_LIST_HEAD(&ddev->channels);

    /* 声明支持的传输类型 */
    dma_cap_set(DMA_SLAVE, ddev->cap_mask);
    dma_cap_set(DMA_CYCLIC, ddev->cap_mask);
    dma_cap_set(DMA_INTERLEAVE, ddev->cap_mask);

    /* 设置回调函数 */
    ddev->dev = &pdev->dev;
    ddev->device_alloc_chan_resources   = my_dma_alloc_chan_resources;
    ddev->device_free_chan_resources    = my_dma_free_chan_resources;
    ddev->device_prep_slave_sg          = my_dma_prep_slave_sg;
    ddev->device_prep_dma_cyclic        = my_dma_prep_dma_cyclic;
    ddev->device_prep_interleaved_dma   = my_dma_prep_interleaved;
    ddev->device_config                 = my_dma_config;
    ddev->device_pause                  = my_dma_pause;
    ddev->device_resume                 = my_dma_resume;
    ddev->device_terminate_all          = my_dma_terminate_all;
    ddev->device_tx_status              = my_dma_tx_status;
    ddev->device_issue_pending          = my_dma_issue_pending;

    /* SG 约束 */
    ddev->copy_align = DMAENGINE_ALIGN_4_BYTES;
    ddev->src_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) |
                            BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) |
                            BIT(DMA_SLAVE_BUSWIDTH_4_BYTES);
    ddev->dst_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) |
                            BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) |
                            BIT(DMA_SLAVE_BUSWIDTH_4_BYTES);
    ddev->directions = BIT(DMA_MEM_TO_DEV) | BIT(DMA_DEV_TO_MEM) |
                       BIT(DMA_MEM_TO_MEM);

    ret = dmaenginem_async_device_register(ddev);
    if (ret) {
        dev_err(&pdev->dev, "Failed to register DMA device\n");
        return ret;
    }

    /* 注册 suspend/resume */
    platform_set_drvdata(pdev, mdev);
    return 0;
}

关键要点:

  • dma_cap_set 声明控制器支持的能力。如果只支持 slave DMA 就只设 DMA_SLAVE;支持循环缓冲设 DMA_CYCLIC;支持交织传输设 DMA_INTERLEAVE。
  • dmaenginem_async_device_register() 是 5.x+ 推荐的注册函数,它会异步初始化 channel 并发送 uevent,让 DMA controller 在 probe 阶段就可以被访问。
  • src_addr_widths / dst_addr_widths / directions 这三个字段是 dmaengine core 做客户端请求合法性检查的依据,务必精确填写。

三、核心操作详解

3.1 alloc/free_chan_resources:通道资源管理


static int my_dma_alloc_chan_resources(struct dma_chan *chan)
{
    struct my_dma_chan *mchan = to_my_dma_chan(chan);
    struct my_dma_dev *mdev = to_my_dma_dev(chan->device);

    /* 使能通道时钟 */
    ret = clk_prepare_enable(mchan->clk);
    if (ret)
        return ret;

    /* 配置默认仲裁优先级 */
    writel(CHAN_PRIORITY_DEFAULT, mchan->regs + CHAN_REG_PRIORITY);

    spin_lock_init(&mchan->lock);
    return 0;
}

在 free 阶段,务必确保在释放资源前调用 dmaengine_terminate_sync()(从驱动内部看,客户端调用 terminate 后的 free 路径保证 channel 已停止)。

3.2 prep_slave_sg:构建 Slave Scatter-Gather 传输

这是客户端驱动最常调用的接口,用于提交一个或一组 memory buffer 到外设 FIFO 的传输:


static struct dma_async_tx_descriptor *
my_dma_prep_slave_sg(struct dma_chan *chan, struct scatterlist *sgl,
                     unsigned int sg_len, enum dma_transfer_direction dir,
                     unsigned long flags, void *context)
{
    struct my_dma_chan *mchan = to_my_dma_chan(chan);
    struct my_dma_desc *desc;
    struct scatterlist *sg;
    int i;

    if (sg_len > MAX_DESC_ENTRIES)
        return NULL;

    desc = kzalloc(sizeof(*desc) + sg_len * sizeof(desc->nodes[0]),
                   GFP_NOWAIT);
    if (!desc)
        return NULL;

    desc->direction = dir;
    desc->sg_count  = sg_len;
    desc->cyclic    = false;

    for_each_sg(sgl, sg, sg_len, i) {
        desc->nodes[i].addr   = sg_dma_address(sg);
        desc->nodes[i].len    = sg_dma_len(sg);

        /* 硬件 FIFO 宽度校验 */
        if (dir == DMA_MEM_TO_DEV) {
            desc->nodes[i].src_addr = sg_dma_address(sg);
            desc->nodes[i].dst_addr = mchan->slave_config.dst_addr;
            desc->nodes[i].src_addr_width = mchan->src_width;
            desc->nodes[i].dst_addr_width = mchan->dst_width;
        }
    }

    /* 在 virt_dma 框架下初始化 descriptor */
    desc->vd.tx.tx_submit   = vchan_tx_init;
    desc->vd.tx.flags       = flags;
    desc->vd.tx.phys        = 0;

    return vchan_tx_prep(&mchan->vc, &desc->vd, flags);
}

工程细节:

  • GFP_NOWAIT 是必须的,因为 prep_* 函数可能在中断上下文中调用
  • 硬件 FIFO 宽度(src_addr_width / dst_addr_width)必须在 device_config 阶段通过 dmaengine_slave_config() 传递,并记录在 my_dma_chan 中
  • 控制器如有硬件 Linked List 或 Scatter-Gather 支持,尽量在硬件层面做链式装载以减少中断频率

3.3 prep_dma_cyclic:音频场景的零中断方案

循环传输在音频(ALSA PCM DMA)、SDIO 等场景中是刚需。它利用控制器自身的环形缓冲能力,在中断触发时只切换过半/完成回调,从而大幅降低中断频率:


static struct dma_async_tx_descriptor *
my_dma_prep_dma_cyclic(struct dma_chan *chan, dma_addr_t buf_addr,
                       size_t buf_len, size_t period_len,
                       enum dma_transfer_direction dir,
                       unsigned long flags)
{
    struct my_dma_desc *desc;
    unsigned int periods;

    if (buf_len % period_len)
        return NULL;

    periods = buf_len / period_len;
    if (periods < 2 || periods > MAX_PERIODS)
        return NULL;

    desc = kzalloc(sizeof(*desc) + periods * sizeof(desc->nodes[0]),
                   GFP_NOWAIT);
    if (!desc)
        return NULL;

    desc->direction  = dir;
    desc->cyclic     = true;
    desc->buf_addr   = buf_addr;
    desc->buf_len    = buf_len;
    desc->period_len = period_len;
    desc->sg_count   = periods;

    for (i = 0; i < periods; i++) {
        desc->nodes[i].addr = buf_addr + i * period_len;
        desc->nodes[i].len  = period_len;
    }

    desc->vd.tx.tx_submit = vchan_tx_init;
    return vchan_tx_prep(&mchan->vc, &desc->vd, flags);
}

实战经验:

  • period_len 的选择直接影响中断粒度。音频双声道 48kHz 16bit 场景下,典型值取 4096 bytes(对应 ~21ms),中断频率约 238 Hz
  • 在硬件层面,周期性传输通常需要配置 Cyclic Buffer Mode 或 Repeat Count 寄存器
  • 务必在 prep 阶段校验 buf_len 必须是 period_len 的整数倍,否则会在中断中产生越界

3.4 issue_pending:启动传输与硬件队列装载

issue_pending 是 spawn point,从 virt_dma 的 issued_list 中取出 descriptor 并写到硬件:


static void my_dma_issue_pending(struct dma_chan *chan)
{
    struct my_dma_chan *mchan = to_my_dma_chan(chan);
    struct virt_dma_desc *vd;
    unsigned long flags;

    spin_lock_irqsave(&mchan->vc.lock, flags);
    if (vchan_issue_pending(&mchan->vc) && !mchan->current) {
        vd = list_first_entry(&mchan->vc.desc_issued_list,
                              struct virt_dma_desc, node);
        list_del(&vd->node);
        mchan->current = to_my_dma_desc(vd);
        my_dma_start_transfer(mchan);
    }
    spin_unlock_irqrestore(&mchan->vc.lock, flags);
}

static void my_dma_start_transfer(struct my_dma_chan *mchan)
{
    struct my_dma_desc *desc = mchan->current;

    /* 写入第一个节点地址并启动 */
    writel(lower_32_bits(desc->nodes[0].addr),
           mchan->regs + CHAN_REG_SRC_ADDR);
    writel(desc->nodes[0].len | CHAN_START_IRQ_EN,
           mchan->regs + CHAN_REG_CONTROL);

    mchan->completed_node = 0;
}

关键设计:

  • 在 issue_pending 中只启动第一个节点(或第一个硬件链表项),后续节点通过中断或硬件链式触发推进
  • 如果控制器硬件支持 LLI(Linked List Item)描述符,应该在 start_transfer 中一次性装载全部节点链以减少 CPU 干预

四、中断处理与描述符生命周期

中断处理是驱动中最复杂的部分。以支持 LLI 的控制器为例:


static irqreturn_t my_dma_irq(int irq, void *dev_id)
{
    struct my_dma_dev *mdev = dev_id;
    struct my_dma_chan *mchan;
    struct my_dma_desc *desc;
    u32 status;
    int i;

    for (i = 0; i < MAX_CHANNELS; i++) {
        mchan = &mdev->channels[i];
        status = readl(mchan->regs + CHAN_REG_IRQ_STATUS);

        if (!(status & CHAN_IRQ_MASK))
            continue;

        /* 清中断 */
        writel(status & CHAN_IRQ_MASK, mchan->regs + CHAN_REG_IRQ_CLEAR);

        desc = mchan->current;
        if (!desc)
            continue;

        if (status & CHAN_IRQ_BLOCK) {
            mchan->completed_node++;

            if (desc->cyclic) {
                vchan_cyclic_callback(&desc->vd);
            } else if (mchan->completed_node >= desc->sg_count) {
                /* 传输完成 */
                spin_lock(&mchan->vc.lock);
                vchan_cookie_complete(&desc->vd);
                mchan->current = NULL;
                spin_unlock(&mchan->vc.lock);

                /* 检查 issued_list,启动下一个传输 */
                tasklet_schedule(&mchan->vc.task);
            }
        }

        if (status & CHAN_IRQ_ERROR) {
            /* 错误处理:终止通道并通知客户端 */
            dev_err(mdev->dma_dev.dev,
                    "DMA channel %d error irq status 0x%08x\n",
                    i, status);
        }
    }

    return IRQ_HANDLED;
}

vchan_cookie_complete() 是 dmaengine 框架的标准回调通知机制,它会将 descriptor 从 active_list 移到 done_list,并调用客户端在 tx_submit() 时注册的 callback。在中断上下文里必须先获取 vc.lock(spin_lock 版本)再调用此函数。


五、terminate 与 pause/resume:流式控制实现

流式控制是 dmaengine 相对容易出错的地方。重点关注 terminate_all 必须真正停止硬件:


static int my_dma_terminate_all(struct dma_chan *chan)
{
    struct my_dma_chan *mchan = to_my_dma_chan(chan);
    unsigned long flags;
    LIST_HEAD(head);

    spin_lock_irqsave(&mchan->vc.lock, flags);

    /* 1. 硬件停止 */
    writel(CHAN_CTRL_STOP, mchan->regs + CHAN_REG_CONTROL);
    writel(0, mchan->regs + CHAN_REG_IRQ_MASK);

    /* 2. 等待硬件实际停止 */
    readl_relaxed_poll_timeout(mchan->regs + CHAN_REG_STATUS,
                               val, !(val & CHAN_STATUS_BUSY),
                               1, 1000);

    /* 3. 将该通道所属的 virt_dma 清空 */
    vchan_get_all_descriptors(&mchan->vc, &head);
    mchan->current = NULL;

    spin_unlock_irqrestore(&mchan->vc.lock, flags);

    /* 4. 在进程上下文中调用回调 */
    vchan_dma_desc_free_list(&mchan->vc, &head);

    return 0;
}

实战踩坑经验:

  • 不能在 spin_lock 中调用 vchan_dma_desc_free_list(),因为它最终会调用 dma_descriptor_unmap(),后者可能触发 IOMMU 的 TLB 同步操作,在某些架构上会 sleep
  • pause 实现不是简单关闭中断,而是必须让硬件暂停在当前 block边界,并且在 resume 时能在恢复后继续传输
  • 对于有 embedded descriptor 的控制器(如 dw_dmac),terminate 前要清 LLI 指针防止硬件执行野指针

六、DMA_INTERLEAVE:高维数据搬运

DMA_INTERLEAVE 能力允许 DMA 控制器做非连续、带 skip 的传输——常见于图像处理(搬运 ROI 区域)、矩阵转置上游数据准备等场景。


static struct dma_async_tx_descriptor *
my_dma_prep_interleaved(struct dma_chan *chan,
                        struct dma_interleaved_template *xt,
                        unsigned long flags)
{
    struct my_dma_desc *desc;
    size_t dst_sg;
    size_t src_sg;

    /* 校验:控制器仅支持 4-byte aligned 的 frame interleaving */
    if (!IS_ALIGNED(xt->sgl[0].size, 4) ||
        !IS_ALIGNED(xt->numf, 1))
        return NULL;

    desc = kzalloc(sizeof(*desc), GFP_NOWAIT);
    if (!desc)
        return NULL;

    desc->direction = xt->dir;
    desc->cyclic    = false;
    desc->interleave true;
    desc->xt        = *xt;

    /* 将交错模板映射到控制器寄存器:
     * frame_size → XT_FRAME_LEN
     * numf       → XT_FRAME_COUNT
     * sgl.size   → XT_BLOCK_SIZE
     * sgl.steps  → XT_FRAME_STEP
     */
    writel(xt->sgl[0].size, mchan->regs + CHAN_REG_XT_BLOCK);
    writel(xt->numf, mchan->regs + CHAN_REG_XT_FRAMES);
    writel(xt->frame_size, mchan->regs + CHAN_REG_XT_FRAME_STEP);

    desc->vd.tx.tx_submit = vchan_tx_init;
    return vchan_tx_prep(&mchan->vc, &desc->vd, flags);
}

在开源驱动中,xilinx_dpdma 和 mcdp_dma 是 interleaved DMA 的参考实现。如果你的 SoC 支持 scatter/gather LLI 但不直接支持 interleave 模式,可以在驱动内部将 interleaved 模板展开为 SG 列表——代价是增加 descriptor 节点数量。


七、DMA_SLAVE_BUSWIDTH 与 FIFO 阈值的实战考量

在 slave DMA 场景中,源宽度和目的宽度通常不同——例如从 32-bit APB SRAM(源宽度 4 bytes)写到 16-bit I2S TX FIFO(目的宽度 2 bytes)。这就要求控制器支持 auto-reload 或 bus-width 转换。


static int my_dma_config(struct dma_chan *chan,
                         struct dma_slave_config *config)
{
    struct my_dma_chan *mchan = to_my_dma_chan(chan);

    mchan->slave_config.dst_addr     = config->dst_addr;
    mchan->slave_config.dst_addr_width = config->dst_addr_width;
    mchan->slave_config.dst_maxburst  = config->dst_maxburst;
    mchan->slave_config.src_addr      = config->src_addr;
    mchan->slave_config.src_addr_width = config->src_addr_width;
    mchan->slave_config.src_maxburst   = config->src_maxburst;

    /* 硬件 FIFO 阈值配置:
     * - dst_maxburst < FIFO_DEPTH 时,设置 FIFO_THRESHOLD = dst_maxburst - 1
     * - 若控制器支持 dynamic burst,根据 period_len 计算最优 burst
     */
    if (config->dst_addr_width < mchan->fifo_depth) {
        writel(config->dst_addr_width - 1,
               mchan->regs + CHAN_REG_FIFO_THRESH);
    }

    return 0;
}
  • maxburst 参数直接关联 FIFO 阈值:FIFO 满时触发 DMA request,burst 数对应一次 DMA 传输的字节数
  • 对于循环音频传输,period_len / (data_width * channel_count) 通常就是硬件应该用的 burst count
  • src_addr_width 必须能被 dst_addr_width 整除,否则会产生 bus width mismatch 错误

八、DMA_MEM_TO_MEM:Memcpy 通道的特殊处理

如果你的控制器支持 memorial-to-memorial 传输(许多 SoC DMA 都支持),还需要实现 device_prep_dma_memcpy。但要注意一点:memcpy 通道和 slave 通道通常是物理上分离的,很多 SoC 的 DMA 控制器有多个通道,一部分只能做 slave,一部分只能做 memcpy。


static struct dma_async_tx_descriptor *
my_dma_prep_dma_memcpy(struct dma_chan *chan, dma_addr_t dest,
                       dma_addr_t src, size_t len, unsigned long flags)
{
    struct dma_async_tx_descriptor *tx;

    /* 要求:长度必须对齐到最小总线宽度 */
    if (!IS_ALIGNED(len, 4) ||
        !IS_ALIGNED(dest, 4) ||
        !IS_ALIGNED(src, 4))
        return NULL;

    tx = my_dma_prep_sg(chan, NULL, 0, dest, src, len,
                        DMA_MEM_TO_MEM, flags);
    /* ... 实际硬件配置 ... */

    return tx;
}

参考实现:pl330_dma 和 bcm2835-dma。这两款驱动的 memcpy 通道和 slave 通道分离模式非常经典。


九、Device Tree 绑定与客户端驱动发现

DMA controller 设备树节点示例:


my_dma: dma-controller@10020000 {
    compatible = "myvendor,my-dma-v2";
    reg = <0x10020000 0x10000>;
    interrupts = <GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clk_dma>;
    #dma-cells = <1>;
    dma-channels = <8>;
    dma-requests = <32>;
};

客户端(如 I2S 驱动)在 dts 中引用:


i2s: sound@10030000 {
    compatible = "myvendor,my-i2s";
    dmas = <&my_dma 4>, <&my_dma 5>;
    dma-names = "tx", "rx";
};

客户端通过 dma_request_chan(dev, "tx") 获取通道,内部走 of_dma_router 查找路由表完成 request→channel 的映射。

调试 tip:如果客户端 dma_request_chan 返回 -EPROBE_DEFER,检查 controller 是否比 client 晚 probe——这在新版 kernel 中不再致命,client 会等待。


十、性能调优与生产实践要点

10.1 中断合并为关键指标

对于一个支持 16 通道的音频 DMA 控制器,若每 block 产生一次中断,1 ms 粒度下每秒产生 16000 次中断。通过配置 LLI 链式传输(每 16 个 block 才进一次中断),中断频率降至 1000 Hz,CPU 占用下降 90% 以上。

10.2 资源占用与释放的最佳实践

  • alloc 和 free 必须成对:clients 可能在 stream on/off 多次调用 request/free,务必保证无泄漏
  • terminate_all 之后不要复用 virt_dma_desc:在启动新的传输前应从 issued_list 重新取出

10.3 多通道仲裁与优先级

在有多通道同时请求的场景(如 ADC+DAC 同步采集),寄存器级的通道优先级仲裁是关键。若硬件轮询,CPU 可通过动态调整优先级寄存器实现 QoS。

10.4 DMA coherent 与 DMA streaming 的选择

  • 对于 surround 音频双缓冲(2 * period_len),使用 dma_alloc_coherent() 分配环形 buffer,避免 SGI 同步开销
  • 对于从用户态拷贝的 playback/capture buffer,必须使用 dma_map_sg() 并设置 DMA_ATTR_SKIP_CPU_SYNC 以减轻 cache flush 开销

十一、调试与可观测性

dmaengine 提供 sysfs 接口:/sys/kernel/debug/dmaengine/summary。但往往不够用。实战中建议在驱动里加 tracepoint:


#include <trace/events/dmaengine.h>

static void my_dma_start_transfer(struct my_dma_chan *mchan)
{
    /* ... 硬件操作 ... */
    trace_dmaengine_start(mchan->vc.chan.device->dev,
                          to_virt_dma_desc(mchan->current));
}

在驱动端 ftrace 可以追踪 dmaengine_prep_* / dmaengine_submit / dma_async_issue_pending 的完整时序,定位客户端侧中断延迟和 descriptor 堆积问题。


十二、总结

Linux dmaengine 驱动开发是连接硬件设计和系统软件的桥梁。核心套路是:

  1. 声明能力 → dma_cap_set + src/dst_addr_widths + directions
  2. 实现回调 → prep_slave_sg / prep_dma_cyclic / prep_interleaved / config / terminate / issue_pending
  3. 管理 virt_dma 框架 → 用 vchan_tx_prep、vchan_cookie_complete、vchan_get_all_descriptors 与 core 交互
  4. 中断通知 → vchan_cyclic_callback 处理循环,vchan_cookie_complete 处理单次
  5. 流式控制 → pause/resume 保证 FIFO 一致性,terminate_all 保证无野指针
  6. DT 绑定 → dma-requests / dma-channels / dma-geqable 兼容框架约定

dmaengine 框架在 6.x 持续演进中,引入了 DMA_ASYNC_TX_KMEM_CACHE 标志以加速 descriptor 分配,以及对 async device 注册的推荐。掌握这套 API,你就能在 SoC 驱动开发中将 DMA 这个最古老的计算机概念发挥到极致。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部