10. Linux的设备模型

在前面写的驱动中,我们发现编写驱动有个固定的模式只有往里面套代码就可以了,它们之间的大致流程可以总结如下:

  • 实现入口函数xxx_init()和卸载函数xxx_exit()

  • 申请设备号 register_chrdev_region()

  • 初始化字符设备,cdev_init函数、cdev_add函数

  • 硬件初始化,如时钟寄存器配置使能,GPIO设置为输入输出模式等。

  • 构建file_operation结构体内容,实现硬件各个相关的操作

  • 在终端上使用mknod根据设备号来进行创建设备文件(节点)或者自动创建 (驱动使用class_create创建设备类、在类的下面device_create创建设备节点)

因此,在Linux开发驱动,只要能够掌握了这些“套路”,开发一个驱动便不是难事。

在内核源码的drivers中存放了大量的设备驱动代码,在我们写驱动之前先查看这里的内容,说不定可以在这些目录找到想要的驱动代码。如图所示:

内核提供的驱动代码

10.1. 设备驱动模型的机制

这样根据步骤来编写我们的驱动代码简单粗暴,但是这存在着问题,我们将硬件的信息都写进了驱动里了,根据某个硬件编写的驱动只要修改了一下引脚接口,这个驱动代码就得重新修改才能使用,这显然是不合理的,那有没有合适的解决方案呢?

答案是肯定的,Linux引入了设备驱动模型分层的概念,将我们编写的驱动代码分成了两块:设备与驱动。

设备负责提供硬件资源而驱动代码负责去使用这些设备提供的硬件资源。并由总线将它们联系起来。这样子就构成以下图形中的关系。

../../../_images/linux_device_model01.png

设备模型通过几个数据结构来反映当前系统中总线、设备以及驱动的工作状况,提出了以下几个重要概念:

  • 设备(device) :挂载在某个总线的物理设备;

  • 驱动(driver) :与特定设备相关的软件,负责初始化该设备以及提供一些操作该设备的操作方式;

  • 总线(bus) :负责管理挂载对应总线的设备以及驱动;

  • 类(class) :对于具有相同功能的设备,归结到一种类别,进行分类管理;

我们知道在Linux中一切皆“文件”,在根文件系统中有个/sys文件目录,里面记录各个设备之间的关系。 下面介绍/sys下几个较为重要目录的作用。

/sys下几个重要目录

目录

作用

/sys/bus

这里是系统中的所有设备按照总线类型分层放置的目录结构。

/sys/devices 目录下每一种设备都是挂在某种总线下的,譬如 i2c 设备挂在 I2C 总线下。

/sys/bus 目录下的文件通常也是链接到了/sys/devices 目录。

/sys/devices

这是是内核对系统中所有设备的分层次表达模型,也是/sys文件系统管理设备的最重要的目录结构。

/sys/devices目录下是全局设备结构体系,包含所有被发现的注册在各种总线上的各种物理设备。

/sys/class

这是系统中的所有设备按照其功能分类放置的目录结构,该目录下的文件也是链接到了/sys/devices 目录。

我们知道每种设备都具有自己特定的功能,比如:鼠标的功能是作为人机交互的输入,

按照设备功能分类无论它挂载在哪条总线上都是归类到/sys/class/input下。

../../../_images/linux_device_model02.png

注解

将它们统一起来就形成了上面的拓扑图,记录着设备与设备之间的关系。

而我们这章的重心则放在bus文件夹目录下,创建自己的总线类型以及devices和drivers。

了解上面设备与设备的拓扑图之后,让我们再回来“总线-设备-驱动”模型中来。“总线-设备-驱动”它们之间是如何相互配合工作的呢?

../../../_images/linux_device_model03.png

在总线上管理着两个链表,分别管理着设备和驱动,当我们向系统注册一个驱动时,便会向驱动的管理链表插入我们的新驱动,同样当我们向系统注册一个设备时,便会向设备的管理链表插入我们的新设备。

在插入的同时总线会执行一个bus_type结构体中match的方法对新插入的设备/驱动进行匹配。 (它们之间最简单的匹配方式则是对比名字,存在名字相同的设备/驱动便成功匹配)。 在匹配成功的时候会调用驱动device_driver结构体中probe方法(通常在probe中获取设备资源,具体的功能可由驱动编写人员自定义),并且在移除设备或驱动时,会调用device_driver结构体中remove方法。

以上只是设备驱动模型的 机制 ,上面的match、probe、remove等方法需要我们来实现需要的功能。看到这里相信我们都已经对设备驱动模型有了粗略的整体认识。

无论以后学习平台设备驱动、块设备驱动或者是其他总线设备,都跟Linux设备模型息息相关。sysfs文件系统用于把内核的设备驱动导出到用户空间,用户便可通过访问sys目录及其下的文件,来查看甚至控制内核的一些驱动设备。

10.2. 总线

注意

总线是连接处理器和设备之间的桥梁,总线代表着同类设备需要共同遵守的工作时序,是连接处理器和设备之间的桥梁。

我们接触到的设备大部分是依靠总线来进行通信的,它们之间的物理连接如图所示,对于野火开发板而言,触摸芯片是依赖于I2C,鼠标、键盘等HID设备,则是依赖于USB。从功能上讲,这些设备都是将文字、字符、控制命令或采集的数据等信息输入到计算机。

../../../_images/bus01.jpg

注解

总线驱动则负责实现总线的各种行为,其管理着两个链表,分别是添加到该总线的设备链表以及注册到该总线的驱动链表。

当你向总线添加(移除)一个设备(驱动)时,便会在对应的列表上添加新的节点,同时对挂载在该总线的驱动以及设备进行匹配,在匹配过程中会忽略掉那些已经有驱动匹配的设备。

总线结构

在内核中使用结构体bus_type来表示总线,如下所示:

bus_type结构体(内核源码/include/linux/device.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
struct bus_type {
    const char              *name;
    const char              *dev_name;
    struct device           *dev_root;
    const struct attribute_group **bus_groups;
    const struct attribute_group **dev_groups;
    const struct attribute_group **drv_groups;

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

    int (*online)(struct device *dev);
    int (*offline)(struct device *dev);

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

    int (*num_vf)(struct device *dev);

    int (*dma_configure)(struct device *dev);

    const struct dev_pm_ops *pm;

    const struct iommu_ops *iommu_ops;

    struct subsys_private *p;
    struct lock_class_key lock_key;

    bool need_parent_lock;

    ANDROID_KABI_RESERVE(1);
    ANDROID_KABI_RESERVE(2);
    ANDROID_KABI_RESERVE(3);
    ANDROID_KABI_RESERVE(4);
};
bus_type结构体成员

成员

描述

name

指定总线的名称,当新注册一种总线类型时,会在/sys/bus目录创建一个新的目录,目录名就是该参数的值

bus_groups

总线属性,表现为/sys/bus/<name>/文件夹下的文件

dev_groups

设备属性,设备则在目录/sys/bus/<name>/devices/中

drv_groups

驱动属性,在目录/sys/bus/<name>/drivers/下存放了设备的驱动属性

match

当一个新设备或者驱动被添加到这个总线时,均会执行该函数进行匹配

uevent

总线上的设备发生添加、移除或者其它动作时,就会调用该函数,来通知驱动做出相应的对策

probe

当总线将设备以及驱动相匹配之后,执行该回调函数,最终会调用驱动提供的probe函数

remove

当设备从总线移除时,调用该回调函数

suspend、resume

电源管理相关函数,当总线进入睡眠模式时,会调用suspend回调函数;而resume回调函数则是在唤醒总线的状态下执行

pm

电源管理的结构体,存放了一系列跟总线电源管理有关的函数,与device_driver结构体中的pm_ops有关

p

该结构体用于存放特定的私有数据,其成员klist_devices和klist_drivers记录了挂载在该总线的设备和驱动

在实际编写linux驱动模块时,Linux内核已经为我们写好了大部分总线驱动,正常情况下我们一般不会去注册一个新的总线,内核中提供了bus_register函数来注册总线,以及bus_unregister函数来注销总线,其函数原型如下:

注册/注销总线API(内核源码/drivers/base/bus.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/*******************************************************************************
*  @brief     bus_register
*  @note      注册总线
*  @param     bus        bus_type类型的结构体指针
*  @return    返回0表示注册成功,返回负数表示注册失败
*******************************************************************************/
int bus_register(struct bus_type *bus)

/*******************************************************************************
*  @brief     bus_unregister
*  @note      注销总线
*  @param     bus        bus_type类型的结构体指针
*  @return    无
*******************************************************************************/
void bus_unregister(struct bus_type *bus);

当我们成功注册总线时,会在/sys/bus/目录下创建一个新目录,目录名为我们新注册的总线名。

bus目录中包含了当前系统中已经注册了的所有总线,例如i2c,spi,platform等。我们看到每个总线目录都拥有两个子目录devices和drivers,分别记录着挂载在该总线的所有设备以及驱动。

在/sys/bus目录下输入 tree 可以查看注册了的所有总线。这里以查找i2c总线为例:

tree i2c
../../../_images/bus03.png

10.3. 设备

驱动开发的过程中,我们最关心的莫过于设备以及对应的驱动了。我们编写驱动的目的,最终就是为了使设备可以正常工作。在Linux中,一切都是以文件的形式存在,设备也不例外。

注意

/sys/devices目录记录了系统中所有设备,实际上在sys目录下所有设备文件最终都会指向该目录对应的设备文件;此外还有另一个目录/sys/dev记录所有的设备节点,但实际上都是些链接文件,同样指向了devices目录下的文件。

../../../_images/device01.png

在内核使用device结构体来描述我们的物理设备,如下所示(有省略)。

device结构体(内核源码/include/linux/device.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
struct device {
    ......
    struct device           *parent;
    struct device_private   *p;
    const char              *init_name; /* initial name of the device */
    struct bus_type *bus;           /* type of bus device is on */
    struct device_driver *driver;   /* which driver has allocated this device */
    void            *platform_data; /* Platform specific data, device core doesn't touch it */
    void            *driver_data;   /* Driver data, set and get with
                    dev_set_drvdata/dev_get_drvdata */
    struct device_node      *of_node; /* associated device tree node */
    dev_t                   devt;   /* dev_t, creates the sysfs "dev" */
    struct class            *class;
    const struct attribute_group **groups;  /* optional groups */
    void    (*release)(struct device *dev);
    ......
};
device结构体成员

成员

描述

parent

指向设备的“父”设备,它所连接的设备,在大多数情况下,父设备是某种总线或主机控制器,

如果该成员为NULL,则该设备为顶级设备;前面提到过,旧版本的设备之间没有任何关联,

引入Linux设备模型之后,设备之间呈树状结构,便于管理各种设备

p

是私有数据结构指针,该指针中会保存子设备链表、用于添加到bus/driver/prent等设备中的链表头等等

init_name

指定该设备的初始名称

bus

表示该设备依赖于哪个总线,当我们注册设备时,内核便会将该设备注册到对应的总线

driver

该设备所分配的驱动程序

platform_data

一个指针,用于保存具体的平台相关的数据(设备中特定的平台数据)

driver_data

指向驱动程序特定的私有数据

of_node

与设备数相联系的结构体指针,存放设备树中匹配的设备节点

devt

用于表示设备的设备号

class

指向设备所属class的指针

groups

指向struct attribute_group类型的指针,表示该设备的属性集合

release

函数指针,当设备需要释放时调用此函数。如果我们没定义该函数时,移除设备时,会提示

“Device ‘xxxx’ does not have a release() function, it is broken and must be fixed”的错误。

内核也提供相关的API来注册和注销设备,如下所示:

内核注册/注销设备(内核源码/driver/base/core.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*******************************************************************************
*  @brief     device_register
*  @note      注册设备
*  @param     dev        struct device结构体类型指针
*  @return    返回0表示注册成功,返回负数表示注册失败
*******************************************************************************/
int device_register(struct device *dev)
{
    device_initialize(dev);
    return device_add(dev);
}

/*******************************************************************************
*  @brief     device_unregister
*  @note      注销设备
*  @param     dev        struct device结构体类型指针
*  @return    无
*******************************************************************************/
void device_unregister(struct device *dev)
{
    pr_debug("device: '%s': %s\n", dev_name(dev), __func__);
    device_del(dev);
    put_device(dev);
}

在讲解总线的时候,我们说过,当成功注册总线时,会在/sys/bus目录下创建对应总线的目录,该目录下有两个子目录,分别是drivers和devices,我们使用device_register注册的设备从属于某个总线时,该总线的devices目录下便会存在该设备文件。

10.4. 驱动

前面两小节,已经大致介绍完总线以及设备。设备能否正常工作,取决于驱动。驱动需要告诉内核,自己可以驱动哪些设备,如何初始化设备。

在内核中,使用device_driver结构体来描述我们的驱动,如下所示:

device_driver结构体(内核源码/include/linux/device.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
struct device_driver {
    const char              *name;
    struct bus_type         *bus;
    struct module           *owner;
    const char              *mod_name;      /* used for built-in modules */
    bool suppress_bind_attrs;       /* disables bind/unbind via sysfs */
    const struct of_device_id       *of_match_table;
    const struct acpi_device_id     *acpi_match_table;
    int (*probe) (struct device *dev);
    void (*sync_state)(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);
    const struct attribute_group **groups;
    struct driver_private *p;
    ......
};
device_driver结构体成员

成员

描述

name

设备驱动程序的名称

bus

指向总线描述符的指针,总线连接所支持的设备

owner

设备驱动的owner,通常为THIS_MODULE,表示实现设备驱动程序的模块

mod_name

表示实现设备驱动程序的模块,mod_name为模块名称

suppress_bind_attrs

通过sysfs操作设备驱动的bind/unbind,用来使能/关闭设备与驱动的自动匹配

of_match_table

用于dtb 启动时匹配

acpi_match_table

用于acpi 启动时匹配

probe,remove

当设备匹配/移除的时候,会调用设备驱动的probe/remove函数。

shutdown,suspend,resume

代表设备驱动在调用管理的时候的回调函数。

sync_state

让内核干净利落地从引导加载程序接管设备管理的函数

groups

设备驱动的属性

p

设备驱动的私有数据结构,通常可以将驱动的信息放入此结构中。

内核提供了driver_register函数以及driver_unregister函数来注册/注销驱动,成功注册的驱动会记录在/sys/bus/<bus>/drivers目录,函数原型如下所示:

注册/注销驱动(内核源码/drivers/base/driver.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/*******************************************************************************
*  @brief     driver_register
*  @note      注册驱动
*  @param     drv       struct device_driver结构体类型指针
*  @return    返回0表示注册成功,返回负数表示注册失败
*******************************************************************************/
int driver_register(struct device_driver *drv)
{
    int ret;
    struct device_driver *other;

    if (!drv->bus->p) {
        pr_err("Driver '%s' was unable to register with bus_type '%s' because the bus was not initialized.\n",
            drv->name, drv->bus->name);
        return -EINVAL;
    }

    if ((drv->bus->probe && drv->probe) ||
        (drv->bus->remove && drv->remove) ||
        (drv->bus->shutdown && drv->shutdown))
        printk(KERN_WARNING "Driver '%s' needs updating - please use "
            "bus_type methods\n", drv->name);

    other = driver_find(drv->name, drv->bus);
    if (other) {
        printk(KERN_ERR "Error: Driver '%s' is already registered, "
            "aborting...\n", drv->name);
        return -EBUSY;
    }

    ret = bus_add_driver(drv);
    if (ret)
        return ret;
    ret = driver_add_groups(drv, drv->groups);
    if (ret) {
        bus_remove_driver(drv);
        return ret;
    }
    kobject_uevent(&drv->p->kobj, KOBJ_ADD);

    return ret;
}

/*******************************************************************************
*  @brief     driver_unregister
*  @note      注销驱动
*  @param     drv       struct device_drive结构体类型指针
*  @return    无
*******************************************************************************/
void driver_unregister(struct device_driver *drv)
{
    if (!drv || !drv->p) {
        WARN(1, "Unexpected driver unregister!\n");
        return;
    }
    driver_remove_groups(drv, drv->groups);
    bus_remove_driver(drv);
}

到为止简单地介绍了总线、设备、驱动的数据结构以及注册/注销接口函数。

下图是总线关联上设备与驱动之后的数据结构关系图:

../../../_images/linux_device_modle04.png

大致注册流程如下

../../../_images/linux_device_modle05.png

注解

系统启动之后会调用buses_init函数创建/sys/bus文件目录,这部分系统在开机时已经帮我们准备好了。接下去就是通过bus_register函数进行总线注册,注册完总线后在总线的目录下生成devices文件夹和drivers文件夹,最后分别通过device_register以及driver_register函数注册相对应的设备和驱动。

10.5. attribute属性文件

/sys目录有各种子目录以及文件,前面讲过当我们注册新的总线、设备或驱动时,内核会在对应的地方创建一个新的目录,目录名为各自结构体的name成员,每个子目录下的文件,都是内核导出到用户空间,用于控制我们的设备的。

内核中以attribute结构体来描述/sys目录下的文件,如下所示:

attribute结构体(内核源码/include/linux/sysfs.h)
1
2
3
4
5
6
7
8
9
struct attribute {
    const char              *name; // 指定文件的文件名
    umode_t                 mode; // 指定文件的权限
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    bool                    ignore_lockdep:1;
    struct lock_class_key   *key;
    struct lock_class_key   skey;
#endif
};

bus_type、device、device_driver结构体中都包含了一种数据类型struct attribute_group,如下所示,它是多个attribute文件的集合,利用它进行初始化,可以避免一个个注册attribute。

attribute_group结构体(内核源码/include/linux/sysfs.h)
1
2
3
4
5
6
7
8
9
struct attribute_group {
    const char              *name;
    umode_t                 (*is_visible)(struct kobject *,
                        struct attribute *, int);
    umode_t                 (*is_bin_visible)(struct kobject *,
                        struct bin_attribute *, int);
    struct attribute        **attrs;
    struct bin_attribute    **bin_attrs;
};

10.5.1. 设备属性文件

在开发单片机的时候,如果想要读取某个寄存器的值,你可能需要加入一些新的代码,并重新编译。但对于Linux内核来讲,每次都需要编译一遍源码,实在太浪费时间和精力了。

为此,Linux提供以下接口,来注册和注销一个设备属性文件。我们可以通过这些接口直接在用户层进行查询/修改,避免了重新编译内核的麻烦。

设备属性文件接口(内核源码/include/linux/device.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/* interface for exporting device attributes */
struct device_attribute {
    struct attribute        attr;
    ssize_t (*show)(struct device *dev, struct device_attribute *attr,
            char *buf);
    ssize_t (*store)(struct device *dev, struct device_attribute *attr,
            const char *buf, size_t count);
};


/*********************************************************************************
device_create_file函数用于创建文件。
device_remove_file函数用于删除文件,当我们的驱动注销时,对应目录以及文件都需要被移除。
*********************************************************************************/
#define DEVICE_ATTR(_name, _mode, _show, _store) \
        struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
extern int device_create_file(struct device *device,
                const struct device_attribute *entry);
extern void device_remove_file(struct device *dev,
                const struct device_attribute *attr);

注解

DEVICE_ATTR宏定义用于定义一个“device_attribute”类型的变量,## 表示将两边的标签拼接在一起,因此,我们得到变量的名称应该是带有dev_attr_前缀的。

该宏定义需要传入四个参数_name,_mode,_show,_store,分别代表了文件名,文件权限,show回调函数,store回调函数。

show回调函数以及store回调函数分别对应着用户层的cat和echo命令,当我们使用cat命令,来获取/sys目录下某个文件时,最终会执行show回调函数。使用echo命令,则会执行store回调函数。

参数_mode的值,可以使用S_IRUSR、S_IWUSR、S_IXUSR等宏定义,更多选项可以查看读写文件章节关于文件权限的内容。

10.5.2. 驱动属性文件

驱动属性文件,和设备属性文件的作用是一样,唯一的区别在于函数参数的不同,函数接口如下:

驱动属性文件接口(内核源码/include/linux/device.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
struct driver_attribute {
    struct attribute attr;
    ssize_t (*show)(struct device_driver *driver, char *buf);
    ssize_t (*store)(struct device_driver *driver, const char *buf,
            size_t count);
};

#define DRIVER_ATTR_RW(_name) \
    struct driver_attribute driver_attr_##_name = __ATTR_RW(_name)
#define DRIVER_ATTR_RO(_name) \
    struct driver_attribute driver_attr_##_name = __ATTR_RO(_name)
#define DRIVER_ATTR_WO(_name) \
    struct driver_attribute driver_attr_##_name = __ATTR_WO(_name)

/*********************************************************************************
driver_create_file 和 driver_remove_file 函数用于创建和移除文件。
*********************************************************************************/
extern int __must_check driver_create_file(struct device_driver *driver,
                    const struct driver_attribute *attr);
extern void driver_remove_file(struct device_driver *driver,
                const struct driver_attribute *attr);

注解

DRIVER_ATTR_RW/DRIVER_ATTR_RO/DRIVER_ATTR_WO 宏定义用于定义一个driver_attribute类型的变量,带有driver_attr_的前缀,区别在于文件权限不同,RW后缀表示文件可读写,RO后缀表示文件仅可读,WO后缀表示文件仅可写。

而且你会发现,DRIVER_ATTR类型的宏定义没有参数来设置show和store回调函数,那如何设置这两个参数呢?

在写驱动代码时,只需要你提供xxx_store以及xxx_show这两个函数,并确保两个函数的xxx和DRIVER_ATTR类型的宏定义中名字是一致的即可。

10.5.3. 总线属性文件

同样的,Linux也为总线通过了相应的函数接口,如下所示:

总线属性文件接口(内核源码/include/linux/device.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
struct bus_attribute {
    struct attribute        attr;
    ssize_t (*show)(struct bus_type *bus, char *buf);
    ssize_t (*store)(struct bus_type *bus, const char *buf, size_t count);
};

// BUS_ATTR宏定义用于定义一个bus_attribute变量。
#define BUS_ATTR_RW(_name) \
    struct bus_attribute bus_attr_##_name = __ATTR_RW(_name)
#define BUS_ATTR_RO(_name) \
    struct bus_attribute bus_attr_##_name = __ATTR_RO(_name)
#define BUS_ATTR_WO(_name) \
    struct bus_attribute bus_attr_##_name = __ATTR_WO(_name)

/*********************************************************************************
bus_create_file函数,会在/sys/bus/<bus-name>下创建对应的文件。
bus_remove_file则用于移除该文件。
*********************************************************************************/
extern int __must_check bus_create_file(struct bus_type *,
                    struct bus_attribute *);
extern void bus_remove_file(struct bus_type *, struct bus_attribute *);

10.6. 驱动设备模型代码编写

在设备模型框架下,设备驱动的开发是一件很简单的事情:

  1. 先分配一个struct device类型的变量,填充必要的信息后,把它注册到对应总线中;

  2. 然后创建一个struct device_driver类型,填充必要的信息后注册。

  3. 在合适的时机(驱动和设备匹配时),就调用驱动的probe、release等回调函数。

另外,在实际编程中较少直接使用device和device_drivere,而是在它们上面加一层封装,比如下一章的platform device等

下面实验利用前面学到的理论知识,来创建一个虚拟的总线xbus,分别挂载了驱动xdrv以及设备xdev。

本章的示例代码目录为: linux_driver/linux_device_model

10.6.1. 编程思路

  1. 编写Makefile文件

  2. 声明一个总线结构体并创建一个总线xbus,实现match方法,对设备和驱动进行匹配

  3. 声明一个设备结构体,挂载到我们的xbus总线中

  4. 声明一个驱动结构体,挂载到xbus总线,实现probe、remove方法

  5. 将总线、设备、驱动导出属性文件到用户空间。

10.6.2. Makefile

工欲善其事必先利其器,在开始写程序之前,我们需要先准备好我们的Makefile。

Makefile(位于../linux_driver/linux_device_model/Makefile)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
KERNEL_DIR=../../kernel/

ARCH=arm64
CROSS_COMPILE=aarch64-linux-gnu-
export  ARCH  CROSS_COMPILE

obj-m := xdev.o xbus.o xdrv.o
all:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules

.PHONE:clean

clean:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean

10.6.3. 总线

10.6.3.1. 定义新的总线

定义总线(位于../linux_driver/linux_device_model/xbus.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/************************************************************************
* 函数负责总线下的设备以及驱动匹配
* 使用字符串比较的方式,通过对比驱动以及设备的名字来确定是否匹配,
* 如果相同, 则说明匹配成功,返回1;反之,则返回0
***********************************************************************/
int xbus_match(struct device *dev, struct device_driver *drv)
{
    printk("%s-%s\n", __FILE__, __func__);
    if (!strncmp(dev_name(dev), drv->name, strlen(drv->name))) {
        printk("dev & drv match\n");
        return 1;
    }
    return 0;
}

//定义了一种新的总线,名为xbus,总线结构体中最重要的一个成员,便是match回调函数
static struct bus_type xbus = {
    .name = "xbus",
    .match = xbus_match,
};
EXPORT_SYMBOL(xbus);
  • 第6-14行:我们使用字符串比较的方式,通过对比驱动以及设备的名字来确定是否匹配,如果相同,则说明匹配成功,返回1;反之,则返回0。

  • 第17-21行:定义了一个名为xbus的总线总线结构体中最重要的一个成员,便是match回调函数,这个函数负责总线下的设备以及驱动匹配,没有这个函数,设备与驱动便不可以进行匹配。

10.6.3.2. 导出总线属性文件

我们通过BUS_ATTR宏,将我们自定义的变量导出到/sys目录,方便用户查询。

导出总线属性文件(位于../linux_driver/linux_device_model/xbus.c)
1
2
3
4
5
6
7
8
9
//定义了一个bus_name变量,存放了该总线的名字
static char *bus_name = "xbus";
//提供show回调函数,这样用户便可以通过cat命令, 来查询总线的名称
ssize_t xbus_test_show(struct bus_type *bus, char *buf)
{
    return sprintf(buf, "%s\n", bus_name);
}
//设置该文件的文件权限为文件拥有者可读,组内成员以及其他成员不可操作
BUS_ATTR(xbus_test, S_IRUSR, xbus_test_show, NULL);

10.6.3.3. 注册/注销总线

内核的驱动代码,都是基于内核模块,我们在模块初始化的函数中注册总线,在模块注销的函数中注销该总线。

注册/注销总线(位于../linux_driver/linux_device_model/xbus.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//注册总线
static __init int xbus_init(void)
{
    int ret;
    printk("xbus init\n");

    ret = bus_register(&xbus);
    if(ret)
        printk("xbus register failed\n");

    ret = bus_create_file(&xbus, &bus_attr_xbus_test);
    return 0;
}
module_init(xbus_init);

//注销总线
static __exit void xbus_exit(void)
{
    printk("xbus exit\n");
    bus_remove_file(&xbus, &bus_attr_xbus_test);
    bus_unregister(&xbus);
}
module_exit(xbus_exit);

注意

编译后,将生成的.ko文件传输到鲁班猫板卡。

# 使用insmod命令加载xbus内核模块
sudo insmod xbus.ko

# 查看/sys/bus/xbus/目录
tree /sys/bus/xbus/
ls /sys/bus/xbus/

当我们成功加载编译出来的内核模块时,内核便会出现一种新的总线xbus,如图所示:

../../../_images/xbus.png

注解

红框处便是我们自定义的总线属性文件。可以看到devices和drivers目录都是空的,并没有什么设备和驱动挂载在该总线下。

10.6.4. 设备

Linux设备模型中,总线已经注册好了,还缺少设备和驱动。

注册一个新的设备,主要完成这两个工作:一个是名字,这是总相匹配的依据;另一个就是总线,该设备挂载在哪个总线上,不能张冠李戴。

这里,我们注册一个设备xdev,并且定义一个变量id,将该变量导出到用户空间,使得用户可以通过sysfs文件系统来修改该变量的值。

10.6.4.1. 定义新的设备

定义新设备(位于../linux_driver/linux_device_model/xdev.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 声明了外部的总线变量xbus
extern struct bus_type xbus;
// 编写release函数,防止卸载模块时会报错
void xdev_release(struct device *dev)
{
    printk("%s-%s\n", __FILE__, __func__);
}

// 定义了一个名为xdev的设备,将其挂载在xbus上。
static struct device xdev = {
    .init_name = "xdev",
    .bus = &xbus,
    .release = xdev_release,
};

10.6.4.2. 导出设备属性文件

导出设备属性文件(位于../linux_driver/linux_device_model/xdev.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned long id = 0;
//show回调函数中,直接将id的值通过sprintf函数拷贝至buf中。
ssize_t xdev_id_show(struct device *dev, struct device_attribute *attr,
            char *buf)
{
    return sprintf(buf, "%ld\n", id);
}
/*********************************************************************************************
* store回调函数则是利用kstrtoul函数,该函数有三个参数,其中第二个参数是采用几进制的方式,这里我们
* 传入的是10,意味着buf中的内容将转换为10进制的数传递给id,实现了通过sysfs修改驱动的目的。
*********************************************************************************************/
ssize_t xdev_id_store(struct device * dev, struct device_attribute * attr,
            const char *buf, size_t count)
{
    int val = 0;
    val = kstrtoul(buf, 10, &id);
    return count;
}

//DEVICE_ATTR宏定义定义了xdev_id,设置该文件的文件权限是文件拥有者可读可写,组内成员以及其他成员不可操作
DEVICE_ATTR(xdev_id, S_IRUSR | S_IWUSR, xdev_id_show, xdev_id_store);

关于kstrtoul()函数:

kstrtoul()函数解析 (内核源码/include/linux/kernel.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/********************************************************************************************
*  @brief     kstrtoul
*  @note      将一个字符串转换成一个无符号长整型的数据
*  @param     s        字符串的起始地址,该字符串必须以空字符结尾
*  @param     base     转换基数,如果base=0,则函数会自动判断字符串的类型,且按十进制输出
*  @param     res      一个指向被转换成功后的结果的地址。
*  @return    转换成功后返回0,溢出将返回-ERANGE,解析出错返回-EINVAL
********************************************************************************************/
static inline int __must_check kstrtoul(const char *s, unsigned int base, unsigned long *res)
{
    /*
    * We want to shortcut function call, but
    * __builtin_types_compatible_p(unsigned long, unsigned long long) = 0.
    */
    if (sizeof(unsigned long) == sizeof(unsigned long long) &&
        __alignof__(unsigned long) == __alignof__(unsigned long long))
        return kstrtoull(s, base, (unsigned long long *)res);
    else
        return _kstrtoul(s, base, res);
}

10.6.4.3. 注册/注销设备

最后,只需要调用device_register函数以及device_create_file函数,将上面的设备结构体以及属性文件结构体注册到内核即可。

注册/注销设备(位于../linux_driver/linux_device_model/xdev.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//设备结构体以及属性文件结构体注册
static __init int xdev_init(void)
{
    int ret;

    printk("xdev init\n");
    ret = device_register(&xdev);
    if(ret)
        printk("Unable to register xdev\n");

    device_create_file(&xdev, &dev_attr_xdev_id);
    return 0;
}
module_init(xdev_init);


//设备结构体以及属性文件结构体注销。
static __exit void xdev_exit(void)
{
    printk("xdev exit\n");
    device_remove_file(&xdev, &dev_attr_xdev_id);
    device_unregister(&xdev);
}
module_exit(xdev_exit);

加载模块进行验证

# 加载xbus内核模块
sudo insmod xbus.ko
# 加载xdev内核模块
sudo insmod xdev.ko

# 查看/sys/bus/xbus/目录
tree /sys/bus/xbus/
../../../_images/xdev1.png

注解

加载内核模块后,我们可以看到在/sys/bus/xbus/devices/中多了个设备xdev,它是个链接文件,最终指向了/sys/devices中的设备。

我们直接切换到xdev的目录下,可以看到,我们自定义的属性文件xdev_id。通过echo以及cat命令,可以进行修改和查询,如下所示:

../../../_images/xdev2.png

10.6.5. 驱动

关于驱动的部分,由于本章实验没有具体的物理设备,因此,没有涉及到设备初始化、设备的函数接口等内容。

10.6.5.1. 定义新的驱动

定义新驱动(位于../linux_driver/linux_device_model/xdrv.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*声明了外部的总线变量xbus*/
extern struct bus_type xbus;

// 当驱动和设备匹配成功之后,便会执行驱动的probe函数,这里只是在终端上打印当前的文件以及函数名。
int xdrv_probe(struct device *dev)
{
    printk("%s-%s\n", __FILE__, __func__);
    return 0;
}
//当注销驱动时,需要关闭物理设备的某些功能等,这里也只是打印出当前的文件名以及函数名。
int xdrv_remove(struct device *dev)
{
    printk("%s-%s\n", __FILE__, __func__);
    return 0;
}

//定义了一个驱动结构体xdrv,名字需要和设备的名字相同,否则就不能成功匹配
static struct device_driver xdrv = {
    .name = "xdev",
    //该驱动挂载在已经注册好的总线xbus下。
    .bus = &xbus,
    //当驱动和设备匹配成功之后,便会执行驱动的probe函数
    .probe = xdrv_probe,
    //当注销驱动时,需要关闭物理设备的某些功能等
    .remove = xdrv_remove,
};

10.6.5.2. 导出驱动属性文件

导出驱动属性文件(位于../linux_driver/linux_device_model/xdrv.c)
1
2
3
4
5
6
7
8
char *name = "xdrv";
//保证store和show函数的前缀与驱动属性文件一致,drvname_show()的前缀和drvname
ssize_t drvname_show(struct device_driver *drv, char *buf)
{
    return sprintf(buf, "%s\n", name);
}
//DRIVER_ATTR_RO定义了一个drvname属性文件
DRIVER_ATTR_RO(drvname);

注解

在讲驱动属性文件时,我们讲到DRIVER_ATTR_RO定义驱动属性文件时,没有参数可以设置show和store回调函数,我们只要保证store和show函数的前缀与驱动属性文件一致即可。如代码所示,定义了一个drvname属性文件,show回调函数的函数名则为drvname_show,这样便可以完成两者之间的关联。

10.6.5.3. 注册/注销驱动

最后,调用“driver_register”函数以及“driver_create_file”函数进行注册我们的驱动以及驱动属性文件。

注册/注销驱动(位于../linux_driver/linux_device_model/xdrv.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//调用 driver_register 和driver_create_file 函数进行注册我们的驱动以及驱动属性文件
static __init int xdrv_init(void)
{
    int ret;
    printk("xdrv init\n");
    ret = driver_register(&xdrv);
    ret = driver_create_file(&xdrv, &driver_attr_drvname);
    return 0;
}
module_init(xdrv_init);

//注销驱动以及驱动属性文件
static __exit void xdrv_exit(void)
{
    printk("xdrv exit\n");
    driver_remove_file(&xdrv, &driver_attr_drvname);
    driver_unregister(&xdrv);
}
module_exit(xdrv_exit);

加载模块进行验证

# 加载xbus内核模块
sudo insmod xbus.ko
# 加载xdev内核模块
sudo insmod xdev.ko
# 加载xdrv内核模块
sudo insmod xdrv.ko

# 查看/sys/bus/xbus/目录
tree /sys/bus/xbus/
../../../_images/xdrv.png

注解

成功加载驱动后,可以看到/sys/bus/xbus/drivers多了个驱动xdev目录。

当我们加载完设备和驱动之后,总线开始进行匹配,执行match函数,发现这两个设备的名字是一致的,就将设备和驱动关联到一起,最后会执行驱动的probe函数。

使用命令 dmesg | tail 来查看模块加载过程的打印信息。

../../../_images/dmesg.png