嵌入式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写内存的时候有两种方式:
- write through: 任一从CPU发出的写信号送到Cache的同时,也写入主存,以保证主存的数据能同步地更新。
- 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动态映射指南
更多推荐
所有评论(0)