platform设备驱动框架

platform驱动框架

由驱动分离的思想引出了总线、驱动和设备模型,Linux提出了platform驱动框架,为设备的注册、匹配、管理等操作提供了统一的接口。但是并不是所有设备都有物理总线,比如SoC内部的I2C、SPI、LCD等控制器与CPU内核的连接。为了解决此问题,platform驱动框架虚拟出一条platform总线,使得所有的设备都可以应用驱动分离模型

platform框架和字符设备框架是独立的,2者一般需要同时使用,前者是对整个驱动开发流程的一个统一化,主要包括硬件资源描述和驱动的匹配和资源管理;后者负责具体字符设备的设备节点、设备号的创建、以及与用户空间交互的IO操作之类的东西

platform总线

Linux内核定义结构体struct bus_type来定义各种总线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
};

一般一个总线实例至少定义以下函数:

  • match:用于匹配设备dev以及设备驱动drv

platform虚拟总线、iic总线、spi总线等其实都是该结构体的一个实例,并没有bus_type的子类

  • 以下实例和函数都定义在驱动源码里的drivers/base/platform.c
1
2
3
4
5
6
7
struct bus_type platform_bus_type = { 
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match,
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};

驱动与设备的匹配

驱动与设备的匹配共有4种匹配方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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);

/* 1.设备树匹配 */
if (of_driver_match_device(dev, drv))
return 1;

/* 2.ACPI匹配 */
if (acpi_driver_match_device(dev, drv))
return 1;

/* 3.ID表匹配 */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;

/* 4.名称匹配 */
return (strcmp(pdev->name, drv->name) == 0);
}
  • 法一(==最常用==):使用compatibale属性匹配:device_dirver结构体中有个of_match_table的成员变量,此成员变量保存着驱动的compatible匹配表,设备树中的每个设备节点的 compatible 属性会和 of_match_table 表中的所有成员比较,查看是否有相同的条目,如果有的话就表示设备和此驱动匹配,设备和驱动匹配成功以后 probe 函数就会执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 设备树节点示例(.dts)
my_device {
compatible = "vendor,my-device";
reg = <0x1000>;
};

// 驱动中的匹配表
static const struct of_device_id my_driver_match[] = {
{ .compatible = "vendor,my-device" },
{}
};

static struct platform_driver my_driver = {
.driver = {
.name = "my_driver",
.of_match_table = my_driver_match, // 关键:绑定设备树匹配表
},
.probe = my_probe_function,
};
  • 法二:使用ACPI匹配
1
2
3
4
5
6
7
8
9
10
11
12
static const struct acpi_device_id my_acpi_match[] = {
{ "VEND1234", 0 }, // 匹配ACPI _HID
{}
};

static struct platform_driver my_driver = {
.driver = {
.name = "my_driver",
.acpi_match_table = my_acpi_match, // 关键:绑定ACPI匹配表
},
.probe = my_probe_function,
};
  • 法三:id_table 匹配,每个 platform_driver 结构体有一个 id_table成员变量,保存了很多 id 信息。这些 id 信息存放着这个驱动所支持的类型
1
2
3
4
5
6
7
8
9
10
static const struct pci_device_id my_pci_ids[] = {
{ PCI_DEVICE(VENDOR_ID, DEVICE_ID) }, // 厂商ID和设备ID
{}
};

static struct pci_driver my_pci_driver = {
.name = "my_pci_driver",
.id_table = my_pci_ids, // 关键:绑定ID表
.probe = my_probe_function,
};
  • 法四:如果第三种匹配方式的 id_table 不存在的话就直接比较驱动和设备的 name 字段,看看是不是相等,如果相等的话就匹配成功
1
2
3
4
5
6
7
8
9
10
11
12
// 设备注册
static struct platform_device my_device = {
.name = "my_platform_device", // 设备名称
};

// 驱动定义
static struct platform_driver my_driver = {
.driver = {
.name = "my_platform_device", // 与设备名称一致
},
.probe = my_probe_function,
};

platform驱动

Linux内核定义结构体struct platform_driver来在platform设备框架下开发设备驱动

1
2
3
4
5
6
7
8
9
10
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:当设备与驱动匹配后,会调用该函数注册驱动
  • device_driver:没用platform框架时的设备驱动,其中定义了一些必要的变量
  • id_table:用于匹配设备和驱动的第三种方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct device_driver { 
const char *name;
struct bus_type *bus;
struct module *owner;
...
const struct of_device_id *of_match_table;
const struct acpi_device_id *acpi_match_table;
...
};

struct of_device_id {
char name[32];
char type[32];
char compatible[128]; //最后一个匹配项必须为空
const void *data;
};

可见:of_match_tablename这两个用于匹配驱动和设备的属性是定义在device_driver结构体中

在创建了platform_driver实例后,需要在设备驱动模块的入口注册驱动,在出口解除

1
2
platform_driver_register(&xxx_driver); 
platform_driver_unregister(&xxx_driver);

系统加载的总线都在/sys/bus/目录下,cd到某个总线的目录中ls drivers可以看到系统现在加载了哪些驱动、ls devices可以看到现在设备树里加载了哪些设备

platform设备

Linux内核定义结构体platform_device来对硬件设备进行抽象

  • 注意:所谓的platform_device并不是与字符设备、块设备和网络设备并列的概念,而是Linux系统提供的一种附加手段,例如,我们通常把在SoC内部集成的I2C、RTC、LCD、看门狗等控制器都归纳为 platform_device,而它们本身就是字符设备
1
2
3
4
5
6
7
8
9
10
11
struct platform_device {
const char *name; // 设备名称,通常与设备驱动匹配
int id; // 设备ID,用于区分同类型的多个设备
struct device dev; // 嵌入的 device 结构体,用于设备模型
struct resource *resource; // 设备资源(如内存、IRQ等)的指针
unsigned int num_resources; // 资源的数量
const struct platform_device_id *id_entry; // 设备ID表,用于匹配设备
struct mfd_cell *mfd_cell; // 用于多功能设备(MFD)的单元
void *platform_data; // 平台特定的数据,通常传递给驱动
struct pdev_archdata archdata; // 平台特定的架构数据
};

platform_device创建流程

实际开发过程中,不需要手动创建该对象,因为==内核启动==时会遍历设备树,然后创建platform_device实例,此时如果有匹配了的platform_driver,则会自动调用.probe()

代码模板

驱动代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/* 设备结构体 */ 
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,
};


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 */ } //这里必须空一个
};

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("LRQ");

设备树

注意:

  • platform device的设备树节点==必须==写在根节点或者soc节点的某个总线下

  • platform相关的属性只有compatiable,用于驱动和设备的匹配

举个例子:

1
2
3
4
5
6
7
8
9
&i2c2 {
...
ap3216c@1e {
compatible = "alientek,ap3216c";
reg = <0x1e>;
status = "okay";
};
...
}

再举个例子:

1
2
3
4
5
6
7
8
9
10
11
/{
gpioled {
#address-cells = <1>;
#size-cells = <1>;
compatible = "gpio-led";
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_led>;
led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
};
}

platform框架到底有什么用

这个问题我之前一直不是很懂,感觉他的引入跟我直接用设备数没啥区别,后来发现是正点原子给的代码实例都有问题,不能很好的体现出这个框架真正的作用…

platform框架的作用:

  • 1.统一了驱动开发的模型,通过虚拟总线,让所有设备的驱动开发都是divice–bus–driver形式
  • 2.将驱动和板级信息解耦,实际上设备树已经初步实现了这个部分,即使不用platform框架,我们在驱动中也不需要写具体的寄存器地址了,但是此时还要通过设备树的of相关API,从设备树里找到具体的节点,万一设备树的节点名字改了,驱动就得全部修改。而如果用了platform框架,通过compatible属性就可以找到对应节点,并且它还提供了简化的API来获取硬件的资源信息
  • 3.可以实现一份驱动支持多个设备实例,只需要在设备树修改一下板级文件就行了。如果用设备树的话,驱动里面得用ofAPI写死读哪个设备节点,没办法一个驱动支持多个节点

获取硬件资源的相关简化API:

1
2
3
platform_get_resource(pdev, IORESOURCE_MEM, 0) 		//获取寄存器
platform_get_irq(pdev, 0) //获取中断
devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_HIGH) //获取gpio

有了这些API,即使调整了设备树,只要compatible属性不变,驱动照样能用,但是如果直接用设备树的of相关API,就得改驱动的代码了