回顾:
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
#include
#include //struct vm_area_struct
#include
#include
//函数:只做将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
#include
#incldue
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
#include
#include //struct vm_area_struct
#include
#include
//声明描述硬件的数据结构
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