DMA engine的使用步骤 及 DMA一致性

Linux DMA Engine framework(2)_功能介绍及解接口分析

作者:wowo 发布于:2017-5-2 22:47 分类:Linux内核分析

1. 前言

从我们的直观感受来说,DMA并不是一个复杂的东西,要做的事情也很单纯直白。因此Linux kernel对它的抽象和实现,也应该简洁、易懂才是。不过现实却不甚乐观(个人感觉),Linux kernel dmaengine framework的实现,真有点晦涩的感觉。为什么会这样呢?

如果一个软件模块比较复杂、晦涩,要么是设计者的功力不够,要么是需求使然。当然,我们不敢对Linux kernel的那些大神们有丝毫怀疑和不敬,只能从需求上下功夫了:难道Linux kernel中的driver对DMA的使用上,有一些超出了我们日常的认知范围?

要回答这些问题并不难,将dmaengine framework为consumers提供的功能和API梳理一遍就可以了,这就是本文的目的。当然,也可以借助这个过程,加深对DMA的理解,以便在编写那些需要DMA传输的driver的时候,可以更游刃有余。

2. Slave-DMA API和Async TX API

从方向上来说,DMA传输可以分为4类:memory到memory、memory到device、device到memory以及device到device。Linux kernel作为CPU的代理人,从它的视角看,外设都是slave,因此称这些有device参与的传输(MEM2DEV、DEV2MEM、DEV2DEV)为Slave-DMA传输。而另一种memory到memory的传输,被称为Async TX。

为什么强调这种差别呢?因为Linux为了方便基于DMA的memcpy、memset等操作,在dma engine之上,封装了一层更为简洁的API(如下面图片1所示),这种API就是Async TX API(以async_开头,例如async_memcpy、async_memset、async_xor等)。

dma_api

图片1 DMA Engine API示意图

最后,因为memory到memory的DMA传输有了比较简洁的API,没必要直接使用dma engine提供的API,最后就导致dma engine所提供的API就特指为Slave-DMA API(把mem2mem剔除了)。

本文主要介绍dma engine为consumers提供的功能和API,因此就不再涉及Async TX API了(具体可参考本站后续的文章。

注1:Slave-DMA中的“slave”,指的是参与DMA传输的设备。而对应的,“master”就是指DMA controller自身。一定要明白“slave”的概念,才能更好的理解kernel dma engine中有关的术语和逻辑。

3. dma engine的使用步骤

注2:本文大部分内容翻译自kernel document[1],喜欢读英语的读者可以自行参考。

对设备驱动的编写者来说,要基于dma engine提供的Slave-DMA API进行DMA传输的话,需要如下的操作步骤:

1)申请一个DMA channel。

2)根据设备(slave)的特性,配置DMA channel的参数。

3)要进行DMA传输的时候,获取一个用于识别本次传输(transaction)的描述符(descriptor)。

4)将本次传输(transaction)提交给dma engine并启动传输。

5)等待传输(transaction)结束。

然后,重复3~5即可。

上面5个步骤,除了3有点不好理解外,其它的都比较直观易懂,具体可参考后面的介绍。

3.1 申请DMA channel

任何consumer(文档[1]中称作client,也可称作slave driver,意思都差不多,不再特意区分)在开始DMA传输之前,都要申请一个DMA channel(有关DMA channel的概念,请参考[2]中的介绍)。

DMA channel(在kernel中由“struct dma_chan”数据结构表示)由provider(或者是DMA controller)提供,被consumer(或者client)使用。对consumer来说,不需要关心该数据结构的具体内容(我们会在dmaengine provider的介绍中在详细介绍)。

consumer可以通过如下的API申请DMA channel:

struct dma_chan *dma_request_chan(struct device *dev, const char *name);

/自己代码中使用的是 pDma_chan = dma_request_channel(mask, NULL, NULL)

/**
 * __dma_request_channel - try to allocate an exclusive channel
 * @mask: capabilities that the channel must satisfy
 * @fn: optional callback to disposition available channels
 * @fn_param: opaque parameter to pass to dma_filter_fn
 *
 * Returns pointer to appropriate DMA channel on success or NULL.
 */
struct dma_chan *__dma_request_channel(const dma_cap_mask_t *mask,
                                                                      dma_filter_fn fn,

                                                                       void *fn_param)
申请到dma_chan后,可以通过dma_chan->device找到dma_device,从而可以通过chan->device->device_prep_dma_memcpy或chan->device->device_prep_dma_sg申请tx描述符,而不需要使用dmaengine API:dmaengine_prep_dma_memcpy,dmaengine_prep_slave_sg

/

该接口会返回绑定在指定设备(dev)上名称为name的dma channel。dma engine的provider和consumer可以使用device tree、ACPI或者struct dma_slave_map类型的match table提供这种绑定关系,具体可参考XXXX章节的介绍。

最后,申请得到的dma channel可以在不需要使用的时候通过下面的API释放掉:

void dma_release_channel(struct dma_chan *chan);

3.2 配置DMA channel的参数

driver申请到一个为自己使用的DMA channel之后,需要根据自身的实际情况,以及DMA controller的能力,对该channel进行一些配置。可配置的内容由struct dma_slave_config数据结构表示(具体可参考4.1小节的介绍)。driver将它们填充到一个struct dma_slave_config变量中后,可以调用如下API将这些信息告诉给DMA controller:

int dmaengine_slave_config(struct dma_chan *chan, struct dma_slave_config *config)

3.3 获取传输描述(tx descriptor)

DMA传输属于异步传输,在启动传输之前,slave driver需要将此次传输的一些信息(例如src/dst的buffer、传输的方向等)提交给dma engine(本质上是dma controller driver),dma engine确认okay后,返回一个描述符(由struct dma_async_tx_descriptor抽象)。此后,slave driver就可以以该描述符为单位,控制并跟踪此次传输。

struct dma_async_tx_descriptor数据结构可参考4.2小节的介绍。根据传输模式的不同,slave driver可以使用下面三个API获取传输描述符(具体可参考Documentation/dmaengine/client.txt[1]中的说明):

struct dma_async_tx_descriptor *dmaengine_prep_dma_memcpy(
                struct dma_chan *chan, dma_addr_t dest, dma_addr_t src,
                size_t len, unsigned long flags);-------chan->device->
device_prep_dma_memcpy

static inline struct dma_async_tx_descriptor *dmaengine_prep_dma_sg(
 913         struct dma_chan *chan,
 914         struct scatterlist *dst_sg, unsigned int dst_nents,
 915         struct scatterlist *src_sg, unsigned int src_nents,
 916         unsigned long flags)
 917 {
 918     if (!chan || !chan->device || !chan->device->device_prep_dma_sg)
 919         return NULL;
 920
 921     return chan->device->device_prep_dma_sg(chan, dst_sg, dst_nents,
 922             src_sg, src_nents, flags);
 923 }

 

struct dma_async_tx_descriptor *dmaengine_prep_slave_sg
        struct dma_chan *chan, struct scatterlist *sgl, 
        unsigned int sg_len, enum dma_data_direction direction, 
        unsigned long flags);----------------------------chan->device->device_prep_slave_sg

struct dma_async_tx_descriptor *dmaengine_prep_dma_cyclic( 
        struct dma_chan *chan, dma_addr_t buf_addr, size_t buf_len, 
        size_t period_len, enum dma_data_direction direction);

struct dma_async_tx_descriptor *dmaengine_prep_interleaved_dma( 
        struct dma_chan *chan, struct dma_interleaved_template *xt, 
        unsigned long flags);

dmaengine_prep_slave_sg用于在“scatter gather buffers”列表和总线设备之间进行DMA传输,参数如下: 
注3:有关scatterlist 我们在[3][2]中有提及,后续会有专门的文章介绍它,这里暂且按下不表。

chan,本次传输所使用的dma channel。

sgl,要传输的“scatter gather buffers”数组的地址; 
sg_len,“scatter gather buffers”数组的长度。

direction,数据传输的方向,具体可参考enum dma_data_direction (include/linux/dma-direction.h)的定义。

flags,可用于向dma controller driver传递一些额外的信息,包括(具体可参考enum dma_ctrl_flags中以DMA_PREP_开头的定义): 
DMA_PREP_INTERRUPT,告诉DMA controller driver,本次传输完成后,产生一个中断,并调用client提供的回调函数(可在该函数返回后,通过设置struct dma_async_tx_descriptor指针中的相关字段,提供回调函数,具体可参考4.2小节的介绍); 
DMA_PREP_FENCE,告诉DMA controller driver,后续的传输,依赖本次传输的结果(这样controller driver就会小心的组织多个dma传输之间的顺序); 
DMA_PREP_PQ_DISABLE_P、DMA_PREP_PQ_DISABLE_Q、DMA_PREP_CONTINUE,PQ有关的操作,TODO。

dmaengine_prep_dma_cyclic常用于音频等场景中,在进行一定长度的dma传输(buf_addr&buf_len)的过程中,每传输一定的byte(period_len),就会调用一次传输完成的回调函数,参数包括:

chan,本次传输所使用的dma channel。

buf_addr、buf_len,传输的buffer地址和长度。

period_len,每隔多久(单位为byte)调用一次回调函数。需要注意的是,buf_len应该是period_len的整数倍。

direction,数据传输的方向。

dmaengine_prep_interleaved_dma可进行不连续的、交叉的DMA传输,通常用在图像处理、显示等场景中,具体可参考struct dma_interleaved_template结构的定义和解释(这里不再详细介绍,需要用到的时候,再去学习也okay)。

3.4 启动传输

通过3.3章节介绍的API获取传输描述符之后,client driver可以通过dmaengine_submit接口将该描述符放到传输队列上,然后调用dma_async_issue_pending接口,启动传输。

dmaengine_submit的原型如下:

 static inline dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc)
{
        return desc->tx_submit(desc);
}

参数为传输描述符指针,返回一个唯一识别该描述符的cookie,用于后续的跟踪、监控。

dma_async_issue_pending的原型如下:

void dma_async_issue_pending(struct dma_chan *chan);

参数为dma channel,无返回值。

注4:由上面两个API的特征可知,kernel dma engine鼓励client driver一次提交多个传输,然后由kernel(或者dma controller driver)统一完成这些传输。

3.5 等待传输结束

传输请求被提交之后,client driver可以通过回调函数获取传输完成的消息,当然,也可以通过dma_async_is_tx_complete等API,测试传输是否完成。不再详细说明了。

最后,如果等不及了,也可以使用dmaengine_pause、dmaengine_resume、dmaengine_terminate_xxx等API,暂停、终止传输,具体请参考kernel document[1]以及source code。

4. 重要数据结构说明

4.1 struct dma_slave_config

中包含了完成一次DMA传输所需要的所有可能的参数,其定义如下:

/* include/linux/dmaengine.h */ 

struct dma_slave_config { 
        enum dma_transfer_direction direction; 
        phys_addr_t src_addr; 
        phys_addr_t dst_addr; 
        enum dma_slave_buswidth src_addr_width; 
        enum dma_slave_buswidth dst_addr_width; 
        u32 src_maxburst; 
        u32 dst_maxburst; 
        bool device_fc; 
        unsigned int slave_id; 
};

direction,指明传输的方向,包括(具体可参考enum dma_transfer_direction的定义和注释): 
    DMA_MEM_TO_MEM,memory到memory的传输; 
    DMA_MEM_TO_DEV,memory到设备的传输; 
    DMA_DEV_TO_MEM,设备到memory的传输; 
    DMA_DEV_TO_DEV,设备到设备的传输。 
    注5:controller不一定支持所有的DMA传输方向,具体要看provider的实现。 
    注6:参考第2章的介绍,MEM to MEM的传输,一般不会直接使用dma engine提供的API。

src_addr,传输方向是dev2mem或者dev2dev时,读取数据的位置(通常是固定的FIFO地址)。对mem2dev类型的channel,不需配置该参数(每次传输的时候会指定); 
dst_addr,传输方向是mem2dev或者dev2dev时,写入数据的位置(通常是固定的FIFO地址)。对dev2mem类型的channel,不需配置该参数(每次传输的时候会指定); 
src_addr_width、dst_addr_width,src/dst地址的宽度,包括1、2、3、4、8、16、32、64(bytes)等(具体可参考enum dma_slave_buswidth 的定义)。

src_maxburst、dst_maxburst,src/dst最大可传输的burst size(可参考[2]中有关burst size的介绍),单位是src_addr_width/dst_addr_width(注意,不是byte)。

device_fc,当外设是Flow Controller(流控制器)的时候,需要将该字段设置为true。CPU中有关DMA和外部设备之间连接方式的设计中,决定DMA传输是否结束的模块,称作flow controller,DMA controller或者外部设备,都可以作为flow controller,具体要看外设和DMA controller的设计原理、信号连接方式等,不在详细说明(感兴趣的同学可参考[4]中的介绍)。

slave_id,外部设备通过slave_id告诉dma controller自己是谁(一般和某个request line对应)。很多dma controller并不区分slave,只要给它src、dst、len等信息,它就可以进行传输,因此slave_id可以忽略。而有些controller,必须清晰地知道此次传输的对象是哪个外设,就必须要提供slave_id了(至于怎么提供,可dma controller的硬件以及驱动有关,要具体场景具体对待)。

4.2 struct dma_async_tx_descriptor

传输描述符用于描述一次DMA传输(类似于一个文件句柄)。client driver将自己的传输请求通过3.3中介绍的API提交给dma controller driver后,controller driver会返回给client driver一个描述符。

client driver获取描述符后,可以以它为单位,进行后续的操作(启动传输、等待传输完成、等等)。也可以将自己的回调函数通过描述符提供给controller driver。

传输描述符的定义如下:

struct dma_async_tx_descriptor { 
         dma_cookie_t cookie; 
         enum dma_ctrl_flags flags; /* not a 'long' to pack with cookie */ 
         dma_addr_t phys; 
         struct dma_chan *chan; 
         dma_cookie_t (*tx_submit)(struct dma_async_tx_descriptor *tx); 
         int (*desc_free)(struct dma_async_tx_descriptor *tx); 
         dma_async_tx_callback callback
         void *callback_param
         struct dmaengine_unmap_data *unmap; 
#ifdef CONFIG_ASYNC_TX_ENABLE_CHANNEL_SWITCH 
         struct dma_async_tx_descriptor *next; 
         struct dma_async_tx_descriptor *parent; 
         spinlock_t lock; 
#endif 
};

cookie,一个整型数,用于追踪本次传输。一般情况下,dma controller driver会在内部维护一个递增的number,每当client获取传输描述的时候(参考3.3中的介绍),都会将该number赋予cookie,然后加一。 
注7:有关cookie的使用场景,我们会在后续的文章中再详细介绍。

flags, DMA_CTRL_开头的标记,包括: 
DMA_CTRL_REUSE,表明这个描述符可以被重复使用,直到它被清除或者释放; 
DMA_CTRL_ACK,如果该flag为0,表明暂时不能被重复使用。

phys,该描述符的物理地址??不太懂!

chan,对应的dma channel。

tx_submit,controller driver提供的回调函数,用于把改描述符提交到待传输列表。通常由dma engine调用,client driver不会直接和该接口打交道。

desc_free,用于释放该描述符的回调函数,由controller driver提供,dma engine调用,client driver不会直接和该接口打交道。

callback、callback_param,传输完成的回调函数(及其参数),由client driver提供。

后面其它参数,client driver不需要关心,暂不描述了。

5. 参考文档

[1] Documentation/dmaengine/client.txt

[2] Linux DMA Engine framework(1)_概述

[3] Linux MMC framework(2)_host controller driver

[4] https://forums.xilinx.com/xlnx/attachments/xlnx/ELINUX/10658/1/drivers-session4-dma-4public.pdf

原创文章,转发请注明出处。蜗窝科技www.wowotech.net

---------------------------------------------------------------------------------------------------------------------------------------------------------------------

linux dmaengine编程

https://blog.csdn.net/u012247418/article/details/82313959

开发板:A33,运行linux-3.4.39

主机:Ubuntu 14.04

----------------------------------------------

DMA是Direct Memory Access的缩写,顾名思义,就是绕开CPU直接访问memory的意思。在计算机中,相比CPU,memory和外设的速度是非常慢的,因而在memory和memory(或者memory和设备)之间搬运数据,非常浪费CPU的时间,造成CPU无法及时处理一些实时事件。因此,工程师们就设计出来一种专门用来搬运数据的器件----DMA控制器,协助CPU进行数据搬运。

DMA传输可以是内存到内存、内存到外设和外设到内存。这里的代码通过dma驱动实现了内存到内存的数据传输。linux实现了DMA框架,叫做DMA Engine,内核驱动开发者必须按照固定的流程编码才能正确的使用DMA。

1. DMA用法包括以下的步骤: 

1)分配一个DMA通道; 

dma_request_channel()

2)设置controller特定的参数; 

none

3)获取一个传输描述符;

device_prep_dma_memcpy() 

4)提交传输描述符; 

tx_submit();

5)dma_async_issue_pending()

2. 测试:

1)交叉编译成ko模块,下载到A33开发板

2)加载模块:insmod dma.ko

3)执行:cat /dev/dma_test

执行此操作会产生一次DMA请求,将src内存数据复制到dst内存区域,复制完后会调用回调函数,复制过程中不需要CPU的参与。

注:在Ubuntu下dma_request_channel()失败,原因未知,所以转到A33下测试。

3. 其它

申请DMA缓冲区:

void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int flag);

该函数实际获得两个地址, 

1、函数的返回值是一个 void *,代表缓冲区的内核虚拟地址 

2、相关的总线地址(物理地址),保存在dma_handle中

物理地址和虚拟地址存在映射关系。DMA传输使用物理地址,而CPU操作的是虚拟地址。

如在tx = dev->device_prep_dma_memcpy(chan, dma_dst, dma_src, 512, flags);使用了物理地址

在dma_callback_func()中使用了虚拟地址。

使用DMA传输数据必须要基于DMA缓冲区,不能使用其他的内存区域,如kmalloc开辟的内存。

4. 源码dma.c

/*
Function description:When we call dmatest_read(),it will transmit src memory data
to dst memory,then print dst memory data by dma_callback_func(void) function.
*/
#include<linux/module.h>
#include<linux/init.h>
#include<linux/fs.h>
#include<linux/sched.h>
 
#include<linux/device.h>
#include<linux/string.h>
#include<linux/errno.h>
 
#include<linux/types.h>
#include<linux/slab.h>
#include<linux/dmaengine.h>
#include<linux/dma-mapping.h>
 
#include<asm/uaccess.h>
 
 
 
#define DEVICE_NAME "dma_test"
 
unsigned char dmatest_major;
static struct class *dmatest_class;
 
struct dma_chan *chan;
 //bus address
dma_addr_t dma_src;
dma_addr_t dma_dst;
//virtual address
char *src = NULL;
char *dst = NULL ;
struct dma_device *dev;
struct dma_async_tx_descriptor *tx = NULL;
enum dma_ctrl_flags flags;
dma_cookie_t cookie;
 
 
//When dma transfer finished,this function will be called.
void dma_callback_func(void)
{
	int i;
	printk("dma transfer ok.\n");
 
	for (i = 0; i < 512; ){
		printk("dst[%d]:%c ", i, dst[i]);
		i += 10;
	}
	printk("\n");
}
 
int dmatest_open(struct inode *inode, struct file *filp)
{
	return 0;
}
 
int dmatest_release(struct inode *inode, struct file *filp)
{
	return 0;
}
 
static ssize_t dmatest_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
	int ret = 0;
	//alloc a desc,and set dst_addr,src_addr,data_size.
	tx = dev->device_prep_dma_memcpy(chan, dma_dst, dma_src, 512, flags);
	if (!tx){
		printk("Failed to prepare DMA memcpy");
	}
	
	tx->callback = dma_callback_func;//set call back function
	tx->callback_param = NULL;
	cookie = tx->tx_submit(tx); //submit the desc
	if (dma_submit_error(cookie)){
		printk("Failed to do DMA tx_submit");
	}
	
	printk("begin dma transfer.\n");	
	dma_async_issue_pending(chan);	//begin dma transfer
	
	return ret;
}
 
static ssize_t dmatest_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
	int ret = 0;
	return ret;
}
 
static const struct file_operations dmatest_fops = {
	.owner = THIS_MODULE,
	.read = dmatest_read,
	.write = dmatest_write,
	.open = dmatest_open,
	.release = dmatest_release,
};
 
 
int dmatest_init(void)
{
	int i;
	dma_cap_mask_t mask;
	
	//the first parameter 0 means allocate major device number automatically
	dmatest_major = register_chrdev(0, DEVICE_NAME, &dmatest_fops);
	if (dmatest_major < 0) 
		return dmatest_major;
	//create a dmatest class
	dmatest_class = class_create(THIS_MODULE,DEVICE_NAME);
	if (IS_ERR(dmatest_class))
		return -1;
	//create a dmatest device from this class
	device_create(dmatest_class,NULL,MKDEV(dmatest_major,0),NULL,DEVICE_NAME);
	printk("device node create ok.\n");
 
	//alloc 512B src memory and dst memory
	src = dma_alloc_coherent(NULL, 512, &dma_src, GFP_KERNEL);
	printk("src = 0x%x, dma_src = 0x%x\n",src, dma_src);
	
	dst = dma_alloc_coherent(NULL, 512, &dma_dst, GFP_KERNEL);
	printk("dst = 0x%x, dma_dst = 0x%x\n",dst, dma_dst);
		
	for (i = 0; i < 512; i++){
		*(src + i) = 'a';
	}
 
	dma_cap_zero(mask);
	dma_cap_set(DMA_MEMCPY, mask);	//direction:memory to memory
	chan = dma_request_channel(mask,NULL,NULL); 	//request a dma channel
	if(chan)
		printk("dma channel id = %d\n",chan->chan_id);
	else{
		printk("dma_request_channel faild.\n");
		return -1;
	}
	
	flags = DMA_CTRL_ACK | DMA_PREP_INTERRUPT;
	dev = chan->device;
	
	return 0;
}
 
void dmatest_exit(void)
{
	unregister_chrdev(dmatest_major,DEVICE_NAME);//release major device number
	device_destroy(dmatest_class,MKDEV(dmatest_major,0));//destroy globalmem device
	class_destroy(dmatest_class);//destroy globalmem class
	
	//free memory and dma channel
	dma_free_coherent(NULL, 512, src, &dma_src);
	dma_free_coherent(NULL, 512, dst, &dma_dst);
	
	dma_release_channel(chan);
}
 
 
module_init(dmatest_init);
module_exit(dmatest_exit);
 
MODULE_LICENSE("GPL");

Linux内存管理 —— DMA和一致性缓存

https://blog.csdn.net/jasonchen_gbd/article/details/79462064

1. 出现内存不一致的原因

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从内存里去读数据,就发送了错误的数据。 
这里写图片描述

2. 如何解决一致性问题

主要靠两类APIs: 
1.一致性DMA缓存(Coherent DMA buffers), 
DMA需要的内存由内核去申请,内核可能需要对这段内存重新做一遍映射,特点是
映射的时候标记这些页是不带cache的,这个特性也是存放在页表里面的。 
上面说“可能”需要重新做映射,如果内核在highmem映射区申请内存并将这个地址通过vmap映射到vmalloc区域,则需要修改相应页表项并将页面设置为非cache的,而如果内核从lowmem申请内存,我们知道这部分是已经线性映射好了,因此不需要修改页表,只需修改相应页表项为非cache即可。

相关的接口就是dma_alloc_coherent()和dma_free_coherent()。dma_alloc_coherent()会传一个device结构体指明给哪个设备申请一致性DMA内存,它会产生两个地址,一个是给CPU看的,一个是给DMA看的。CPU需要通过返回的虚拟地址来访问这段内存,才是非cache的。至于dma_alloc_coherent()的内部实现可以不关注,它是和体系结构如何实现非cache(如mips的kseg1)相关,也可能与硬件特性(如是否支持CMA)相关。

还有一个接口dma_cache_sync(),可以手动去做cache同步,上面说dma_alloc_coherent()分配的是uncached内存,但有时给DMA用的内存是其他模块已经分配好的,例如协议栈发包时,最终要把skb的地址和长度交给DMA,除了将skb地址转换为物理地址外,还要将CPU cache写回(因为cache里可能是新的,内存里是旧的)。 
贴出一种实现:

void dma_cache_sync(struct device *dev, void *vaddr, size_t size,
            enum dma_data_direction direction)
{
    void *addr;

    addr = __in_29bit_mode() ?
           (void *)CAC_ADDR((unsigned long)vaddr) : vaddr;

    switch (direction) {
    case DMA_FROM_DEVICE:       /* invalidate only */
        __flush_invalidate_region(addr, size);
        break;
    case DMA_TO_DEVICE:     /* writeback only */
        __flush_wback_region(addr, size);
        break;
    case DMA_BIDIRECTIONAL:     /* writeback and invalidate */
        __flush_purge_region(addr, size);
        break;
    default:
        BUG();
    }
}

调用这个函数的时刻就是上面描述的情况:因为内存是可cache的,因此在DMA读内存(内存到设备方向)时,由于cache中可能有新的数据,因此要先将cache中的数据写回到内存;在DMA写内存(设备到内存方向)时,cache中可能还有数据没有写回,为了防止cache数据覆盖DMA要写的内容,要先将cache无效。注意这个函数的vaddr参数接收的是虚拟地址

例如在发包时将协议栈的skb放进ring buffer之前,要做一次DMA_TO_DEVICE的flush。对应的,在收包后为ring buffer中已被使用的skb数据buffer重新分配内存后,要做一次DMA_FROM_DEVICE的flush(invalidate的时候要注意cache align)。

还有一种针对可cache的内存做一致性的方式,就是流式DMA映射。

2.流式DMA映射(DMA Streaming Mapping), 
相关接口为 dma_map_sg(), dma_unmap_sg(),dma_map_single(),dma_unmap_single()。 
一致性缓存的方式是内核专门申请好一块内存给DMA用。而有时驱动并没这样做,而是让DMA引擎直接在上层传下来的内存里做事情。例如从协议栈里发下来的一个包,想通过网卡发送出去。 
但是协议栈并不知道这个包要往哪里走,因此分配内存的时候并没有特殊对待,这个包所在的内存通常都是可以cache的。 
这时,内存在给DMA使用之前,就要调用一次dma_map_sg()或dma_map_single(),取决于你的DMA引擎是否支持聚集散列(DMA scatter-gather),支持就用dma_map_sg(),不支持就用dma_map_single()。DMA用完之后要调用对应的unmap接口。

由于协议栈下来的包的数据有可能还在cache里面,调用dma_map_single()后,CPU就会做一次cache的flush,将cache的数据刷到内存,这样DMA去读内存就读到新的数据了

注意,在map的时候要指定一个参数,来指明数据的方向是从外设到内存还是从内存到外设: 
从内存到外设:CPU会做cache的flush操作,将cache中新的数据刷到内存。 
从外设到内存:CPU将cache置无效,这样CPU读的时候不命中,就会从内存去读新的数据。

还要注意,这几个接口都是一次性的,每次操作数据都要调用一次map和unmap。并且在map期间,CPU不能去操作这段内存,因此如果CPU去写,就又不一致了。 
同样的,dma_map_sg()和dma_map_single()的后端实现也都是和硬件特性相关。

其他方式 
上面说的是常规DMA,有些SoC可以用硬件做CPU和外设的cache coherence,例如在SoC中集成了叫做“Cache Coherent interconnect”的硬件,它可以做到让DMA踏到CPU的cache或者帮忙做cache的刷新。这样的话,dma_alloc_coherent()申请的内存就没必要是非cache的了。

------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 

关于linux kernel中dma内存的使用

参考资料:

ldd3第15章:内存映射和DMA

linux source/Documentation/DMA-API.txt

linux source/Documentation/DMA-API-HOWTO.txt

=============================================================

对DMA内存的使用有两种方式:

1,一致DMA映射

通过dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag)来直接得到一块用于dma的内存,同时得到这一段内存的虚拟地址和总线地址,分别用于CPU和device的访问。

通过这种方式得到的dma内存,开发者不用担心cache的问题,但是要注意在执行DMA操作之前flush write buffer。

2,流式DMA映射

先通过kmalloc, get_free_pages等得到一段物理连续的内存空间(注意,除非目标平台有IOMMU,否则必须要求物理地址连续,即vmalloc分配得到的内存空间不能用于DMA操作。)

然后使用dma_map_single, dma_map_pages, dma_map_sg将之前分配的内存空间映射,得到总线地址,使之能被device访问。

这种方式不保证cache的一致性,需要开发者手动处理(调用dma_sync_single_for_cpu/device函数?);

另外,必须保证内存的虚拟地址空间边界与cache line length对齐,因cache line length不确定,所以一般选择page 对齐。

一致DMA映射具有更长的生命周期,它在driver的整个生命周期内都有效,且不用关心cache效应。

流式DMA映射则只在driver填充完要传输的内容到device完成传输这段时间内有效(理论上是从map到unmap,但有效时间如前所述),凡使用流式DMA映射的内存区域在map之后,就只对device有效,driver在unmap之前不能在读写这一段内存区域。或者使用dma_sync_single_for_cpu由cpu获得读写权利,然后driver可对其进行读写。

待续。。。

------------------------------------------------------------------------------------------------------------------------------------------------------------------------

https://www.cnblogs.com/gjfhopeful/p/3669057.html

DMA映射 dma_addr_t

DMA映射

一个DMA映射是要分配的DMA缓冲区与为该缓冲区生成的、设备可访问地址的组合。

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

根据DMA缓冲区期望保留的时间长短,PCI代码有两种DMA映射: 1) 一致性映射 ; 2) 流式DMA映射(推荐)。

建立一致性DMA映射

void *dma_alloc_coherent(struct device *dev,size_t size, dma_addr_t *dma_handle,int flag); 该函数处理了缓冲区的分配和映射。

前两个参数是device结构和所需缓冲区的大小。 函数在两处返回结果: 1)函数的返回值时缓冲区的内核虚拟地址,可以被驱动程序使用。 2)相关的总线地址则保存在dma_handle中。通常只能通过get_free_pages函数分配内存(size是以字节为单位的)。flag参数通常是描述如何分配内存的GFP_值;通常是GFP_KERNEL或者是GFP_ATOMIC(在原子上下文中运行时)。

向系统返回缓冲区: 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池了。

在<linux/dmapool.h>中定义了DMA池的函数。DMA池必须在使用前,调用下面的函数创建:

struct dma_pool *dma_pool_create(const char *name,struct device *dev, size_t size,size_t align, size_t allocation); name是DMA池的名字,dev是device结构,size是从该池中分配的缓冲区大小, align是该池分配操作所必须遵守的硬件对齐原则(用字节表示),如果allocation不为零,表示内存边界不能超越allocation。

释放DMA池: void dma_pool_destroy(struct dma_pool *pool);

在销毁前,必须向DMA池返回所有分配的内存。

DMA池分配内存 void *dma_pool_alloc(struct dma_pool *pool,int mem_flags, dma_addr_t *handle);

在这个函数中,mem_flags通常设置为GFP_分配标志。像dma_alloc_coherent函数一样,返回的DMA缓冲区的地址是内核虚拟地址,并作为总线地址保存在handle中。

释放内存 void dma_pool_free(struct dma_pool *pool,void *vaddr,dma_addr_t addr);

建立流式DMA映射

流式映射具有比一致性映射更为复杂的接口。 这些映射希望能与已经由驱动程序分配的缓冲区协同工作, 因而不得不处理那些不是它们选择的地址。

当建立流式映射时,必须告诉内核数据流动的方向。 枚举类型dma_data_direction:

  DMA_TO_DEVICE  数据发送到设备(如write系统调用)

  DMA_FROM_DEVICE  数据被发送到

  CPU DMA_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_t dma_addr,size_t size, enum dma_data_direction direction);

流式DMA映射的几条原则:

  • 缓冲区只能用于这样的传送,即其传送方向匹配于映射时给定的方向。
  • 一旦缓冲区被映射,它将属于设备,而不是处理器。 直到缓冲区被撤销映射前,驱动程序不能以任何方式访问其中的内容。
  • 在DMA处于活动期间内,不能撤销对缓冲区映射,否则会严重破坏系统的稳定性。

驱动程序需要不经过撤销映射就访问流式DMA缓冲区的内容,有如下调用:

void dma_sync_single_for_cpu(struct device *dev,dma_handle_t bus_addr, size_t size,enum dma_data_direction direction);

在设备访问缓冲区前,应调用下面函数将缓冲区所有权交还给设备:

void dma_sync_single_for_device(struct device *dev,dma_handle_t bus_addr, size_t size,enum dma_data_direction direction);

单页流式映射

有时候,要为page结构指针指向的缓冲区建立映射,比如 为get_user_pages获得的用户空间缓冲区。使用下面的函数,建立和撤销使用page结构指针的流式映射:

dma_addr_t dma_map_page(struct device *dev, struct page *page, unsigned long offset, size_t size, enum dma_data_direction direction);

void dma_unmap_page(struct device *dev, dma_addr_t dma_address, size_t size, enum dma_data_direction direction);

分散/聚集映射

分散/聚集映射是一种特殊类型的流式DMA映射。假设有几个缓冲区,它们需要与设备双向传输数据。可以简单地依次映射每一个缓冲区并且执行请求的操作, 但是一次映射整个缓冲区表还是很有利的。

映射分散表的第一步是建立并填充一个描述被传输缓冲区的 scatterlist结构的数组。 <linux/scatterlist.h>

scatterlist结构的成员:struct page *page;

           unsigned int length;

           unsigned int offset;

然后调用:

int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);

这里nents是传入的分散表入口的数量。返回值是要传送的DMA缓冲区数,它可能小于nents。

驱动程序应该传输有dma_map_sg函数返回的每个缓冲区。总线地址和每个缓冲区的长度被保存在scatterlist结构中,但是它们在结构中的位置会随体系结构的不同而不同。使用已经定义的两个宏,以用来编写可移植代码:

dma_addr_t sg_dma_address(struct scatterlist *sg); 从该分散表的入口项中返回总线(DMA)地址。

unsigned int sg_dma_len(struct scatterlist *sg); 返回缓冲区的长度。

一旦传输完毕,解除分散/聚集映射:

void dma_unmap_sg(struct device *dev, struct scatterlsit *list, int nents, enum dma_data_direction direction);

nents一定是先前传递给dma_map_sg函数的入口项的数量,而不是函数返回的DMA缓冲区的数量。

分散/聚集映射是流失DMA映射,因此适用于流式映射的规则也适用于该种映射。如果必须访问映射的分散/聚集列表,必须首先对其进行同步:

void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);

void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);

PCI双重地址周期映射

通用DMA支持层使用32位总线地址,其为设备的DMA掩码所约束。然而PCI总线还支持64位地址模式,即 双重地址周期(DAC )。通用DMA层并不支持该模式。

如果设备需要使用放在高端内存的大块缓冲区,可以考虑实现DAC支持。这种支持只有对PCI总线有效,因此必须要使用与PCI总线有关的例程。

要使用DAC,驱动程序必须包含头文件<linux/pci.h>,还必须设置一个单独的DMA掩码:

int pci_dac_set_dma_mask(struct pci_dev *pdev, u64 mask); 只有返回0时,才能使用DAC地址。

在DAC映射中使用了一个特殊类型(dma64_addr_t)。使用下面函数建立一个这样的映射:

dma64_addr_t pci_dac_page_to_dma(struct pci_dev *pdev, struct page *page, unsigned long offset, int direction);

可以只使用page结构指针来建立DAC映射,而且必须以一次一页的方式创建它们。direction参数与在通用DMA层中使用的dma_data_direction枚举类型等价,因此可以取PCI_DMA_TODEVICE、PCI_DMA_FROMDEVICE或者PCI_DMA_BIDIRECTIONAL。

DAC映射不需要显示释放它。有一套用于同步DMA缓冲区的函数,其形式如下:

void pci_dac_dma_sync_single_for_cpu(struct pci_dev *pdev, dma64_addr_t dma_addr, size_t len, int direction);

void pci_dac_dma_sync_single_for_device(struct pci_dev *pdev, dma64_addr_t dma_addr, size_t len, int direction);

一个简单的PCI DMA例子

这里提供了一个PCI设备的DMA例子(不能应用于任何真实设备),是一个假定的叫dad(DMA Acquisition Device,DMA获取设备)驱动程序的一部分,说明如何使用DMA映射:

int dad_transfer(struct dad_dev *dev, int write, void *buffer, size_t count)

{

  dma_addr_t bus_addr;

  /*映射DMA需要的缓冲区*/

  dev->dma_dir = (write ? DMA_TO_DEVICE : DMA_FROM_DEVICE);

  dev->dma_size = count;

  bus_addr = dma_map_single(&dev->pci_dev->dev, buffer, count, dev->dma_dir);

  

  dev_>dma_addr = bus_addr;

  

  /*设置设备*/

  writeb(dev->registers.command, DAD_CMD_DISABLEDMA);

  writeb(dev->registers.command, write ? DAD_CMD_WR : DAD_CMD_RD);

  writel(dev->registers.addr, cpu_to_le32(bus_addr));

  writel(dev->registers.len, cpu_to_le32(count));

  /*开始操作*/

  writeb(dev->registers.command, DAD_CMD_ENABLEDMA);

  return 0;

}

该函数映射了准备进行传输的缓冲区并且启动设备操作。

另一半工作必须在中断服务例程中完成:

void dad_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

  struct dad_dev *dev = (struct dad_dev *)dev_id;

  /* 确定中断是由对应的设备发来的*/

  /*释放对DMA缓冲区的映射*/

  dma_unmap_single(dev->pci_dev->dev, dev->dma_addr, dev->dma_size, dev->dma_dir);

  /* 释放之后,才能访问缓冲区,把它拷贝给用户 */

  ...

}

ISA设备的DMA

ISA总线允许两种DMA传输:本地DMA和ISA总线控制DMA。

只讨论本地(native)DMA。

本地DMA使用主板上的标准DMA控制器电路来驱动ISA总线上的信号线。

本地DMA,要关注三种实体:

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

在PC中使用的早期DMA控制器能够管理四个“通道”, 每个通道都与一套DMA寄存器相关联。

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

猜你喜欢

转载自blog.csdn.net/u014426028/article/details/108978685
DMA
今日推荐