25、深入理解DMA(Direct Memory Access)技术

深入理解DMA(Direct Memory Access)技术

1. DMA映射方向定义

在进行DMA映射时,需要指定数据传输的方向,这通过 enum dma_data_direction 类型的符号来实现,其定义在 include/linux/dma-direction.h 中:

enum dma_data_direction {
    DMA_BIDIRECTIONAL = 0,
    DMA_TO_DEVICE = 1,
    DMA_FROM_DEVICE = 2,
    DMA_NONE = 3,
};

2. 单缓冲区映射

单缓冲区映射适用于偶尔的映射操作。可以使用以下函数来设置单缓冲区:

dma_addr_t dma_map_single(struct device *dev, void *ptr,
                          size_t size, enum dma_data_direction direction);

其中, direction 可以是 DMA_TO_DEVICE DMA_FROM_DEVICE DMA_BIDIRECTIONAL ptr 是缓冲区的内核虚拟地址, dma_addr_t 是返回给设备的总线地址。使用时要根据实际需求选择合适的方向,而不是总是使用 DMA_BIDIRECTIONAL

当不再需要该映射时,使用以下函数释放映射:

void dma_unmap_single(struct device *dev, dma_addr_t dma_addr,
                      size_t size, enum dma_data_direction direction);

3. 分散/聚集映射

分散/聚集映射是一种特殊的流式DMA映射,它允许一次性传输多个缓冲区区域,而不是逐个映射和传输每个缓冲区。以下情况可能会用到分散/聚集映射:
- readv writev 系统调用
- 磁盘I/O请求
- 映射的内核I/O缓冲区中的页面列表

内核使用 struct scatterlist 来表示分散列表:

struct scatterlist {
    unsigned long page_link;
    unsigned int offset;
    unsigned int length;
    dma_addr_t dma_address;
    unsigned int dma_length;
};

设置分散列表映射的步骤如下:
1. 分配分散的缓冲区。
2. 创建分散列表数组,并使用 sg_set_buf() 填充分配的内存。注意,分散列表项必须是页面大小(除了末尾项)。
3. 对分散列表调用 dma_map_sg()
4. DMA操作完成后,调用 dma_unmap_sg() 取消映射。

以下是一个示例代码:

u32 *wbuf, *wbuf2, *wbuf3;
wbuf = kzalloc(SDMA_BUF_SIZE, GFP_DMA);
wbuf2 = kzalloc(SDMA_BUF_SIZE, GFP_DMA);
wbuf3 = kzalloc(SDMA_BUF_SIZE/2, GFP_DMA);
struct scatterlist sg[3];
sg_init_table(sg, 3);
sg_set_buf(&sg[0], wbuf, SDMA_BUF_SIZE);
sg_set_buf(&sg[1], wbuf2, SDMA_BUF_SIZE);
sg_set_buf(&sg[2], wbuf3, SDMA_BUF_SIZE/2);
ret = dma_map_sg(NULL, sg, 3, DMA_MEM_TO_MEM);

3.1 缓存一致性同步

dma_map_sg() dma_unmap_sg() 会处理缓存一致性问题。但如果需要在DMA传输之间使用相同的映射来访问(读/写)数据,则必须在每次传输之间以适当的方式同步缓冲区。如果CPU需要访问缓冲区,可以使用 dma_sync_sg_for_cpu() ;如果是设备需要访问,则使用 dma_sync_sg_for_device() 。单区域映射的类似函数是 dma_sync_single_for_cpu() dma_sync_single_for_device()

void dma_sync_sg_for_cpu(struct device *dev,
                         struct scatterlist *sg,
                         int nents,
                         enum dma_data_direction direction);
void dma_sync_sg_for_device(struct device *dev,
                            struct scatterlist *sg, int nents,
                            enum dma_data_direction direction);
void dma_sync_single_for_cpu(struct device *dev, dma_addr_t addr,
                             size_t size,
                             enum dma_data_direction dir);
void dma_sync_single_for_device(struct device *dev,
                                dma_addr_t addr, size_t size,
                                enum dma_data_direction dir);

缓冲区取消映射后,就不需要再次调用上述函数,可以直接读取内容。

4. 完成机制

完成机制是内核编程中常用的一种模式,用于在发起某个活动后等待该活动完成。在等待缓冲区使用时,完成机制是 sleep() 的一个很好的替代方案,它适用于数据感知,这正是DMA回调所做的事情。

使用完成机制需要包含以下头文件:

<linux/completion.h>

可以静态或动态地创建 struct completion 结构的实例:
- 静态声明和初始化:

DECLARE_COMPLETION(my_comp);
  • 动态分配:
struct completion my_comp;
init_completion(&my_comp);

当驱动程序开始一项需要等待完成的工作(在我们的例子中是DMA事务)时,只需将完成事件传递给 wait_for_completion() 函数:

void wait_for_completion(struct completion *comp);

当代码的其他部分确定完成事件已经发生(事务完成)时,可以使用以下函数唤醒等待的进程:

void complete(struct completion *comp);
void complete_all(struct completion *comp);

complete() 只会唤醒一个等待的进程,而 complete_all() 会唤醒所有等待该事件的进程。完成机制的实现方式使得即使在 wait_for_completion() 之前调用 complete() ,它也能正常工作。

5. DMA引擎API

DMA引擎是一个通用的内核框架,用于开发DMA控制器驱动。DMA的主要目标是在内存复制时减轻CPU的负担。通过使用通道将事务(I/O数据传输)委托给DMA引擎。

5.1 使用从DMA的步骤

使用从DMA的步骤如下:
1. 分配一个DMA从通道。
2. 设置从设备和控制器特定的参数。
3. 获取事务的描述符。
4. 提交事务。
5. 发出待处理请求并等待回调通知。

5.2 分配DMA从通道

使用 dma_request_channel() 函数请求一个通道:

struct dma_chan *dma_request_channel(const dma_cap_mask_t *mask,
                                     dma_filter_fn fn, void *fn_param);

mask 是一个位图掩码,用于表示通道必须满足的能力,通过它可以指定驱动程序需要执行的传输类型,如下所示:

enum dma_transaction_type {
    DMA_MEMCPY,     /* Memory to memory copy */
    DMA_XOR,        /* Memory to memory XOR*/
    DMA_PQ,         /* Memory to memory P+Q computation */
    DMA_XOR_VAL,    /* Memory buffer parity check using XOR */
    DMA_PQ_VAL,     /* Memory buffer parity check using P+Q */
    DMA_INTERRUPT,  /* The device is able to generate dummy transfer that will generate interrupts */
    DMA_SG,         /* Memory to memory scatter gather */
    DMA_PRIVATE,    /* channels are not to be used for global memcpy. Usually used with DMA_SLAVE */
    DMA_SLAVE,      /* Memory to device transfers */
    DMA_CYCLIC,     /* Device is able to handle cyclic transfers */
    DMA_INTERLEAVE, /* Memory to memory interleaved transfer */
};

使用 dma_cap_zero() dma_cap_set() 函数来清除掩码并设置所需的能力,例如:

dma_cap_mask my_dma_cap_mask;
struct dma_chan *chan;
dma_cap_zero(my_dma_cap_mask);
dma_cap_set(DMA_MEMCPY, my_dma_cap_mask); /* Memory to memory copy */
chan = dma_request_channel(my_dma_cap_mask, NULL, NULL);

如果 filter_fn 参数(可选)为 NULL dma_request_channel() 将简单地返回第一个满足能力掩码的通道。否则,可以使用 filter_fn 例程作为系统中可用通道的过滤器。

当不再需要该通道时,使用以下函数释放它:

void dma_release_channel(struct dma_chan *chan);

5.3 设置从设备和控制器特定的参数

这一步引入了 struct dma_slave_config 数据结构,用于表示DMA从通道的运行时配置:

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;
    [...]
};

各元素的含义如下:
| 元素 | 含义 |
| ---- | ---- |
| direction | 指示当前数据在该从通道上的传输方向,可能的值有 DMA_MEM_TO_MEM DMA_MEM_TO_DEV DMA_DEV_TO_MEM DMA_DEV_TO_DEV 等 |
| src_addr | DMA从设备数据应读取的缓冲区的物理地址(实际是总线地址),如果源是内存,则忽略该元素 |
| dst_addr | DMA从设备数据应写入的缓冲区的物理地址(实际是总线地址),如果源是内存,则忽略该元素 |
| src_addr_width | 读取DMA数据的源(RX)寄存器的字节宽度,合法值为1、2、4或8等 |
| dst_addr_width | 与 src_addr_width 类似,但用于目标(TX) |
| src_maxburst | 一次突发传输中可以发送到设备的最大字数(这里的字是指 src_addr_width 成员的单位,而不是字节) |
| dst_maxburst | 与 src_maxburst 类似,但用于目标 |

使用 dmaengine_slave_config() 函数设置配置:

int dmaengine_slave_config(struct dma_chan *chan,
                           struct dma_slave_config *config);

以下是一个示例代码:

struct dma_chan *my_dma_chan;
dma_addr_t dma_src, dma_dst;
struct dma_slave_config my_dma_cfg = {0};
/* No filter callback, neither filter param */
my_dma_chan = dma_request_channel(my_dma_cap_mask, 0, NULL);
/* scr_addr and dst_addr are ignored in this structure for mem to mem copy */
my_dma_cfg.direction = DMA_MEM_TO_MEM;
my_dma_cfg.dst_addr_width = DMA_SLAVE_BUSWIDTH_32_BYTES;
dmaengine_slave_config(my_dma_chan, &my_dma_cfg);
char *rx_data, *tx_data;
/* No error check */
rx_data = kzalloc(BUFFER_SIZE, GFP_DMA);
tx_data = kzalloc(BUFFER_SIZE, GFP_DMA);
feed_data(tx_data);
/* get dma addresses */
dma_src_addr = dma_map_single(NULL, tx_data,
                              BUFFER_SIZE, DMA_MEM_TO_MEM);
dma_dst_addr = dma_map_single(NULL, rx_data,
                              BUFFER_SIZE, DMA_MEM_TO_MEM);

5.4 获取事务的描述符

请求DMA通道时,返回值是 struct dma_chan 结构的实例,该结构包含一个 struct dma_device *device 字段,代表提供该通道的DMA设备(实际上是控制器)。控制器的内核驱动负责提供一组函数来准备DMA事务,每个函数对应一种DMA事务类型。

一些常用的函数如下:
- device_prep_dma_memcpy() :准备内存复制操作
- device_prep_dma_sg() :准备分散/聚集内存复制操作
- device_prep_dma_xor() :用于异或操作
- device_prep_dma_xor_val() :准备异或验证操作
- device_prep_dma_pq() :准备P+Q操作
- device_prep_dma_pq_val() :准备P+Q零和操作
- device_prep_dma_memset() :准备内存设置操作
- device_prep_dma_memset_sg() :用于对分散列表进行内存设置操作
- device_prep_slave_sg() :准备从DMA操作
- device_prep_interleaved_dma() :以通用方式传输表达式

以内存到内存复制为例,使用 device_prep_dma_memcpy

struct dma_device *dma_dev = my_dma_chan->device;
struct dma_async_tx_descriptor *tx = NULL;
tx = dma_dev->device_prep_dma_memcpy(my_dma_chan, dma_dst_addr,
                                     dma_src_addr, BUFFER_SIZE, 0);
if (!tx) {
    printk(KERN_ERR "%s: Failed to prepare DMA transfer\n",
           __FUNCTION__);
    /* dma_unmap_* the buffer */
}

实际上,也可以使用 dmaengine_prep_* 系列函数,例如 dmaengine_prep_dma_memcpy

static inline struct dma_async_tx_descriptor *dmaengine_prep_dma_memcpy(
    struct dma_chan *chan, dma_addr_t dest, dma_addr_t src,
    size_t len, unsigned long flags);

5.5 提交事务

使用 dmaengine_submit() 函数将事务放入驱动程序的待处理队列:

dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc);

该函数返回一个cookie,可以通过其他DMA引擎使用它来检查DMA活动的进度。 dmaengine_submit() 不会启动DMA操作,只是将其添加到待处理队列。

以下是示例代码:

struct completion transfer_ok;
init_completion(&transfer_ok);
tx->callback = my_dma_callback;
/* Submit our dma transfer */
dma_cookie_t cookie = dmaengine_submit(tx);
if (dma_submit_error(cookie)) {
    printk(KERN_ERR "%s: Failed to start DMA transfer\n", __FUNCTION__);
    /* Handle that */
    [...]
}

5.6 发出待处理请求并等待回调通知

使用 dma_async_issue_pending() 函数启动通道中的待处理事务:

void dma_async_issue_pending(struct dma_chan *chan);

如果通道空闲,则队列中的第一个事务将被启动,后续事务将排队。DMA操作完成后,队列中的下一个事务将被启动,并触发一个小任务,该小任务负责调用客户端驱动程序的完成回调例程进行通知(如果设置了回调)。

以下是示例代码:

dma_async_issue_pending(my_dma_chan);
wait_for_completion(&transfer_ok);
dma_unmap_single(my_dma_chan->device->dev, dma_src_addr,
                 BUFFER_SIZE, DMA_MEM_TO_MEM);
dma_unmap_single(my_dma_chan->device->dev, dma_src_addr,
                 BUFFER_SIZE, DMA_MEM_TO_MEM);
/* Process buffer through rx_data and tx_data virtualaddresses. */

wait_for_completion() 函数会阻塞,直到DMA回调被调用,回调会更新(完成)完成变量,从而恢复之前阻塞的代码。

回调函数示例:

static void my_dma_callback()
{
    complete(transfer_ok);
    return;
}

实际上, dmaengine_issue_pending(struct dma_chan *chan) dma_async_issue_pending() 的包装函数。

5.7 流程图

graph TD;
    A[分配DMA从通道] --> B[设置从设备和控制器特定的参数];
    B --> C[获取事务的描述符];
    C --> D[提交事务];
    D --> E[发出待处理请求并等待回调通知];

通过以上内容,我们对DMA技术的映射方式、完成机制以及DMA引擎API有了较为深入的了解,这些知识对于开发DMA相关的驱动程序和应用程序非常有帮助。

6. DMA技术关键要点总结

6.1 映射方向与类型

映射方向/类型 描述 代码示例
DMA_BIDIRECTIONAL 双向数据传输 -
DMA_TO_DEVICE 从内存到设备的数据传输 -
DMA_FROM_DEVICE 从设备到内存的数据传输 -
单缓冲区映射 适用于偶尔的映射操作 dma_addr_t dma_map_single(struct device *dev, void *ptr, size_t size, enum dma_data_direction direction);
分散/聚集映射 可一次性传输多个缓冲区区域 见前文分散/聚集映射示例代码

6.2 完成机制要点

  • 头文件 <linux/completion.h>
  • 实例创建方式
    • 静态: DECLARE_COMPLETION(my_comp);
    • 动态:
struct completion my_comp;
init_completion(&my_comp);
  • 等待完成 void wait_for_completion(struct completion *comp);
  • 唤醒等待进程
    • 唤醒一个: void complete(struct completion *comp);
    • 唤醒所有: void complete_all(struct completion *comp);

6.3 DMA引擎API操作步骤

步骤 操作 代码示例
1 分配DMA从通道 struct dma_chan *dma_request_channel(const dma_cap_mask_t *mask, dma_filter_fn fn, void *fn_param);
2 设置从设备和控制器特定的参数 int dmaengine_slave_config(struct dma_chan *chan, struct dma_slave_config *config);
3 获取事务的描述符 struct dma_async_tx_descriptor *tx = dma_dev->device_prep_dma_memcpy(my_dma_chan, dma_dst_addr, dma_src_addr, BUFFER_SIZE, 0);
4 提交事务 dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc);
5 发出待处理请求并等待回调通知 void dma_async_issue_pending(struct dma_chan *chan);

7. DMA技术应用场景分析

7.1 磁盘I/O请求

在磁盘I/O请求中,DMA技术可以显著提高数据传输效率。当应用程序需要从磁盘读取数据或向磁盘写入数据时,传统的方式是CPU负责数据的传输,这会占用大量的CPU时间。而使用DMA技术,CPU只需发起DMA事务,然后可以继续执行其他任务,DMA控制器会自动完成数据的传输。

操作步骤如下:
1. 分配DMA从通道,设置通道的能力掩码,例如支持 DMA_DEV_TO_MEM (从设备到内存)或 DMA_MEM_TO_DEV (从内存到设备)。
2. 设置从设备和控制器特定的参数,包括数据传输方向、源地址和目标地址等。
3. 获取事务的描述符,根据具体的操作(如读取或写入)选择合适的准备函数。
4. 提交事务,将事务放入驱动程序的待处理队列。
5. 发出待处理请求并等待回调通知,当DMA操作完成后,回调函数会被调用,通知应用程序数据传输已完成。

7.2 网络数据传输

在网络数据传输中,DMA技术同样可以发挥重要作用。当网络接口接收到数据包时,使用DMA技术可以快速将数据包从网络接口卡(NIC)的缓冲区传输到系统内存中,而不需要CPU的干预。

操作步骤与磁盘I/O请求类似:
1. 分配DMA从通道,设置通道支持 DMA_DEV_TO_MEM
2. 设置从设备和控制器特定的参数,如网卡的物理地址和系统内存的目标地址。
3. 获取事务的描述符,准备数据传输。
4. 提交事务,将数据包传输任务交给DMA控制器。
5. 等待回调通知,当数据包传输完成后,进行后续的处理,如解析数据包。

7.3 流程图

graph LR;
    A[应用场景] --> B[磁盘I/O请求];
    A --> C[网络数据传输];
    B --> D[分配DMA从通道];
    D --> E[设置参数];
    E --> F[获取描述符];
    F --> G[提交事务];
    G --> H[等待回调通知];
    C --> D;

8. DMA技术的优势与挑战

8.1 优势

  • 减轻CPU负担 :DMA技术可以将数据传输任务从CPU中分离出来,使CPU可以专注于其他重要的计算任务,提高系统的整体性能。
  • 提高数据传输效率 :DMA控制器可以直接在内存和设备之间进行数据传输,避免了CPU在数据传输过程中的频繁干预,从而提高了数据传输的速度。
  • 支持多任务处理 :由于CPU不需要参与数据传输,因此可以同时处理其他任务,实现多任务的并行执行。

8.2 挑战

  • 缓存一致性问题 :在DMA操作过程中,可能会出现缓存一致性问题。当CPU和DMA控制器同时访问内存时,如果缓存中的数据与内存中的数据不一致,可能会导致数据错误。因此,需要使用缓存同步函数(如 dma_sync_sg_for_cpu() dma_sync_sg_for_device() )来确保缓存的一致性。
  • 硬件兼容性 :不同的硬件平台可能对DMA技术的支持有所不同,需要根据具体的硬件情况进行配置和开发。
  • 编程复杂性 :使用DMA技术需要掌握一定的编程知识,包括DMA引擎API的使用、完成机制的实现等,这增加了开发的难度。

9. 总结

DMA(Direct Memory Access)技术是一种非常重要的计算机技术,它通过将数据传输任务从CPU中分离出来,提高了系统的性能和效率。本文详细介绍了DMA的映射方向、单缓冲区映射、分散/聚集映射、完成机制以及DMA引擎API的使用方法,并分析了DMA技术在磁盘I/O请求和网络数据传输等应用场景中的操作步骤。同时,也讨论了DMA技术的优势和挑战。

在实际应用中,开发人员需要根据具体的需求和硬件平台,合理使用DMA技术,充分发挥其优势,同时解决可能遇到的挑战。通过掌握DMA技术,开发人员可以开发出更加高效、稳定的驱动程序和应用程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值