Linux设备驱动第十天(mmap、linux内核分离(软硬分离)思想)

回顾:
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

你可能感兴趣的:(嵌入式Linux驱动)