Linux动态DMA映射
1. 几种地址类型
虚拟地址
Linux内核使用的地址是虚拟地址,数据类型为void *。例如,kmalloc()和vmalloc()函数返回值就是虚拟地址。
物理地址
处理器真实地址总线上的地址,数据类型为phys_addr_t。
对I/O设备寄存器和内存统一编址的处理器,如ARM/PowerPC,参考手册一般会给出memory map,也就是各种I/O设备的寄存器在物理地址空间的分布。对I/O设备寄存器独立编址的处理器。如X86,访问I/O设备寄存器或内存时,向地址总线发送地址,并通过控制信号来实现对I/O设备寄存器和内存的不同寻址。这些I/O设备寄存器的地址可以在/proc/iomem中查看,必须使用ioremap()映射到虚拟地址空间才可以使用。
总线地址
从I/O设备的角度看,I/O设备使用的地址是总线地址。DMA使用地址也是总线地址,数据类型为dma_addr_t。对一些简单的系统,设备通过DMA可以直接访问物理地址,但大多数系统都有IOMMU将总线地址转换为物理地址。
2. DMA寻址能力
默认情况下Linux认为设备DMA可以进行32位寻址。必须对DMA mask进行设置,将设备的DMA寻址能力通知内核。
int dma_set_mask_and_coherent(struct device *dev, u64 mask)
该函数也可以分为如下两个函数,如果有需要,可以分别对流式映射设置DMA mask,对一致性分配设置DMA mask。
int dma_set_mask(struct device *dev, u64 mask); int dma_set_coherent_mask(struct device *dev, u64 mask);
3. DMA映射的类型
3.1 一致性DMA映射
一致性DMA映射通常在驱动初始化阶段分配buffer,并且保持cache的一致性。分配和释放一致性DMA buffer通常使用下面方法。
dma_addr_t dma_handle; cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, gfp);
dma_free_coherent(dev, size, cpu_addr, dma_handle)
当分配的buffer较小,小于一个页的时候,通常使用dma_pool的APIs。
struct dma_pool *pool; pool = dma_pool_create(name, dev, size, align, boundary); cpu_addr = dma_pool_alloc(pool, flags, &dma_handle);
dma_pool_free(pool, cpu_addr, dma_handle);
dma_pool_destroy(pool)
3.2 流式DMA映射
流式DMA映射必须声明DMA数据流的方向。
- DMA_BIDIRECTIONAL
- DMA_TO_DEVICE
- DMA_FROM_DEVICE
- DMA_NONE
对单个内存区域的映射和取消映射使用如下方法。
struct device *dev = &my_dev->dev; dma_addr_t dma_handle; void *addr = buffer->ptr; size_t size = buffer->len; dma_handle = dma_map_single(dev, addr, size, direction); dma_unmap_single(dev, dma_handle, size, direction);
该方法直接使用虚拟地址addr的缺点就是不能对HIGHMEM内存进行映射。下面的函数提供对page映射和取消映射的方法。
struct device *dev = &my_dev->dev; dma_addr_t dma_handle; struct page *page = buffer->page; unsigned long offset = buffer->offset; size_t size = buffer->len; dma_handle = dma_map_page(dev, page, offset, size, direction); dma_unmap_page(dev, dma_handle, size, direction);
对散列表的映射和取消映射如下,nents是sglist中entry的数目。通过散列表,将多个不连续的内存区域进行映射。返回的count的数值可能比nents小,因为有些scatterlist在内存区域连续可能进行了合并。
int i, count = dma_map_sg(dev, sglist, nents, direction); struct scatterlist *sg; for_each_sg(sglist, sg, count, i) { hw_address[i] = sg_dma_address(sg); hw_len[i] = sg_dma_len(sg); }
dma_unmap_sg(dev, sglist, nents, direction)
在流式DMA映射取消映射之前,CPU不应该访问DMA buffer,如果需要访问,则必须在DMA传输后相应地调用如下函数。
dma_sync_single_for_cpu(dev, dma_handle, size, direction);
dma_sync_sg_for_cpu(dev, sglist, nents, direction);
CPU访问结束后,将buffer还给设备DMA使用时,需要相应调用如下函数。
dma_sync_single_for_device(dev, dma_handle, size, direction);
dma_sync_sg_for_device(dev, sglist, nents, direction);
参考
https://elixir.bootlin.com/linux/v5.4/source/Documentation/DMA-API-HOWTO.txt