Linux DMA驱动程序
学习目的:
- 熟悉Linux下DMA驱动程序编写
1、DMA基本概念
DMA,全称Direct Memory Access,即直接储存器访问。
它是一种高速的数据传送操作,可用于芯片的外设和存储器或存储器和存储器或外设和外设之间数据的传输。DMA的数据传送过程不需要CPU干预,是通过DMA控制器完成的,因此在DMA数据传送过程中不占用CPU的资源。不过在启动DMA控制器进行数据传输前,需要CPU来告诉DMA控制器数据传输的源地址、目的地址、数据传输长度等信息。
2、硬件相关配置
DMA控制器的配置依赖于硬件平台,这里使用的是S3C2440芯片,它有一个4通道的DMA控制器,每个通道都可以不受限制地在系统总线和/或外围总线中的设备之间执行数据移动。
2.1 请求模式设置
S3C2440芯片的DMA数据传输支持两者请求模式,一种是硬件请求模式,另一种是软件请求模式。请求模式可以通过DMA控制寄存器DCONn的第23位来设置,如果设置成0,该通道的DMA工作在软件请求模式,如果设置成1,则该通道DMA工作在硬件请求模式。
对于工作在硬件请求模式的DMA,各通道的请求源,如下图所示:
各通道请求源的选择可以通过DMA控制寄存器DCONn的[26:24]设置,如想设置通道0的DMA传输的请求源为UART0,只要设置DCON0[26:24] = 001,这样通道0的DMA何时进行数据传送将由UART0外设决定。
对于工作在软件请求模式的DMA,可以由软件来触发该通道的DMA进行数据的传输。设置DMASKTRIGn寄存器的SW_TRIG位为1,请求DMA数据传输。
2.2 传输源、目的地址设置
DMA数据传输的源起始地址、目的起始地址分别是在寄存器DISRCn和寄存器DIDSTn里设置的
如我们想使用DMA通道0将0x00000000为起始地址数据传输到0x10000000的地址中去,直接将0x00000000写入到DISRC0源起始地址寄存器,将0x10000000写入到DIDST0寄存器,这样就告诉了DMA控制器数据传输的起始位置信息
但是DMA控制器不像我们想的那么智能,只告诉数据传输地址信息还是不够的,还要去指定可以访问源、目的地址的总线信息,以及传输过程地址是否增加。这些信息的设置分别是在DISRCCn和DIDSTCn寄存器进行的
DISRCCn [1]:源地址所在的总线 DISRCCn[0]: 传输过程源地址是否增加
DIDSTCn [2]:中断发生时间 DIDSTCn [1]:目的地址所在的总线 DIDSTCn[0]: 传输过程目的地址是否增加
2.3 传输长度设置
我们知道了源、目的地址如何设置的,这在传输数据过程还是不够的,还需要知道传输数据的长度。传输数据的长度是通过DCONn寄存器的TSZ 、DSZ 、TC位一起间接确定的,传输数据长度 = TSZ * DSZ * TC
DCONn寄存器的TSZ位[28]用来选择DMA数据传输的方式,0表示单元传输,1表示突发传输
1)单元传输
指传输过程中,每执行一次,则读1次,写1次
2)突发传输
指传输过程中,每执行一次,则读4次,然后写4次
DCONn寄存器的DSZ位[21:20]决定的是每次传输的数据位宽 00=Byte 01=Half Word 10=Word
DCONn寄存器的TC位[19:0]表示传输次数
假设我们要传输1024字节数据,我们多种选择方式,如选择单元传输,传输数据位宽是1个字节,那么传输次数TC就是1024,也可以选单元传输,传输数组位宽是4字节,那么传输次数TC就是256。
我们只需保证TSZ * DSZ * TC求出结果等于我们要传输数据长度即可
3、内核DMA缓冲区分配函数
对于DMA传输使用的缓冲区,需要使用物理地址连续的内存,而且对物理地址和虚拟地址的操作都应该直接改变缓冲区的内容。所以如果DMA使用cache,那么一定要考虑cache的一致性。解决DMA导致的一致性的方法最简单的是禁止DMA目标地址范围内的cache功能,因此我们不能直接使用kzmalloc直接分配
(注:经过DMA操作,cache缓存对应的内存数据已经被修改了,而CPU本身不知道(DMA传输是不通过CPU的),它仍然认为cache中的数 据就是内存中的数据,以后访问Cache映射的内存时,它仍然使用旧的Cache数据。这样就发生Cache与内存的数据“不一致性”错误。)
内核中提供了一些专门用于DMA使用内存的分配和释放的函数,在这里做个简单介绍
1)DMA缓冲区分配
void *dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp)
dma_alloc_writecombine函数分配的内存禁止了C(cache)域,不会使用缓存,但会使用写缓冲区,函数参数如下:
- dev:平台初始化里指定,主要用到dma_mask之类参数,不使用可设置成NULL
- size:申请分配内存大小
- handle:返回申请到的物理内存起始地址
- gfp:分配出内存的参数
函数返回值是分配内存起始地址的虚拟地址
2)DMA缓冲区释放
#define dma_free_writecombine(dev,size,cpu_addr,handle) \ dma_free_coherent(dev,size,cpu_addr,handle)
dma_free_writecombine函数用来释放分配的DAM内存,函数参数如下:
- dev:平台初始化里指定,主要用到dma_mask之类参数,不使用可设置成NULL
- size:释放内存大小
- cpu_addr:释放内存虚拟地址
- handle:释放内存物理地址
4、DMA驱动编写
编写一个字符驱动程序,程序中分配两个固定大小的缓冲区,分别支持普通数据传输和DMA数据传输。然后,编写应用程序分别使用这两种传输方式进行数据传输,来进一步感受DMA数据传输的优势
注册字符设备的file_operations结构体中实现了.open、.unlocked_ioctl、.release成员
static struct file_operations fops = { .owner = THIS_MODULE, .open = dma_drv_open, .unlocked_ioctl = dma_drv_ioctl, .release = dma_drv_close, };
dma_drv_open函数
static int dma_drv_open(struct inode *inode, struct file *file) { if (request_irq(IRQ_DMA3, s3c_dma_irq, 0, "s3c_dma", NULL))-------------->s3c_dam_irq是DMA数据传输完成后的中断处理函数 { printk("can't request_irq for DMA\n"); return -EBUSY; } src_addr = (char *)dma_alloc_writecombine(NULL, COPY_BUFF_SIZE, &src_phy_addr, GFP_KERNEL); if(src_addr == NULL) { free_irq(IRQ_DMA3, NULL); printk("can't alloc buffer for src\n"); return -ENOMEM; } dst_addr = (char *)dma_alloc_writecombine(NULL, COPY_BUFF_SIZE, &dst_phy_addr, GFP_KERNEL); if(dst_addr == NULL) { free_irq(IRQ_DMA3, 1); dma_free_writecombine(NULL, COPY_BUFF_SIZE, src_addr, src_phy_addr); printk("can't alloc buffer for dst\n"); return -ENOMEM; } dma_regs = ioremap(DMA3_BASE_ADDR, sizeof(struct s3c_dma_regs)); return 0; }
dma_drv_open函数在使用open打开设备节点时被调用,open函数中分配了用于DMA传输的内存,注册了DMA数据传输完成后中断处理函数,完成了S3C2440的DMA控制器的寄存器物理地址到虚拟地址映射
dma_drv_ioctl函数
static long dma_drv_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { unsigned int n; memset(src_addr, 0x55, COPY_BUFF_SIZE); memset(dst_addr, 0, COPY_BUFF_SIZE); switch(cmd) { case COPY_NO_DMA: for(n = 0; n < COPY_BUFF_SIZE; n++) dst_addr[n] = src_addr[n]; if(memcmp(src_addr, dst_addr, COPY_BUFF_SIZE) == 0) { printk("MEM_CPY_NO_DMA OK\n"); } else { printk("MEM_CPY_DMA ERROR\n"); } break; case COPY_BY_DMA: trans_ok = 0; /* 把源,目的,长度告诉DMA */ dma_regs->disrc = src_phy_addr; /* 源的物理地址 */ dma_regs->disrcc = (0<<1) | (0<<0); /* 源位于AHB总线, 源地址递增 */ dma_regs->didst = dst_phy_addr; /* 目的的物理地址 */ 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)|(COPY_BUFF_SIZE<<0); /* 使能中断,单个传输,软件触发, */ /* 启动DMA */ dma_regs->dmasktrig = (1<<1) | (1<<0); wait_event_interruptible(dma_waitq, trans_ok); if (memcmp(src_addr, dst_addr, COPY_BUFF_SIZE) == 0) { printk("MEM_CPY_DMA OK\n"); } else { printk("MEM_CPY_DMA ERROR\n"); } break; default: break; } return 0; }
dma_drc_ioctl函数在应用程序使用ioctl操作打开设备文件描述符时被调用,这个函数实现了驱动程序的核心功能。该函数支持应用程序传入两种类型命令,一种是使用普通传输模式进行数据传送,另一种是使用DMA传输模式进行数据传送
cmd参数:
COPY_NO_DMA:普通数据传输方式
COPY_BY_DMA:使用DMA控制器进行数据传送
对于DMA传输模式在启动DMA控制器进行传输后,应用程序进入休眠状态,直到DMA控制器将数据传输完毕后触发相关中断程序,在中断程序中被唤醒。唤醒之后来比较源地址和目的地址内容,如果内容一致传输成功,打印提示语句
dma_drv_close函数
static int dma_drv_close(struct inode *inode, struct file *file) { printk("dma_drv_close...\n"); dma_free_writecombine(NULL, COPY_BUFF_SIZE, src_addr, src_phy_addr); dma_free_writecombine(NULL, COPY_BUFF_SIZE, dst_addr, dst_phy_addr); free_irq(IRQ_DMA3, NULL); iounmap(dma_regs); return 0; }
dma_drv_close在close文件描述符时被调用,函数的功能和dma_drv_open函数相反,释放申请的源、目的缓存区,释放中断,取消DMA寄存器物理地址到虚拟地址的映射
5、小结
编写了一个简单的DMA驱动程序,同时也对S3C2440的DMA控制器的设置做了简单介绍。通过对驱动程序进行测试,可以体会到DMA控制器在数据传送过程中的优势所在。另外,对于DMA硬件相关的操作,在linux内核中三星已封装好了,在编写实际的驱动程序中可以尝试着区直接使用
完成驱动程序
#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 <plat/gpio-fns.h> #include <mach/gpio-nrs.h> #include <linux/interrupt.h> #include <linux/wait.h> #include <linux/sched.h> #include <linux/device.h> #include <linux/gpio.h> #include <linux/dma-mapping.h> #define COPY_NO_DMA 0 #define COPY_BY_DMA 1 #define COPY_BUFF_SIZE (1024*512) #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 int trans_ok = 0; static char *src_addr; static dma_addr_t *src_phy_addr; static char *dst_addr; static dma_addr_t *dst_phy_addr; static volatile struct s3c_dma_regs *dma_regs; static DECLARE_WAIT_QUEUE_HEAD(dma_waitq); static struct class *cls; static struct class_device *cls_dev; static irqreturn_t s3c_dma_irq(int irq, void *dev_id) { trans_ok = 1; wake_up_interruptible(&dma_waitq); return IRQ_HANDLED; } static int dma_drv_open(struct inode *inode, struct file *file) { if (request_irq(IRQ_DMA3, s3c_dma_irq, 0, "s3c_dma", NULL)) { printk("can't request_irq for DMA\n"); return -EBUSY; } src_addr = (char *)dma_alloc_writecombine(NULL, COPY_BUFF_SIZE, &src_phy_addr, GFP_KERNEL); if(src_addr == NULL) { free_irq(IRQ_DMA3, NULL); printk("can't alloc buffer for src\n"); return -ENOMEM; } dst_addr = (char *)dma_alloc_writecombine(NULL, COPY_BUFF_SIZE, &dst_phy_addr, GFP_KERNEL); if(dst_addr == NULL) { free_irq(IRQ_DMA3, 1); dma_free_writecombine(NULL, COPY_BUFF_SIZE, src_addr, src_phy_addr); printk("can't alloc buffer for dst\n"); return -ENOMEM; } dma_regs = ioremap(DMA3_BASE_ADDR, sizeof(struct s3c_dma_regs)); return 0; } static long dma_drv_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { unsigned int n; memset(src_addr, 0x55, COPY_BUFF_SIZE); memset(dst_addr, 0, COPY_BUFF_SIZE); switch(cmd) { case COPY_NO_DMA: for(n = 0; n < COPY_BUFF_SIZE; n++) dst_addr[n] = src_addr[n]; if(memcmp(src_addr, dst_addr, COPY_BUFF_SIZE) == 0) { printk("MEM_CPY_NO_DMA OK\n"); } else { printk("MEM_CPY_DMA ERROR\n"); } break; case COPY_BY_DMA: trans_ok = 0; /* 把源,目的,长度告诉DMA */ dma_regs->disrc = src_phy_addr; /* 源的物理地址 */ dma_regs->disrcc = (0<<1) | (0<<0); /* 源位于AHB总线, 源地址递增 */ dma_regs->didst = dst_phy_addr; /* 目的的物理地址 */ 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)|(COPY_BUFF_SIZE<<0); /* 使能中断,单个传输,软件触发, */ /* 启动DMA */ dma_regs->dmasktrig = (1<<1) | (1<<0); wait_event_interruptible(dma_waitq, trans_ok); if (memcmp(src_addr, dst_addr, COPY_BUFF_SIZE) == 0) { printk("MEM_CPY_DMA OK\n"); } else { printk("MEM_CPY_DMA ERROR\n"); } break; default: break; } return 0; } static int dma_drv_close(struct inode *inode, struct file *file) { printk("dma_drv_close...\n"); dma_free_writecombine(NULL, COPY_BUFF_SIZE, src_addr, src_phy_addr); dma_free_writecombine(NULL, COPY_BUFF_SIZE, dst_addr, dst_phy_addr); free_irq(IRQ_DMA3, NULL); iounmap(dma_regs); return 0; } static struct file_operations fops = { .owner = THIS_MODULE, .open = dma_drv_open, .unlocked_ioctl = dma_drv_ioctl, .release = dma_drv_close, }; static int __init dma_drv_init(void) { major = register_chrdev(0, "dma_dev", &fops); cls = class_create(THIS_MODULE, "dma_cls"); cls_dev = device_create(cls, NULL, MKDEV(major, 0), NULL, "dma_drv"); return 0; } static void __exit dma_drv_exit(void) { unregister_chrdev(major, "dma_dev"); device_unregister(cls_dev); class_destroy(cls); } module_init(dma_drv_init); module_exit(dma_drv_exit); MODULE_LICENSE("GPL");
完整测试程序
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #define COPY_NO_DMA 0 #define COPY_BY_DMA 1 #define IOCTL_CNT (100*10000) void print_usage(char *name) { printf("Usage: %s <no_dma | dma>\n", name); } int main(int argc, char **argv) { int fd; int n, cmd; if(argc != 2) { print_usage(argv[0]); return -1; } fd = open("/dev/dma_drv", O_RDWR); if(fd < 0) { printf("can't open\n"); return -1; } if(strcmp(argv[1], "no_dma") == 0) { cmd = COPY_NO_DMA; } else if(strcmp(argv[1], "dma") == 0) { cmd = COPY_BY_DMA; } else { print_usage(argv[0]); return -1; } while(n < IOCTL_CNT) { ioctl(fd, cmd); n++; } close(fd); return 0; }