一,简介
本文主要介绍在内核中简单使用DMA实现显存数据传递。因为本篇文章中没有介绍与框架相关的程序,只是使用字符设备来操作DMA,同时也没有具象的层次,因而本文中代码剖析部份就相对简单。但我还是会将文章分为两部份,第一部份我将介绍与DMA相关的知识。而第二部份讲解在内核中怎样通过代码实现DMA的数据传递。
所用开发板:JZ2440V3(S3C2440A)
第一部份:DMA相关知识
在介绍DMA之前我想问你们:我们为何要引入DMA,DMA对我们有哪些用处那?
答:计算机系统中各类常用的数据输入/输出方式有查询方法(包括无条件及条件传送形式)和中断方法,这种方法适用于CPU与慢速及中速外设之间的数据交换。但当高速外设要与系统显存或则要在系统显存的不同区域之间进行大量数据的快速传送时,就在一定程度上限制了数据传送的速度。直接储存器存取(DMA)就是为解决这个问题提出的,采用DMA形式,在一定时间段内,由DMA控制器代替CPU,获得总线控制权,来实现显存与外设或则显存的不同区域之间大量数据的快速传送。同时很重要的一点是当DMA传输数据时,并不占用CPU资源,在这个时侯CPU可以空出手来做其他的事情。这样我们既可以做大量数据的高速传输又可以让CPU有时间和资源去做其他的事情。
里面介绍了哪些是DMA,也介绍了DMA的重要性。这么我们就要瞧瞧我们芯片中的DMA了。在S3C2440A中,我们集成了DMA模块,可以拿来传递高速传输数据。既然是数据传输这么我又要问了,我们晓得数据传输三要素:源,目的,宽度。这是我们数据传输时要晓得的。这么在S3C2440A中,源,目的,宽度是如何表示的那?
在2440中我们的源与目的地选择有四种情况:
2440中源与目的就是通讯的双方,而这双方是通过恳求DMA传递信息的,所以我们将前面那些向DMA发送恳求的(不管是源还是目的)称为恳求源。她们恳求DMA来传输数据。而在2440中有四条通道来设置不同的恳求源,她们为:
介绍完缘由与目的,我们原本是要介绍数据传输宽度的。并且我想还是先介绍数据传输方法比较好。介绍完数据传输方法我们了解了数据是以哪些样的形式传输的。这样我们自然就晓得要传输的数据宽度了。
DMA模式介绍:
DMAservicemode:singleservice&Wholeservice。前一模式下,一次DMA恳求完成一项原子操作,而且transfercount的值减1。后一模式下,一次DMA恳求完成一批原子操作,直至transfercount等于0表示完成一次整体服务。具体对应DCON[27]。
DMADREQ/DACKPROTOCOL:DMA恳求和应答的合同有两种,Demondmode和Handshakemode。
二者对Request和Ack的时序定义有所不同:
下边我们来介绍DMA中数据传输的格式,注意,这儿说的是传输格式,而不是传输的大小。在DMA中有两种传输格式,单元传输和burst4传输,相对于单元传输的每次读写一个单元,burst4可以一次完成四个单元的读写。而这儿的单元就是我们说的数据的大小有:字节,半字,字。
有了前面的知识我们就可以介绍总的数据的传输大小了:DSZxTSZxTC,其中DSZ就是前面说的数据的大小,TSZ是传输的格式,而TC是传输的次数。她们的乘积就是整个数据的大小了。
介绍了数据传输的三要素,我想你们又要问了:既然有了数据传输的三要素linux系统,这么我们的数据是怎样传输的?
在我们的S3C2440中他是使用三态的有限状态机(FSM)来实现数据的传输的。其实是使用的有限状态机,但2440中还有两种传输模式:单服务模式和全服务模式。我下边用一幅图来展示三态的有限状态机:
在状态1时,DMA等待DMA恳求,此时DMAACK和INTREQ为0。当DMA收到DMA恳求时,他跳转到状态2。
在状态2时,DMAACK为1,INTREQ还为0,此时CURR_TC从DCON[19:0]从加载计数值。在她们完成后他跳到状态3。
在状态3时,引入子状态机拿来处理一次原子操作linux网络内核分析与开发,即完成一次数据从源读出之后讲到目的中。而在这时我们就要分单服务模式和全服务模式来讨论了。在单服务模式中,子有限状态机完成一次原子操作后CURR_TC-1,主有限状态机将DMAACK设为0,之后调回到状态1,之后等待下一次的DMA恳求。而在全服务模式时,子有限状态机将仍然运行直至CURR_TC为0,之后他再将INTREQ设为1而将DMAACK设为0,之后调回到状态1,之后等待下一次的DMA恳求。
前面我们介绍了DMA是怎样传输数据的,同时也讲了数据传输的三要素。这么我们下边就要讲一下在2440中DMA的配置步骤和要点了(主要针对寄存器):
1.数据从哪儿来,到那里去?
使用DMA首先我们要晓得数据的流向,DISRCx寄存器是DMA初始源寄存器,储存了数据的源地址。DIDSTx是DMA的初始目的寄存器,储存数据的目的地址。
2.数据走的哪些总线?地址是否是固定的?
我们还要晓得源与目的数据储存设备在哪些总线上(AHB系统总线,通常是高速的诸如显存,APB外围总线,低速的例如SD,UART;具体走哪些总线可以在datasheet上查到);以及数据传输结束之后起始地址还原到发送前的起始地址呢,还是在现今的末尾+1作为新的起始地址。这种设置在DISRCCx与DIDSTCx两个寄存器上面配置。
3.数据以哪些方法传输?始于哪些目的是哪些呢?要不要手动重载?
须要确定数据的传输方法有恳求还是握手,按照前面的总线确定与哪些时钟同步(HCLK,PCLK),是单元传输还是突发传输,是以字节传输还是字传输,是否重载。是单服务(只发送一次)还是多服务(不停循环发送),以及数据的传送大小。选择源与目的设备。最后还要确定中断是不是传输结束发生(CURR_TC记数是不是0)。那些都在DCONx中设置。
4.如何开始传输DMA和停止DMA,那些在DMASKTRIG中设置。
更多Linux内核视频教程文档资料免费发放后台私信【内核】自行获取。
内核学习网站:
Linux内核源码/显存调优/文件系统/进程管理/设备驱动/网路合同栈-学习视频教程-腾讯课堂
第二部份:在内核中怎样通过代码实现DMA数据传递。
通过前面对DMA的介绍,我对DMA有了一定的认识。这么我将在这一部份用代码实现对DMA的简单控制。如今我写一下我在代码中要实现的功能:对比于使用和不使用DMA来传递显存中的数据,来确定DMA对于解放CPU的重要性。
我们将利用于字符设备驱动在他的操作函数中实现对DMA的控制。
所以我们可以大体总结出这个程序的写作步骤:
而此处须要非常说明的一点是,在使用DMA时我们要求DMA操作的数据的数学地址和虚拟地址都是要连续的。为此我们使用dma_alloc_writecombine函数来为DMA分配显存缓冲区。而不用我们常使用的kmalloc来申请空间。而我在右图中做了这两个函数的对比:
从上图可以看出kmalloc和dma_alloc_writecombine这两个函数申请的虚拟地址都是连续的,而且她们的数学地址却不相同,kmalloc的数学地址是离散的,而dma_alloc_writecombine的化学地址是连续的。
下边是dma_alloc_writecombine函数的介绍:
/*该函数只禁止cache缓冲,保持写缓冲区,也就是对注册的物理区写入数据,也会更新到对应的虚拟缓存区上*/
void *dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp);
//分配DMA缓存区
//返回值为:申请到的DMA缓冲区的虚拟地址,若为NULL,表示分配失败,需要释放,避免内存泄漏
//参数如下:
//*dev:指针,这里填0,表示这个申请的缓冲区里没有内容
//size:分配的地址大小(字节单位)
//*handle:申请到的物理起始地址
//gfp:分配出来的内存参数,标志定义在,常用标志如下:
//GFP_ATOMIC 用来从中断处理和进程上下文之外的其他代码中分配内存. 从不睡眠.
//GFP_KERNEL 内核内存的正常分配. 可能睡眠.
//GFP_USER 用来为用户空间页来分配内存; 它可能睡眠.
好了,有了前面的知识我们就可以上代码了。我们还是按着老师的步骤来讲,我们先看入口函数中做了哪些:
static int s3c_dma_init(void)
{
/* 申请DMA内存 */
if(request_irq(IRQ_DMA3,s3c_dma_irq,0,"s3c_dma",1)){
free_irq(IRQ_DMA3,1);
printk("can't request_irq for DMA n");
return -ENOMEM;
}
/* 为源分配内存对应的缓存区 */
src = dma_alloc_writecombine(NULL,BUF_SIZE,&src_phys,GFP_KERNEL);
if(NULL == src){
printk("can't alloc buffer for src n");
return -ENOMEM;
}
/* 为目的分配内存对应的缓存区 */
dst = dma_alloc_writecombine(NULL,BUF_SIZE,&dst_phys,GFP_KERNEL);
if(NULL == dst){
printk("can't alloc buffer for dst n");
dma_free_writecombine(NULL,BUF_SIZE, src, src_phys);
return -ENOMEM;
}
/* 对DMA寄存器做重映射 */
dma_regs = ioremap(DMA3_BASE_ADDR, sizeof(struct s3c_dma_regs));
/* 注册字符设备 */
auto_major = register_chrdev(auto_major,"s3c_dma",&dma_fops);
/* 创建设备节点 */
cls = class_create(THIS_MODULE,"s3c_dma");
class_device_create(cls,NULL,MKDEV(auto_major,0),NULL,"s3c_dma");
return 0;
}
从里面看我们做了:
在前面我们分配为源和目的分配显存对应的缓冲区,并对2440中DMA相关的寄存器做了重映射,这是为下边对DMA的操作做打算,而与DMA相关的寄存器我们将她们放在了一个结构体中,这样便捷我们的调用:
#define DMA0_BASE_ADDR 0x4b000000 /* DMA0 寄存器的基地址 */
#define DMA1_BASE_ADDR 0x4b000040 /* DMA1 寄存器的基地址 */
#define DMA2_BASE_ADDR 0x4b000080 /* DMA2 寄存器的基地址 */
#define DMA3_BASE_ADDR 0x4b0000c0 /* DMA3 寄存器的基地址 */
struct s3c_dma_regs{
unsigned long disrc;
unsigned long disrcc;
unsigned long didst;
unsigned long didstc;
unsigned long dcon;
unsigned long dstat;
unsigned long dcsrc;
unsigned long dcdst;
unsigned long dmasktrig;
};
static volatile struct s3c_dma_regs *dma_regs;
里面就是我们定义的DMA寄存器的基地址和寄存器结构体。我们会依照DMA恳求源的不同而调用不同的DMA基地址。因而操作不同DMA的寄存器,在本实例中我们使用DMA3。
下边我们看file_operations:
static struct file_operations dma_fops = {
.owner = THIS_MODULE,
.ioctl = s3c_dma_ioctl,
};
由于这个只是一个简单的事例linux网络内核分析与开发,所以我们并没有做太多的操作函数,而只是使用ioctl这个函数来实现我们前面说的:对比使用和不使用DMA来实现显存数据的拷贝。下边我们进ioctl中瞧瞧他是如何实现的:
int s3c_dma_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
int i;
static int cnt;
memset(src,0xaa,BUF_SIZE);
memset(dst,0x55,BUF_SIZE);
switch(cmd){
case MEM_CPY_NO_DMA :
{
for(i=0;i= 30){
cnt = 0;
if(memcmp(src,dst,BUF_SIZE)==0){
printk("MEM_CPY_NO_DMA ok n");
}else{
printk("MEM_CPY_NO_DMA file n");
}
}
break;
}
case MEM_CPY_DMA :
{
/* 把源,目的,长度告诉DMA */
/* 源的物理地址 */
dma_regs->disrc = src_phys;
/* 源位于AHB总线,原地址递增 */
dma_regs->disrcc = (0<<1) | (0<didst = dst_phys;
/* 目的位于AHB总线,目的地址递增 */
dma_regs->didstc = (0<<2) | (0<<1) | (0<<0);
/* 使能中断,单个传输,软件触发,读写单位为byte */
dma_regs->dcon = (1<<30) | (1<<29) | (0<<28) | (1<<27) | (0<<23) | (0<<20) | (BUF_SIZE <dmasktrig = (1<<1) | (1<= 30){
cnt = 0;
if(memcmp(src,dst,BUF_SIZE)==0){
printk("MEM_CPY_DMA ok n");
}else{
printk("MEM_CPY_DMA file n");
}
}
break;
}
}
return 0;
}
通过switch句子判定,当我们从应用程序的ioctl中接收到cmd并将其传输到我们驱动中的ioctl中linux重启,进而判定是MEM_CPY_NO_DMA还是MEM_CPY_DMA。当cmd为MEM_CPY_NO_DMA时,我们不使用DMA而是用CPU来将数据从源拷贝到目的,而当cmd为MEM_CPY_DMA时,我们使用DMA将源中的数据拷贝到目的。
讲解到这儿,我想好多人要问了,我们既然写了ioctl,这么我们去哪儿使用这个ioctl函数呢?
所以我们还要写一个测试程序在应用层测试这个驱动程序。而具体的测试程序为:
/*
* 用法:
* ./s3c_dma_test
*/
#include
#include
#include
#include
#include
#define MEM_CPY_NO_DMA 0
#define MEM_CPY_DMA 1
void print_usage(char *data)
{
printf("usage: n");
printf("%s n",data);
}
int main(int argc,char **argv)
{
int fd;
/* 判断输入的参数是不是两个 */
if(2 != argc){
print_usage(argv[0]);
return -1;
}
/* 打开DMA设备 */
fd = open("/dev/s3c_dma",O_RDWR);
if(fd<0){
printf("can't open /dev/s3c_dma . n");
}
/* 判断参数并调用ioctl函数 */
if(strcmp(argv[1], "nodma") == 0){
while(1){
ioctl(fd,MEM_CPY_NO_DMA);
}
}else if(strcmp(argv[1], "dma") == 0){
while(1){
ioctl(fd,MEM_CPY_DMA);
}
}else{
print_usage(argv[0]);
}
return 0;
}
从前面看他做的是:
测试:
下边我介绍如何通过这个测试程序测试DMA驱动。
之后比较两次的反应时间就可以看出使用DMA的优越性了。
本文原创地址://lrxjmw.cn/srjxlnhzsyds.html编辑:刘遄,审核员:暂无