前言
platform是Linux内核抽象出来的软件代码,用于设备与驱动的连接,设备与驱动通过总线进行匹配;匹配成功后会执行驱动中的probe函数,在probe函数中可以获取到设备的信息;
嵌入式驱动学习专栏将详细记录博主学习驱动的详细过程,未来预计四个月将高强度更新本专栏,喜欢的可以关注本博主并订阅本专栏,一起讨论一起学习。现在关注就是老粉啦!
目录
- 前言
- 1. platform总线介绍
- 1.1 总线式设备驱动
- 1.2 platform总线
- 2. platform驱动框架
- 2.1 相关函数与结构体
- 2.2 platform驱动框架
- 3. platform的匹配
- 参考资料
1. platform总线介绍
1.1 总线式设备驱动
外设与Soc的连接是通过接口,但是接口的不同本质上是通信协议的不同,于是内核就抽象出各种总线,将同一协议的设备连接在一起管理。
总线分为物理总线和虚拟总线,物理总线是客观存在的,比如usb总线,有usb协议的设备连接在上面;虚拟总线式软件上模拟的,如platform总线,没有哪种设备用的platform协议,是内核为了统一用总线上方式管理设备虚拟出来的总线。
使用总线的话,可以实现数据和方法分离,设备里包含了数据,驱动是硬件操作的方法。
如下图所示,每个平台下的I2C控制器不同,因此主机驱动是必须的,但是对于每个SOC而言,mpu6050都是一样的,通过I2C的接口读写数据就行了,只需要一个MPU6050的驱动程序。如果再来几个 I2C 设备,比如 AT24C02、FT5206,那么只需要编写设备驱动,然后调用统一的I2C接口即可。
当我们向系统注册一个驱动的时候,总线就会在右侧的设备中查找,看看有没有与之匹配的设备,如果有的话就将两者联系起来。同样的,当向系统中注册一个设备的时候,总线就会在左侧的驱动中查找看有没有与之匹配的设备,有的话也联系起来。
1.2 platform总线
并不是所有外设都有总线这个概念,比如LED,蜂鸣器,这种只是操作GPIO的,但是需要使用总线——驱动——设备模型怎么办呢,为此Linux提出platform这个虚拟总线。
platform总线是虚拟总线,是软件层虚拟出来方便管理设备的,主要是管理类似LED灯这种比较简单,不需要专用接口的设备;
比如LED设备,一个设备可能有好几个LED,不同的LED设备之间寄存器数目、操作方法是一样的,不一样的是寄存器的基地址不同、连接的gpio口不同,于是我们可以把不同的数据放在总线上设备信息里(描述设备的结构体里有描述资源),然后传给LED驱动,这样就可以实现一个驱动适配几个设备。
2. platform驱动框架
2.1 相关函数与结构体
platform_driver
表示platform驱动,其定义在 include/linux/platform_device.h
中。
struct platform_driver {
int (*probe)(struct platform_device *);
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;
bool prevent_deferred_probe;
};
probe函数是当驱动与设备匹配成功后就会执行,非常重要的函数,如果自己要编写一个全新的驱动,那么 probe 就需要自行实现。
id_table 是个表( 也就是数组) ,每个元素的类型为 platform_device_id
,其定义在include/linux/mod_devicetable.h
中
struct platform_device_id {
char name[PLATFORM_NAME_SIZE];
kernel_ulong_t driver_data;
};
driver 成员,为 device_driver 结构体变量,Linux 内核里面大量使用到了面向对象的思维,device_driver 相当于基类,提供了最基础的驱动框架。plaform_driver 继承了这个基类,然后在此基础上又添加了一些特有的成员变量。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);
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;
};
其中of_match_table是采用设备树时驱动使用的匹配表,其类型为of_device_id,用于将device和driver相匹配,定义在include/linux/device.h
中
struct of_device_id {
char name[32];
char type[32];
char compatible[128];
const void *data;
};
其中的compatible很重要,通过设备节点的compatible属性值和of_match_table中每个项目的compatible成员变量进行比较,如果有相等的就表示设备和此驱动匹配成功。
在不支持设备树的Linux版本中,用户需要编写platform_device变量来描述设备信息,然后使用 platform_device_register
函数将设备信息注册到 Linux 内核中。
/*
* @description: 将设备信息注册到Linux内核中
* @param-pdev : 要注册的platform设备
* @return : 负数,失败;0,成功
*/
int platform_device_register(struct platform_device *pdev)
如果不再使用 platform 的话可以通过 platform_device_unregister 函数注销掉相应的 platform设备
/*
* @description: 注销掉对应的platform设备
* @param-pdev : 要注销的platform设备
* @return : 无
*/
void platform_device_unregister(struct platform_device *pdev)
2.2 platform驱动框架
首先要定义一个设备结构体并定义一个设备对象
struct xxx_dev {
struct cdev cdev;
};
struct xxx_dev xxxdev;
定义open,write等内核函数并设置file_operations结构体
static int xxx_open(struct inode* inode, struct file* filp) {
......
return 0;
}
static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) {
......
return 0;
}
static struct file_operations xxx_fops = {
.owner = THIS_MODULE;
.open = xxx_open;
.write = xxx_write;
};
编写probe函数,驱动与设备匹配成功后此函数会执行,里面就是具体的驱动函数,比如字符设备驱动注册,添加cdev,创建类等。
static int xxx_probe(struct platform_device *dev)
{
......
cdev_init(&xxxdev.cdev, &xxx_fops); // 注册字符设备驱动
...... // 函数具体内容
return 0;
}
编写remove函数,卸载驱动的时候就会执行该函数,以前在exit函数中做的事情在此函数中,比如删除cdev,注销设备号。
static int xxx_remove(struct platform_device *dev)
{
......
cdev_init(&xxxdev.cdev, &xxx_fops); // 注册字符设备驱动
...... // 函数具体内容
return 0;
}
写匹配列表,即of_device_id
的compatible
属性,如果使用设备树的话将通过此匹配表进行驱动和设备的匹配。此匹配项的 compatible 值为“xxx”,因此当设备树中设备节点的 compatible 属性值为“xxx”的时候此设备就会与此驱动匹配。最后一行是一个标记,of_device_id 表最后一个匹配项必须是空的,因为相关的操作API会读取这个数组直到遇到一个空。
static const struct of_device_id xxx_of_match {
{.compatible = "xxxx"},
{ //Sentinel }
};
platform平台驱动结构体,其中name 属性用于传统的驱动与设备匹配,也就是检查驱动和设备的 name 字段是不是相同。of_match_table 属性就是用于设备树下的驱动与设备检查。对于一个完整的驱动程序,必须提供有设备树和无设备树两种匹配方法。
static struct platform_driver xxx_driver = {
.driver = {
.name = "xxx",
.of_match_table = xxx_of_match,
},
.probe = xxx_probe,
.remove = xxx_remove,
};
模块驱动加载和卸载
static int __init xxxdriver_init(void) {
return platform_driver_register(&xxx_driver);
}
static void __exit xxxdriver_exit(void) {
platform_driver_unregister(&xxx_driver);
}
module_init(xxxdriver_init);
module_exit(xxxdriver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wp");
3. platform的匹配
学会怎么用后,来看看一些底层的东西,那就是如何将设备与驱动之间进行匹配的。
首先我们来看Linux系统中的总线,结构体为bus_type,定义在include/linux/device.h
中。
struct bus_type {
const char *name;
const char *dev_name;
struct device *dev_root;
struct device_attribute *dev_attrs; /* use dev_groups instead */
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;
};
其中重要的是match函数,设备与驱动件的匹配就是match
函数实现的,总线就是使用match函数根据注册的设备来查找对应的驱动,或者根据注册的驱动来查找对应的设备,因此每条总线都必须实现该函数。
match 函数有两个参数:dev 和 drv,分别为 device 和 device_driver 类型,即设备与驱动。
而要介绍的platform总线,是bus_type的一个具体实例,定义在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,
};
EXPORT_SYMBOL_GPL(platform_bus_type);
其中找到match,对应的函数是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);
/* Attempt an OF style match first */
if (of_driver_match_device(dev, drv))
return 1;
/* Then try ACPI style match */
if (acpi_driver_match_device(dev, drv))
return 1;
/* Then try to match against the id table */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;
/* fall-back to driver name match */
return (strcmp(pdev->name, drv->name) == 0);
}
可以看到函数中一共定义了4种匹配方式。
- 第一个if,of类型的匹配,即设备树采用的匹配方式,设备树中每个设备节点的compatible会和上面我们2中device_driver结构体中of_match_table的成员变量中存储的compatible匹配表进行比较,有的话就表示匹配成功,然后执行probe 函数;
- 第二个if,ACPI匹配方式;
- 第三个if,id_table匹配,每个platform_driver结构体有一个id_table成员变量,保存了很多id信息,这些 id 信息存放着这个 platformd 驱动所支持的驱动类型;
- 第四个if,如果第三种匹配方式的 id_table 不存在的话就直接比较驱动和设备的 name 字段,看看是不是相等,如果相等的话就匹配成功。
对于支持设备树的 Linux 版本号,一般设备驱动为了兼容性都支持设备树和无设备树两种匹配方式。也就是第一种匹配方式一般都会存在,第三种和第四种只要存在一种就可以,一般用的最多的还是第四种,也就是直接比较驱动和设备的 name 字段,毕竟这种方式最简单了。
参考资料
[1] 【正点原子】I.MX6U嵌入式Linux驱区动开发指南 第五十四章、第五十五章
[2] platform总线
[3] 内核platform总线详解:定义、注册、匹配、使用示例