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传输。
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控制器控制 |
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;
}
4.6 工程实践:I2C DMA移植适配要点
在将I2C驱动从PIO模式迁移到DMA模式时,以下要点需要特别注意:
- 设备树配置:确保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"; }; - 通道申请时机:在I2C适配器
probe()函数中申请DMA通道,并检查返回值。如果DMA通道不可用,应回退到PIO模式。 - 缓冲区对齐:DMA传输要求缓冲区物理地址对齐到数据总线宽度(通常为4字节)。使用
kmalloc()或dma_alloc_coherent()分配缓冲区。 - 传输长度限制:某些DMA控制器支持的最大传输长度有限(如65535字节),需要在驱动中做分段处理。
- 错误恢复:当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传输的所有元数据和控制信息。