分享

Linux驱动程序开发 - 设备驱动模型初探

 waston 2011-02-09

Linux驱动程序开发 - 设备驱动模型初探

序言
从这一章开始,我们将详细的介绍Linux的设备驱动模型。Linux设备驱动模型是一个相当复杂的系统,对于初学者来说真有些无从入手。而且更加困难的是,随着新的Linux Kernel的release,Linux的设备驱动模型总会有或大或小的变化,我们将尽量展现 Linux Kernel 的这种变化。

早期的Linux内核(版本2.4之前)并没有实现一个统一的设备模型,设备节点的创建一般是mknod命令手动创建或利用devfs文件系统创建。早期 的Linux发行版一般会采用手动创建的方式预先把通常用到的节点都创建出来,而嵌入式系统则会采用devfs的方式。起初Linux 2.6 内核还支持devfs,但从2.6.18开始,内核完全移除了devfs系统而采用的udev的方式动态的创建设备节点。因此,新的Linux发行版都采 用udev的方式管理设备节点文件。(关于udev的详细信息,请参考:http://www./pub/linux/utils/kernel/hotplug/udev.html)。

Linux2.6设备驱动模型的基本元素是Class、Bus、Device、Driver,下面我们分别介绍各个部分。

Class 和Class Device
驱动模型最基本的概念是设备及其类别,Linux中使用struct class 和struct class_device来管理不同类别的设备。由于设备驱动模型是一个复杂的系统,我们还是从一个简单的例子开始介绍,然后在逐步展开。其实实现设备节点的动态创建是一个很简单的事情,并不需要太多的代码。我们修改我们的驱动初始化函数如下:

#include <linux/device.h>
#define DEVNAME "hello"

static dev_t dev;
static struct class *hello_class;
static struct cdev *hello_cdev;
static int __init hello_init(void)
{
    int error;

    error = alloc_chrdev_region(&dev, 0, 2, "hello");
    if (error)
    {
        printk("hello: alloc_chardev_region failed!\n");
        goto out;
    }
    hello_cdev = cdev_alloc();
    if (hello_cdev == NULL)
    {
        printk("hello: alloc cdev failed!\n");
        error = -ENOMEM;
        goto out_chrdev;
    }
    hello_cdev->ops = &hello_fops;
    hello_cdev->owner = THIS_MODULE;
    error = cdev_add(hello_cdev, dev, 1);
    if (error)
    {
        printk("hello: cdev_add failed!\n");
        goto out_cdev;
    }
    hello_class = class_create(THIS_MODULE, DEVNAME);
    if (IS_ERR(hello_class))
    {
        error = PTR_ERR(hello_class);
        goto out_chrdev;
    }
    class_device_create(hello_class, NULL, dev, NULL, DEVNAME);
    memset (hello_buf, 0, sizeof(hello_buf));
    memcpy(hello_buf, DEFAULT_MSG, sizeof(DEFAULT_MSG));
    printk("hello: Hello World!\n");
    return 0;

out_cdev:
    cdev_del(hello_cdev);
out_chrdev:
    unregister_chrdev_region(hello_cdev->dev, 2);
out:
    return error;
}
static void __exit hello_exit(void)
{
    class_device_destroy(hello_class, dev);
    class_destroy(hello_class);
    unregister_chrdev_region(hello_cdev->dev, 2);
    cdev_del(hello_cdev);
    printk("hello: Goodbye World\n");
}


重新编译这个驱动程序,当加载这个驱动到内核中时,系统(一般是hotplug和udev系统)就会自动的创建我们指定的设备名字:/dev/hello,同时,你也可以发现在sysfs系统中添加了新的文件:/sys/class/hello/hello/。
当然并不需要把所有的代码都贴到这里,但是这样做可能更加清楚。我们把添加的代码显示成蓝色,这样你可以清楚的看到代码的简单程度。这里主要用到了class_create 和class_device_create函数,它们定义在<linux/device.h>头文件中。

extern struct class *class_create(struct module *owner, const char *name);
extern void class_destroy(struct class *cls);
extern struct class_device *class_device_create(struct class *cls,
                        struct class_device *parent,
                        dev_t devt,
                        struct device *device,
                        const char *fmt, ...)
                    __attribute__((format(printf,5,6)));
extern void class_device_destroy(struct class *cls, dev_t devt);


Linux是通过设备与设备类来管理设备的,当你调用这些函数向系统注册设备及其类的时候,内核会自动的在sysfs文件系统中创建对应的文件。如果想了 解更多的关于设备及其类的函数接口,请你阅读Linux kernel的源文件(include/device.h头文件和drivers/base/class.c实现文件)。这里简单说明 class_device_create函数,它的最后一个参数是一个变参,类似于printf函数参数,用于指定添加设备的名称也就是显示在/dev/ 目录下设备文件名称。

创建的class_device设备会自动注册到系统中,这样对于给定的设备,系统会自动找到匹配的设备驱动。

你可以在设备类或设备目录下(/sys/class)创建文件,这个文件提供了内核同用户空间程序的交互接口。内核提供了设备、设备类的内核函数接口,这些接口也定义在<include/device.h>头文件中。

int class_create_file(struct class *cls, const struct class_attribute *attr);
void class_remove_file(struct class *cls, const struct class_attribute *attr);
int class_device_create_file(struct class_device *class_dev,
                 const struct class_device_attribute *attr);
void class_device_remove_file(struct class_device *class_dev,
                  const struct class_device_attribute *attr);
int class_device_create_bin_file(struct class_device *class_dev,
                 struct bin_attribute *attr);
void class_device_remove_bin_file(struct class_device *class_dev,
                  struct bin_attribute *attr);


其实,这些函数仅仅是简单的封装了sysfs文件系统函数,但它为设备及设备类提供了统一的函数接口。

Drivers
当一个设备注册到系统中时,它就向系统表明了哪个驱动匹配这个设备。Linux内核会在注册的设备驱动中查找匹配的驱动并调用对应的探测函数(probe)来初始化设备。设备驱动接口也定义在<linux/device.h>头文件中。

struct device_driver {
    const char        *name;
    struct bus_type        *bus;
    struct module        *owner;
    const char         *mod_name;   
    int (*probe) (struct device *dev);
    int (*remove) (struct device *dev);
    void (*shutdown) (struct device *dev);
    int (*suspend) (struct device *dev, pm_message_t state);
    int (*resume) (struct device *dev);
    struct attribute_group **groups;
    struct driver_private *p;
};


其中,比较重要的数据是name,Linux内核就是根据name来匹配设备与驱动的。probe函数用于设备的探测及初始化,remove函数在设备移出系统时被触发调用。

一般来说,我们的驱动需要实现name、module、probe、remove函数。

Bus
通常我们的驱动并不需要实现Bus接口,也没有这个必要,因此你完全可以跳过这段,除非你想添加一个新的总线到系统中。

在Linux2.6内核中,struct bus_type描述了一个bus对象,它定义在<linux/device.h>头文件中。(你发现没有,到目前Linux设备模型接口都定义在这个文件中)

struct bus_type {
    const char        *name;
    struct bus_attribute    *bus_attrs;
    struct device_attribute    *dev_attrs;
    struct driver_attribute    *drv_attrs;

    int (*match)(struct device *dev, struct device_driver *drv);
    int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
    int (*probe)(struct device *dev);
    int (*remove)(struct device *dev);
    void (*shutdown)(struct device *dev);

    int (*suspend)(struct device *dev, pm_message_t state);
    int (*suspend_late)(struct device *dev, pm_message_t state);
    int (*resume_early)(struct device *dev);
    int (*resume)(struct device *dev);

    struct bus_type_private *p;
};

extern int __must_check bus_register(struct bus_type *bus);
extern void bus_unregister(struct bus_type *bus);

extern int __must_check bus_create_file(struct bus_type *,
                    struct bus_attribute *);
extern void bus_remove_file(struct bus_type *, struct bus_attribute *);


在这个结构中,name描述了bus的名字,如它会显示在/sys/bus/中。match函数用于匹配属于这个bus的设备和驱动,uevent用于处理Linux uevent事件。probe和remove类似与driver的函数接口,主要用于设备的Hotplug处理。其他的函数是Power相关的函数接口。

同样,bus也需要注册到系统中,并可以在sysfs中创建文件。

sysfs文件系统
sysfs类似于proc文件系统,用于用户空间程序和内核空间交互数据的接口。但sysfs提供了更多的功能,其中之一就是显示Linux驱动程序模型的分层结构关系。Ubuntu 804的sysfs文件系统的目录显示如下:
           block  bus  class  devices  firmware  fs  kernel  module  power  slab
当你浏览这个文件系统的时候,你会发现里面有很多链接文件,其实正是这些链接文件展现了Linux驱动模型各个组成部分之间的关系。
sysfs文件系统中,最重要的就是struct attribute结构,它被用来管理内核sysfs文件的接口(名字,属性,读写函数等)。内核sysfs提供了基本的attribute接口,不同的 设备如bus、device在基本attribute的基础上定义了自己的读写函数,sysfs提供了对应的宏来简化属性的操作。请参考< linux/sysfs.h>头文件中。

struct attribute {  
    const char        *name;
    struct module     *owner;
    mode_t            mode;
};

#define __ATTR(_name,_mode,_show,_store) { \
    .attr = {.name = __stringify(_name), .mode = _mode },    \
    .show    = _show,                    \
    .store    = _store,                    \
}

int __must_check sysfs_create_file(struct kobject *kobj, const struct attribute *attr);
int __must_check sysfs_create_dir(struct kobject *kobj);


我们看到,sysfs的struct attribute结构本身并不包含读写访问函数,驱动模型的各个部分都会扩展这个结构并定义自己的属性结构来引入各自的操作函数,如 class:(这个结构定义在<linux/device.h>头文件中)。

struct class_attribute {
    struct attribute    attr;
    ssize_t (*show)(struct class *, char * buf);
    ssize_t (*store)(struct class *, const char * buf, size_t count);
};
#define CLASS_ATTR(_name, _mode, _show, _store)            \
struct class_attribute class_attr_##_name = __ATTR(_name, _mode, _show, _store)


关于sysfs的更多信息,请参考 Linux内核源代码树中的Documentation/filesystems/sysfs.txt文件。

Platform总线
platform总线是Linux内核中的一个虚拟总线,它使得设备的管理更加简单化。目前大部分的驱动都是用platform总线来写的。 platform总线模型的各个部分都是继承自Device模型(姑且这么说吧),它在系统内实现了个虚拟的总线,即platform_bus,如果你的 设备需要platform总线管理,那么就需要向系统中注册platform设备及其驱动程序。就像前面所介绍的那样,platform总线分为 platform_bus, platform_device 和platform_driver几个部分,他们的接口定义在<linux/platform.h>头文件中。
  • platform bus
我们先来看看platform_bus的定义:

struct device platform_bus = {
    .bus_id        = "platform",
};

struct bus_type platform_bus_type = {
    .name        = "platform",
    .dev_attrs    = platform_dev_attrs,
    .match        = platform_match,
    .uevent        = platform_uevent,
    .suspend    = platform_suspend,
    .suspend_late    = platform_suspend_late,
    .resume_early    = platform_resume_early,
    .resume        = platform_resume,
};

int __init platform_bus_init(void)
{
    int error;

    error = device_register(&platform_bus);
    if (error)
        return error;
    error =  bus_register(&platform_bus_type);
    if (error)
        device_unregister(&platform_bus);
    return error;
}


platform_bus数据结构描述了platform bus设备,platform_bus_type描述了platform bus总线,它提供了platform总线设备和驱动的匹配函数。platform总线是由函数platform_bus_init(void)初始化的。
对于Linux我们一般的设备驱动程序来说,就像前面Bus一段提到的那样,我们不需要关心platform总线本身,我们只要调用我们的设备和驱动接口就可以了。

  • Platform Device
如果你想让platform总线来管理设备,那么,你需要先向platform系统注册设备,这个过程是通过下面的函数接口来实现的:

int platform_device_add(struct platform_device *pdev);
int platform_device_register(struct platform_device *pdev);


我们一般需要调用platform_device_register函数来向系统添加platform设备。这两个函数唯一的差别就是platform_device_register在添加设备前会初始化platform_device的dev数据成员,它是一个struct device类型数据。当一个platform_device添加到platform总线中后,platform总线就会为它找到匹配的设备驱动程序,很显然,在这之前,你需要向系统注册platform_driver。
  • Platform Driver
我们先来看看platform总线设备驱动的结构:

struct platform_driver {
    int (*probe)(struct platform_device *);
    int (*remove)(struct platform_device *);
    void (*shutdown)(struct platform_device *);
    int (*suspend)(struct platform_device *, pm_message_t state);
    int (*suspend_late)(struct platform_device *, pm_message_t state);
    int (*resume_early)(struct platform_device *);
    int (*resume)(struct platform_device *);
    struct device_driver driver;
};

extern int platform_driver_register(struct platform_driver *);


很显然,它“继承”自struct device_driver,同样类似于struct device_driver,一般我们需要实现probe函数,及指定platform_driver能驱动的设备的名字。

  • 使用Platform总线
下面这个例子告诉你如何使用platoform总线,这是一个Android Goldfish GPIO驱动程序。它本身就是一个platform设备驱动(goldfish-gpio),同时,它又会向系统注新的设备(android-timed-gpio),这个新设备又被timed_output.c驱动程序驱动。

......
#include <linux/platform_device.h>

struct platform_device timed_gpio_device = {
    .name  = "android-timed-gpio",
    .id    = -1,
    .dev.platform_data = &timed_gpio_platform_data,
};

static int goldfish_gpio_probe(struct platform_device *pdev)
{
    struct goldfish_gpio_data *gpio_data;
    ......
    error = platform_device_register(&timed_gpio_device);
    ......
    return 0;
}

static int goldfish_gpio_remove(struct platform_device *pdev)
{
    int i;
    struct goldfish_gpio_data *gpio_data;
    ......
        platform_device_unregister(&timed_gpio_device);
    ......
    return 0;
}

static struct platform_driver goldfish_gpio_driver = {
    .probe    = goldfish_gpio_probe,
    .remove   = goldfish_gpio_remove,
    .driver = {
        .name = "goldfish-gpio"
    }
};

static int __init goldfish_gpio_init(void)
{
    return platform_driver_register(&goldfish_gpio_driver);
}

static void __exit goldfish_gpio_exit(void)
{
    platform_driver_unregister(&goldfish_gpio_driver);
}


这个新注册的设备(timed_gpio_device)由timed_output驱动管理,通过浏览这段代码,你应该对如何使用platform总线有个全面的了解。(本想把全部code放在这里,但超过最大字数限制!)

static struct class *timed_gpio_class;
struct timed_gpio_data {
    struct device *dev;
    struct hrtimer timer;
    spinlock_t lock;
    unsigned     gpio;
    int         max_timeout;
    u8         active_low;
};
......

static int android_timed_gpio_probe(struct platform_device *pdev)
{
    struct timed_gpio_platform_data *pdata = pdev->dev.platform_data;
    struct timed_gpio *cur_gpio;
    struct timed_gpio_data *gpio_data, *gpio_dat;
    int i, ret = 0;
    ......
}

static int android_timed_gpio_remove(struct platform_device *pdev)
{
}

static struct platform_driver android_timed_gpio_driver = {
    .probe        = android_timed_gpio_probe,
    .remove        = android_timed_gpio_remove,
    .driver        = {
        .name        = "android-timed-gpio",
        .owner        = THIS_MODULE,
    },
};

static int __init android_timed_gpio_init(void)
{
    timed_gpio_class = class_create(THIS_MODULE, "timed_output");
    if (IS_ERR(timed_gpio_class))
        return PTR_ERR(timed_gpio_class);
    return platform_driver_register(&android_timed_gpio_driver);
}

static void __exit android_timed_gpio_exit(void)
{
    class_destroy(timed_gpio_class);
    platform_driver_unregister(&android_timed_gpio_driver);
}


Kobject和kset
提到Linux的设备模型,就不得不提kobject和kset这两个内核对象,他们才是Linux内核设备模型的最基础的结构,但讲解他们却是一个枯燥 过程,限于篇幅,这个就不作介绍了,请参考Linux文档<documentation/kobject.txt>。

后记
在这里,我们简单的介绍了Linux的设备模型,包括基本总线、设备、驱动的关系,同时也简单的介绍了Linux2.6内核的platform总线。这些内容应该足够让你了解如何使用Linux设备模型来管理设备了。

原文

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多