深入理解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技术,开发人员可以开发出更加高效、稳定的驱动程序和应用程序。
627

被折叠的 条评论
为什么被折叠?



