DMA驱动程序编写

原创 2016年06月01日 00:10:48

一.  DMA的引入

    假设我们让2440来拷贝一段数据, 从内存的src拷贝到dst,拷贝的长度为size.

char *src = AAA;

char *dst = BBB;

int i;

for (i = 0; i <size; i++)

    dst[i] = src[i];

    这写出来的代码是让cpu来执行数据的拷贝工作, 显然,cpu在执行这个过程中,同一时间内只能做一件事情,如果要拷贝的数据很大的话,cpu在拷贝的过程中就不能做其他事情了,这样就会引起卡顿.


    所以我们就引入了DMA (Direct Memory Access,直接内存存取), 它允许不同速度的硬件装置来沟通, 而不需要依赖于 CPU 的大量中断负载.

    1. 把源告诉DMA;

    2. 把目的告诉DMA;

    3. 把size告诉DMA;

    4. 设置DMA参数 a. 地址递增/递减/不变; b. 手工启动(设置某些寄存器让它自动的启动DMA去拷贝这些数据), 外部启动(麦克MIC得到数据存在I2S的数据缓冲区里,当这个缓冲区里有了数据之后就会去触发DMA产生一个DMA请求, 由DMA将数据拷贝到内存里, 这时的源就是I2S的缓冲区, 目的就是内存的某个地址处)

    5.启动DMA.

    启动DMA之后, DMA就会将数据拷从src拷贝到dst,拷贝完之后就会发出一个中断告诉cpu数据拷贝完成.


二. DMA驱动程序编写

(一). 怎么写字符设备驱动

    应用程序APP要open, 要read, 要write等 ;应用程序就提供对映的drv_open, drv_read, drv_write等. 为了便于管理引入一个 file_operations 结构体, 把这些open, read等函数放入这个结构体. 构造好file_operations 结构体之后要用起来, 要注册进内核:register_chrdev(主设备号, 名字, file_operations);

    0. 确定主设备号;

    1. 构造file_operations 结构体

    2. 注册:register_chrdev(主设备号, 名字, file_operations);

    3. 入口

    4. 出口

    分配缓冲区不能用kmalloc来分配, 因为kmalloc分配出来的内存是虚拟地址上连续的, 物理地址上并非连续. 而想用DMA的话就必须用连续的物理内存, 这个DMA没那么高的智能, 它只能处理物理地址连续的内存.


(二). DMA驱动初步框架

dma.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

static int major = 0;

#define MEM_CPY_NO_DMA  0
#define MEM_CPY_DMA     1

static char *src;      /* 源 */
static u32 src_phys;   /* 源的物理地址 */

static char *dst;      /* 目的 */
static u32 dst_phys;   /* 目的的物理地址 */

static struct class *cls;

#define BUF_SIZE  (512*1024)   //512k

static int s3c_dma_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
	switch (cmd)
	{
		case MEM_CPY_NO_DMA :
		{
			break;
		}

		case MEM_CPY_DMA :
		{
			break;
		}
	}

	return 0;
}

/* 构造file_operations结构体 */
static struct file_operations dma_fops = {
	.owner  = THIS_MODULE,
	.ioctl  = s3c_dma_ioctl,
};

static int s3c_dma_init(void)
{
	/* 分配SRC, DST对应的缓冲区 */
	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)
	{
		dma_free_writecombine(NULL, BUF_SIZE, src, src_phys);
		printk("can't alloc buffer for dst\n");
		return -ENOMEM;
	}

	major = register_chrdev(0, "s3c_dma", &dma_fops);

	/* 为了自动创建设备节点 */
	cls = class_create(THIS_MODULE, "s3c_dma");
	class_device_create(cls, NULL, MKDEV(major, 0), NULL, "dma"); /* /dev/dma */
		
	return 0;
}

static void s3c_dma_exit(void)
{
	class_device_destroy(cls, MKDEV(major, 0));
	class_destroy(cls);
	unregister_chrdev(major, "s3c_dma");
	dma_free_writecombine(NULL, BUF_SIZE, src, src_phys);
	dma_free_writecombine(NULL, BUF_SIZE, dst, dst_phys);	
}

module_init(s3c_dma_init);
module_exit(s3c_dma_exit);

MODULE_LICENSE("GPL");


     注: 第66行里所说的内存泄漏是指, 这块内存被分配了, 却没有被调用,也没有被释放.

    上面的代码里只分配了两块内存,我们把重点的东西放在s3c_dma_ioctl里, 下面再来写.

(三). DMA驱动填充框架

      在开发板上执行# ls /dev/dma*  查看开发板上有没有这个设备节点, 没有继续, 有就去掉这个设备节点.

      填充s3c_dma_ioctl, 不用DMA:

static int s3c_dma_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
	int i;

	memset(src, 0xAA, BUF_SIZE);/* 把源的地址改为0xAA */
	memset(dst, 0x55, BUF_SIZE);/* 把目的的地址改为0x55 */
	
	switch (cmd)
	{
		case MEM_CPY_NO_DMA :
		{
			for (i = 0; i < BUF_SIZE; i++)
				dst[i] = src[i];/* 不使用DMA来拷贝 */
			if (memcmp(src, dst, BUF_SIZE) == 0)
			{
				printk("MEM_CPY_NO_DMA OK\n");
			}
			else
			{
				printk("MEM_CPY_DMA ERROR\n");
			}
			break;
		}

		case MEM_CPY_DMA :
		{
			break;
		}
	}

	return 0;
}

      手工启动DMA: 在本节例子中讲的DMA没有涉及其它设备, 可以手动启动DMA. 让cpu执行一条指令, 设置好源, 目的, 长度, 再来设置某个寄存器就可以启动DMA了.

     查看2440的的芯片手册:


    从上面2440芯片手册P262页可以看出2440里的这个DMA有四种传输方式.

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



    2440P263页4个通道的7个不同的触发源

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

    2440P265

    Demand Mode时:       XnXDREQ为低电平时, DMA就不断的读写数据, 直到XnXDREQ拉高为止;

    Handshake Mode时: XnXDREQ为低电平时, DMA只读写传输一次数据, 要想在传输下一个数据, 就需要再将XnXDREQ拉高再拉低, 如此重复.

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

     2440P266

     突发传输模式: 可以一次连读4次再连写4次; 也可以读一次写一次.

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


     2440P58

     设置DMA寄存器, 一个一个设置太麻烦, 定义一个结构体:

#define DMA0_BASE_ADDR  0x4B000000
#define DMA1_BASE_ADDR  0x4B000040
#define DMA2_BASE_ADDR  0x4B000080
#define DMA3_BASE_ADDR  0x4B0000C0

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 struct s3c_dma_regs *dma_regs; /* 定义一个结构体指针 */
    dma_regs = ioremap(DMA0_BASE_ADDR, sizeof(struct s3c_dma_regs));/* 用DMA的0通道 */

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




    2440芯片手册 P268-275页DMA

    设置以上寄存器, 设置的值我都标出来了就不一一对应来写了:

        case MEM_CPY_DMA:
        {
            
            
            /* 把源, 目的, 长度告诉DMA */
            dma_regs->disrc     = src_phys;                    /* 源的物理地址 */
            dma_regs->disrcc    = (0<<1) | (0<<0);             /* 源在AHB上, 源地址是递增的 */
            dma_regs->didst     = dst_phys;                    /* 目的的物理地址 */
            dma_regs->didstc    = (0<<2) | (0<<1) | (0<<0);    /* TC=0时发起中断,目的在AHB上,目的地址递增 */
            dma_regs->dcon      = (1<<29) | (0<<28) | (0<<23); /* 当数据传完之后使能中断,单个传输,用软件出发 */

            /* 启动DMA */
            dma_regs->dmasktrig = (1<<1) | (1<<0);             /* 打开DMA, 软件触发 */

            /* 如何知道DMA什么时候完成? */
            
            break;
        }

注意:在测试的时候我们发现: 30位和27位也要设为1;第27位如果不设置为1, 是0的话, 这个DMA在执行一次之后就不执行了; 第30位设为1是因为我们这时是在AHB总线上:

<strong><span style="background-color: rgb(255, 153, 255);">dma_regs->dcon      = (1<<30) | (1<<29) | (0<<28) | (1<<27) | (0<<23);</span></strong>


    在开发板上执行:

    # cat /proc/interrupts

    看看开发板上哪些中断被用了, 有没有人用过DMA的哪个通道, 我们只能选取没有被占用的通道(这里选IRQ_DMA3):

    request_irq(IRQ_DMA3, s3c_dma_irq, 0, "s3c_dma", 1);/* [中断号(用DMA3),中断处理函数, 触发边沿, 名字, id] 注册中断 */

    dma_regs = ioremap(DMA3_BASE_ADDR, sizeof(struct s3c_dma_regs));/* 用DMA的3通道 */

     如何唤醒DMA:

static DECLARE_WAIT_QUEUE_HEAD(dma_waitq);/* 定义一个队列 */
/* 中断事件标志, 中断服务程序将它置1,ioctl将它清0 */
static volatile int ev_dma = 0;/* 初始值为0 */

            /* 如何知道DMA什么时候完成? */
            /* 休眠 */
            wait_event_interruptible(dma_waitq, ev_dma);  /* 定义一个队列dma_waitq; 如果ev_dma = 0就休眠 */
static irqreturn_t s3c_dma_irq(int irq, void *devid);/* 中断号, devid */
{
    /* 当DMA完成之后在这里唤醒应用程序 */
    ev_dma = 1;
    wake_up_interruptible(&dma_waitq);   /* 唤醒休眠的进程 */
    return IRQ_HANDLED;
}
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

(四). 填充好出口, 完整的驱动程序如下:

#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/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
#include <linux/poll.h>
#include <linux/dma-mapping.h>

#define MEM_CPY_NO_DMA  0
#define MEM_CPY_DMA     1

#define BUF_SIZE  (512*1024)/* 512K */

#define DMA0_BASE_ADDR  0x4B000000
#define DMA1_BASE_ADDR  0x4B000040
#define DMA2_BASE_ADDR  0x4B000080
#define DMA3_BASE_ADDR  0x4B0000C0

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 int major = 0;

static char *src;        /* 源 */
static u32 src_phys;     /* 源的物理地址 */

static char *dst;        /* 目的 */
static u32 dst_phys;     /* 目的物理地址 */

static struct class *cls;  /* 创建类 */

static volatile struct s3c_dma_regs *dma_regs;/* 定义一个结构体指针 就上volatile避免被优化 */

static DECLARE_WAIT_QUEUE_HEAD(dma_waitq);/* 定义一个队列 */
/* 中断事件标志, 中断服务程序将它置1,ioctl将它清0 */
static volatile int ev_dma = 0;

static int s3c_dma_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
	int i;

	memset(src, 0xAA, BUF_SIZE);/* 把源的地址改为0xAA */
	memset(dst, 0x55, BUF_SIZE);/* 把目的的地址改为0x55 */
	
	switch (cmd)
	{
		case MEM_CPY_NO_DMA :
		{
			for (i = 0; i < BUF_SIZE; i++)
				dst[i] = src[i];              /* 不使用DMA来拷贝 */
			if (memcmp(src, dst, BUF_SIZE) == 0)  /* 拷贝完之后来比较一下, 如果它们等于0就表示相等 */
			{
				printk("MEM_CPY_NO_DMA OK\n");
			}
			else
			{
				printk("MEM_CPY_DMA ERROR\n");
			}
			break;
		}

		case MEM_CPY_DMA :
		{
			ev_dma = 0;
			
			/* 把源,目的,长度告诉DMA */
			dma_regs->disrc      = src_phys;        /* 源的物理地址 */
			dma_regs->disrcc     = (0<<1) | (0<<0); /* 源位于AHB总线, 源地址递增 */
			dma_regs->didst      = dst_phys;        /* 目的的物理地址 */
			dma_regs->didstc     = (0<<2) | (0<<1) | (0<<0); /* 目的位于AHB总线, 目的地址递增 */
			dma_regs->dcon       = (1<<30)|(1<<29)|(0<<28)|(1<<27)|(0<<23)|(0<<20)|(BUF_SIZE<<0);  /* 使能中断,单个传输,软件触发, */

			/* 启动DMA */
			dma_regs->dmasktrig  = (1<<1) | (1<<0); /* 打开DMA, 软件触发 */

			/* 如何知道DMA什么时候完成? */
			/* 休眠 */
			wait_event_interruptible(dma_waitq, ev_dma);/* 定义一个队列dma_waitq; 如果ev_dma = 0就休眠 */

			if (memcmp(src, dst, BUF_SIZE) == 0)
			{
				printk("MEM_CPY_DMA OK\n");
			}
			else
			{
				printk("MEM_CPY_DMA ERROR\n");
			}
			
			break;
		}
	}

	return 0;
}

static struct file_operations dma_fops = {
	.owner  = THIS_MODULE,
	.ioctl  = s3c_dma_ioctl,
};

static irqreturn_t s3c_dma_irq(int irq, void *devid)
{
	/* 唤醒 */
	ev_dma = 1;
    wake_up_interruptible(&dma_waitq);   /* 唤醒休眠的进程 */
	return IRQ_HANDLED;
}

static int s3c_dma_init(void)
{
	if (request_irq(IRQ_DMA3, s3c_dma_irq, 0, "s3c_dma", 1))/* [中断号(用DMA3),中断处理函数, 触发边沿, 名字, id] 注册中断 */
	{/* 如果返回值不等于0就表明失败 */
		printk("can't request_irq for DMA\n");
		return -EBUSY;
	}
	
	/* 分配SRC, DST对应的缓冲区 */
	src = dma_alloc_writecombine(NULL, BUF_SIZE, &src_phys, GFP_KERNEL);
	if (NULL == src)
	{
		printk("can't alloc buffer for src\n");
		free_irq(IRQ_DMA3, 1);/* 如果request_irq(IRQ_DMA3, s3c_dma_irq, 0, "s3c_dma", 1)分配失败就要释放它 */
		return -ENOMEM;
	}
	
	dst = dma_alloc_writecombine(NULL, BUF_SIZE, &dst_phys, GFP_KERNEL);
	if (NULL == dst)
	{
		free_irq(IRQ_DMA3, 1);/* 如果request_irq(IRQ_DMA3, s3c_dma_irq, 0, "s3c_dma", 1)分配失败就要释放它 */
		dma_free_writecombine(NULL, BUF_SIZE, src, src_phys);
		printk("can't alloc buffer for dst\n");
		return -ENOMEM;
	}

       /* 分配成功之后来注册字符设备 */
	major = register_chrdev(0, "s3c_dma", &dma_fops);

	/* 为了自动创建设备节点 */
	cls = class_create(THIS_MODULE, "s3c_dma");
	class_device_create(cls, NULL, MKDEV(major, 0), NULL, "dma"); /* /dev/dma */

	dma_regs = ioremap(DMA3_BASE_ADDR, sizeof(struct s3c_dma_regs));/* 用DMA的3通道 */
		
	return 0;
}

static void s3c_dma_exit(void)
{
	iounmap(dma_regs);                                    /* 释放ioremap */
	class_device_destroy(cls, MKDEV(major, 0));           /* 消除设备节点 */
	class_destroy(cls);                                   /* 消除类 */
	unregister_chrdev(major, "s3c_dma");                  /* 注销字符设备 */
	dma_free_writecombine(NULL, BUF_SIZE, src, src_phys); /* 释放src的内存 */
	dma_free_writecombine(NULL, BUF_SIZE, dst, dst_phys); /* 释放dst的内存 */
	free_irq(IRQ_DMA3, 1);                                /* 释放DMA */
}

module_init(s3c_dma_init);
module_exit(s3c_dma_exit);

MODULE_LICENSE("GPL");

三. DMA测试程序编写

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <string.h>

/* 用法:
 * ./dma_test nodma
 * ./dma_test dma
 */
#define MEM_CPY_NO_DMA  0
#define MEM_CPY_DMA     1

void print_usage(char *name)
{
	printf("Usage:\n");
	printf("%s <nodma | dma>\n", name);
}


int main(int argc, char **argv)
{
	int fd;
	
 	if (argc != 2)
	{
		print_usage(argv[0]);
		return -1;
	}

	fd = open("/dev/dma", O_RDWR);/* 打开文件 */
	if (fd < 0)
	{
		printf("can't open /dev/dma\n");/* fd<0 说明打开失败 */
		return -1;
	}

	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 -1;
	}
	return 0; 	
}

     把测试程序拷贝到服务器里:

编译:

$ arm-linux-gcc -o dma_tast dma_test.c

把编译好的测试程序和驱动程序拷贝到网络文件系统里来测试一下:

装载新驱动:

# insmod dma.ko

看看有没有装载成功:

# cat /proc/interrupts

# ls /dev/dma -l

查看用法:

# ./dma_test

不用dma测试(让它在后台运行):

# ./dma_test nodma &

    它会调用

            for (i = 0; i < BUF_SIZE; i++)
                dst[i] = src[i]; /* 不使用DMA来拷贝 */

    来传输数据, 这样会一直消耗cpu资源, 你在执行什么指令它都要等到很久之后才能执行, 或者说它们都在竞争cpu.
# ./dma_test dma &

这时候我可一个看到已经在运行了, 而且我们在这时执行其它命令也可以立刻执行, 这就是MDA的好处.

    好了,就讲到这里,有不对的地方请联系我,谢谢阅读!!















版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

DMA基本概念及linux2440下DMA驱动程序编写与测试

1、基本概念 DMA即Direct Memory Access(直接存储器存取),那么为什么要引入这么个东东呢?它的作用又是什么呢?我们通过一个例子来说明: 比 如当我们要往内存里面拷贝一...
  • Aniu127
  • Aniu127
  • 2014年09月03日 15:10
  • 450

DMA基本概念及linux2440下DMA驱动程序编写与测试

DMA基本概念及linux2440下DMA驱动程序编写与测试 分类: linux 6410驱动2013-02-25 19:11 1063人阅读 评论(5) 收藏 举报 转自:htt...

WINDOWS下PCI接口卡WDM驱动程序的DMA编程技术

      本文主要讨论了在Windows环境下开发PCI接口卡DMA应用的WDM编程技术,并给出了一个应用DriverWorks和VC++开发的实例程序代码。关键词:Windows、PCI总线、WD...
  • Augusdi
  • Augusdi
  • 2011年03月05日 18:41
  • 3365

linux下S3C2410的DMA驱动程序开发

网上介绍LINUX下的一般驱动程序开发示例浩如烟海,或是因为简单,关于DMA驱动的介绍却寥寥无几;近期zhaoyang因工作需要,花了几日时间开发了某设备在S3C2410处理器Linux下DMA通信的...

S3C2410的linux 下DMA驱动程序开发

网上介绍LINUX下的一般驱动程序开发示例浩如烟海,或是因为简单,关于DMA驱动的介绍却寥寥无几;近期zhaoyang因工作需要,花了几日时间开发了某设备在S3C2410处理器Linux下DMA通信的...

WINDOWS下PCI接口卡WDM驱动程序的DMA编程技术1

摘要:       本文主要讨论了在Windows环境下开发PCI接口卡DMA应用的WDM编程技术,并给出了一个应用DriverWorks和VC++开发的实例程序代码。 关键词:Windo...

DMA驱动程序

DMA驱动程序 在单CPU系统中,CPU同一时间只能做一件事,为了提高效率引入了DMA。 下面的代码用DMA方式实现两块内存之间的数据传输 大概步骤如下: 1. 把源,目的,长度告诉DMA 2.设置D...

Nand Flash驱动程序编写指南-1

  本文根据我的项目经验并结合博客园Sankye的《Linux下nand flash驱动》一文编写的,有雷同之处请多海涵。在编写nand flash之前,我们需要先了解以下要处理的设备的硬件特性。Fl...
  • JuanA1
  • JuanA1
  • 2011年06月30日 16:27
  • 2491
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:DMA驱动程序编写
举报原因:
原因补充:

(最多只允许输入30个字)