DMA映射
参考资料:
《宋宝华:Linux设备驱动开发详解》
VA和PA的相互转换:
系统启动时,MMU便会建立映射表,将DRAM物理地址和虚拟地址进行映射,在linux内核中,可以使用下面的函数完成VA和PA的转换
#define virt_to_bus(virt) (virt_to_phys(virt)) void *phys_to_virt(phys_addr_t address); phys_addr_t virt_to_phys(const volatile void *address); // 参数说明 address:要转换的虚拟地址 #define phys_to_virt(vaddr) ((void *)((unsigned long)(vaddr)+PAGE_OFFSET))
但从代码发现,phys_to_virt和virt_to_phys都获取到的内核地址仅做了简单的偏移,例如0x817000000经过phys_to_virt获取到的内核地址为0xffff000817000000
DMA地址掩码:
设备不一定能够在所有的内存地址上进行DMA操作,在这这种情况下应该通过下列函数执行DMA地址掩码:
int dma_set_mask(strcut device *dev, u64 mask);
例如,对于只能在32位地址上执行DMA操作的设备而言,就应该调用dma_set_mask(dev, 0xffffffff)。
其实该API的本质就是修改了device结构体中的dma_mask成员,如下:
int arm_dma_set_mask(struct device* dev, u64 dma_mask) { if(!dev->dma_mask || !dma_supported(dev, dma_mask)) return -EIO; *dev->dma_mask = dma_mask; return 0; }
在device结构体中,除了有dma_mask之外,还有一个coherent_dma_mask成员,dma_mask是设备DMA可以寻址的范围,而coherent_dma_mask作用用于申请一致性的DMA缓冲区:
一致性DMA缓冲区:
DMA映射包括两个部分的工作:分配一片DMA缓冲区,为这片缓冲区产生设备可访问的地址。同时,DMA映射必须也要考虑cache一致性的问题。内核提供如下函数以分配DMA一致性的内存区域:
void * dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp); //分配DMA缓存区 //返回值为:申请到的DMA缓冲区的虚拟地址,若为NULL,表示分配失败,需要释放,避免内存泄漏 //参数如下: //*dev:指针,这里填0,表示这个申请的缓冲区里没有内容 //size:分配的地址大小(字节单位) //*handle:申请到的物理起始地址 //gfp:分配出来的内存参数,标志定义在<linux/gfp.h>,常用标志如下: //GFP_ATOMIC 用来从中断处理和进程上下文之外的其他代码中分配内存. 从不睡眠. //GFP_KERNEL 内核内存的正常分配. 可能睡眠. //GFP_USER 用来为用户空间页来分配内存; 它可能睡眠.
dma_alloc_coherent函数返回值表示kernel看到的地址,返回值表示dma设备看到的地址
对应的释放函数如下:
dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle) //释放DMA缓存,与dma_alloc_coherent ()对应 //size:释放长度 //cpu_addr:虚拟地址, //handle:物理地址
两者的区别:
- dma_alloc_writecombine
- dma_alloc_writecombine 函数用于分配一块内存区域,该内存区域可以通过 DMA 进行写操作(如数据传输到设备)。
- 分配的内存区域通常是通过写结合(write-combining)机制来实现的,这种机制可以提高性能,减少内存复制和刷新的开销。
- 适用于一些需要高性能写操作的场景,比如网络设备的数据传输。
- dma_alloc_coherent
- dma_alloc_coherent 函数也用于分配一块内存区域,但是它分配的内存区域是直接连续的物理内存,适合 DMA 读写操作。
- 内核会保证返回的内存区域是物理上连续的,以便于 DMA 控制器直接访问这块内存进行数据传输。
- 适用于需要进行 DMA 读写操作的场景,如一些设备对内存访问要求严格的情况。
dma_alloc_writecombine对应的释放函数:dma_free_writecombine
dma_free_writecombine函数定义如下:
void *dma_free_writecombine(struct device *dev, size_t size, cpu_addr, handle) \ dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle)
需要注意的一点是,dma_alloc_xxx()函数虽然是dma_alloc_开头的,但是其申请的区域不一定在DMA区域中。以32位ARM处理器为例,当coherent_dma_mask小于0xffffffff时,才会设置GPA_DMA标记,并从DMA区域去申请内存。
当使用ARM时,像GPU、 isp等设备需要预留大量连续的内存,这部分内存平时不用,但是一般的做法又必须先预留。CMA机制,可以做到不预留内存,这些内存是平时可用的,只是当有需要的时候才会被分配给isp设备。
流式DMA映射:
需要流式映射的原因:
并不是所有的DMA缓冲区都是驱动申请的,如果是驱动申请的,用一致性DMA映射自然最方便,一致性直接考虑了cache一致性问题了。但是,缓冲区来自内核的较上层(例如网络报文,块设备要写入设备的数据),上层很可能使用的是kmalloc()等方法去申请的,这时就需要用到流式DMA映射。
流式映射相对于一致性映射有更多的限制点,以下原因不同于一致性映射:
1、映射需要使用已经分配的缓冲区
2、映射可以接收几个分散的不连续缓冲区
3、映射的缓冲区属于设备而不属于cpu。cpu使用缓冲区之前,应该首先解除映射(在dma_unmap_single()或者dma_unmap_sg()之后)。这是为了缓存
4、对于写入事务(CPU到设备),驱动程序应该在映射之前将数据放入缓冲区
5、必须指定数据移动的方向,只能基于该方向使用数据
为啥在取消映射之前不能访问缓冲区呢?原因很简单:CPU映射是可缓存的。用于流式映射的dma_map_xxx()系列函数将首先清理与缓冲区相关的缓存使之无效,在出现相应的dma_unmap_xxx()之前,CPU不能访问。
流式DMA映射本质上大多就是进行cache的使无效或清除操作
流式映射有两种形式:
1、单缓冲区映射,只允许单页映射
2、分散/聚集映射,允许传递多个缓冲区(分散在内存中)
对于这两种形式,都必须指定方向:
enum dam_data_direction { DMA_BIDIRECTIONAL = 0, DMA_TO_DEVICE = 1; DMA_FROM_DEVICE = 2; DMA_NONE = 3; };
单缓冲区映射:
适用于偶然使用的映射,可用dma_map_single实现流式DMA映射,dma_unmap_single取消流式映射:
dma_addr_t dma_map_single(struct device *dev, void *ptr, size_t size, enum dma_data_direction direction); // 参数和返回值说明 dev:指向代表要进行 DMA 操作的设备的 struct device 结构体的指针。 ptr:指向要映射的内存区域的起始虚拟地址。 size:要映射的内存区域的大小。 direction:数据传输方向,可以是 DMA_TO_DEVICE(表示数据从内存传输到设备)、DMA_FROM_DEVICE(表示数据从设备传输到内存)或者 DMA_BIDIRECTIONAL(表示双向数据传输)。 dma_addr_t:表示映射后的物理地址(即 DMA 地址)。如果映射成功,则返回映射后的物理地址;如果映射失败,则返回一个特定的错误码。 void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction); // 参数说明 dev:指向代表进行 DMA 操作的设备的 struct device 结构体的指针。 dma_addr:要取消映射的 DMA 地址,即之前调用 dma_map_single 返回的物理地址。 size:取消映射的内存区域的大小,应与之前映射时传入的大小相同。 direction:数据传输方向,应与之前映射时传入的方向参数相同。
通常情况下,设备驱动不应该访问unmap的流式映射DMA缓冲区,如果一定要这么做,可以先使用下列函数获得DMA缓冲区的拥有权:
void dma_sync_single_for_cpu(struct device *dev, dma_addr_t dma_handle, size_t size, enum dma_data_direction direction); // 参数说明 dev:指向进行 DMA 操作的设备的 struct device 结构体的指针。 dma_handle:要同步的 DMA 地址,即之前通过 dma_map_single 函数获得的 DMA 地址。 size:要同步的内存区域的大小,应与之前映射时传入的大小相同。 direction:数据传输方向,通常应与之前映射时传入的方向参数相同,用于指示数据流向。
驱动访问完DMA缓冲区后,应该将其所有权返还给设备,如下:
dma_sync_single_for_device(struct device *dev, dma_addr_t dma_handle, size_t size, enum dma_data_direction direction); // 参数说明 dev:指向进行 DMA 操作的设备的 struct device 结构体的指针。 dma_handle:要同步的 DMA 地址,即之前通过 dma_map_single 函数获得的 DMA 地址。 size:要同步的内存区域的大小,应与之前映射时传入的大小相同。 direction:数据传输方向,通常应与之前映射时传入的方向参数相同,用于指示数据流向。
分散/聚集映射:
分散/聚集映射是一种特殊类型的流式DMA映射,可以在单个槽中传输多个缓冲区区域,而不是单独映射每个缓冲区并逐个传输。假设有几个缓冲区物理上可能不是连续的,所有这些缓冲区都需要同时传输到设备或者从设备传输,这些情况可能出现的原因:
1、readv或writev系统调用
2、磁盘I/O请求
内核将分散列表表示为:
struct scatterlist { struct scatterlist *next; // 指向下一个 scatterlist 结构体的指针 unsigned int offset; // 缓冲区在页中的偏移 unsigned long dma_address; // DMA 地址 unsigned int dma_length; // 数据长度 unsigned int dma_offset; // 数据偏移量 };
为了设置分散列表映射,应该进行如下操作:
1、分配分散的缓冲区
2、创建分散列表数组,并使用sg_set_buf()分配的内存填充它。注:分散页表必须是页面大小(除结尾外)
3、在该分散列表上调用dma_map_sg();负责缓存一致性
4、一旦完成DMA映射,就调用dma_unmap_sg()来取消映射分散列表
例如:
u32 *wbuf, *wbuf2, *wbuf3; wbuf = kzmalloc(SDMA_BUF_SIZE, GFP_DMA); wbuf2 = kzmalloc(SDMA_BUF_SIZE, GFP_DMA); wbuf3 = kzmalloc(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);
单缓冲区映射使用demo:
#include <linux/init.h> #include <linux/module.h> #include <linux/dma-mapping.h> #define BUF_SIZE 4096 static struct device *dummy_device; static dma_addr_t dma_handle; static char *dma_buffer; static int __init dma_sync_demo_init(void) { // 分配内存作为 DMA 缓冲区 dma_buffer = kmalloc(BUF_SIZE, GFP_KERNEL); if (!dma_buffer) { pr_err("Failed to allocate DMA buffer\n"); return -ENOMEM; } // 映射 DMA 缓冲区 dma_handle = dma_map_single(dummy_device, dma_buffer, BUF_SIZE, DMA_BIDIRECTIONAL); if (dma_mapping_error(dummy_device, dma_handle)) { pr_err("Failed to map DMA buffer\n"); kfree(dma_buffer); return -ENOMEM; } // 修改 DMA 缓冲区数据 memset(dma_buffer, 0xAA, BUF_SIZE); // 同步 DMA 内存到 CPU 缓存 dma_sync_single_for_cpu(dummy_device, dma_handle, BUF_SIZE, DMA_BIDIRECTIONAL); // 在这里可以处理同步后的数据 return 0; } static void __exit dma_sync_demo_exit(void) { // 取消映射 DMA 缓冲区 dma_unmap_single(dummy_device, dma_handle, BUF_SIZE, DMA_BIDIRECTIONAL); // 释放 DMA 缓冲区内存 kfree(dma_buffer); } module_init(dma_sync_demo_init); module_exit(dma_sync_demo_exit); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("DMA Sync Demo");