时间:2023-01-23 08:25:03 | 栏目:Linux | 点击:次
先讲 Linux 驱动的分离,Linux 操作系统支持在各类 CPU 上运行,因为每一种 CPU 对设备的驱动不一样,这样就造成了 Linux 内核中积累了大量代码,并且这些代码关于同一设备的描述大致相同,这就使得内核代码很冗余。以 CPU 通过 I2C 控制 MPU6050 为例:
从图可以看出每一种平台下都有一套主机驱动和一套设备驱动,因为每个平台的 I2C 控制器不同,所以这个主机驱动得每个平台配一个自己的,但大家所用的 MPU6050 是一样的,所以完全可以就共用一套设备驱动代码。完善后框架如下:
当然,这只是对于 I2C 下的 MPU6050 这个设备,实际情况下,I2C 下肯定会挂载很多设备,根据这个思路,我们可以得到框架为:
而在实际开发中,I2C 主机驱动半导体厂家会编写好,设备驱动也由设备厂家编写好,我们只需要提供设备信息即可,如设备接到那个 I2C 接口上,I2C 速度为多少。这样就相当于把设备信息从设备驱动中剥离出来,而设备驱动也会用标准方法去获取设备信息(如从设备树中获取设备信息)。这样就相当于驱动只负责驱动,设备(信息)只负责设备,想办法将两者进行匹配即可,来做这个匹配工作的就是总线,这就构成了 Linux 中的 总线-驱动-设备 模型。结构图如下:
上面我们讲做设备驱动的分离,得到 总线-驱动-设备 模型,这个总线就是我平常所说的 I2C、SPI、USB 等总线。但问题是有些设备是不需要通过某一跟总线的,这是就引入了 platform 总线。
这里需要注意的是,platform 总线是区别于 USB、SPI、I2C 这些总线的虚拟总线。说它虚拟是因为 SoC 与一些外设如 LED、定时器、蜂鸣器是通过内存的寻址空间来进行寻址的,所以 CPU 与这些设备通信压根就不需要总线,那么硬件上也就没有这样一个总线。但内核有对这些设备做统一管理的需求,所以就对这些直接通过内存寻址的设备虚拟了一条 platform 总线,所有直接通过内存寻址的设备都映射到这条虚拟总线上。
platform 总线的优点:
1、通过 platform 总线,可以遍历所有挂载在 platform 总线上的设备;
2、实现设备和驱动的分离,通过 platform 总线,设备和驱动是分开注册的,因为有 probe 函数,可以随时检测与设备匹配的驱动,匹配成功就会把这个驱动向内核注册;
3、一个驱动可供同类的几个设备使用,这个功能的实现是因为驱动注册过程中有一个遍历设备的操作。
Linux 内核用 bus_type 结构体来表示总线,我们所用的 I2C、SPI、USB 都是用这个结构体来定义的。该结构体如下:
/* include/linux/device.h */ struct bus_type { const char *name; /* 总线名字 */ const char *dev_name; struct device *dev_root; struct device_attribute *dev_attrs; 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); 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); const struct dev_pm_ops *pm; const struct iommu_ops *iommu_ops; struct subsys_private *p; struct lock_class_key lock_key; };
platform 总线是 bus_type 类型的常量,之所以说它是常量是因为这个变量已经被 Linux 内核赋值好了,其结构体成员对应的函数也已经在内核里面写好。
定义如下:
/* drivers/base/platform.c */ struct bus_type platform_bus_type = { .name = "platform", .dev_groups = platform_dev_groups, .match = platform_match, /* 匹配函数 */ .uevent = platform_uevent, .pm = &platform_dev_pm_ops, };
platform_bus_type 中的 platform_match 就是我们前面所说的做驱动和设备匹配的函数,该函数定义如下:
/* drivers/base/platform.c */ static int platform_match(struct device *dev, struct device_driver *drv) { struct platform_device *pdev = to_platform_device(dev); struct platform_driver *pdrv = to_platform_driver(drv); /*When driver_override is set,only bind to the matching driver*/ if (pdev->driver_override) return !strcmp(pdev->driver_override, drv->name); /* 设备树OF类型匹配 驱动基类的 of_match_table 里的 compatible 匹配表与设备树 每一个设备节点的 compatible 属性作比较,有相同就表示匹配成功 */ if (of_driver_match_device(dev, drv)) return 1; /* ACPI 匹配 */ if (acpi_driver_match_device(dev, drv)) return 1; /* id_table 匹配 platform 驱动里的 id_table 数组会保存很多 id 信息 */ if (pdrv->id_table) return platform_match_id(pdrv->id_table, pdev) != NULL; /* name 匹配 直接粗暴比较platform 的驱动和设备里面的 name 信息 */ return (strcmp(pdev->name, drv->name) == 0); }
这个匹配函数什么时候用,在哪里用,我们不妨先留一个悬念。
platform 驱动用结构体 platform_driver 来表示,该结构体内容为:
/* include/linux/platform_device.h */ struct platform_driver { int (*probe)(struct platform_device *); /* platform驱动和platform设备匹配后会执行这个probe函数 */ int (*remove)(struct platform_device *); void (*shutdown)(struct platform_device *); int (*suspend)(struct platform_device *, pm_message_t state); int (*resume)(struct platform_device *); struct device_driver driver; /* 驱动基类 */ const struct platform_device_id *id_table; /* id_table表 */ bool prevent_deferred_probe; };
platform_driver 中 const struct platform_device_id *id_table 是 id_table 表,在 platform 总线匹配驱动和设备时 id_table 表匹配法时使用的,这个 id_table 表其实是一个数组,里面的每个元素类型都为 platform_device_id,platform_device_id 是一个结构体,内容如下:
struct platform_device_id { char name[PLATFORM_NAME_SIZE]; kernel_ulong_t driver_data; };
platform_driver 中 driver 是一个驱动基类,相当于驱动具有的最基础的属性,在不同总线下具有的属性则存放在 platform_driver 结构体下。
驱动基类结构体 device_driver 内容为:
/* include/linux/device.h */ struct device_driver { const char *name; /* platform 总线来匹配设备与驱动的第四种方 法就是直接粗暴匹配两者的 name 字段 */ struct bus_type *bus; struct module *owner; const char *mod_name; bool suppress_bind_attrs; 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); void (*shutdown) (struct device *dev); int (*suspend) (struct device *dev, pm_message_t state); int (*resume) (struct device *dev); const struct attribute_group **groups; const struct dev_pm_ops *pm; struct driver_private *p; };
driver 中 of_match_table 也是一个匹配表,这个匹配表是 platform 总线给驱动和设备做匹配时使用设备树匹配时用的,也是一个数组,数组元素都为 of_device_id 类型,该类型结构体如下:
/* include/linux/mod_devicetable.h */ struct of_device_id { char name[32]; char type[32]; char compatible[128]; /* 使用设备树匹配时就是把设备节点的 compatible 属性值和 of_match_table 中 每个项目的这个 compatible 作比较,如果有相等的就表示设备和驱动匹配成功 */ const void *data; };
用 platform_driver 结构体定义好 platform 驱动后,用 platform_driver_register 函数向 Linux 内核注册 platform 驱动,函数大致流程如下:
platform_driver_register (drv) -> __platform_driver_register -> drv->driver.probe = platform_drv_probe; /* 把 platform_drv_probe 这个函数赋给 platform 驱动里的驱动基类 drier 的 probe 函数 */ -> driver_registe (&drv->driver) /* 向 Linux 内核注册驱动基类 driver */ -> ...... -> drv->driver->probe /* 最终执行驱动基类 driver 的 probe 函数, 其实就是上面给的 platform_drv_probe 函数 */ -> platform_drv_probe -> drv->probe /* platform_drv_probe 函数又会执行 platform 驱动 drv 的 probe 函数 */
上面的分析中从 driver_register (&drv->driver) 到 drv->driver->probe 这一步我们用省略号代替了,现在来做一下分析:
driver_register(&drv->driver) -> bus_add_driver /* 向总线添加驱动 */ -> driver_attach -> bus_for_each_dev /* 查找总线下每一个设备,即遍历操作 */ -> __driver_attach /* 每个设备都调用此函数 */ -> driver_match_device /* 检查是否匹配 */ -> 调用bus下的match匹配函数 -> driver_probe_device /* 匹配成功后执行此函数 */ -> really_probe -> drv->probe /* 执行drv下的probe函数 */
根据 driver_register 函数流程,我们就知道了总线的 match 匹配函数会在这里遍历使用,这就回答了我们之前留下的一个问题:总线 match 函数在哪里用,一旦匹配成功就会进入到驱动的 probe 函数。
根据 platform_driver_register 函数流程,我们可以得出一个结论:向 Linux 内核注册 platform driver 过程里面会有一个遍历驱动和设备匹配的过程,匹配成功后最终会执行 platform driver 的 probe 函数,过程中 的驱动基类 driver 的 probe 函数和 platform_drv_probe 函数都是达到这个目的的中转函数而已。
值得注意的是,最终会执行的 platform driver 的 probe 函数是由我们来写的,所以主动权又回到我们手里。
如果我们用的 Linux 版本支持设备树,那就在设备树中去描述设备,如果不支持设备树,就要定义好 platform 设备。这里我们需要考虑的一个点是,总线下的匹配函数 match 在做匹配时是先设备树匹配,然后 id_table 表匹配,然后才是 name 字段匹配。支持设备树时,直接在设备树节点里面改设备信息,内核启动时会自动遍历设备树节点,匹配成功就会自动生成一个 platform_device,给下一步来使用。不是设备树的话,这个 platform_device 就是由开发者来写。
这里我们先不用设备树,自己来定义 platform 设备。platform 设备用 platform_device 结构体来表示,该结构体定义如下:
/* include/linux/platform_device.h */ struct platform_device { const char *name; /* 设备名,得和对应的 platform 驱动的 name 一样, 否则设备就无法匹配到对应驱动 */ int id; bool id_auto; struct device dev; u32 num_resources; struct resource *resource; const struct platform_device_id *id_entry; char *driver_override; /* Driver name to force a match */ /* MFD cell pointer */ struct mfd_cell *mfd_cell; /* arch specific additions */ struct pdev_archdata archdata; };
platform 总线对驱动和设备的匹配过程其实上面零零碎碎也已经讲的差不多了,现在我们汇总起来在过一遍。
前面也说过,总线下的驱动和设备的匹配是通过总线下的 match 函数来实现的,不同的总线对应的 match 函数肯定不一样,这个我们不用管,内核都会写好。我们所用的 platform 总线对应的 match 函数是 platform_match 函数,分析一下这个函数:
platform_match -> of_driver_match_device /* 设备树匹配 */ -> acpi_driver_match_device /* ACPI 匹配 */ -> platform_match_id /* platform_driver->id_table 匹配 */ -> strcmp(pdev->name, drv->name) /* name 匹配 */
通过对上面匹配函数的一个简单分析,我们知道匹配函数做匹配的顺序是先匹配设备树,然后匹配 id_table 表,然后才是暴力匹配 name 字段。对于支持设备树的 Linux 版本,我们一上来做设备树匹配就完事。不支持设备树时,我们就得定义 platform 设备,再用 id_tabale 表或 name 匹配,一般情况下都是选用 name 匹配。
现在我们来具体看一下设备树条件下的匹配过程:
of_driver_match_device /* of函数一般是用于设备树,这也算给了我们提示 */ -> of_match_device (drv->of_match_table, dev) -> of_match_node -> __of_match_node -> __of_device_is_compatible -> __of_find_property(device, "compatible", NULL) /* 取出compatible属性值 */
看上面的分析我们就知道了这个匹配过程最终是驱动基类的 of_match_table 里的 compatible 去设备树节点里面的 compatible 属性作比较。这个就是把设备树与 platform 总线串起来的一个机理,从而实现了在设备树对应节点里面写设备信息,驱动另外单独写的目的,也就是我们前面讲的驱动分离。
在具体的开发过程中我们并不需要真的去写一个 platform 总线模型,内核中都已经给我们定义好了。我们对 platform 总线模型的分析主要是搞清楚如何将驱动和设备匹配的,即当我们插入设备是如何找到对应驱动或插入驱动如何找到对应设备的,并最终调用 probe 函数。其实不管是先有驱动后有设备、还是先有设备后有驱动,最终匹配成功后第一件事都是执行驱动的 probe 函数,所以我们尽可放心的忽略中间曲折的情感纠葛,直接把注意力放在最终的 probe 函数。