22、DMA驱动程序框架
一、使用DMA的优点及DMA支持的请求源(请求源是启动DMA传输的事件,可以认为是触发。它可以是软件,也可以是中断,或者外部事件)
1、DMA优点是其进行数据传输时不需要CPU的干涉,可以大大提高CPU的工作效率。
2、DMA在大容量数据传输中非常重要,比如图像数据传输,SD卡数据传输,USB数据传输等。
3、S3C2440有四个DMA,每个DMA支持的工作方式基本相同,但支持的DMA请求源可能略有不同。如下为四个DMA通道分别支持的DMA请求源:
Ch0: nXDREQ0, UART0, SDI, Timer, USB EP1
Ch1: nXDREQ1, UART1, I2SSDI, SPI0, USB EP2
Ch2: I2SSDO, I2SSDI, SDI, Timer, USB EP3
Ch3: UART1, SDI, SPI1, Timer, USB EP4
那么怎么使用DMA呢,S3C2440内部集成了DMA控制器,我们只需要简单的配置一下寄存器就可以实现DMA的传输了。
二、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中设置。
DMA(Direct Memory Access)
即直接存储器访问, DMA 传输方式无需 CPU 直接控制传输,通过硬件为 RAM 、I/O 设备开辟一条直接传送数据的通路,能使 CPU 的效率大为提高。
学了这么多驱动,不难推出DMA的编写套路:
- 1)注册DMA中断,分配缓冲区
- 2)注册字符设备,并提供文件操作集合fops
- -> 2.1)file_operations里设置DMA硬件相关操作,来启动DMA
由于我们是用字符设备的测试方法测试的,而本例子只是用两个地址之间的拷贝来演示DMA的作用,所以采用字符设备方式编写
1.驱动编写之前,先来讲如何分配释放缓冲区、DMA相关寄存器介绍、使用DMA中断
1.1在linux中,分配释放DMA缓冲区,只能使用以下几个函数
1)
/*该函数只禁止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:分配出来的内存参数,标志定义在<linux/gfp.h>,常用标志如下: //GFP_ATOMIC 用来从中断处理和进程上下文之外的其他代码中分配内存. 从不睡眠. //GFP_KERNEL 内核内存的正常分配. 可能睡眠. //GFP_USER 用来为用户空间页来分配内存; 它可能睡眠.
2)
/*该函数禁止cache缓存以及禁止写入缓冲区*/ void * dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp); //分配DMA缓存区,返回值和参数和上面的函数一直
3)
dma_free_writecombine(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle); //释放DMA缓存,与dma_alloc_writecombine()对应 //size:释放长度 //cpu_addr:虚拟地址, //handle:物理地址
4)
dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle) //释放DMA缓存,与dma_alloc_coherent ()对应 //size:释放长度//cpu_addr:虚拟地址, //handle:物理地址
(PS: dma_free_writecombine()其实就是dma_free_conherent(),只不过是用了#define重命名而已。)
而我们之前用的内存分配kmalloc()函数,是不能用在DMA上,因为分配出来的内存可能在物理地址上是不连续的.
1.2 那么2440开发板如何来启动DMA,先来看2440的DMA寄存器
(PS:实际这些DMA相关的寄存器,在linux内核中三星已封装好了,可以直接调用,不过非常麻烦,还不如直接设置寄存器,可以参考: http://blog.csdn.net/mirkerson/article/details/6632273)
1.2.1 2440支持4个通道的DMA控制器
其中4个通道的DMA外设请求源,如下图所示(通过DCONn寄存器的[26:24]来设置)
(PS:如果请求源是系统总线上的,就只需要设置DCONn寄存器的[23]=0即可)
1.2.2 且每个通道都可以处理以下4种情况:
1) 源和目标都在系统总线上(比如:两个物理内存地址) 2) 当目标在外设总线上时,源在系统总线上(外设指:串口,定时器,I2C,I2S等) 3) 当目标在系统总线上时,源在外设总线上 4) 源和目标都在外设总线上
1.2.3 DMA有两种工作模式(通过DCONn寄存器的[28]来设置)
查询模式:
当DMA请求XnXDREQ为低电平时,则DMA会一直传输数据,直到DMA请求拉高,才停止
握手模式:
当DMA请求XnXDREQ有下降沿触发时,则DMA会传输一次数据
1.2.4 DMA有两种传输模式(通过DCONn寄存器的[31]来设置)
单元传输:
指传输过程中,每执行一次,则读1次,写1次.(如上图所示)
突发4传输:
指传输过程中,每执行一次,则读4次,然后写4次(如下图所示)
1.2.5 2440中的DMA寄存器如下图所示:
共有4个通道的寄存器,且每个通道的寄存器内容都一致,所以我们以DMA通道0为例:
1)DISRC0初始源寄存器
[30:0] : 存放DMA源的基地址
2)DISRCC0初始源控制寄存器
[1] : 源位置选择,0:源在系统总线上, 1:源在外设总线上
[0] : 源地址选择,0:传输时源地址自动增加, 1:源地址固定
3)DIDST0初始目标寄存器
[30:0] : 设置DMA目的的基地址
4)DIDSTC0初始目标控制寄存器
[2] : 中断时间选择, 0:当DMA传输计数=0,立即发生中断 1:执行完自动加载后再发送中断(也就是计数为0,然后重新加载计数值)
[1] : 目的位置选择, 0:目的在系统总线上, 1:目的在外设总线上
[0] : 目的地址选择, 0:传输时目的地址自动增加, 1:目的地址固定
5)DCON0控制寄存器
[31] : 工作模式选择, 0:查询模式 1:握手模式 (当源处于外设时,尽量选择握手模式)
[30] : 中断请求(DREQ)/中断回应(DACK)的同步时钟选择, 0:PCLK同步 1:HCLK同步
(PS:如果有设备在HCLK上,该位应当设为1,比如:(SDRAM)内存数组, 反之当这些设备在PCLK上,应当设为0,比如:ADC,IIS,I2C,UART)
[29] : DMA传输计数中断使能/禁止 0:禁止中断 1:当传输完成后,产生中断
[28] : 传输模式选择, 0:单元传输 1:突发4传输
[27] : 传输服务模式
0:单服务模式,比如:有2个DMA请求,它们会被顺序执行一次(单元传输/突发4传输)后停止,然后直到有下一次DMA请求,再重新开始另一次循环。
1:全服务模式,指该DMA若有请求,则会占用DMA总线,一直传输,期间若有其它DMA请求,只有等待传输计数TC为0,才会执行其它DMA请求
[26:24] : DMA外设请求源选择
[23] : 软件/硬件请求源选择 0:软件请求 1:硬件请求(还需要设置[26:24]来选择外设源)
[22] : 重新加载开关选项 为0即可
[21:20] : 传输数据大小 为00(8位)即可
[19:0] : 设置DMA传输的计数TC
6)DSTAT0状态寄存器
[21:20] : DMA状态 00:空闲 01:忙
[19:0] : 传输计数当前值CURR_TC 为0表示传输结束
7)DCSRC0当前源寄存器
[30:0] : 存放DMA当前的源基地址
8)DCDST0当前目标寄存器
[30:0] : 存放DMA当前的目的基地址
9)DMASKTRIG0触发屏蔽寄存器
[2] : 停止STOP 该位写1,立刻停止DMA当前的传输
[1] : DMA通道使能 0:关闭DMA的通道0(禁止DMA请求) 1:开启DMA的通道0(开启DMA请求)
[0] : 软件请求触发 1:表示启动一次软件请求DMA,只有DCONn[23]=0和DMASKTRIGn[1]=1才有效,DMA传输时,该位自动清0
1.3接下来就开始讲linux注册DMA中断
首先,DMA的每个通道只能有一个源- >目的,所以输入命令 cat /proc/interrupts ,找到DMA3中断未被使用
所以在linux中使用:
request_irq(IRQ_DMA3, s3c_dma_irq, NULL, "s3c_dma", 1);// s3c_dma_irq:中断服务函数,这里注册DMA3中断服务函数 //NULL:中断产生类型, 不需要,所以填NULL //1:表示中断时,传入中断函数的参数,本节不需要所以填1,切记不能填0,否则注册失败
2.接下来,我们便来写一个DMA的字符设备驱动
步骤如下:
- 1) 注册DMA中断,分配两个DMA缓冲区(源、目的)
- 2) 注册字符设备,并提供文件操作集合fops
- -> 2.1) 通过ioctl的cmd来判断是使用DMA启动两个地址之间的拷贝,还是直接两个地址之间的拷贝
- -> 2.2)若是DMA启动,则设置DMA的相关硬件,并启动DMA传输
2.1 所以,驱动代码如下所示:
#include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/init.h> #include <linux/delay.h> #include <linux/irq.h> #include <asm/irq.h> #include <asm/arch/regs-gpio.h> #include <asm/hardware.h> #include <asm/uaccess.h> #include <asm/io.h> #include <linux/dma-mapping.h> #define S3C_DMA_SIZE 512*1024 //DMA传输长度 512KB #define NORMAL_COPY 0 //两个地址之间的正常拷贝 #define DMA_COPY 1 //两个地址之间的DMA拷贝 /*函数声明*/ static DECLARE_WAIT_QUEUE_HEAD(s3c_dma_queue); //声明等待队列 static int s3c_dma_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long flags); /* * 定义中断事件标志 * 0:进入等待队列 1:退出等待队列 */ static int s3c_dma_even=0; static unsigned char *source_virt; //源虚拟地址 static unsigned int source_phys; //源物理地址 static unsigned char *dest_virt; //目的虚拟地址 static unsigned int dest_phys; //目的虚拟地址 /*DMA3寄存器*/ struct S3c_dma3_regs{ unsigned int disrc3 ; //0x4b0000c0 unsigned int disrcc3 ; unsigned int didst3 ; unsigned int didstc3 ; unsigned int dcon3 ; unsigned int dstat3 ; unsigned int dcsrc3 ; unsigned int dcdst3 ; unsigned int dmasktrig3; //0x4b0000e0 }; static volatile struct S3c_dma3_regs *s3c_dma3_regs; /*字符设备操作*/ static struct file_operations s3c_dma_fops={ .owner = THIS_MODULE, .ioctl = s3c_dma_ioctl, }; /*中断服务函数*/ static irqreturn_t s3c_dma_irq (int irq, void *dev_id) { s3c_dma_even=1; //退出等待队列 wake_up_interruptible(&s3c_dma_queue); //唤醒 中断 return IRQ_HANDLED; } /*ioctl函数*/ static int s3c_dma_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long flags) { int i; memset(source_virt, 0xAA, S3C_DMA_SIZE); memset(dest_virt, 0x55, S3C_DMA_SIZE); switch(cmd) { case NORMAL_COPY: //正常拷贝 for(i=0;i<S3C_DMA_SIZE;i++) dest_virt[i] = source_virt[i]; if(memcmp(dest_virt, source_virt, S3C_DMA_SIZE)==0) { printk("NORMAL_COPY OK\n"); return 0; } else { printk("NORMAL_COPY ERROR\n"); return -EAGAIN; } case DMA_COPY: //DMA拷贝 s3c_dma_even=0; //进入等待队列 /*设置DMA寄存器,启动一次DMA传输 */ /* 源的物理地址 */ s3c_dma3_regs->disrc3 = source_phys; /* 源位于AHB总线, 源地址递增 */ s3c_dma3_regs->disrcc3 = (0