本帖最后由 weidongshan 于 2018-1-2 17:41 编辑
首发平台:威信订阅号baiwenkeji
最近在看韦老师的视频,讲解了很多种字符设备的驱动写法。经过自己的研究之后,我发现还有另外一种写法,直接在应用层操作,省去了内核中的地址映射部分,使得用户可以在应用层直接操作LED。
mmap方法是把设备物理地址直接映射到用户空间的一种系统调用方法,他使得用户可以在应用层直接操作硬件设备,而不必在驱动里使用ioremap做地址映射。这在一定程度上实现了传说中的“零拷贝”技术。即,设备的数据不用经过内核的转存,再向应用层提交数据。由于设备物理地址直接映射到了用户空间,所以就相当于省去了内核的中间媒介,用户空间直接去操作硬件设备。
总结一下,mmap方法的用处是把设备(文件)内容直接映射到进程虚拟空间,通过对这个虚拟地址的读写修改,实现对设备(文件)的读写和修改,从而不必使用read、write等系统调用即可实现对设备的操作。
mmap的内核态函数为:
- int (*mmap)(struct file *filp,struct vm_area_struct *vma)
复制代码
结构体struct vm_area_struct *vma是我们在使用mmap系统调用的时候内核帮我们找到的虚拟地址区间,它的主要成员是:
vma->vm_start: 映射后的用户态虚拟地址起始地址;
vma->vm_end: 映射后的用户态虚拟地址结束地址;
vma->vm_pgoff: 物理地址所在的页帧号,它的值由用户空间传进来的物理地址右移PAGE_SHIFT位得到,PAGE_SHIFT值为12,那么它右移12位就得到物理地址的页帧号(一页大小为4KB)。
下面是我写的内核驱动程序,在TQ2440 开发板上运行: #includelinux/kernel.h #include linux/init.h #include linux/module.h #include linux/fs.h #include linux/cdev.h #include linux/mm.h #include linux/device.h #define DEV_NAME mmapled struct mmapled { dev_t devno; struct cdev mcdev; struct class *mmap_class; }; structmmapled *mpt; intmmapled_open(struct inode *inode, struct file *filp) { printk(In kernel open,major =%d,minor =%dn,MAJOR(mpt->devno),MINOR(mpt->devno)); return 0; } intmmapled_close(struct inode *inode,struct file *filp) { return 0; } /*mmap系统调用函数 */ intmmapled_mmap(struct file *filp,structvm_area_struct *vma) { int ret; vma->vm_flags |= VM_RESERVED; vma->vm_flags |= VM_IO; vma->vm_page_prot =pgprot_noncached(vma->vm_page_prot); /* vma->vm_pgoff为用户层off, PAGE_SHIFT,即物理地址的页帧号,映射大小必为PAGE_SIZE整数倍*/ ret=remap_pfn_range(vma,vma->vm_start,vma->vm_pgoff,vma->vm_end-vma->vm_start,vma->vm_page_prot); if(ret) { printk(remap_pfn_range err!n); return -EAGAIN; } printk(In%s,pgoff = %lx, start= %lx,end = %lxn,__func__,vma->vm_pgoff,vma->vm_start,vma->vm_end); return0; } /* 文件操作结构体 */ structfile_operations mmapled_fops = { .owner =THIS_MODULE, .open =mmapled_open, .release = mmapled_close, .mmap = mmapled_mmap, }; /* 驱动程序入口函数 */ intmmapled_init(void) { int ret; mpt = kzalloc(sizeof(structmmapled),GFP_KERNEL); if(!mpt) { printk(kzalloc mpt err!n); return -ENOMEM; } /* 动态分配主设备号,起始次设备号为0 */ ret= alloc_chrdev_region(mpt->devno,0,1,DEV_NAME); if(ret) { printk(register chrdev err!n); kfree(mpt); return ret; } /* 创建类,用于自动创建设备节点 */ mpt->mmap_class=class_create(THIS_MODULE,DEV_NAME); if(IS_ERR(mpt->mmap_class)) { printk(KERN_ALERTabsmem_class createfailed.n); kfree(mpt); unregister_chrdev_region(mpt->devno,1); return -1; } cdev_init(mpt-mcdev,mmapled_fops); mpt->mcdev.owner=THIS_MODULE; cdev_add(mpt->mcdev,mpt->devno,1); /* 创建设备节点mmapled0 */ device_create(mpt->mmap_class,NULL,mpt->devno,NULL,mmapled%d,0); return0; } /* 驱动程序出口函数 */ voidmmapled_exit(void) { device_destroy(mpt->mmap_class,mpt->devno); cdev_del(mpt->mcdev); class_destroy(mpt->mmap_class); unregister_chrdev_region(mpt->devno,1); kfree(mpt); } module_init(mmapled_init); module_exit(mmapled_exit); MODULE_LICENSE(GPL);
用户态的mmap函数接口为:
- void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_toffset)
复制代码
函数参数的意义如下:
addr:指定映射的起始地址,即进程虚拟空间的虚拟地址,人为的指定;通常设置为NULL,让内核帮我们去指定;
len: 要映射的区间大小;
prot: 映射区的保护方式,可以取以下值:
PROC_EXEC:映射区可被执行;
PROC_READ:映射区可被读取;
PROC_WRITE:映射区可写;
PROC_NONE:映射区不能存取。
flags是映射区的特性,可以取以下值:
MAP_SHARED:写入映射区的数据会复制回文件,且允许其他映射该文件的进程共享;
MAP_PRIVATE:对映射区的写入会产生一个映射区的复制(COPY_ON_WRITE),对此映射区的修改不会写入源文件;
fd:由open函数返回的文件描述符;
offset:文件开始处的偏移量,必须是分页大小的整数倍。
函数返回值:映射得到的用户虚拟地址;
下面是我写的用户态的程序,供大家参考(在TQ2440开发板上运行,如果是其他开发板,可以参考原理图做一些修改): /* 函数功能:实现4个LED灯的同时亮灭,间隔为1秒 */ #includestdio.h #includestring.h #includefcntl.h #includesys/mman.h #defineDEV_NAME /dev/mmapled0 intmain() { int fd,k; void *start, *reg=NULL; fd = open(DEV_NAME,O_RDWR); if(fd&0) { printf(Open device err!n); return -1; } /*参数解释: * NULL:映射到的内核虚拟地址,设置为NULL由内核决定 * 1024*4:映射大小,最小一页,必为页大小的整数倍 * 映射区的权限 * 对映射区的修改改变映射区的内容 * fd:open返回的文件描述符 * 物理地址,一个页的起始物理地址,它PAGE_SHIFT之后传给驱动的vma结构体的vm_pgoff */ /*0x56000000是LED等所在的GPIO口的BANK起始物理地址*/ /*start是得到的虚拟地址*/ start=mmap(NULL,1024*4,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0x56000000); if(start== NULL) { printf(mmap err!n); return -1; } reg= start + 0x10; //GPBCON,控制寄存器 *(unsigned*)reg= 0xfffc03ff; // [17:10]清零 *(unsigned*)reg|= 0x00015400; // [17:10]=01010101,输出功能 reg= start + 0x14; //GPBDAT /* 量灭k次,实现对LED的操作 */ k=25; while(k--) { *(unsigned*)reg & = ~(0x1e0); //[8:5], set 0,led on sleep(1); *(unsigned*)reg |= 0x1e0; // [8:5], set 1,led off sleep(1); printf(k= %dn,k); } /* 取消映射 */ munmap(start,1024*4); close(fd); return0; } 完。 稍微修改代码即可在jz2440上运行,需要代码的学员请留下邮箱,我们发给您。
|