DMA是一种无须CPU的参与就可以让外设与系统内存之间进行双向数据传输的硬件机制。使用DMA可以是系统CPU从实际的IO数据传输过程中摆脱出来,从而大大提
供系统的吞吐率。DMA方式的数据传输由DMA控制器(DMAC)控制,在传输期间,CPU可以并发地执行其他任务,当DMA结束后,DMAC通过中断通知CPU数据传输已经结束,然后由CPU执行相应的中断服务程序进行后续处理。
在内存中用于与外设交互数据的一块区域被称作DMA缓冲区,在设备不支持scatter/gatherCSG,分散/聚集操作的情况下,DMA缓冲区必须是物理上联系的。
对于ISA设备而言,其DMA操作只能在16MB以下的内存进行,因此,在使用kmalloc()和__get_free_pages()及其类似函数申请DMA缓冲区时应使用GFP_DMA标志,这样能保证获得的内存是具备DMA能力的。
DMA的硬件使用总线地址而非物理地址,总线地址是从设备角度上看到的内存地址,物理地址是从CPU角度上看到的未经转换的内存地址(经过转换的那叫虚拟地址)。
在PC上,对于ISA和PCI而言,总线即为物理地址,但并非每个平台都是如此。由于有时候接口总线是通过桥接电路被连接,桥接电路会将IO地址映射为不同的物理地址。
设备不一定能在所有的内存地址上执行DMA操作,在这种情况下应该通过下列函数执行DMA地址掩码:
int dma_set_mask(struct device *dev, u64 mask);
DMA映射包括两个方面的工作:
- 分配一片DMA缓冲区;
- 为这片缓冲区产生设备可访问的地址。
内核中提供了一下函数用于分配一个DMA一致性的内存区域:
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp);
这个函数的返回值为申请到的DMA缓冲区的虚拟地址。此外,该函数还通过参数handle返回DMA缓冲区的总线地址。与之对应的释放函数为:
void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle);
以下函数用于分配一个写合并(writecombinbing)的DMA缓冲区:
void *dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp);
与之对应的是释放函数:dma_free_writecombine(),它其实就是dma_free_conherent,只不过是用了#define重命名而已。
对于单个已经分配的缓冲区而言,使用dma_map_single()可实现流式DMA映射:
dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction direction); 如果映射成功,返回的是总线地址,否则返回NULL.最后一个参数DMA的方向,可能取DMA_TO_DEVICE, DMA_FORM_DEVICE, DMA_BIDIRECTIONAL和DMA_NONE;
与之对应的反函数是:
void dma_unmap_single(struct device *dev,dma_addr_t *dma_addrp,size_t size,enum dma_data_direction direction);
MMC的scatter list相关操作
MMC作为块设备,它的存储空间,最小单位由struct bio_vec 描述,它代表一段物理地址范围。
struct bio_vec { struct page *bv_page; unsigned int bv_len; unsigned int bv_offset; };
一次块设备传输请求,会涉及到很多个这样的不连续的物理空间。不连续的物理空间,不能直接使用DMA
这时,可以利用sg操作,让每个bio_vec结构,对应一个scatterlist结构:
struct scatterlist { unsigned long page_link; unsigned int offset; /* buffer offset */ dma_addr_t dma_address; /* dma address */ unsigned int length; /* length */ };
在MMC的请求处理函数中,遍历每一request中所有bio_vec结构,对应一个scatterlis结构描述:
rq_for_each_segment(bvec, rq, iter) { ... ... sg = sg_next(sg); //指向sg链表中的下一个scatterlist sg_set_page(sg, bvec->bv_page, bvec->bv_len, bvec->bv_offset); //使用sg描述一个页 ... ... } ... ... sg_mark_end(sg); //标志sg链表到此sg节点就结束了
上面sg这个链表初始化代码如下:
sg_init_table(mq->bounce_sg, bouncesz / 512); 第一个参数为链表头,第二个为成员数量。
上边所涉及到的几个函数:
void sg_init_table(struct scatterlist *sg, unsigned int nents);
sg是sg链表(数组)的表头,nents是要分配的数组的个数。
void sg_set_page(struct scatterlist *sg, struct page *page, unsigned int len, unsigned int offset); void sg_set_buf(struct scatterlist *sg, const void *buf, unsigned int buflen);
使用指定的参数,填充sg结构。第一个以页为页地址,偏移量,长度为接线;第二个以地址和长度为界限。
struct scatterlist *sg_next(struct scatterlist *sg);
返回下一个sg成员地址。
也有一个宏,来遍历sg链表上的所有sg结构,它的使用方法通常如下:
int i; struct scatterlist *list, *sgentry; /* Fill in list and pass it to dma_map_sg(). Then... */ for_each_sg(i, list, sgentry, nentries) { program_hw(device, sg_dma_address(sgentry), sg_dma_len(sgentry)); }
sgentry为sg链表入口,nentryies是sg数组(链表)总长。