前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux笔记(20)| Linux的设备模型

Linux笔记(20)| Linux的设备模型

作者头像
飞哥
发布2021-02-02 10:55:38
3.1K0
发布2021-02-02 10:55:38
举报

//本文主要参考《野火Linux开发指南》

大家好,今天跟大家分享的是在Linux中驱动led。今天的文章包括后面还有一篇是酝酿了近两个星期才开始动手写,可见这部分内容会比较抽象一些。

其实早在之前有一篇关于字符设备驱动的,讲的也是驱动led,大家可以回顾一下:Linux笔记(13)| 字符设备驱动基础入门 Linux笔记(14)| 字符设备驱动基础入门(续)

有朋友可能会产生疑问,既然都是驱动led,那么有什么不同呢?

实际上有非常大的区别。我们可以先回顾一下当时我们的驱动是怎么写的。简单来说,是这样子:比如要驱动led,我们先写一个led的模块文件,在模块的init函数里面初始化硬件,然后调用register_chrdev函数向内核注册字符设备文件,这个函数里面最重要的就是填充file_operation结构体,因为这个结构体里有我们的read & write函数的接口,注册好了之后创建设备文件,通过设备号将设备文件和刚刚注册的字符设备相绑定,这样我们就可以在应用程序里面通过设备文件来操作硬件。

这样做看起来好像挺好的,但是有一个很大的问题,就是如果硬件发生一点改变,就要重新改写驱动代码,然后重新编译,重新安装模块,这是非常麻烦,非常糟糕的。

所以就有了今天的设备模型。在早期的Linux里面就是像上面那样做的,但是到后来设备越来越多,越来越复杂,维护起来非常不方便,于是发明了设备模型。

那么,设备模型是怎么一回事呢?简单说设备模型就是让驱动代码分成两个部分,一部分是驱动,一部分是设备,驱动文件里有对设备的驱动,但是并不涉及具体硬件资源,硬件资源由设备文件提供,然后使用总线将两者联系起来。这样当硬件发生改变时,驱动文件不需要动,只要修改设备文件就行。这样修改会更简单一些,因为改一改寄存器,改一改管脚什么的是相对来说比较容易的,但是驱动的逻辑我们希望一次写好之后就再也不需要动。

要了解设备模型,我们需要先了解几个概念:

设备:设备就是一些物理设备,比如一个led就可以叫做一个设备;

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

总线:将设备和驱动联系起来,负责管理设备和驱动;

:具有相同功能的设备把它们归为一类。

这实际上就是内核提供给我们的一套框架,按照之前的方法也可以写驱动,只是说按照这样的框架来写驱动的话,会使得代码更好维护,也更具有可操作性。

所以我们要做的无非就是学习内核的这一套框架,先熟悉这套框架,学会如何使用即可。不能够一下子太深入,因为这里面一直深入的话就是非常复杂的各种数据结构了,这对于我们来说不是一下子能够掌握的,我们只需要站在一个驱动工程师的角度来学习它,而不需要站在一个内核开发者的角度来研究它。

那么接下来就说一下如何套用设备模型来写代码。

主要是涉及总线、设备、驱动。它们的共性就是都需要向内核注册(或者注销),都具有自己的属性,而差别就在于属性不同。这里面有大量面向对象的思想,在面向对象的语言里面,会有属性和方法的概念,其实属性就是变量,方法就是函数,但是C语言的结构体里没有函数,只有函数指针,所以这里我干脆把函数指针叫做方法。

1、总线

先看一下总线是一种什么类型的变量

代码语言:javascript
复制
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;

};

在这里面很重要的几个就是match方法和probe方法。

match方法就是当向总线注册一个新的设备或者是新的驱动时,会调用该回调函数。该回调函数主要负责判断是否有注册了的驱动适合新的设备,或者新的驱动能否驱动总线上已注册但没有驱动匹配的设备;

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

然后就是总线的注册和注销:

代码语言:javascript
复制
int bus_register(struct bus_type *bus);
代码语言:javascript
复制
void bus_unregister(struct bus_type *bus);

2、设备

代码语言:javascript
复制
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,总线类型bus,还有设备的数据driver_data和platform_data

设备的名字不能乱起,必须要和待会儿的驱动里的名字一样,因为总线是通过名字来匹配的。数据就是硬件相关的一些数据了。

然后就是设备的注册和注销:

代码语言:javascript
复制
int device_register(struct device *dev);
代码语言:javascript
复制
void device_unregister(struct device *dev);

3、驱动

代码语言:javascript
复制
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,probe方法。名字需要和刚刚的设备里的名字一样,总线当然也是一样的,probe方法就很重要了,它是驱动和设备匹配的时候执行的函数,也就是操作硬件的具体逻辑。

讲到这里,估计很多人已经猜到驱动应该怎么写了。确实很简单,我们写三个模块文件,一个是总线的,一个是设备的,一个是驱动的。(实际通常不需要写新的总线,只需要用内核提供的总线即可)。

核心就是调用各自的register函数来向注册,比如我要注册一个设备,就先定义一个struct device类型的变量,往这个变量里面填充相应的内容,然后调用device_register函数注册即可,就这么简单。总线和驱动也是这样子。

所以最关键的部分实际上是结构体变量如何填充,里面函数指针(方法)应该怎么编写。

那么这里先把他们之间的联系先说一下。

首先在bus里面有个match方法,它是用来匹配设备和驱动的,当我们有新设备或者是驱动时,它会去匹配,如果是我们自己去实现这个函数,那么我们就要去读取设备和驱动的名字,然后看是否匹配。

设备里面主要是提供硬件的信息,驱动里面就是使用probe函数来执行具体的操作。

比如你安装了一个设备和驱动,然后总线就会去使用match函数去匹配他们,假设可以匹配上,那么就会执行probe函数,而probe函数里不携带具体的数据,它的数据是从设备文件里来的。

通过他们的配合,于是完成了对硬件的驱动。而且实现了驱动和硬件相分离。

不过还有一点是没提到的,上面这一套确实可以操作硬件了,但是并没有给应用层留下操作的接口啊。

比如说操作led,led里面关于寄存器等的信息已经在设备文件了,驱动led亮灭的逻辑在驱动文件里写好了,但是光有驱动,没有给应用层留下接口,我应用层还是调用不了驱动,那也是没有用的,所以我们需要把接口导出到用户空间。

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

来看一下属性结构体:

代码语言:javascript
复制
struct attribute {
    const char              *name;
    umode_t                 mode;
};

这是一个“基类”,当然这也是面向对象的语言里才有的概念。基类就是大家都有的,而每个对象又有自己独特的属性。

来看一下设备的属性:

代码语言:javascript
复制
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);
};

#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);

除了包含基类attr之外,还有show方法和store方法。

DEVICE_ATTR宏 定义用于定义一个device_attribute类型的变量,##表示将##左右两边的标签拼接在一起,因此, 我们得到变量的名称应该是带有dev_attr_前缀的。该宏定义需要传入四个参数_name,_mode,_show,_store,分别代表了文件名, 文件权限,show回调函数,store回调函数。show回调函数以及store回调函数分别对应着用户层的cat和echo命令, 当我们使用cat命令,来获取/sys目录下某个文件时,最终会执行show回调函数;使用echo命令,则会执行store回调函数。

驱动和总线里的属性也是类似,这里就不多赘述了。添加了这些属性,系统会在sysfs上生成对应的文件。比如可以通过

代码语言:javascript
复制
ls /sys/bus
ls /sys/devices

来查看总线和设备,在设备里面有对应的驱动

写好了代码之后,我们要写一个Makefile来控制编译

代码语言:javascript
复制
NATIVE ?= true
ifeq ($(NATIVE), false)
  KERNEL_DIR = /home/embedfire/linux4.19
else
  KERNEL_DIR = /lib/modules/$(shell uname -r)/build
endif
obj-m := xdev.o xbus.o xdrv.o
all:modules
modules clean:
  $(MAKE) -C $(KERNEL_DIR) M=$(shell pwd) $@

这个Makefile没有太多内容,主要就是内核源码树的路径不要写错了。可以在PC机上交叉编译,也可以直接在开发板上编译。在开发板上编译时,依赖/lib/modules/4.19.71-imx-r1/build 里面的文件。如果发现build目录不存在,可以使用命令

代码语言:javascript
复制
sudo apt-get install linux-headers-`uname -r`

如果安装失败可能需要先update一下。

最后编译生成.ko的模块,使用insmod命令安装即可。

今天主要是介绍了一下设备模型是什么,以及该如何套用设备模型来写驱动。但是今天并没有涉及具体硬件,也没有叙述太多操作细节。因为这个是框架性的东西,我们只需要从宏观上有一个认识就行了。

后面将会结合平台总线(platform)来具体看一下led驱动是如何写出来的。实际上也是基于今天的这些概念,没有今天的这些概念是很难理解的,platform总线不过是总线中的一种而已。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-01-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 电子技术研习社 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档