您现在的位置是:首页 >技术杂谈 >【Linux驱动开发】023 platform设备驱动网站首页技术杂谈
【Linux驱动开发】023 platform设备驱动
一、前言
驱动分离目的:提高Linux代码重用性和可移植性。
二、驱动的分隔与分离
百度看了很多,大多都没讲清楚为什么使用platform驱动,为什么驱动分隔与分离可以提高代码重用性,只是在讲实现的结构体、函数接口等等,现在我们就来分析一下:
先拿stm32单片机举个例子,如果使用I2C驱动的MPU6050,我们需要写一个mpu6050.c文件对其进行初始化,包括I2C初始化和读写函数、mpu6050初始化函数,如下图所示,其中对I2C的操作是stm32的I2C主机驱动,而对于mpu6050的初始化时设备的驱动。
如果现在我们改用MSP430驱动MPU6050,那么需要重新编写上面的mpu6050.c文件,也就是需要同时对主机驱动和设备驱动进行更改,总结一下上面的操作如下:
对于不同的主机(这里是单片机),更改主机的驱动文件是必要的,因为不同的主机I2C驱动文件肯定不一样,但是对于同一个MPU6050设备而言,重新编写相应的驱动文件是没有必要的!
为了保持设备驱动文件的可重用性,我们将主机驱动和设备驱动文件进行隔离,如下图所示:
并且我们统一命名所有主机(单片机)的I2C主机驱动中函数(I2C操作init、read、write函数),也就是stm32和MSP430中的I2C初始化函数、读写函数名字相同,这样在对mpu6050_init函数中使用统一的I2C读写操作进行初始化操作。
这样从stm32移植到MSP430中就无需重写mpu6050.c,也就是只需要重写I2C主机驱动,无需重写MPU6050设备驱动,重写主机驱动是可以理解的,毕竟平台不同。主机和设备驱动分隔示意图如下:
如果有多个使用I2C的设备,每个设备驱动对于不同的平台只需要写一次即可,这样代码的重用性就非常好。多个I2C设备的主机驱动与设备驱动隔离示意图如下:
类比到Linux,传统的驱动是这样的:
驱动分隔之后是这样的:
对于多个I2C设备的驱动分隔示意图是这样的:
相当于通过驱动分隔提高了代码的重用性! (分离、分隔都是一个意思,也就是分离)
上面说的主机驱动一般由半导体厂家开发,设备驱动由设备器件厂家开发,我们只需要提供设备的信息即可,比如I2C设备连接到哪个I2C接口上,速度是多少等等。也即是说将设备信息从设备驱动中分离出来,也就是驱动只负责驱动,设备只负责设备,使用总线对两者进行匹配。这个就是linux中的驱动-总线-设备模型,也就是所谓的驱动分离。通过着则这种方式,我们可以进一步分离,使代码重用性变得更高!
当某个注册某个驱动的时候,总线就会在右侧设备查找匹配项,匹配成功则将两者联系起来。
OK到此为止,Linux驱动分离讲解完成!
三、驱动的分层
分层就是将一个复杂的工作分成了4层, 分而做之,降低难度。每一层只专注于自己的事情, 系统已经将其中的核心层和事件处理层写好了,所以我们只需要来写硬件相关的驱动层代码即可。
四、platform平台驱动
SOC中的某些的某些外设可能没有总线这个概念,但是又需要使用驱动-总线-设备模型,因此,提出了platform虚拟总线,对应的驱动为platform_driver,对应的设备为platforn_device。
1、platform总线
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;
};
使用上面的结构体struct bus_type表示总线,match函数完成驱动和设备之间的匹配。
platform是结构体struct bus_type的一个实例,定义和初始化为:
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_match就是匹配函数,其定义如下:
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);
}
我们需要学习一下驱动和设备是如何匹配的,这样才能学会配置两者匹配的方式,好的,现在看上面的代码:
第二个if语句:OF匹配形式,即设备树匹配方式。of_driver_match_device(dev, drv)函数形参drv表示platform驱动,也就是一个形参变量,该变量中有个of_match_table的成员变量,此成员变量保存着驱动的compatible匹配表,设备树节点compatible属性值和此匹配表进行比较,查看是否匹配,成功匹配probe函数(platform设备驱动中的一个函数,下面讲解)就会执行。
第三个if语句:ACPI匹配方式。
第四个if语句:id_table匹配,每个platform_driver(platform驱动结构体)有一个id_table成员变量,保存着这个platform所支持的设备类型。
最后一个return语句:如果id_table不存在的话,直接比较驱动和设备的name字段,相当表明匹配成功。
2、platform驱动
platform_driver结构体表示platform驱动,定义如下:
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函数就会执行。非常重要的函数!!
driver成员:device_driver 结构体变量,相当于C++中的基类, device_driver 结构体是驱动最基础的框架。
id_table:上面讲解第三种驱动和设备匹配方法(非设备树)。
上方 struct device_driver 结构体是 struct platform_driver 的基类结构体,其中:
- name 是前面说的第四种方式,直接比较驱动和设备的name字段,就是这里基类里面的name。
- of_match_table 为采用设备树的时候使用的匹配表。
struct device_driver { const char *name;// 非设备树匹配方式 ... const struct of_device_id *of_match_table;// 设备树匹配方式 ... }
上面的设备树匹配方式 struct of_device_id 结构体如下,其中 compatible 非常重要,因为对于设备树而言,就是通过设备节点的 compatible 属性值和 of_match_table 中每个项目的 compatible 成员变量进行比较,如果有相等的就表示设备和此驱动匹配成功。
struct of_device_id { char name[32]; char type[32]; char compatible[128]; const void *data; };
3、platform 驱动API 函数
当我们定义并初始化好 platform_driver 结构体变量以后,需要在驱动入口函数里面调用
platform_driver_register 函数向 Linux 内核注册一个 platform 驱动:
int platform_driver_register (struct platform_driver *driver)
driver:要注册的 platform 驱动。
返回值:负数,失败;0,成功。
还需要在驱动卸载函数中通过 platform_driver_unregister 函数卸载 platform 驱动,
void platform_driver_unregister(struct platform_driver *drv)
drv:要卸载的 platform 驱动。
返回值:无。
/* 设备结构体 */
struct xxx_dev{
struct cdev cdev;
/* 设备结构体其他具体内容 */
};
struct xxx_dev xxxdev; /* 定义个设备结构体变量 */
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,
};
/*
* platform 驱动的 probe 函数
* 驱动与设备匹配成功以后此函数就会执行
*/
static int xxx_probe(struct platform_device *dev)
{
cdev_init(&xxxdev.cdev, &xxx_fops); /* 注册字符设备驱动 */
/* 函数具体内容 */
return 0;
}
static int xxx_remove(struct platform_device *dev)
{
cdev_del(&xxxdev.cdev);/* 删除 cdev */
/* 函数具体内容 */
return 0;
}
/* 匹配列表 */
static const struct of_device_id xxx_of_match[] = {
{ .compatible = "xxx-gpio" },
{ /* Sentinel */ }
};
/*
* platform 平台驱动结构体
*/
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");
4、不使用设备树的 platform 设备
platform 驱动已经准备好了,我们还需要 platform 设备,驱动写了一个文件了,设备还要写一个文件嘛?是的,不使用设备树是要将设备信息写为一个文件的,总要描述设备信息吧,还不能和驱动写在同一个文件中,只能单独写一个文件喽。
platform_device 这个结构体表示 platform 设备:
struct platform_device {
const char *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;
};
name:设备名字,要和所使用的 platform 驱动的 name 字段相同,否则的话设备就无法匹配到对应的驱动;
num_resources 表示资源数量,一般为第 resource 资源的大小;
resource 表示资源,也就是设备信息,比如外设寄存器等。
Linux 内核使用 struct resource 表示资源:
struct resource {
resource_size_t start;// 资源的起始和终止信息
resource_size_t end;
const char *name;// 资源名字
unsigned long flags;// 资源类型, 可选的资源类型都定义在了文件 include/linux/ioport.h 里面
struct resource *parent, *sibling, *child;
};
5、不使用设备树的 platform 设备API 函数
在以前不支持设备树的Linux 版本中,用户需要编写platform_device 变量来描述设备信息,然后使用 platform_device_register 函数将设备信息注册到 Linux 内核中:
int platform_device_register(struct platform_device *pdev)
pdev:要注册的 platform 设备。
返回值:负数,失败;0,成功。
如果不再使用 platform 可以通过 platform_device_unregister 函数注销掉相应的 platform 设备:
void platform_device_unregister(struct platform_device *pdev)
pdev:要注销的 platform 设备。
返回值:无。
platform 设备信息框架如下所示:
/* 寄存器地址定义*/
#define PERIPH1_REGISTER_BASE (0X20000000) /* 外设 1 寄存器首地址 */
#define PERIPH2_REGISTER_BASE (0X020E0068) /* 外设 2 寄存器首地址 */
#define REGISTER_LENGTH 4
/* 资源 */
static struct resource xxx_resources[] = {
[0] = {
start = PERIPH1_REGISTER_BASE,
end = (PERIPH1_REGISTER_BASE + REGISTER_LENGTH - 1),
flags = IORESOURCE_MEM,
},
[1] = {
start = PERIPH2_REGISTER_BASE,
end = (PERIPH2_REGISTER_BASE + REGISTER_LENGTH - 1),
flags = IORESOURCE_MEM,
},
};
/* platform 设备结构体 */
static struct platform_device xxxdevice = {
name = "xxx-gpio",
id = -1,
num_resources = ARRAY_SIZE(xxx_resources),
resource = xxx_resources,
};
/* 设备模块加载 */
static int __init xxxdevice_init(void)
{
return platform_device_register(&xxxdevice);
}
/* 设备模块注销 */
static void __exit xxx_resourcesdevice_exit(void)
{
platform_device_unregister(&xxxdevice);
}
module_init(xxxdevice_init);
module_exit(xxxdevice_exit);
MODULE_LICENSE("GPL");
6、设备树下的platform驱动
在没有设备树的 Linux 内核下,我们需要分别编写并注册 platform_device 和 platform_driver,分别代表设备和驱动。
在使用设备树的时候,设备的描述被放到了设备树中,因此 platform_device 就不需要我们去编写了,我们只需要实现 platform_driver 即可。
Linux 内核启动的时候会从设备树中读取设备信息,然后将其组织成platform_device 形式,至于设备树到 platform_device 的具体过程就不去详细的追究了。
1、在设备树中创建设备节点
毫无疑问,肯定要先在设备树中创建设备节点来描述设备信息,重点是要设置好 compatible 属性的值,因为 platform 总线需要通过设备节点的 compatible 属性值来匹配驱动!
gpioled {
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-gpioled";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_led>;
led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
status = "okay";
};
注意第 4 行的 compatible 属性值为“atkalpha-gpioled”,因此一会在编写 platform 驱动的时候 of_match_table 属性表中要有“atkalpha-gpioled”。
2、编写 platform 驱动的时候要注意兼容属性
上一章已经详细的讲解过了,在使用设备树的时候 platform 驱动会通过 of_match_table 来保存兼容性值,也就是表明此驱动兼容哪些设备。所以,of_match_table 将会尤为重要:
static const struct of_device_id leds_of_match[] = {
{ .compatible = "atkalpha-gpioled" }, /* 兼容属性 */
{ /* Sentinel */ } // 最后一个必须为空
};
MODULE_DEVICE_TABLE(of, leds_of_match);
static struct platform_driver leds_platform_driver = {
driver = {
name = "imx6ul-led",
of_match_table = leds_of_match,
},
.probe = leds_probe,
.remove = leds_remove,
};
- 在编写 of_device_id 的时候最后一个元素一定要为空!
- 通过 MODULE_DEVICE_TABLE 声明一下 leds_of_match 这个设备匹配表。
3、编写 platform 驱动
基于设备树的 platform 驱动和上一章无设备树的 platform 驱动基本一样,都是当驱动和设备匹配成功以后就会执行 probe 函数。