Linux 嵌入式开发记录(二)设备驱动

        在上一节中,我们主要讲解了怎么去编译一个驱动。这一节,我们主要讲解驱动发展的进程。

        1、为什么需要驱动程序?

        设备驱动,顾名思义,就是一个设备,需要用起来,在linux 中,应用层是不可以直接操纵硬件设备的,那么我们就需要一个专门操纵设备的程序来提供给应用层调用,这样的程序就叫驱动程序。嗯,其实就是一个需要与设备打交道的程序。

        2、驱动发展的历程?

        对于2.6版本以前的驱动,现在我们基本不用去关注,我们主要关注2.6 版本以后的驱动程序。我们主要把它分为四个过程。下面主要以字符设备驱动为例。

        在讲驱动前,我们先看看我们应用层怎么使用一个设备的。基本的步骤都是先要打开一个设备,open("/dev/led"); 然后再调用 write,read,ioctl 等方法来操作这个设备。最后当我们用完后使用close()来关掉这个设备。嗯,对应用层来讲,它最需要关注的是什么呢?它是在dev 下这个节点。这个节点怎么来的呢?有二种方式产生,一个是在驱动程序中使用udev 设备管理的方法产生,一个使用 mknod 的指令产生。如果设备驱动不支持自动创建设备节点,那么需要由驱动的使用者来创建。假定这个设备的名字角 led ,主设备号是 240, 次设备号是 0, 则创建设备节点的命令为  #mknod  /dev/led  c   240  0  这样在 /dev/下就生成了一个节点 /led.  udev 方式怎么创建设备节点呢? 它主要调用二个固定的函数  首先,class_create() 来为设备创建一个类,再次调用 device_create 来为设备创建对应的设备节点。下面为一个udev 创建节点的代码。

//在 /sys/class 下创建  led_class  目录。

led_class = class_create(THIS_MODULE,"led_class");

//创建  /dev/led%d  节点文件。

device_create(led_class,NULL,devno,NULL,"led","%d",MINOR(devno));

这里出现了 一个 devno  MINOR(devno)  ,这些是什么呢,我们再回过头来看看我们使用 mknod 怎么创建一个节点的,在使用 mknod 创建节点时,我们需要 知道 三个参数, 名字,主设备号,次设备号。名字 led, 这个我们容易猜出来,那么剩下就二个,一个主设备号,一个次设备号,那么谁是主呢?  devno 是主, MINOR(devno) 是根据主设备号来找到它的次设备号。

由上面我们是不是很容易发现,一个驱动完成后,那么它需要给我们提供那些东西?第一个 主设备号,第二个次设备号,第三个 设备名,第四个操作设备的接口。嗯,驱动程序就是这么简单,你完成了上面四个问题,那么你的驱动就写好了!好了,我们看看下面是不是完成这四个问题,我们的驱动就完成了。

 一、常规的字符设备驱动。

1、次设备号

static int minor = 0; // 我们设定它的次设备号为 0

2、主设备号

dev_t  devno;

ret = alloc_chrdev_region(&devno,minor,1,"led"); //从系统获取主设备号。

major = MAJOR(devno);//获取主设备号

minor = MINOR(devno); //获取次设备号

3、操作设备接口。

struct file_operations led_cdev_fops = {

        .owner = THIS_MODULE,

        .read = led_cdev_read,

        .write = led_cdev_write,

        .open = led_cdev_open,

        .ioctl = led_cdev_ioctl

};   //设备操作接口

struct cdev *led_dev ; //cdev 数据结构

led_dev = cdev_alloc(); //分配 led_dev 结构。

cdev_init(led_dev,&led_cdev_fops ); //初始化led_dev 结构,注意这个地方把这个结构跟设备操作关联起来了。

cdev_add(led_dev,devno,1) ; //增加 led_dev  到系统,注意,这个地方引入了 devno ,这样就把操作跟主设备号关联起来了。、

4、在/dev/ 下创建设备

led_class = class_create(THIS_MODULE,"led_class");

device_create(led_class,NULL,devno,NULL,"led",NULL); 

至此,一个常规的设备驱动就完成了,把它装入到 module_init(); 里面,使用对应的编译工具编译,放入到 Arm 板,就可以应用了。

 二、设备驱动模型的字符驱动。

        设备驱动模型,它主要是内核对设备,驱动,总线的管理。并向应用层暴露这些管理的信息,比如我们在上面说的udev 创建设备一样,它就属于设备驱动模型的一部分。它给用户暴露了 /dev/led. 让用户了解和使用。在Linux 内核中通过 kobject , kset, 和subsys 来进行管理,驱动编写可以忽略这些管理机制的具体实现,但是我们需要知道它给用户暴露了那些信息,和这些信息表示什么内容。下面主要来描述,它主要暴露了那些信息,这些信息间的关系。

1、驱动模型的信息通过sysfs文件系统来进行暴露。一、设备驱动模型的上下级关系通过 sysfs 的父目录和子目录来实现。二、设备驱动模型的平级关系通过 sysfs 的目录符号链接来实现。三、驱动模型的属性则通过sysfs 文件系统的文件内容来实现。四、设备驱动模型数据结构中的kobject对应于sysfs文件系统中的目录,而数据结构中的struct attribute成员则对应于sysfs文件系统中的文件。 sysfs文件系统挂载在  /sys 下面。

2、设备,驱动,总线。往总线上添加一个新设备或者新驱动的时候,bus 的match 方法会被调用,为设备或者驱动寻找匹配的驱动程序或者设备。

3、类是Linux 设备驱动模型中的一个高层抽象,为用户空间提供设备的高层视图。在sysfs 中 类一般都放在  /sys/class 目录下。 在类子系统中,可以向用户空间导出信息,用户空间可以通过这些信息与内核交互。最典型的就是我们前面讲过的 udev. udev 是用户空间的程序,根据 ./sys/class 目录下的 dev 文件来创建设备节点。可以理解为在 class 目录下的节点在 ./dev 下都有节点。

4、在/sys/devices/是创建了实际的文件节点。而其他目录,如设备类和总线以下的子目录中出现的设备都是用符号链接指向/sys/devices/目录下的文件。

三、平台设备和驱动的字符驱动。

        平台设备抽象出了 platform_device 和 platform_driver 两个核心概念。与此相关的还有一个重要的概念就是资源 resource.

        struct resource{

                resource_size_t  start; //资源在CPU上的起始地址

                resource_size_t end;  //资源在CPU上的物理结束地址

                const char *name;    //资源的名称

                unsigned long flags;  //资源的标志

                struct resource *parent, *sibing,*child; //资源的父亲,兄弟和子资源

        }; //对于资源,我们应该需要关注它的结构,看看资源到底包含那些内容

一个设备是可以占用多个资源的。

1、平台设备。并不是任何设备都可以抽象成为 platform_device. platform_device 是在系统中以独立实体出现的设备。这些设备的一个共同点是CPU都可以通过总线直接对他们进行访问。首先我们看看一个平台设备包含那些内容。

 strut platform_device{

        const char *name; //设备名称

        int  id;  //设备ID

        struct device  dev; //设备的数据结构

        u32  num_resources; //资源的个数

        struct resources *resources; //设备的资源

        const struct platform_device_id *id_entry; //设备ID入口

        struct pdev_archdata  archdata;  //体系结构相关的数据

};

        name 是设备的名称,用于与 platform_driver 进行匹配绑定,resource 用于描述设备的资源,如地址,IRQ等。对一个设备来讲,我们主要关注它的名字和资源。

2、平台驱动 . platform_driver 是 device_driver 的封装,提供了驱动的 probe 和 remove 方法。也提供了与电源管理相关的shutdown 和 suspend 等方法。

struct platform_driver{

        int (*probe)(struct platform_device *)    // probe 方法

        int (*remove)(struct platform_device *) // remove 方法

        void (*shutdown)(struct platform_device *);  //shutdown 方法

        int (*suspend)(struct platform_device*, pm_message_t state); //suspend 方法

        int (*resume)(struct platform_device *); //resume 方法

        struct device_drive  driver;  //设备驱动

        const struct platform_device_id *id_table;  //设备ID表

};

3、普通驱动和平台驱动的差异。 普通驱动和平台驱动差异在于框架结构发生了变法,资源申请和释放的位置发生了变化。一、资源申请,设备注册等从普通字符驱动的模块初始化部分移到了平台驱动的 probe 方法。二、设备的注销。资源释放等从普通字符驱动的模块退出代码移到了平台驱动的 remove 方法。三、平台驱动还增加了资源定义和初始化,平台设备和驱动的定义和初始化。

4、平台驱动范例:

       一、平台设备。

        对于平台设备,我们只需要干三件事,1、这个设备有那些资源。2、这个设备数据结构。3、注册平台设备。

        (1)、定义资源。

        static struct resource led_resource[]={

        [0]={

                .start = GPIO_LED_PIN,

                .end = GPIO_LED_PIN,

                .flags = IORESOURCE_IO,

                },

        };

        (2)、设备数据结构

        static struct platform_device *led_platform_device = {

                .name = "led", //驱动中的名字必须与该名字相同

                .id = -1,

                .num_resources = ARRAY_SIZE(led_resource); 

                .resource = led_resource,

                

                

        };

        (3)、注册平台设备。

        platform_device_register(&lef_platform_device);

        二平台驱动。

        对于平台驱动,我们除了包含前面普通驱动需要的 四个步骤,还需要从平台设备中获取资源,然后注册平台驱动。

        1、从平台设备中获取资源。

        struct resource *res_io;

        res_io = platform_get_resource(pdev,IORESOURCE_IO,0);

        int led_io = res_io.start; //获取到GPIO的引脚。

        2、定义次设备号

       static int minor = 0;

        3、主设备号

        ret = alloc_chrdrv_region(&devno,minor,1,"led");

        major = MAJOR(devno);

       4、操作设备接口。

        struct cdev  *led_dev; //cdev 数据结构

        struct file_operations led_fops = {

                .owner = THIS_MODULE,

                .open = led_open,  //led_open 是我需要自己实现的函数,一般是让硬件可用

                .write = led_write,

                .read = led_read,

                .ioctl = led_ioctl

        };

        led_dev = cdev_alloc(); //

        cdev_init(led_dev,&led_fops); //初始化,注意这个位置加上了操作函数

        cdev_add(led_dev,devno,1) ; //增加led 到系统中

        5、在 /dev 下创建设备。

        led_class = class_create(THIS_MODULE,"led_class");

        device_create(led_class,deno,NULL,"led");

        6、注册到平台驱动。

        struct platform_driver led_platform_driver = {

                .probe = led_probe,

                .remove = led_remove,

                .drive={

                        .name = "led",

                        .owner = THIS_MODULE,

                }

        };

        platform_driver_register(&led_platform_driver);

 四、关于设备树的字符驱动。

        设备树的字符驱动,是驱动发展到现在的最新进程。它与普通的设备驱动以及平台设备驱动有什么不同呢?最大的不同为多了一个用来描述硬件的设备树,回过头,我们看看,在普通的设备驱动和平台驱动里面,我们在哪里进行硬件的描述的。在普通的设备驱动里面,我们是在初始化的时候对硬件的信息进行初始化和描述的,在平台驱动里面,我们把硬件资源的信息放在了平台设备里面。那么在设备树的驱动里面,这些硬件信息又是怎么样的呢?首先,我们看看设备树进入内核后最终变成什么样子了。当我们追溯Linux源码,我们会发现设备树通过解析后,最后由of_device_add()将相应的设备树节点生成的device_node节点链入到platform_device的dev.of_node中。没错最后设备树的每个设备被解析成了平台设备。就是系统通过读取设备树已经帮我们写好了平台设备,那么我们只需要实现设备驱动就可以了。剩下就就是跟平台设备驱动的写法一样。在驱动里面获取硬件信息都是调用 of 函数。

猜你喜欢

转载自blog.csdn.net/dreamliweiming/article/details/126990229