4. DMA驱动框架(Linux):dmaengine API、dma_slave_config、dma_async_tx_descriptor

在Linux内核中,DMA(Direct Memory Access)驱动框架通过一套标准化的API——dmaengine,为外设驱动开发者提供了统一的DMA操作接口。本章将深入剖析dmaengine框架的核心数据结构与编程模型,重点讲解dma_slave_config配置结构体、dma_async_tx_descriptor传输描述符以及它们在实际I2C驱动移植中的工程实践。

4.1 dmaengine框架概述

dmaengine框架位于drivers/dma/目录下,向上为外设驱动(如I2C、SPI、UART)提供通用API,向下抽象不同DMA控制器的硬件差异。其核心设计思想是:通道(Channel)、描述符(Descriptor)、传输(Transaction)三层模型。

关键概念: dmaengine将DMA控制器抽象为多个通道,每个通道对应一个独立的DMA传输上下文。外设驱动通过申请通道、配置参数、提交描述符、等待完成四个步骤完成一次DMA传输。
dmaengine框架分层架构 dmaengine框架分层架构 外设驱动(I2C/SPI/UART) dmaengine API(dmaengine.h) 核心数据结构 dma_device | dma_chan | dma_slave_config | dma_async_tx_descriptor DMA控制器驱动(如pl330、edma、dw_dmac) DMA控制器硬件

4.2 dmaengine核心API

dmaengine API是外设驱动与DMA框架交互的唯一入口。以下是最常用的API函数及其功能说明:

API函数 功能描述 返回值
dma_request_chan() 根据设备树或平台数据请求一个DMA通道 struct dma_chan * 或 ERR_PTR
dmaengine_slave_config() 配置从设备模式参数(地址、方向、宽度等) 0 成功,负值失败
dmaengine_prep_slave_single() 准备一次单缓冲从设备传输描述符 struct dma_async_tx_descriptor *
dmaengine_submit() 将描述符提交到DMA引擎的待处理队列 dma_cookie_t
dma_async_issue_pending() 触发DMA引擎开始处理已提交的传输 void
dma_wait_for_async_tx() 同步等待DMA传输完成 enum dma_status
dma_release_channel() 释放DMA通道 void
工程实践提示: 在I2C驱动中,通常使用dma_request_chan()分别申请TX和RX两个通道。如果设备树中只定义了一个DMA通道,则需根据传输方向动态切换配置。

4.3 dma_slave_config 结构体

dma_slave_config是连接外设与DMA控制器的桥梁,它描述了从设备(外设)的物理地址、数据宽度、突发传输大小等关键参数。该结构体定义在include/linux/dmaengine.h中:

struct dma_slave_config {
    enum dma_transfer_direction direction;
    phys_addr_t src_addr;
    phys_addr_t dst_addr;
    enum dma_slave_buswidth src_addr_width;
    enum dma_slave_buswidth dst_addr_width;
    u32 src_maxburst;
    u32 dst_maxburst;
    u32 src_port_window_size;
    u32 dst_port_window_size;
    bool device_fc;
    unsigned int slave_id;
};

各字段含义及在I2C场景下的典型配置:

字段 含义 I2C典型配置
direction 传输方向:DMA_MEM_TO_DEV 或 DMA_DEV_TO_MEM 写操作:MEM_TO_DEV;读操作:DEV_TO_MEM
src_addr 源物理地址(外设数据寄存器地址) I2C数据寄存器物理地址(如0x11C00000 + 0x10)
dst_addr 目的物理地址 写操作时指向I2C数据寄存器;读操作时指向内存缓冲区
src_addr_width 源数据总线宽度 DMA_SLAVE_BUSWIDTH_1_BYTE(I2C为字节传输)
dst_addr_width 目的数据总线宽度 DMA_SLAVE_BUSWIDTH_1_BYTE
src_maxburst 源端最大突发传输长度(单位:数据宽度) 通常设为4或8,取决于I2C FIFO深度
dst_maxburst 目的端最大突发传输长度 与src_maxburst一致
device_fc 是否由外设控制流控 通常设为false,由DMA控制器控制
dma_slave_config 配置流程 dma_slave_config 配置流程(I2C写操作示例) dma_request_chan() direction = MEM_TO_DEV dst_addr = I2C_DR 物理地址 addr_width = 1_BYTE maxburst = 4 device_fc = false dmaengine_slave_config() dmaengine_prep_slave_single()

4.4 dma_async_tx_descriptor 结构体

dma_async_tx_descriptor是DMA传输的“任务单”,它封装了一次传输所需的所有信息:源/目的地址、传输长度、回调函数、标志位等。该结构体由DMA控制器驱动的prep_slave_single回调函数创建,外设驱动通过它来提交传输。

struct dma_async_tx_descriptor {
    dma_cookie_t cookie;
    enum dma_ctrl_flags flags;
    dma_addr_t phys;
    struct dma_chan *chan;
    dma_cookie_t (*tx_submit)(struct dma_async_tx_descriptor *tx);
    int (*desc_free)(struct dma_async_tx_descriptor *tx);
    dma_async_tx_callback callback;
    dma_async_tx_callback_result callback_result;
    void *callback_param;
    struct dmaengine_unmap_data *unmap;
    bool cyclic;
};

关键字段说明:

  • cookie:传输的唯一标识符,由dmaengine_submit()返回,用于查询传输状态。
  • flags:控制标志,如DMA_PREP_INTERRUPT(传输完成时触发中断)、DMA_CTRL_ACK(描述符可重用)。
  • callback / callback_param:传输完成后的回调函数及其参数,用于异步通知。
  • tx_submit:提交描述符到硬件队列的函数指针,由DMA控制器驱动实现。
  • cyclic:是否为循环传输模式,I2C通常不使用。
常见陷阱: 描述符的生命周期管理。在回调函数执行完成之前,不能释放描述符或相关的DMA缓冲区。建议在回调中设置完成标志,并在主线程中等待该标志后再进行后续操作。

4.5 典型DMA传输流程(I2C写操作)

以下代码展示了在I2C驱动中使用dmaengine完成一次写传输的完整流程:

static int i2c_dma_write(struct i2c_adapter *adap, const u8 *buf, size_t len)
{
    struct dma_chan *chan = adap->dma_tx_chan;
    struct dma_slave_config config;
    struct dma_async_tx_descriptor *desc;
    dma_cookie_t cookie;
    dma_addr_t dma_buf;
    unsigned long timeout;
    int ret;

    /* 1. 配置dma_slave_config */
    memset(&config, 0, sizeof(config));
    config.direction = DMA_MEM_TO_DEV;
    config.dst_addr = adap->phys_base + I2C_DR_OFFSET;
    config.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
    config.dst_maxburst = 4;
    config.device_fc = false;

    ret = dmaengine_slave_config(chan, &config);
    if (ret) {
        dev_err(adap->dev, "DMA slave config failed: %d\n", ret);
        return ret;
    }

    /* 2. 映射DMA缓冲区 */
    dma_buf = dma_map_single(chan->device->dev, (void *)buf, len, DMA_TO_DEVICE);
    if (dma_mapping_error(chan->device->dev, dma_buf)) {
        dev_err(adap->dev, "DMA map failed\n");
        return -ENOMEM;
    }

    /* 3. 准备传输描述符 */
    desc = dmaengine_prep_slave_single(chan, dma_buf, len, DMA_MEM_TO_DEV,
                                       DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
    if (!desc) {
        dev_err(adap->dev, "DMA prep slave single failed\n");
        dma_unmap_single(chan->device->dev, dma_buf, len, DMA_TO_DEVICE);
        return -ENOMEM;
    }

    /* 4. 设置回调 */
    desc->callback = i2c_dma_callback;
    desc->callback_param = adap;

    /* 5. 提交传输 */
    cookie = dmaengine_submit(desc);
    if (dma_submit_error(cookie)) {
        dev_err(adap->dev, "DMA submit failed\n");
        dma_unmap_single(chan->device->dev, dma_buf, len, DMA_TO_DEVICE);
        return -EIO;
    }

    /* 6. 触发传输 */
    dma_async_issue_pending(chan);

    /* 7. 等待完成(同步方式) */
    timeout = wait_for_completion_timeout(&adap->dma_complete,
                                          msecs_to_jiffies(1000));
    if (timeout == 0) {
        dev_err(adap->dev, "DMA transfer timeout\n");
        dmaengine_terminate_sync(chan);
        ret = -ETIMEDOUT;
    } else {
        ret = adap->dma_status;
    }

    /* 8. 清理 */
    dma_unmap_single(chan->device->dev, dma_buf, len, DMA_TO_DEVICE);
    return ret;
}
DMA传输状态机 DMA传输状态机(dmaengine) IDLE submit SUBMITTED issue_pending IN_PROGRESS 完成中断 COMPLETE 释放通道 / 重新配置

4.6 工程实践:I2C DMA移植适配要点

在将I2C驱动从PIO模式迁移到DMA模式时,以下要点需要特别注意:

  1. 设备树配置:确保I2C节点中正确添加了dmas和dma-names属性,例如:
    i2c0: i2c@11c00000 {
        compatible = "vendor,i2c";
        reg = <0x11c00000 0x100>;
        interrupts = <GIC_SPI 45 IRQ_TYPE_LEVEL_HIGH>;
        dmas = <&edma0 0 0>, <&edma0 1 0>;
        dma-names = "tx", "rx";
    };
  2. 通道申请时机:在I2C适配器probe()函数中申请DMA通道,并检查返回值。如果DMA通道不可用,应回退到PIO模式。
  3. 缓冲区对齐:DMA传输要求缓冲区物理地址对齐到数据总线宽度(通常为4字节)。使用kmalloc()或dma_alloc_coherent()分配缓冲区。
  4. 传输长度限制:某些DMA控制器支持的最大传输长度有限(如65535字节),需要在驱动中做分段处理。
  5. 错误恢复:当DMA传输超时或出错时,调用dmaengine_terminate_sync()终止当前传输,并重置DMA通道状态。
性能对比: 在I2C高速模式(400kHz)下,使用DMA传输可以将CPU占用率从PIO模式的35%降低到5%以下,特别是在传输大数据块(如EEPROM页写入、传感器数据批量读取)时效果显著。

4.7 本章小结

本章详细介绍了Linux dmaengine框架的三大核心要素:

  • dmaengine API:提供了从通道申请到传输完成的完整操作接口。
  • dma_slave_config:定义了外设与DMA控制器之间的物理连接参数。
  • dma_async_tx_descriptor:封装了一次DMA传输的所有元数据和控制信息。