DMA(Linux设备驱动程序)

直接内存访问
DMA是一种硬件机制。
它允许外围设备和主内存之间直接传输它们的I/O数据,而不需要系统处理器的参与。

DMA需要设备驱动程序分配一个或者多个适合执行DMA的特殊缓冲区。
许多驱动程序在初始化阶段分配了缓冲区,一直使用它们直到关闭。

分配DMA缓冲区

设备使用ISA或者PCI系统总线传输数据时,这两种方式使用的都是物理地址。

但SBus在外围总线上使用了虚拟地址。


模块只能在运行时刻分配它们的缓冲区。
一些PCI设备没能实现全部的PCI标准,不能使用32位地址,而一些ISA设备还局限在使用24位地址的阶段。
对于有这些限制的设备,应使用GFP_DMA标识调用kmalloc或者get_free_pages从DMA区间分配内存。
当设置了该标志时,只有使用24位寻址方式的内存才能被分配。


使用DMA的设备驱动程序将与连接到总线接口上的硬件通信,硬件使用的是物理地址,而程序代码使用的是虚拟地址
基于DMA的硬件使用总线地址,而非物理地址。

unsigned long virt_to_bus(volatile void *address);
void *bus_to_virt(unsigned long address);
这些函数在内核逻辑地址和总线地址间执行了简单的转换。但对于必须使用I/O内存管理单元或者必须使用回弹缓冲区的情况,不能正常工作。
执行这些转换的正确方法是使用通用DMA层。


通用DMA层
DMA操作最终会分配缓冲区,并将总线地址传递给设备。
内核提供了一个与总线——体系架构无关的DMA层。
在使用通用DMA层时,需要使用device结构的指针,它是在Linux设备模型中用来表示设备底层的。

该结构内部隐藏了描述设备的总线细节。

如果设备不支持常见的32位DMA操作,应该调用下面的函数通知内核:
int dma_set_mask(struct device *dev, u64 mask);
如果使用指定的mask时DMA能正常工作,则返回非零值。
如果返回0,则对该设备不能使用DMA。


DMA映射
一个DMA映射是要分配的DMA缓冲区与该缓冲区生成的、设备可访问地址的组合。
(IOMMU在设备可访问的地址范围内规划了物理内存,使得物理上分散的缓冲区对设备来说变成连续的。)

在某些情况下,为设备设置可用地址需要建立回弹缓冲区。当驱动程序要试图在外围设备不可访问的地址执行DMA时(比如高端内存),将创建回弹缓冲区。

DMA映射必须解决缓存一致性的问题。
一些体系结构在硬件中管理缓存的一致性,但是其他一些体系结构则需要软件的支持。

DMA映射建立一个新的结构类型——dma_addr_t来表示总线地址。
dma_addr_t类型的变量对驱动程序是不透明的,唯一允许的操作时将它们传递给DMA支持例程以及设备本身。

根据DMA缓冲区期望保留的时间长短,PCI代码区分两种类型的DMA映射:
一致性DMA映射
流式DMA映射


建立一致性DMA映射
驱动程序可调用pci_alloc_consistent函数建立一致性映射:
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int flag);
该函数处理了缓冲区的分配和映射。前两个参数device结构和所需缓冲区的大小。

函数在两处返回DMA映射的结果,函数的返回值是缓冲区的内核虚拟地址,可以被驱动程序使用;

总线地址返回时保存在dma_handle中。


当不再需要缓冲区时(通常在模块卸载时),调用dma_free_coherent向系统返回缓冲区:
void dma_free_coherent(struct device *dev, size_t size, void *vaddr, dma_addr_t dma_handle);


DMA池
DMA池是一个生成小型、一致性DMA映射的机制。
调用dma_alloc_coherent函数获得的影响,可能起最小大小为单个页,如果设备需要的DMA区域比这还小,就要使用DMA池。
struct dma_pool *dma_pool_create(const char *name, struct device *dev, size_t size,size_t align, size_t allocation);
void dma_pool_destrory(struct dma_pool *pool);
/*返回的DMA缓冲区的地址是内核虚拟地址,并作为总线地址保存在handle中*/
void *dma_pool_alloc(struct dma_pool *pool, int mem_flags, dma_addr_t *handle);


建立流式DMA映射
当建立流式映射时,必须告诉内核数据流动的方向。
DMA_TO_DEVICE DMA_FROM_DEVICEDMA_BIDIRECTIONAL DMA_NONE
当只有一个缓冲区要被传输时,使用dma_map_single函数映射它:
/*返回的是总线地址,可以把它传递给设备*/
dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction direction);
当传输完毕后,使用dma_unmap_single函数删除映射:
void dma_unmap_single(struct device *dev, dma_addr_ dma_addr, size_t size, enum dma_data_direction direction);
size和direction参数必须与映射缓冲区的参数相匹配。

有时候,驱动程序需要不经过撤销映射就访问流式DMA缓冲区的内容,内核提供了如下调用:
void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);
应该在处理器访问流式DMA缓冲区前调用该函数。一旦调用了该函数,处理器将“拥有”DMA缓冲区。
在设备访问缓冲区前,应该调用下面的函数将所有权交还给设备:
void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr, size_t size, enmu dma_data_direction direction);
处理器在调用该函数后,不能再访问DMA缓冲区了。


单页流式映射
使用下面的函数,建立和撤销使用page结构指针的流式映射:
dma_addr_t dma_map_page(...);
void dma_unmap_page(...);

分散/聚集映射
分散/聚集映射是一种特殊类型的流式DMA映射。
假设有几个缓冲区,它们需要与设备双向传输数据。
有几种方式能产生这种情形,包括从readv或者writev系统调用产生,从集群的磁盘I/O请求产生,或者从映射的内核I/O缓冲区中的页面链表产生。
许多设备都能接受一个指针数组的分散表,以及它的长度,然后在一次DMA操作中把它们全部传输走。
把分散表作为一个整体的另外一个原因是可以充分利用那些在总线硬件中含有映射寄存器系统的优点。
映射分散表的第一步是建立并填充一个描述被传送缓冲区的scatterlist结构的数组,该结构是与体系结构相关的。

为了映射一个分散/聚集DMA操作,驱动程序应当为传输的每个缓冲区在scatterlist结构毒影入口项上设置page、offset和length成员。

然后调用:

int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_diretcion direction);
函数返回指定设备的正确的总线地址。

驱动程序应该传输由dma_map_sg函数返回的每个缓冲区。总线地址和每个缓冲区的长度被保存在scatterlist结构中,但它们在结构中的位置会随体系架构的不同而不同。

/*从该分散表的入口项中返回总线(DMA)地址*/
dma_addr_t sg_dma_address(struct scatterlist *sg);
/*返回缓冲区的长度*/
unsigned int sg_dma_len(struct scatterlist *sg);
/*解除分散/聚集映射*/
void dma_unmap_sg(struct device *dev, struct scatterlist *list, int nents, enum dma_data_direction direction);

如果必须访问映射的分散/聚集列表,必须首先对其进行同步:
void dma_sync_sg_for_cpu();
void dma_sync_sg_for_device();


ISA设备的DMA
ISA总线允许两种DMA传输:本地(native)DMA和ISA总线控制(bus-master)DMA。
本地DMA使用主板上的标准DMA控制器电路来驱动ISA总线上的信号线。
ISA总线控制DMA完全由外围设备控制。

一个ISA总线控制DMA的例子是1542 SCSI控制器,它的驱动程序在内核代码drivers/scsi/aha1542.c中。

本地DMA涉及到ISA总线上的DMA数据传输:
8237 DMA控制器(DMAC)
控制器保存了有关DMA传输的信息,比如方向、内存地址、传输数据量大小等。它还包含了一个跟踪传送状态的计数器。当控制器接收到一个DMA请求信号时,它将获得总线控制权并驱动信号线,这样设备就能读写数据了。
外围设备
当设备准备传送数据时,必须激活DMA请求信号。DMAC负责管理实际的传输工作;当控制器选通设备后,硬件设备就可以顺序地读写总线上的数据。当传输结束时,设备通常会产生一个中断。
设备驱动程序
负责提供DMA控制器的方向、总线地址、传输量的大小,它还与外围设备通信,做好传输数据的准备,当DMA传输完毕后,响应中断。

在PC中使用的早期DMA控制器能够管理四个“通道”,每个通道都与一套DMA寄存器相关联。四个设备可以同时在控制器中保存它们的DMA信息。
现在的PC包含了两个与DMAC等价的设备。


DMA控制器是系统资源,因此内核协助处理这一资源。
内核使用DMA注册表为DMA通道提供请求/释放机制,并且提供了一组函数在DMA控制器中配置通道信息。


注册DMA
在包含了头文件<asm/dma.h>之后,用下面的函数可以获得和释放对DMA通道的所有权:
int request_dma(unsigned int channel, const char *name);
void free_dma(unsigned int channel);
建议在请求中断信号线之后请求DMA通道,并且在中断之前释放通道。
使用DMA的每个设备还需要一根IRQ线,否则它将无法通知数据已经传输完毕。

与DMA控制器通信
注册之后,驱动程序的主要任务包括为适当的操作配置DMA控制器。
内核导出了驱动程序所需要的所有函数。
当read或者write函数被调用时,或者准备异步传输时,驱动程序都要对DMA控制器进行配置。

DMA通道是一个可共享的资源,如果多于一个的处理器要同时对其进行编程,则会产生冲突。
有一个叫做dma_spin_lock的自旋锁保护控制器。
有两个函数能够对它进行操作:
unsigned long claim_dma_lock();
void release_dma_lock(unsigned long flags);

必须被装入控制器的信息包含三个部分:RAM的地址、必须被传输的原子项个数以及传输的方向。

<asm/dma.h>定义了下面的函数:
void set_dma_mode(unsigned int channel, char mode);
/*为DMA缓冲区分配地址,该函数将addr的最低24为存储到控制器中,addr参数必须是总线地址*/
void set_dma_addr(unsigned int channel, unsigned int addr);
void set_dma_count(unsigned int channel, unsigned int count);

当处理DMA设备时,用于管理设备的函数:
/*控制器内的DMA通道可以被禁用,应该在配置控制器前禁用通道。*/
void disable_dma(unsigned int channel);
/*告诉控制器DAM通道中包含了合法的数据*/
void enable_dma(unsigned int channel);
/*返回还未传输的字节数*/
int get_dma_residue(unsigned int channel);
/*清除DMA的触发器(flip-flop).*/
void clear_dma_ff(unsigned int channel);
(触发器用于控制对16位寄存器的访问。必须在访问DMA寄存器前清除触发器)

你可能感兴趣的:(DMA(Linux设备驱动程序))