对于 Linux 这样一个成熟、庞大、复杂的操作系统,代码的重用性非常重要,否则的话就会在 Linux 内核中存在大量无意义的重复代码。尤其是驱动程序,因为驱动程序占用了 Linux内核代码量的大头,如果不对驱动程序加以管理,任由重复的代码肆意增加,那么用不了多久Linux 内核的文件数量就庞大到无法接受的地步。
图 54.1.1.1 传统的 I2C 设备驱动
假如现在有三个平台 A、B 和 C,这三个平台(这里的平台说的是 SOC)上都有 MPU6050 这 个 I2C 接口的六轴传感器,按照我们写裸机 I2C 驱动的时候的思路,每个平台都有一个MPU6050的驱动,因此编写出来的最简单的驱动框架如图 54.1.1 所示: 图 54.1.1.1 传统的 I2C 设备驱动从图 54.1.1.1 可以看出,每种平台下都有一个主机驱动和设备驱动,主机驱动肯定是必须要的,毕竟不同的平台其 I2C 控制器不同。但是右侧的设备驱动就没必要每个平台都写一个,因为不管对于那个 SOC 来说,MPU6050 都是一样,通过 I2C 接口读写数据就行了,只需要一个 MPU6050 的驱动程序即可。如果再来几个 I2C 设备,比如 AT24C02、FT5206(电容触摸屏)等,如果按照图 54.1.1 中的写法,那么设备端的驱动将会重复的编写好几次。显然在 Linux 驱动程序中这种写法是不推荐的,最好的做法就是每个平台的 I2C 控制器都提供一个统一的接口(也叫做主机驱动),每个设备的话也只提供一个驱动程序(设备驱动),每个设备通过统一的 I2C接口驱动来访问,这样就可以大大简化驱动文件,比如 54.1.1 中三种平台下的 MPU6050 驱动。
框架就可以简化为图 54.1.1.2 所示
图 54.1.1.2 改进后的设备驱动
实际的 I2C 驱动设备肯定有很多种,不止 MPU6050 这一个,那么实际的驱动架构如图
54.1.1.3 所示:图 54.1.1.3 分隔后的驱动框架
这个就是驱动的分隔,也就是将主机驱动和设备驱动分隔开来,比如 I2C、SPI 等等都会采用驱动分隔的方式来简化驱动的开发。在实际的驱动开发中,一般 I2C 主机控制器驱动已经由半导体厂家编写好了,而设备驱动一般也由设备器件的厂家编写好了,我们只需要提供设备信息即可,比如 I2C 设备的话提供设备连接到了哪个 I2C 接口上,I2C 的速度是多少等等。相当于将设备信息从设备驱动中剥离开来,驱动使用标准方法去获取到设备信息(比如从设备树中获取到设备信息),然后根据获取到的设备信息来初始化设备。 这样就相当于驱动只负责驱动,设备只负责设备,想办法将两者进行匹配即可。这个就是 Linux 中的总线(bus)、驱动(driver)和设备(device)模型,也就是常说的驱动分离。总线就是驱动和设备信息的月老,负责给两者牵线搭桥,如图 54.1.1.4 所示:
当我们向系统注册一个驱动的时候,总线就会在右侧的设备中查找,看看有没有与之匹配的设备,如果有的话就将两者联系起来。同样的,当向系统中注册一个设备的时候,总线就会在左侧的驱动中查找看有没有与之匹配的设备,有的话也联系起来。Linux 内核中大量的驱动程序都采用总线、驱动和设备模式,我们一会要重点讲解的 platform 驱动就是这一思想下的产物。
platform 平台驱动模型简介
驱动和设备的匹配有四种方法,我们依次来看一下:
platform
总线是 bus_type
的一个具体实例,定义在文件 drivers/base/platform.c
,platform
总
线定义如下:
示例代码 54.2.1.2 platform 总线实例
1 struct bus_type platform_bus_type = {
2 .name = "platform",
3 .dev_groups = platform_dev_groups,
4 .match = platform_match,
5 .uevent = platform_uevent,
6 .pm = &platform_dev_pm_ops,
};
platform_bus_type
就是 platform 平台总线,其中 platform_match
就是匹配函数。我们来看
一下驱动和设备是如何匹配的,platform_match
函数定义在文件 drivers/base/platform.c
中,函
数内容如下所示:
1 static int platform_match(struct device *dev,
struct device_driver *drv)
2 { 3 struct platform_device *pdev = to_platform_device(dev);
4 struct platform_driver *pdrv = to_platform_driver(drv);
5
6 /*When driver_override is set,only bind to the matching driver*/
7 if (pdev->driver_override) 8 return !strcmp(pdev->driver_override, drv->name);
9
10 /* Attempt an OF style match first */
11 if (of_driver_match_device(dev, drv))
12 return 1;
13
14 /* Then try ACPI style match */
15 if (acpi_driver_match_device(dev, drv))
16 return 1;
17
18 /* Then try to match against the id table */
19 if (pdrv->id_table)
20 return platform_match_id(pdrv->id_table, pdev) != NULL;
21
22 /* fall-back to driver name match */
23 return (strcmp(pdev->name, drv->name) == 0);
24 }
驱动和设备的匹配有四种方法,我们依次来看一下:
第 11~12 行,第一种匹配方式, OF 类型的匹配,也就是设备树采用的匹配方式,
of_driver_match_device
函数定义在文件 include/linux/of_device.h
中。device_driver
结构体(表示
设备驱动)中有个名为of_match_table
的成员变量,此成员变量保存着驱动的compatible
匹配表,
设备树中的每个设备节点的 compatible
属性会和 of_match_table
表中的所有成员比较,查看是
否有相同的条目,如果有的话就表示设备和此驱动匹配,设备和驱动匹配成功以后 probe 函数
就会执行。
第 15~16 行,第二种匹配方式,ACPI 匹配方式。
第 19~20 行,第三种匹配方式,id_table 匹配,每个 platform_driver
结构体有一个 id_table
成员变量,顾名思义,保存了很多 id 信息。这些 id 信息存放着这个 platformd
驱动所支持的驱
动类型。
第 23 行,第四种匹配方式,如果第三种匹配方式的 id_table
不存在的话就直接比较驱动和
设备的 name 字段,看看是不是相等,如果相等的话就匹配成功。
对于支持设备树的 Linux 版本号,一般设备驱动为了兼容性都支持设备树和无设备树两种
匹配方式。也就是第一种匹配方式一般都会存在,第三种和第四种只要存在一种就可以,一般
用的最多的还是第四种,也就是直接比较驱动和设备的 name 字段,毕竟这种方式最简单了。
platform 驱动框架如下所示:
示例代码 54.2.2.5 platform 驱动框架
/* 设备结构体 */
1 struct xxx_dev{ 2 struct cdev cdev; 3 /* 设备结构体其他具体内容 */
4 };
5
6 struct xxx_dev xxxdev; /* 定义个设备结构体变量 */
7
8 static int xxx_open(struct inode *inode, struct file *filp) 9 {
10 /* 函数具体内容 */
11 return 0;
12 }
13
14 static ssize_t xxx_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *offt)
15 {
16 /* 函数具体内容 */
17 return 0;
18 }
19
20 /*
21 * 字符设备驱动操作集
22 */
23 static struct file_operations xxx_fops = {
24 .owner = THIS_MODULE,
25 .open = xxx_open,
26 .write = xxx_write,
27 };
28
29 /*
30 * platform 驱动的 probe 函数
31 * 驱动与设备匹配成功以后此函数就会执行
32 */
33 static int xxx_probe(struct platform_device *dev)
34 {
35 ......
36 cdev_init(&xxxdev.cdev, &xxx_fops); /* 注册字符设备驱动 */
37 /* 函数具体内容 */
38 return 0;
39 }
40
41 static int xxx_remove(struct platform_device *dev)
42 {
43 ......
44 cdev_del(&xxxdev.cdev);/* 删除 cdev */
45 /* 函数具体内容 */
46 return 0;
47 }
48
49 /* 匹配列表 */
50 static const struct of_device_id xxx_of_match[] = {
51 { .compatible = "xxx-gpio" },
52 { /* Sentinel */ }
53 };
54
55 /*
56 * platform 平台驱动结构体
57 */
58 static struct platform_driver xxx_driver = {
59 .driver = {
60 .name = "xxx",
61 .of_match_table = xxx_of_match,
62 },
63 .probe = xxx_probe,
64 .remove = xxx_remove,
65 };
66
67 /* 驱动模块加载 */
68 static int __init xxxdriver_init(void)
69 {
70 return platform_driver_register(&xxx_driver);
71 }
72
73 /* 驱动模块卸载 */
74 static void __exit xxxdriver_exit(void)
75 {
76 platform_driver_unregister(&xxx_driver);
77 }
78
79 module_init(xxxdriver_init);
80 module_exit(xxxdriver_exit);
81 MODULE_LICENSE("GPL");
82 MODULE_AUTHOR("zuozhongkai")
可以看到与字符设备驱动不同的是,platform
驱动框架①拥有一个用于匹配设备的of_device_id
用于匹配设备,②probe
函数里注册字符设备或者其他类型的设备注册操作、设备树里IO的获取和设置
更多文章请关注嵌入式机器人公众号