背景
前面,我们介绍了写驱动代码的一些常规步骤,并且也写了最基本的驱动代码,但是那些代码存在着问题,我们将硬件的信息都写进了驱动里了,如果我们在杂项设备驱动中控制led,那么会在硬件操作接口中包含硬件信息,如果引脚有变化,这个驱动代码就得重新修改,虽然修改也很简单,但是从框架的角度来看,这是不合理的,相当于驱动代码写死了。
于是,Linux引入了设备驱动模型分层的概念,将我们编写的驱动代码分为两块:设备和驱动。设备负责提供硬件资源,驱动代码负责去使用这些设备提供的硬件资源。并由总线将他们联系起来。
设备(device):挂载在某个总线的物理设备。
驱动(driver):与特定设备相关的软件,负责初始化该设备以及提供一些操作该设备的操作方式。
总线(bus):负责管理挂载对应总线的设备以及驱动。
类(class):对于具有相同功能的设备,归结到一种类别,进行分类管理。
重要目录
/sys/bus
目录下的每个子目录都是注册好了的总线类型,每个总线类型下包含两个子目录(/sys/bus/devices
和sys/bus/drivers
)。其中/sys/bus/devices
下是该总线下的所有设备,而这些设备都是符号链接,他们分别指向真正的设备(/sys/devices
目录下)。sys/bus/drivers
目录下是所有注册在这个总线上的驱动,每个子目录下是一些可以观察和修改的driver参数。
/sys/devices
目录下是全局设备结构体系,包含所有被发现的注册在各种总线上的各种物理设备。
/sys/dev
目录下是所有的设备节点,但实际上都是些链接文件,同样指向了/sys/devices
目录下的文件。
/sys/class
目录下则是包含所有注册在kernel里面的设备类型,这是按照设备功能分类的设备模型。
框架原理
- 总线管理着两个链表:设备链表和驱动链表。
- 当我们向内核注册一个驱动时,便插入到总线的驱动链表。
- 当我们向内核注册一个设备时,便插入到总线的设备链表。
- 在插入的同时,总线会执行一个
bus_type
结构体中的match
方法对新插入的设备或驱动进行匹配。 - 匹配成功后,会调用驱动
device_driver
结构体中的probe
方法。通常在probe
方法中获取设备资源,具体的功能可自定义。 - 移除设备或驱动时,会调用
device_driver
结构体中remove方法。
总线
总线是连接处理器和设备之间的桥梁,总线代表着同类设备需要共同遵守的工作时序。
bus_type结构体(内核源码/include/linux/device.h)struct bus_type {const char *name;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);int (*remove)(struct device *dev);int (*suspend)(struct device *dev, pm_message_t state);int (*resume)(struct device *dev);const struct dev_pm_ops *pm;struct subsys_private *p;};
- name :指定总线的名称,当新注册一种总线类型时,会在/sys/bus目录创建一个新的目录,目录名就是该参数的值;
- drv_groups、dev_groups、bus_groups :分别表示驱动、设备以及总线的属性。这些属性可以是内部变量、字符串等等。通常会在对应的/sys目录下在以文件的形式存在,对于驱动而言,在目录/sys/bus//driver/存放了设备的默认属性;设备则在目录/sys/bus//devices/中。这些文件一般是可读写的,用户可以通过读写操作来获取和设置这些attribute的值。
- 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)int bus_register(struct bus_type *bus);
void bus_unregister(struct bus_type *bus);
设备
device结构体(内核源码/include/linux/device.h)struct device {
const char *init_name;struct device *parent;struct bus_type *bus;struct device_driver *driver;void *platform_data;void *driver_data;struct device_node *of_node;dev_t devt;struct class *class;
void (*release)(struct device *dev);const struct attribute_group **groups; /* optional groups */
struct device_private *p;
........
};
- init_name :指定该设备的名称,总线匹配时,一般会根据比较名字,来进行配对;
- parent :表示该设备的父对象,前面提到过,旧版本的设备之间没有任何关联,引入Linux设备模型之后,设备之间呈树状结构,便于管理各种设备;
- bus :表示该设备依赖于哪个总线,当我们注册设备时,内核便会将该设备注册到对应的总线。
- of_node :存放设备树中匹配的设备节点。当内核使能设备树,总线负责将驱动的of_match_table以及设备树的compatible属性进行比较之后,将匹配的节点保存到该变量。
- platform_data :一个指针,用于保存具体的平台相关的数据。具体的driver模块,可以将一些私有的数据,暂存在这里,需要使用的时候,再拿出来,因此设备模型并不关心该指针得实际含义。
- driver_data :同上,驱动层可通过dev_set/get_drvdata函数来获取该成员;
- class :指向了该设备对应类,开篇我们提到的触摸,鼠标以及键盘等设备,对于计算机而言,他们都具有相同的功能,都归属于输入设备。我们可以在/sys/class目录下对应的类找到该设备,如input、leds、pwm等目录;
- dev :dev_t类型变量,字符设备章节提及过,它是用于标识设备的设备号,该变量主要用于向/sys目录中导出对应的设备。
- release :回调函数,当设备被注销时,会调用该函数。如果我们没定义该函数时,移除设备时,会提示“Device ‘xxxx’ does not have a release() function, it is broken and must be fixed”的错误。
- group :指向struct attribute_group类型的指针,指定该设备的属性;
- *p:是私有数据结构指针,该指针中会保存子设备链表、用于添加到bus/driver/prent等设备中的链表头等等。
内核也提供相关的API来注册和注销设备int device_register(struct device *dev);
void device_unregister(struct device *dev);
在讲解总线的时候,我们说过,当成功注册总线时,会在/sys/bus目录下创建对应总线的目录,该目录下有两个子目录,分别是drivers和devices, 我们使用device_register注册的设备从属于某个总线时,该总线的devices目录下便会存在该设备文件。
驱动
device_driver结构体(内核源码/include/linux/device.h)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);int (*remove) (struct device *dev);const struct attribute_group **groups;struct driver_private *p;};
- name :指定驱动名称,总线进行匹配时,利用该成员与设备名进行比较;
- bus :表示该驱动依赖于哪个总线,内核需要保证在驱动执行之前,对应的总线能够正常工作;
- suppress_bind_attrs :布尔量,用于指定是否通过sysfs导出bind与unbind文件,bind与unbind文件是驱动用于绑定/解绑关联的设备。
- owner :表示该驱动的拥有者,一般设置为THIS_MODULE;
- of_match_table :指定该驱动支持的设备类型。当内核使能设备树时,会利用该成员与设备树中的compatible属性进行比较。
- remove :当设备从操作系统中拔出或者是系统重启时,会调用该回调函数;
- probe :当驱动以及设备匹配后,会执行该回调函数,对设备进行初始化。通常的代码,都是以main函数开始执行的,但是在内核的驱动代码,都是从probe函数开始的。
- group :指向struct attribute_group类型的指针,指定该驱动的属性;
内核提供了driver_register函数以及driver_unregister函数来注册/注销驱动,成功注册的驱动会记录在/sys/bus//drivers目录
device_driver结构体(内核源码/include/linux/device.h)int driver_register(struct device_driver *drv);
void driver_unregister(struct device_driver *drv);
示例
暂无。一般情况我们都会在系统已注册的总线上写驱动和设备。这篇文章主要了解这种架构的原理。