Linux设备驱动第十天(mmap、linux内核分离(软硬分离)思想)
2016-12-30 00:23
465 查看
回顾:
1.linux内核如何管理内存
kmalloc
kzmalloc
__get_free_pages
vmalloc
vmalloc = 256M
mem = 10M
GFP_KERNEL
GFP_ATOMIC
2.1 linux内核地址映射的函数: ioremap
linux内核mmap机制:
mmap映射内存必须是页面大小的整数倍!!
案例:分析LED和按键驱动
结论:对于LED和按键驱动 ,整个数据的访问操作都要经过两次的数据拷贝过程:用户空间和内核空间数据拷贝,内核空间和硬件的数据拷贝;如果数据量比较小,对系统性能的影响几乎可以忽略不计,但是如果数据量比较大,这种影响就不能忽略不计,比如视频采集卡、摄像头、显卡、声卡此类设备,硬件处理的数据量比较在在,如果还是采用两次的数据拷贝,无形会对系统的性能造成很大的影响;
如何解决这类设备的数据访问呢?
如果采用read、write、ioctl势必经过两次的数据拷贝,但实际上数据在处理的时候,关键涉及到用户空间和硬件,而内核空间仅仅是作为数据的一个暂存!所以为了提高数据的访问性能,只需要将硬件映射到用户空间即可,一经映射,以后用户在用户空间访问设备,就无需经过内核空间,将数据的处理由2次变成1次的数据拷贝,大大提高数据的处理能力,就需要利用mmap进行映射(把硬件设备的物理信息映射到用户空间)!!
linux系统mmap系统调用过程:
1,应用程序调用mmap,首先调用C库的mmap
2,C库的mmap保存mmap的系统调用到R7中
3,C库的mmap然后调用svc指令,触发软中断,用户进程由用户空间陷入内核空间
4,进入内核空间以后,跳转到内核准备好的异常向量表的软中断处理入口vector_swi;
5,从R7中取出系统调用号,在系统调用表中找到对应的系统调用函数sys_mmap
6,sys_mmap做如下事情:
在当前进程的3G用户虚拟内存的MMAP 内存映射工找到一块空闲的内存区域,一旦找到,内核struct vm_area_struct结构体创建一个对象来描述这块空闲的内存区域的信息:
7,最终sys_mmap调用底层驱动的mmap接口
8,底层驱动的mmap接口中只做一件事:将设备的物理地址映射到用户空间3G的MMAP内存映射区中空闲的内存区域!
9,用户空间的mmap函数的返回值就是这块空闲区域的首地址(vm_start)
底层驱动的mmap接口:
struct file_operations{
int (*mmap)(struct file *file,struct vm area struct *vma)
}
接口函数功能:
只做一件事:将设备的物理地址和用户空间的虚拟地址进行映射!一旦完成映射,用户在用户空间来访问设备!
参数说明:
vma:指向内核帮你一块空闲的虚拟内存区域,然后内核创建的描述 这块虚拟内存的struct vm_area struct对象!
所以:底层驱动的mmap接口函数通过这个指针能获取这块虚拟内存区域的信息
问:底层驱动的mmap如何将物理地址映射到用户空间的虚拟地址上?
答:remmap_pfn_range函数完成这个动作!
函数功能:用于底层驱动 的mmap函数,将物理地址映射用户的虚拟地址上!
vma:用户虚拟内存区域指针
addr:用户虚拟内存起始地址
pfn:要映射的物理地址所在页帧号,可以通过物理地址>>PAGE_SHIFT -12 得到
size:待映射的内存区域的大小
prot:vma保存属性,vm_page_prot
切记:在进行地址映射时,指定的虚拟地址和物理地址必须是页的整数倍。
例如:GPC1_3,GPC1_4的寄存器地址;
0xE0200080,0xE0200084,这两个地址不是页的整数倍,但是:通过芯片手册,GP10对应的寄存器基地址为:0xE0200000,所有在地址映射时,可以指定为0xE02000000这个物理地址对应的GPC1_3,GPC1_4的地址偏移量:
物理地址 用户虚拟地址
0xE0200000 A
0xE0200080 A+0x80
0xE0200084 A+0x84
案例:利用mmap实现LED驱动!!
切记:对于GPIO这类设备,在地址映射时,一定要关闭cache功能!关闭cache的方法:
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot)
一个物理地址可以有多个对应的虚拟地址!!
对应的测试程序:
案例:硬件LED的管脚发生变化:
GPC1_3 -> GPF1_5
GPC1_4 -> GPF1_6
修改GPC1的驱动!
总结:硬件一旦发生变化 ,驱动跟着变!
驱动程序一般包括两部分内容,一部分纯硬件内容,另一部份是纯软件,一旦硬件发生变化以后,之前的驱动程序在实现都需要重头到尾挨个检查然后进行修改,驱动的跨硬件平台的可移植性非常差!对这种驱动,linux内核采用分离思想优化驱动:
linux内核分离思想其实本质就是将软件和硬件分离开,软件一旦写好以后,如果仅仅是硬件变化,以后软件无需改动,只需要该硬件即可,让驱动开发者的重心放在硬件上!
问:linux内核分离思想如何实现?
答:linux内核分离思想采用:设备-总线-驱动模型实现!
在内核里面已经帮你用软件定义好了一个虚拟总线,这个虚拟总线叫平台总线(platform_bus_type);在这个总线上维护着两个链表:dev链表、drv链表;
dev链表上的每一个节点存放的硬件相关信息,节点的数据结构为struct platform device(平台设备),这个结构体就是用来装载硬件相关的信息;每当用这个数据结构来描述一个硬件信息时,只需要分配初始化这个对象即可,然后添加到dev链表上以后,内核会帮你遍历drv链表,取出drv链接上每一个软件节点,内核通过调用总线提供的match函数,比较硬件节点和软件的name字段,如果相等,说明硬件和软件匹配成功,内核调用软件节点的probe函数,然后把匹配的硬件节点的首地址传递给probe函数,硬件、软件再次结合!probe函数中如何操作,由驱动开发者来实现!
drv链表上的每一个节点存放的软件相关信息,节点的数据结构为struct platform_driver(平台驱动),这个结构体就是用来装载软件相关的信息;每当用这个数据结构来描述一个软件信息时,只需要分配初始化这个对象即可,然后添加到dev链表上以后,内核会帮你遍历dev链表,取出dev链接上每一个软件节点,内核通过调用总线提供的match函数,比较硬件节点和软件的name字段,如果相等,说明硬件和软件匹配成功,内核调用软件节点的probe函数,然后把匹配的硬件节点的首地址传递给probe函数,硬件、软件再次结合!probe函数中如何操作,由驱动开发者来实现!
总结:对于一个硬件的设备驱动实现只需要关注围绕着struct platform_device和struct platform_driver结构体实现驱动即可!
明确,一个完整的驱动必然包括软件和硬件!
问:如何使用以上两个结构体呢?
答:
struct platform_device结构体的使用过程:
作用:专门用来描述和装载硬件相关的信息!
成员说明:
name:硬件节点名称,这个字段相当重要!!由于软件和硬件都是通过来name来匹配的
id:硬件节点的编号,如果只有一个硬件节点,一般指定为-1,如果有多个同名的硬件节点,通过Id编号区分(0,1,2,3….)
dev:在这个结构体变量中,重点关注其中的platform_data(数据类型为void *),platform_data指针用来装载驱动开发者自己声明描述硬件信息的结构体(struct led_resource,struct btn_resource),例如:
resource:用来装载resource类型硬件资源信息,数据类型为:
struct resource {
unsigned long start;//硬件资源的起始信息
unsigned long end;//硬件资源的结束信息
unsigned long flags;//硬件资源的标志
}
作用:内核用来描述硬件相关的数据结构
flags:有两类标志:
IQRESOURCE_MEM:内存资源信息(寄存器的地址)
IQRESOURCE_LRQ:IQ资源信息(GPIO编号、中断号)
num_resources:resource这类硬件资源的个数!
例如:用resource来装载硬件相关信息
注意:两种装载硬件信息的方法可以同时使用!
问:如何使用?
1,分配初始化硬件节点对象
2,将硬件节点添加到平台总线的dev链表中,然后进行匹配platfrom_device_resgister(&led_dev);
函数功能:
1,添加硬件节点到dev链表上
2,遍历dev链表,取出每一个软件点进行匹配,如果一旦匹配成功,内核调用probe函数,然后把led_dev硬件节点的首地址传递给probe函数
3,如果要卸载硬件节点
platform_device_unregister(&led_dev);
案例:利用分离思想实现LED驱动
led_dev.c 仅仅操作struct platform_device
led_drv.c 仅仅操作struct paltform_driver
两个.c的目的就是为了软硬分离
涉及到的头文件:#include
day 11 am 2:16
1.linux内核如何管理内存
kmalloc
kzmalloc
__get_free_pages
vmalloc
vmalloc = 256M
mem = 10M
GFP_KERNEL
GFP_ATOMIC
2.1 linux内核地址映射的函数: ioremap
linux内核mmap机制:
mmap映射内存必须是页面大小的整数倍!!
案例:分析LED和按键驱动
结论:对于LED和按键驱动 ,整个数据的访问操作都要经过两次的数据拷贝过程:用户空间和内核空间数据拷贝,内核空间和硬件的数据拷贝;如果数据量比较小,对系统性能的影响几乎可以忽略不计,但是如果数据量比较大,这种影响就不能忽略不计,比如视频采集卡、摄像头、显卡、声卡此类设备,硬件处理的数据量比较在在,如果还是采用两次的数据拷贝,无形会对系统的性能造成很大的影响;
如何解决这类设备的数据访问呢?
如果采用read、write、ioctl势必经过两次的数据拷贝,但实际上数据在处理的时候,关键涉及到用户空间和硬件,而内核空间仅仅是作为数据的一个暂存!所以为了提高数据的访问性能,只需要将硬件映射到用户空间即可,一经映射,以后用户在用户空间访问设备,就无需经过内核空间,将数据的处理由2次变成1次的数据拷贝,大大提高数据的处理能力,就需要利用mmap进行映射(把硬件设备的物理信息映射到用户空间)!!
linux系统mmap系统调用过程:
1,应用程序调用mmap,首先调用C库的mmap
2,C库的mmap保存mmap的系统调用到R7中
3,C库的mmap然后调用svc指令,触发软中断,用户进程由用户空间陷入内核空间
4,进入内核空间以后,跳转到内核准备好的异常向量表的软中断处理入口vector_swi;
5,从R7中取出系统调用号,在系统调用表中找到对应的系统调用函数sys_mmap
6,sys_mmap做如下事情:
在当前进程的3G用户虚拟内存的MMAP 内存映射工找到一块空闲的内存区域,一旦找到,内核struct vm_area_struct结构体创建一个对象来描述这块空闲的内存区域的信息:
struct vm_area_struct { struct mm_struct * vm_mm; /* The address space we belong to. */ unsigned long vm_start; /* 空闲内存区域的首地址 */ unsigned long vm_end; ..... }
7,最终sys_mmap调用底层驱动的mmap接口
8,底层驱动的mmap接口中只做一件事:将设备的物理地址映射到用户空间3G的MMAP内存映射区中空闲的内存区域!
9,用户空间的mmap函数的返回值就是这块空闲区域的首地址(vm_start)
底层驱动的mmap接口:
struct file_operations{
int (*mmap)(struct file *file,struct vm area struct *vma)
}
接口函数功能:
只做一件事:将设备的物理地址和用户空间的虚拟地址进行映射!一旦完成映射,用户在用户空间来访问设备!
参数说明:
vma:指向内核帮你一块空闲的虚拟内存区域,然后内核创建的描述 这块虚拟内存的struct vm_area struct对象!
所以:底层驱动的mmap接口函数通过这个指针能获取这块虚拟内存区域的信息
问:底层驱动的mmap如何将物理地址映射到用户空间的虚拟地址上?
答:remmap_pfn_range函数完成这个动作!
int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned size, pgprot_t prot);
函数功能:用于底层驱动 的mmap函数,将物理地址映射用户的虚拟地址上!
vma:用户虚拟内存区域指针
addr:用户虚拟内存起始地址
pfn:要映射的物理地址所在页帧号,可以通过物理地址>>PAGE_SHIFT -12 得到
size:待映射的内存区域的大小
prot:vma保存属性,vm_page_prot
切记:在进行地址映射时,指定的虚拟地址和物理地址必须是页的整数倍。
例如:GPC1_3,GPC1_4的寄存器地址;
0xE0200080,0xE0200084,这两个地址不是页的整数倍,但是:通过芯片手册,GP10对应的寄存器基地址为:0xE0200000,所有在地址映射时,可以指定为0xE02000000这个物理地址对应的GPC1_3,GPC1_4的地址偏移量:
物理地址 用户虚拟地址
0xE0200000 A
0xE0200080 A+0x80
0xE0200084 A+0x84
案例:利用mmap实现LED驱动!!
#include <linux/init.h> #include <linux/module.h> #include <linux/mm.h>//struct vm_area_struct #include <linux/fs.h> #include <linux/miscdevice.h> //函数:只做将LED的物理地址映射到用户的虚拟地址上 //vma:指向内核创建用于要映射的用户虚拟内存的对象 static int led_mmap(struct file *file,struct vm_area_struct *vma){ //地址映射的时候,关闭cache功能;否则关灯可能关不了 vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); //将物理地址映射到虚拟地址 remap_pfn_range(vma, vma->start, 0xE0200000>>12, vma->vm_end - vma->start, vma->vm_page_prot ); return 0; } //分配初始化硬件操作接口 static struct file_operatioins led_fops = { .owner = THIS_MODULE, .mmap = led_mmap }; //分配初始化混杂设备对象 static struct miscdevice led_misc = { .minor = MISC_DYNAMIC_MINOR, .name = "myled", .fops = &led_fops } static int led_init(void){ misc_register(&led_misc); return 0; } static void led_exit(void){ misc_deregister(&led_misc); } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL");
切记:对于GPIO这类设备,在地址映射时,一定要关闭cache功能!关闭cache的方法:
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot)
一个物理地址可以有多个对应的虚拟地址!!
对应的测试程序:
include <stdio.h> #include <sys/types.h> #incldue <sys/sta.h> int main(int argc,char *argv[]){ int fd; unsigned char *vir_base; unsigned long *gpiocon,*gpiodata; if(argc < 2){ return -1; } fd = open("/dev/myled",O_RDWR); if(fd < 0){ return -1; } //将LED设备映射到用户3G的MMAP内存映射区 //将LED的物理地址映射到用户的虚拟地址上 //vir_base = vm_start对应的物理地址 = 0xE0200000 vir_base = mmap(0,0x1000, PROT_READ|PROT_WRITE, MAP_SHARED,fd,0); gpiocon = (unsigned long*)(vir_base+0x80); gpiodata = (unsigned long*)(vir_base+0x84); //配置GPIO为输出口,输出0 *gpiocon &= ~(0xf<<12)|(0xf<<16)); *gpiocon |= (1<<12)|(1<<16)); *gpidat &= ~((1<<3)|(1<<4)); if(!strcmp(argv[1],"on")){ *gpiodata |= ((1<<3)|(1<<4)); }else if(!strcmp(argv[1],"off")){ *gpiodata &= ~((1<<3)|(1<<4)) } munmap(vir_base,0x1000);//解除地址映射 close(fd); retrun 0; }
案例:硬件LED的管脚发生变化:
GPC1_3 -> GPF1_5
GPC1_4 -> GPF1_6
修改GPC1的驱动!
总结:硬件一旦发生变化 ,驱动跟着变!
驱动程序一般包括两部分内容,一部分纯硬件内容,另一部份是纯软件,一旦硬件发生变化以后,之前的驱动程序在实现都需要重头到尾挨个检查然后进行修改,驱动的跨硬件平台的可移植性非常差!对这种驱动,linux内核采用分离思想优化驱动:
linux内核分离思想其实本质就是将软件和硬件分离开,软件一旦写好以后,如果仅仅是硬件变化,以后软件无需改动,只需要该硬件即可,让驱动开发者的重心放在硬件上!
问:linux内核分离思想如何实现?
答:linux内核分离思想采用:设备-总线-驱动模型实现!
在内核里面已经帮你用软件定义好了一个虚拟总线,这个虚拟总线叫平台总线(platform_bus_type);在这个总线上维护着两个链表:dev链表、drv链表;
dev链表上的每一个节点存放的硬件相关信息,节点的数据结构为struct platform device(平台设备),这个结构体就是用来装载硬件相关的信息;每当用这个数据结构来描述一个硬件信息时,只需要分配初始化这个对象即可,然后添加到dev链表上以后,内核会帮你遍历drv链表,取出drv链接上每一个软件节点,内核通过调用总线提供的match函数,比较硬件节点和软件的name字段,如果相等,说明硬件和软件匹配成功,内核调用软件节点的probe函数,然后把匹配的硬件节点的首地址传递给probe函数,硬件、软件再次结合!probe函数中如何操作,由驱动开发者来实现!
drv链表上的每一个节点存放的软件相关信息,节点的数据结构为struct platform_driver(平台驱动),这个结构体就是用来装载软件相关的信息;每当用这个数据结构来描述一个软件信息时,只需要分配初始化这个对象即可,然后添加到dev链表上以后,内核会帮你遍历dev链表,取出dev链接上每一个软件节点,内核通过调用总线提供的match函数,比较硬件节点和软件的name字段,如果相等,说明硬件和软件匹配成功,内核调用软件节点的probe函数,然后把匹配的硬件节点的首地址传递给probe函数,硬件、软件再次结合!probe函数中如何操作,由驱动开发者来实现!
总结:对于一个硬件的设备驱动实现只需要关注围绕着struct platform_device和struct platform_driver结构体实现驱动即可!
明确,一个完整的驱动必然包括软件和硬件!
问:如何使用以上两个结构体呢?
答:
struct platform_device结构体的使用过程:
struct platform_device { const char * name u32 id struct device dev u32 num_resources struct resource *resource }
作用:专门用来描述和装载硬件相关的信息!
成员说明:
name:硬件节点名称,这个字段相当重要!!由于软件和硬件都是通过来name来匹配的
id:硬件节点的编号,如果只有一个硬件节点,一般指定为-1,如果有多个同名的硬件节点,通过Id编号区分(0,1,2,3….)
dev:在这个结构体变量中,重点关注其中的platform_data(数据类型为void *),platform_data指针用来装载驱动开发者自己声明描述硬件信息的结构体(struct led_resource,struct btn_resource),例如:
struct led_resource{ unsinged long phys_addr, int pin; }; static struct led_resource led_info = { .phys_addr = 0xE200080, .pin = 3 }; static struct platform_device led_dev = { .dev = { .platform_data = &led_info//装载硬件信息 } }
resource:用来装载resource类型硬件资源信息,数据类型为:
struct resource {
unsigned long start;//硬件资源的起始信息
unsigned long end;//硬件资源的结束信息
unsigned long flags;//硬件资源的标志
}
作用:内核用来描述硬件相关的数据结构
flags:有两类标志:
IQRESOURCE_MEM:内存资源信息(寄存器的地址)
IQRESOURCE_LRQ:IQ资源信息(GPIO编号、中断号)
num_resources:resource这类硬件资源的个数!
例如:用resource来装载硬件相关信息
static struct resource led_res[] = { [0] = { .start = 0xe0200080, .end = 0xe0200080+8, flags = IORESORUCE_MEM } } static struct platform_device led_res = { .resource = led_res, .num_resource = ARRAY_SIZE(led_res) }
注意:两种装载硬件信息的方法可以同时使用!
问:如何使用?
1,分配初始化硬件节点对象
static struct paltfrom_device led_dev = { .name = "leds",//必须有 .id = -1, .dev = { .paltfrom_data = 装载自己申请描述硬件信息的结构对象 }, .resource = 装载resource类型硬件资源信息 .num_resource = resource资源个数 };
2,将硬件节点添加到平台总线的dev链表中,然后进行匹配platfrom_device_resgister(&led_dev);
函数功能:
1,添加硬件节点到dev链表上
2,遍历dev链表,取出每一个软件点进行匹配,如果一旦匹配成功,内核调用probe函数,然后把led_dev硬件节点的首地址传递给probe函数
3,如果要卸载硬件节点
platform_device_unregister(&led_dev);
案例:利用分离思想实现LED驱动
led_dev.c 仅仅操作struct platform_device
led_drv.c 仅仅操作struct paltform_driver
两个.c的目的就是为了软硬分离
涉及到的头文件:#include
#include <linux/init.h> #include <linux/module.h> #include <linux/mm.h>//struct vm_area_struct #include <linux/fs.h> #include <linux/miscdevice.h> //声明描述硬件的数据结构 static struct led_resource{ char *name;//厂家名称 int prductid;//产品Id }; //分配初始化硬件相关的信息 static struct led_resouce led_info={ .name = "myled", .productid = 0x123456 }; //分配初始化resource这类硬件相关的信息 static struct resource led_res[]={ [0] = { .start = 0xE0200080,//寄存器的起始物理地址 .end = 0xE0200080+8,//寄存器的结束物理地址 .flags = IQRESOURCE_MEM//内存资源类型 }, [1] = { .start = 3,//GPIO编硬件编号 .end = 3,//GPIO编硬件编号 .flags = IQRESOURCE_IRQ//IO资源类型 } } static void led_release(struct device *dev){ } //分配初始化硬件节点对象 static struct platfrom_device led_dev={ .name ="myled",//必须 有,用于匹配 .id = -1, .resource = led_res,//装载硬件信息 .num_resource = ARRAY_SIZE(led_res), .dev = { .platform_data = &led_info,//装载硬件信息 .release = led_release //release函数指针,否则出现警告 } }; static int led_dev_init(void){ //注册硬件节点到链表,然后进行匹配 platform_device_regist(&led_dev); return 0; } static void led_dev_exit(void){ //卸载硬件节点 platform_device_unregist(&led_dev); } module_init(led_dev_init); module_exit(led_dev_exit); MODULE_LICENSE("GPL");
day 11 am 2:16
相关文章推荐
- Linux 设备驱动开发思想 —— 驱动分层与驱动分离
- Linux 设备驱动开发思想 —— 驱动分层与驱动分离
- Linux主机驱动与外设驱动分离思想
- Linux设备驱动的分层设计思想
- Linux设备驱动的分层设计思想
- Linux主机驱动与外设驱动分离思想
- linux设备驱动--ioremap和mmap
- Linux设备驱动之内存映射--mmap--转 .
- Linux 设备驱动--- mmap设备操作
- [快速上手Linux设备驱动]之一切皆是文件思想
- Linux主机驱动与外设驱动分离思想[转宋老师]
- Linux主机驱动与外设驱动分离思想(转自CSDN)
- Linux设备驱动之内存映射--mmap--转
- Linux主机驱动与外设驱动分离思想
- Linux主机驱动与外设驱动分离思想
- Linux设备驱动之mmap设备操作
- Linux设备驱动之内存映射--mmap--转
- linux设备驱动之mmap函数
- [快速上手Linux设备驱动]之一切皆是文件思想
- 用面向对象思想分析linux字符设备驱动开发