//本文主要参考《野火Linux开发指南》
大家好,今天跟大家分享的是在Linux中驱动led。今天的文章包括后面还有一篇是酝酿了近两个星期才开始动手写,可见这部分内容会比较抽象一些。
其实早在之前有一篇关于字符设备驱动的,讲的也是驱动led,大家可以回顾一下:Linux笔记(13)| 字符设备驱动基础入门 Linux笔记(14)| 字符设备驱动基础入门(续)
有朋友可能会产生疑问,既然都是驱动led,那么有什么不同呢?
实际上有非常大的区别。我们可以先回顾一下当时我们的驱动是怎么写的。简单来说,是这样子:比如要驱动led,我们先写一个led的模块文件,在模块的init函数里面初始化硬件,然后调用register_chrdev函数向内核注册字符设备文件,这个函数里面最重要的就是填充file_operation结构体,因为这个结构体里有我们的read & write函数的接口,注册好了之后创建设备文件,通过设备号将设备文件和刚刚注册的字符设备相绑定,这样我们就可以在应用程序里面通过设备文件来操作硬件。
这样做看起来好像挺好的,但是有一个很大的问题,就是如果硬件发生一点改变,就要重新改写驱动代码,然后重新编译,重新安装模块,这是非常麻烦,非常糟糕的。
所以就有了今天的设备模型。在早期的Linux里面就是像上面那样做的,但是到后来设备越来越多,越来越复杂,维护起来非常不方便,于是发明了设备模型。
那么,设备模型是怎么一回事呢?简单说设备模型就是让驱动代码分成两个部分,一部分是驱动,一部分是设备,驱动文件里有对设备的驱动,但是并不涉及具体硬件资源,硬件资源由设备文件提供,然后使用总线将两者联系起来。这样当硬件发生改变时,驱动文件不需要动,只要修改设备文件就行。这样修改会更简单一些,因为改一改寄存器,改一改管脚什么的是相对来说比较容易的,但是驱动的逻辑我们希望一次写好之后就再也不需要动。
要了解设备模型,我们需要先了解几个概念:
设备:设备就是一些物理设备,比如一个led就可以叫做一个设备;
驱动:与特定设备相关的软件,负责初始化该设备以及提供一些操作该设备的操作方式;
总线:将设备和驱动联系起来,负责管理设备和驱动;
类:具有相同功能的设备把它们归为一类。
这实际上就是内核提供给我们的一套框架,按照之前的方法也可以写驱动,只是说按照这样的框架来写驱动的话,会使得代码更好维护,也更具有可操作性。
所以我们要做的无非就是学习内核的这一套框架,先熟悉这套框架,学会如何使用即可。不能够一下子太深入,因为这里面一直深入的话就是非常复杂的各种数据结构了,这对于我们来说不是一下子能够掌握的,我们只需要站在一个驱动工程师的角度来学习它,而不需要站在一个内核开发者的角度来研究它。
那么接下来就说一下如何套用设备模型来写代码。
主要是涉及总线、设备、驱动。它们的共性就是都需要向内核注册(或者注销),都具有自己的属性,而差别就在于属性不同。这里面有大量面向对象的思想,在面向对象的语言里面,会有属性和方法的概念,其实属性就是变量,方法就是函数,但是C语言的结构体里没有函数,只有函数指针,所以这里我干脆把函数指针叫做方法。
1、总线
先看一下总线是一种什么类型的变量
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函数。
然后就是总线的注册和注销:
int bus_register(struct bus_type *bus);
void bus_unregister(struct bus_type *bus);
2、设备
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
设备的名字不能乱起,必须要和待会儿的驱动里的名字一样,因为总线是通过名字来匹配的。数据就是硬件相关的一些数据了。
然后就是设备的注册和注销:
int device_register(struct device *dev);
void device_unregister(struct device *dev);
3、驱动
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成员, 每个子目录下的文件,都是内核导出到用户空间,用于控制我们的设备的。
来看一下属性结构体:
struct attribute {
const char *name;
umode_t mode;
};
这是一个“基类”,当然这也是面向对象的语言里才有的概念。基类就是大家都有的,而每个对象又有自己独特的属性。
来看一下设备的属性:
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上生成对应的文件。比如可以通过
ls /sys/bus
ls /sys/devices
来查看总线和设备,在设备里面有对应的驱动
写好了代码之后,我们要写一个Makefile来控制编译
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目录不存在,可以使用命令
sudo apt-get install linux-headers-`uname -r`
如果安装失败可能需要先update一下。
最后编译生成.ko的模块,使用insmod命令安装即可。
今天主要是介绍了一下设备模型是什么,以及该如何套用设备模型来写驱动。但是今天并没有涉及具体硬件,也没有叙述太多操作细节。因为这个是框架性的东西,我们只需要从宏观上有一个认识就行了。
后面将会结合平台总线(platform)来具体看一下led驱动是如何写出来的。实际上也是基于今天的这些概念,没有今天的这些概念是很难理解的,platform总线不过是总线中的一种而已。