linuxDMA编程
DMA是一种无需CPU参与就可以让外设和系统内存进行双向数据传输的硬件机制。使用DMA可以使得系统CPU从实际的I/O数据传输过程中解脱出来,从而大大提高系统的吞吐率。
DMA方式的数据传输由DMA控制器(DMAC)控制,在传输期间CPU可以并发执行其它任务。当DMA传输结束后,DMAC通过中断通知CPU数据传输已结束,然后由CPU执行相应的中断服务程序进行后处理。
DMA与Cache一致性
Cache和DMA本身似乎是两个毫不相关的事物。但当DMA针对的内存目的地址与Cache缓存的对象由重叠关系时,经过DMA操作后,与Cache缓存对应的内存中的数据已经被修改,而CPU本身并不知道,这样就会引发Cache与内存之间数据“不一致性”的错误。
所谓Cache与内存数据之间的不一致性,是指采用Cache的系统中,同样一个数据可能既存在于Cache中,也存在于主存中,Cache与主存中的数据一样则具有一致性,不一样则具有不一致性。
如果Cache与内存出现不一致性错误,驱动将无法正常运行,这将导致一系列难以定位的bug。
另外Cache的不一致性问题还可能发生在Cache使能与关闭的时刻,这就是为什么带MMU功能的ARM处理器,在开启MMU之前,需要先置Cache无效的原因。
Linux下的DMA编程
DMA不属于我们常见的字符设备、块设备和网络设备这样的外设,它只是一种外设与内存的交互数据的方式。
内存中用于与外设交互数据的一块区域叫做DMA缓冲区,在设备不支持SG(scatter/gather)操作的情况下,DMA缓冲区必须时连续的。
申请DMA内存
不同的设备对于DMA操作的内存要求不同,如对于x86系统下的ISA设备而言,其DMA操作只能在16MB以下的内存中进行。
因此若直接使用kmalloc、__get_free_pages等接口时必须使用GFP_DMA标志。
1 | kmalloc(size, GFP_DMA) |
与此同时,如果不想使用\log_{2} size$(即order)为参数申请DMA内存,还可以使用dma_mem_alloc()
1 | static unsigned long dma_mem_alloc(unsigned long size) |
虚拟地址、物理地址和总线地址
基于DMA的硬件使用的是总线地址而不是物理地址,总线地址是从设备角度上看到的内存地址,物理地址是从CPU MMU控制器外围角度看到的内存地址,虚拟地址是从CPU核角度看到的内存地址。虽然在PC上,对于ISA和PCI而言,总线地址即为物理地址,但并不是所有平台都一样。因为有时候接口总线通过桥接电路连接,桥接电路会将I/O地址映射为不同的物理地址。如在PReP系统中,物理地址0在设备端看来是0x80000000,而0通常又被映射为虚拟地址0xC0000000,所有同一个地址就具有了三重身份:物理地址0、总线地址0x80000000和虚拟地址0xC0000000。
基于此,我们看到了三种地址的定义,因此需要掌握它们的区别,在没有IOMMU的情况下,内核提供了虚拟地址/总线地址的简单转换函数:
1 | unsigned long virt_to_bug(volatile void *address); |
但在由IOMMU的情况下,就不需要如上函数了,因为IOMMU可以使得外设DMA引擎看到“虚拟地址”。
DMA地址掩码
设备并不一定能在所有的内存地址上执行DMA操作,因此必要时应该使用如下函数执行DMA地址掩码:
1 | int dma_set_mask(struct device *dev, u64 mask); |
一致性DMA缓冲区
DMA映射过程如下:
- 分配一片DMA缓冲区
- 为这片缓冲区产生设备可访问的地址
其中对于有一致性要求的场合,在分配DMA缓冲区时可以使用一致性缓冲API,如(更多参考linux/dma-mapping.h)
1 | void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t gfp); |
流式DMA映射
并不是所有的DMA缓冲区都是驱动申请的,如果是驱动申请的,用一致性DMA缓冲区自然最方便。但是,许多情况下,缓冲区来自内核的较上层,上层可能用的是普通的kmalloc或__get_free_pages等方法申请的,这时候就要使用流式DMA映射了。
流式DM缓冲区使用的步骤如下:
- 进行流式DMA映射
- 执行DMA操作
- 进行流式DMA去映射
流式DMA映射操作本质大多是Cache的使无效或清除操作,以解决Cache一致性问题,相对于一致性DMA映射而言,流式DMA映射的接口较为复杂(为dma_map_*系列函数,参考linux/dma-mapping.h),如下所示:
1 | #define dma_map_single(d, a, s, r) dma_map_single_attrs(d, a, s, r, 0) |
DMA Engine标准API
linux内核目前推荐使用dmaengine的驱动架构来编写DMA控制器的驱动,同时外设的驱动使用标准的dmaengine API进行DMA的准备、发起和完成时的回调工作。
1 | struct dma_async_tx_descriptor *dmaengine_prep_slave_single( |
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 yxhlfx@163.com
文章标题:linuxDMA编程
本文作者:红尘追风
发布时间:2016-08-28, 01:32:43
原始链接:http://www.micernel.com/2016/08/28/linuxDMA%E7%BC%96%E7%A8%8B/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。