DMA编程
DMA是一种无需要CPU的参与就可以让外设与系统内存之间进行双向数据传输的硬件机制,使用DMA可以使系统CPU从实际的I/O数据传输过程中摆脱出来,从而大大提高系统的吞吐率
DMA方式的数据传输由DMA控制器控制,在传输期间,CPU可以并发地执行其他任务,当DMA结束后,DMA控制器通过中断通知CPU数据传输已经结束,然后由CPU执行相应的中断服务程序进行后序处理.DMA可以用做内存与外设之间传输数据的方式,这种传输方式之下,数据并不需要经过CPU中转。
1、 DMA与Cache一致性
假设DMA针对内存的目的地址与Cache缓存的对象没有重叠区域,DMA和Cache之间没有问题,但是,如果DMA的目的地址与Cache所缓存的内存地址访问有重叠,经过DMA操作,Cache缓存对应的内存的数据已经被修改,而CPU本身并不知道,它仍然认为Cache中的数据就是内存中的数据,以后访问Cache映射的内存时,它仍然使用陈旧的Cache数据,这就发生了Cache与内存之间数据不一致性的问题。
解决由于DMA导致的Cache一致性问题的最简单方法是直接禁止DMA目标地址范围内内存的Cache功能
DMA只是外设与内存交互数据的一种方式,内存中用于与外设交互数据的一块区域被称作DMA缓冲区。
一般的DMA操作只能在16MB以下的内存中进行,因此,在使用kmalloc()和__get_free_pages()及其类似函数申请DMA缓冲区时应使用GFP_DMA标志,这样能保证获得的内存位于DMA_ZONE,是具备DMA能力的
在使用DMA区域时需要注意虚拟地址、物理地址和总线地址
基于DMA的硬件使用总线地址而非物理地址,总线地址是从设备角度上看到的内存地址,物理地址是从CPU MMU控制器外围角度看到的内存地址(从CPU核角度看到的是虚拟地址)。虽然在PC上,对于ISA和PCI而言,总线地址即为物理地址,但并非每个平台都是如此。和DMA相关的有IOMMU,它类似于MMU,只不过IOMMU针对的是外设总线地址和内存地址之间的转化
一致性DMA缓冲区
DMA映射包括两方面工作:分配一片DMA缓冲区,为这片缓冲区产生设备可访问的地址。同时,DMA映射页必须考虑Cache一致性问题,内核中提供了以下函数用于分配一个DMA一致性的内存区域:
Void* dma_alloc_coherent(struct device* dev,size_tsize,dma_addr_t* handle,gfp_y gfp);
此函数的返回值为申请到的DMA缓冲区的虚拟地址,此外,该函数还通过参数handle返回DMA缓冲区的总线地址,handle的类型为dma_addr_t,代表的是总线地址
Dma_alloc_coherent()申请一片DMA缓冲区,进行地址映射并保证该缓冲区的Cache一致性
Void dma_free_coherent(struct device* dec,size_tsize,void* cpu_addr,dam_addr_t handle);
内核还提供了PCI设备设备申请DMA缓冲区的函数pci_alloc_consitent(),原型为:
Void* pci_alloc_consistent(struct pci_dev*pdev,size_t size,dma_addr_t* dam_addrp);
流式DMA缓冲区
并非所有的DMA缓冲区都是驱动申请的,如果驱动申请的,用一致性DMA缓冲区自然最方便,直接考虑了Cache一致性问题,但是,缓冲区来自内核的较上层,上层很可能用的是普通的kmalloc() _get_free_pages()等方法,这时候就要使用流式DMA映射。流式DMA缓冲区使用的一般步骤如下:
进行流式DMA映射
执行DMA操作
进行流式DMA去映射
内核提供了和DMA相关的函数
在某些体系结构中,流式映射也能够拥有多个不连续的页和多个“分散/聚集”缓冲区。建立流式映射时,必须告诉内核数据流动的方向。
DMA_TO_DEVICE
DEVICE_TO_DMA
如果数据被发送到设备,使用DMA_TO_DEVICE;而如果数据被发送到CPU,则使用DEVICE_TO_DMA。
DMA_BIDIRECTTONAL
如果数据可双向移动,则使用该值
DMA_NONE
该符号只是出于调试目的。
当只有一个缓冲区要被传输的时候,使用下函数映射它:
dma_addr_t dma_map_single(struct device *dev, void *buffer, size_tsize, enum dma_data_direction direction);
返回值是总线地址,可以把它传递给设备;如果执行错误,返回NULL。
当传输完毕后,使用下函数删除映射:
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr,size_t size, enum dma-data_direction direction);
使用流式DMA的原则:
一是缓冲区只能用于这样的传送,即其传送方向匹配与映射时给定的方向值;
二是一旦缓冲区被映射,它将属于设备,不是处理器。直到缓冲区被撤销映射前,驱动程序不能以任何方式访问其中的内容。只用当dma_unmap_single函数被调用后,显示刷新处理器缓存中的数据,驱动程序才能安全访问其中的内容。
三是在DMA出于活动期间内,不能撤销对缓冲区的映射,否则会严重破坏系统的稳定性。
如果要映射的缓冲区位于设备不能访问的内存区段(高端内存),怎么办?一些体系结构只产生一个错误,但是其他一些系统结构件创建一个回弹缓冲区。回弹缓冲区就是内存中的独立区域,它可被设备访问。如果使用DMA_TO_DEVICE标志映射缓冲区,并且需要使用回弹缓冲区,则在最初缓冲区中的内容作为映射操作的一部分被拷贝。很明显,在拷贝后,最初缓冲区内容的改变对设备不可见。同样DEVICE_TO_DMA回弹缓冲区被 dma_unmap_single函数拷贝回最初的缓冲区中,也就是说,直到拷贝操作完成,来自设备的数据才可用。
有时候,驱动程序需要不经过撤销映射就访问流式DMA缓冲区的内容,为此内核提供了如下调用:
void dma_sync_single_for_cpu(struct device *dev, dma_handle_tbus_addr, size_t size, enum dma_data_directction direction);
应该在处理器访问流式DMA缓冲区前调用该函数。一旦调用了该函数,处理器将“拥有”DMA缓冲区,并可根据需要对它进行访问。然后在设备访问缓冲区前,应该调用下面的函数将所有权交还给设备:
void dma_sync_single_for_device(struct device *dev, dma_handle_tbus_addr, size_t size, enum dma_data_direction direction);
再次强调,处理器在调用该函数后,不能再访问DMA缓冲区了。
申请和释放DMA通道
和中断一样,在使用DMA之前,设备驱动程序需首先向系统申请DMA通道,申请DMA通道函数:
Int request_dma(unsigned int dmanr,const char*device_id);
设备结构体指针可作为传入device_id的最佳参数
使用完DMA通道后,需要释放
Void free_dma(unsigned int dmanr);
Linux设备驱动中DMA相关代码流程:
1、request_dma()并初始化DMAC 申请DMA缓冲区 (在设备驱动模块加载或open()函数中进行)
2、进行DMA传输 (在write() read() ioctl()等功能函数中进行)
3、若使能了对应中断,进行DMA传输后的中断处理 (在中断处理程序中进行)
4、释放DMA缓冲区 free_dma() (在设备驱动模块卸载或relese()函数中进行 )