嵌入式Linux驱动笔记(二十八)------DMA的简单使用分析

你好!这里是风筝的博客,
欢迎和我一起交流。

最近被一个需求折磨,对DMA传输速度有极大要求,被迫对着DMA进行魔改。。。。。
简单复习总结一下关于DMA到一些知识:

在DMA传输里,最耗时到莫过于map操作了,那么,为什么要map呢?
内核通常使用的地址是虚拟地址,对于内存和外设之间使用到地址是总线地址(Bus addresses)。

例如一个PCI 设备支持DMA,那么在驱动中我们可以通过kmalloc或者其他类似接口分配一个DMA buffer,并且返回了虚拟地址X,MMU将X地址映射成了物理地址Y,从而定位了DMA buffer在系统内存中的位置。因此,驱动可以通过访问地址X来操作DMA buffer,但是PCI 设备并不能通过X地址来访问DMA buffer,因为MMU对设备不可见,而且系统内存所在的系统总线和PCI总线属于不同的地址空间。
所以,驱动在调用dma_map_single这样的接口函数的时候会传递一个虚拟地址X,在这个函数中会设定IOMMU的页表,将地址X映射到Z,并且将返回z这个总线地址。驱动可以把Z这个总线地址设定到设备上的DMA相关的寄存器中。这样,当设备发起对地址Z开始的DMA操作的时候,IOMMU可以进行地址映射,并将DMA操作定位到Y地址开始的DMA buffer。

网上说:“根据DMA缓冲区期望保留的时间长短,PCI代码有两种DMA映射:一致性映射和流式映射”。
我觉得说的不太对,对于缓存区保留时间到长短来分区两种映射有失偏见,这只能算是他们表现出来到现象。
当然,我也从网上找到了一些比较靠谱到说法:

CPU写内存的时候有两种方式:

  1. write through: 任一从CPU发出的写信号送到Cache的同时,也写入主存,以保证主存的数据能同步地更新。
  2. write back: CPU只写到cache中。cache的硬件使用LRU算法将cache里面的内容替换到内存。通常是这种方式。

DMA可以完成从内存到外设直接进行数据搬移。但DMA不能访问CPU的cache,CPU在读内存的时候,如果cache命中则只是在cache去读,而不是从内存读,写内存的时候,也可能实际上没有写到内存,而只是直接写到了cache。
这样一来,如果DMA从将数据从外设写到内存,CPU中cache中的数据(如果有的话)就是旧数据了,这时CPU在读内存的时候命中cache了,就是读到了旧数据;CPU写数据到内存时,如果只是先写到了cache,则内存里的数据就是旧数据了。这两种情况(两个方向)都存在cache一致性问题。例如,网卡发包的时候,CPU将数据写到cache,而网卡的DMA从内存里去读数据,就发送了错误的数据。

对于DMA一致性映射来说,CPU和DMA controller在发起对DMA buffer的并行访问的时候不需要考虑cache的影响,也就是说不需要软件进行cache操作,CPU和DMA controller都可以看到对方对DMA buffer的更新。
对于DMA流式映射,可以说是属于非一致性的,它是异步的。相较于DMA一致性映射在驱动初始化时map,在驱动退出的时候unmap,DMA流式映射需要进行DMA传输的时候才进行mapping,一旦DMA传输完成,就立刻ummap,这样充分优化硬件的性能。

DMA一致性映射使用

cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, gfp)

请一块缓冲区.其前两个参数是device结构和所需缓冲区的大小,第三个参数是相关的总线地址,保存在dma_handle中,返回值是缓冲区的内核虚拟地址,可以被驱动程序使用。

DMA流式映射有两种map/ummap:
1)对于单个dma buffer:dma_handle = dma_map_single(dev, addr, size, direction);
1)对于多个形成scatterlist的dma buffer:count = dma_map_sg(dev, sglist, nents, direction)

int i, count = dma_map_sg(dev, sglist, nents, direction);//其中nents为sglist的条目数量
struct scatterlist *sg;
for_each_sg(sglist, sg, count, i) { 
        hw_address[i] = sg_dma_address(sg);
        hw_len[i] = sg_dma_len(sg); 
}
//这种实现可以很方便将若干连续的sglist条目合并成一个大块且连续的总线地址区域。
//然后调用for_each_sg来遍历所有成功映射的mappings(可能会小于nents次)并且使用
//sg_dma_address() 和 sg_dma_len() 这两个宏来得到mapping后的dma地址和长度

DMA流式映射需要定义方向:

DMA_TO_DEVICE    数据从内存传输到设备。
DMA_FROM_DEVICE    数据从设备传输到内存。
DMA_BIDIRECTIONAL    不清楚传输方向则可用该类型。
DMA_NONE    仅用于调试目的

一致性内存映射隐性的设置为DMA_BIDIRECTIONAL。

流式映射具有比一致性映射更为复杂的接口。这些映射希望能与已经由驱动程序分配的缓冲区协同工作,因而不得不处理那些不是它们选择的地址
流式DMA映射的几条原则:
*)缓冲区只能用于这样的传送,即其传送方向匹配于映射时给定的方向。
*)一旦缓冲区被映射,它将属于设备,而不是处理器。
*)直到缓冲区被撤销映射前,驱动程序不能以任何方式访问其中的内容。
*)在DMA处于活动期间内,不能撤销对缓冲区映射,否则会严重破坏系统的稳定性。
多次使用

DMA流映射,同时在DMA传输过程中访问数据,必须确保缓冲区中所有的数据已经被实际写到内存。可能有些数据还会保留在处理器的高速缓冲存储器中,因此必须显式刷新(对DMA buffer进行sync操作),需要合适地同步数据缓冲区,这样可以让处理器及外设可以看到最新的更新和正确的DMA缓冲区数据。
在DMA传输完成后适当地调用:

dma_sync_single_for_cpu(dev, dma_handle, size, direction);

或者

dma_sync_sg_for_cpu(dev, sglist, nents, direction);

如果,CPU操作了DMA buffer的数据,然后你又想把控制权交给设备上的DMA 控制器,让DMA controller访问DMA buffer,这时候,在真正让HW(指DMA控制器)去访问DMA buffer之前,需要调用:

dma_sync_single_for_device(dev, dma_handle, size, direction);

或者:

dma_sync_sg_for_device(dev, sglist, nents, direction);

最后,使用DMA分为五个步骤:
1)申请一个DMA channel。
2)根据设备(slave)的特性,配置DMA channel的参数。
3)要进行DMA传输的时候,获取一个用于识别本次传输(transaction)的描述符(descriptor)。
4)将本次传输(transaction)提交给dma engine并启动传输。
5)等待传输(transaction)结束。

放出一个demo参考,tx_buf写数据,rx_buf读数据,还弄了一个tmp_buf用来模拟device作为中转:

#include <linux/delay.h>
#include <linux/dmaengine.h>
#include <linux/init.h>
#include <linux/kthread.h>
#include <linux/module.h>
#include <linux/of_dma.h>
#include <linux/platform_device.h>
#include <linux/random.h>
#include <linux/slab.h>
#include <linux/wait.h>
#include <asm/uaccess.h>
#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>
#include <linux/dma/sunxi-dma.h>
#include <linux/mm.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>

#include <asm/cacheflush.h>
#include <asm/io.h>
#include <asm/uaccess.h>

#define DBG(string, args...)		\
	do { 				\
		printk("DMA> %s(%u): ", __FUNCTION__, __LINE__); \
		printk(string, ##args); \
	} while(0)

#define BUF_SIZE 256
struct completion tx_cmp;
struct completion rx_cmp;

static struct dma_data{
	u8 *tx_buf;
	u8 *rx_buf;
	struct dma_chan *tx_chan;
	struct dma_chan *rx_chan;
};

struct dma_data* dma;

void dma_tx_callback(void *completion)
{
	DBG("dma -write data end!\n");
	complete(completion);
}

void dma_rx_callback(void *completion)
{
	DBG("dma -read data end!\n");
	complete(completion);
}

static ssize_t send_dma(struct file *filp, const char __user *buf,
				size_t count, loff_t *f_pos)
{
	unsigned long time_left;
	//struct dma_data *dma = filp->private_data;

	DBG("start send\n");
	init_completion(&tx_cmp);

	DBG("wait send\n");
	//Setup 5
	dma_async_issue_pending(dma->tx_chan);
	time_left = wait_for_completion_timeout(&tx_cmp,
			msecs_to_jiffies(10000));
	if(time_left == 0){
		printk("dma tx time  out\n");
	}
	else {
		DBG("Start DMA success\n");
	}
	return 0;
}

static ssize_t recv_dma(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
	unsigned long time_left;
	int i = 0;
	//struct dma_data *dma = filp->private_data; /*获得设备结构体指针*/

	DBG("start recv\n");
	init_completion(&rx_cmp);

	//Setup 3
	dma_async_issue_pending(dma->rx_chan);
	DBG("wait recv\n");
	time_left = wait_for_completion_timeout(&rx_cmp,
			msecs_to_jiffies(10000));
	if(time_left == 0){
			printk("dma rx time  out\n");
	}
	else {
		printk("recv data is :");
		for(i=0; i<BUF_SIZE; i++)
			printk("%d ",dma->rx_buf[i]);
	}

	return 0;
}

static int mmap_dma(struct file*filp, struct vm_area_struct *vma)
{
	//struct dma_data *dma = filp->private_data; /*获得设备结构体指针*/

	vma->vm_flags |= VM_IO;
	vma->vm_flags |= VM_LOCKED;


	if (remap_pfn_range(vma,vma->vm_start,virt_to_phys(dma->tx_buf)>>PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot))
		return  -EAGAIN;

	return 0;
}

static int dma_transfer(struct dma_data * dma)
{
	struct dma_chan *tx_chan = dma->tx_chan;
	struct dma_chan *rx_chan = dma->rx_chan;

	struct device *tx_dev = tx_chan->device->dev;
	struct device *rx_dev = rx_chan->device->dev;
	dma_addr_t dma_srcs;
	dma_addr_t dma_dest;
	struct dma_async_tx_descriptor *dma_desc_tx;
	struct dma_async_tx_descriptor *dma_desc_rx;

	//Setup 3
	dma_srcs = dma_map_single(tx_dev, dma->tx_buf, BUF_SIZE,DMA_MEM_TO_DEV);
	if (dma_mapping_error(tx_dev, dma_srcs)) {
		printk("tx:DMA mapping failed\n");
		goto err_map;
	}
	dma_dest = dma_map_single(rx_dev, dma->rx_buf, BUF_SIZE,DMA_DEV_TO_MEM);
	if (dma_mapping_error(rx_dev, dma_dest)) {
		printk("rx:DMA mapping failed\n");
		goto err_map;
	}

	dma_desc_tx = dmaengine_prep_slave_single(tx_chan, dma_srcs,
			BUF_SIZE, DMA_MEM_TO_DEV,
			DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
	if (!dma_desc_tx) {
		printk("Not able to get desc for DMA xfer\n");
		goto err_desc;
	}
	dma_desc_tx->callback = dma_tx_callback;
	dma_desc_tx->callback_param = &tx_cmp;

	dma_desc_rx = dmaengine_prep_slave_single(rx_chan, dma_dest,
			BUF_SIZE, DMA_DEV_TO_MEM,
			DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
	if (!dma_desc_rx) {
		printk("Not able to get desc for DMA xfer\n");
		goto err_desc;
	}
	dma_desc_rx->callback = dma_rx_callback;
	dma_desc_rx->callback_param = &rx_cmp;



	//Setup 4
	if (dma_submit_error(dmaengine_submit(dma_desc_tx))) {
		printk(" DMA submit failed\n");
		goto err_submit;
	}
	if (dma_submit_error(dmaengine_submit(dma_desc_rx))) {
		printk(" DMA submit failed\n");
		goto err_submit;
	}

	return 0;
err_submit:
err_desc:
	dma_unmap_single(tx_dev, dma_srcs,
			BUF_SIZE,DMA_MEM_TO_DEV );
	dma_unmap_single(rx_dev, dma_dest,
			BUF_SIZE,DMA_DEV_TO_MEM );
err_map:
	return -EINVAL;
}

static int open_dma(struct inode *inode,struct file *filp)
{
	return 0;
}
static struct file_operations dev_fops = {
	.owner   =   THIS_MODULE,
	.open    =   open_dma,
	.write	 =   send_dma,
	.read	 =   recv_dma,
	.mmap	 =   mmap_dma,
};

static struct miscdevice misc = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = "dma_transfer_test",
	.fops = &dev_fops,
};


static int dma_transfer_init(void)
{
	int ret;
	//struct dma_data* dma;
	dma_cap_mask_t mask_tx, mask_rx;
	struct dma_slave_config dma_tx_sconfig;
	struct dma_slave_config dma_rx_sconfig;
	u8 * virt_tmp_buf;

	ret = misc_register(&misc);
	DBG("DMA register\n");

	dma = kzalloc(sizeof(struct dma_data), GFP_KERNEL);
	if (!dma)
		return -ENOMEM;

	dma->tx_buf = kmalloc(BUF_SIZE,GFP_KERNEL);//DMA);
	if (dma->tx_buf == NULL) {
		printk("tx_buf kmalloc faul!\n");
	}
	dma->rx_buf = kmalloc(BUF_SIZE,GFP_KERNEL);//DMA);
	if (dma->rx_buf == NULL) {
		printk("rx_buf kmalloc faul!\n");
	}

	//Setup 1
	dma_cap_zero(mask_tx);
	dma_cap_set(DMA_SLAVE, mask_tx);
	dma->tx_chan =dma_request_channel(mask_tx, NULL, NULL);//dma_request_slave_channel
	if(IS_ERR(dma->tx_chan)){
		printk("request tx chan is fail!\n");
		return PTR_ERR(dma->tx_chan);
	}

	dma_cap_zero(mask_rx);
	dma_cap_set(DMA_SLAVE, mask_rx);
	dma->rx_chan =dma_request_channel(mask_rx, NULL, NULL);//dma_request_slave_channel
	if(IS_ERR(dma->rx_chan)){
		printk("request rx chan is fail!\n");
		return PTR_ERR(dma->rx_chan);
	}

	virt_tmp_buf = kmalloc(BUF_SIZE, GFP_KERNEL);
	memset(virt_tmp_buf, 0, BUF_SIZE);
	memset(dma->tx_buf, 6, BUF_SIZE);

	//Setup 2
	dma_tx_sconfig.dst_addr = virt_to_phys(virt_tmp_buf);
	//dma_tx_sconfig.src_addr = virt_to_phys(dma->tx_buf);
	dma_tx_sconfig.src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
	dma_tx_sconfig.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
	dma_tx_sconfig.src_maxburst = 1;
	dma_tx_sconfig.dst_maxburst = 1;
	dma_tx_sconfig.slave_id = sunxi_slave_id(DRQDST_SDRAM,DRQSRC_SDRAM);
	dma_tx_sconfig.direction = DMA_MEM_TO_DEV;
	ret = dmaengine_slave_config(dma->tx_chan, &dma_tx_sconfig);
	if (ret < 0) {
		printk("can't configure tx channel\n");
		return -1;
	}

	//dma_tx_sconfig.dst_addr = virt_to_phys(dma->tx_buf);
	dma_rx_sconfig.src_addr = virt_to_phys(virt_tmp_buf);
	dma_rx_sconfig.src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
	dma_rx_sconfig.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
	dma_rx_sconfig.src_maxburst = 1;
	dma_rx_sconfig.dst_maxburst = 1;
	dma_rx_sconfig.slave_id = sunxi_slave_id(DRQDST_SDRAM,DRQSRC_SDRAM);
	dma_rx_sconfig.direction = DMA_DEV_TO_MEM;
	ret = dmaengine_slave_config(dma->rx_chan, &dma_rx_sconfig);
	if (ret < 0) {
		printk("can't configure rx channel\n");
		return -1;
	}
	ret = dma_transfer(dma);
	if(ret){
		printk("Unable to init dma transfer\n");
	}

	return 0;
}

static void dma_transfer_exit(void)
{
	misc_deregister(&misc);
}
module_init(dma_transfer_init);
module_exit(dma_transfer_exit);

MODULE_LICENSE("GPL");

其实mem之间copy最好还是使用如下操作比较合适:

tx_chan->device->device_prep_dma_memcpy(chan,
				dma_dest, dma_srcs, len, 0);

最后,关于DMA和Cache一致性问题,墙裂推荐阅读这篇文章:
DMA导致的CACHE一致性问题解决方案
这篇文章里面描述:
dma_alloc_coherent 在 arm 平台上会禁止页表项中的 C (Cacheable) 域以及 B (Bufferable)域。
dma_alloc_writecombine 只禁止 C (Cacheable) 域.
C 代表是否使用高速缓冲存储器, 而 B 代表是否使用写缓冲区。
那么为了性能考虑,是不是最好不要使用dma_alloc_coherent 呢?


更多细节详情参考:
蜗窝科技
Linux之DMA动态映射指南

  • 5
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值