由驱动分离的思想引出了总线、驱动和设备模型,Linux提出了platform驱动框架,为设备的注册、匹配、管理等操作提供了统一的接口。但是并不是所有设备都有物理总线,比如SoC内部的I2C、SPI、LCD等控制器与CPU内核的连接。为了解决此问题,platform驱动框架虚拟出一条platform总线,使得所有的设备都可以应用驱动分离模型
platform框架和字符设备框架是独立的,2者一般需要同时使用,前者是对整个驱动开发流程的一个统一化,主要包括硬件资源描述和驱动的匹配和资源管理;后者负责具体字符设备的设备节点、设备号的创建、以及与用户空间交互的IO操作之类的东西
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; };
|
一般一个总线实例至少定义以下函数:
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); if (pdev->driver_override) return !strcmp(pdev->driver_override, drv->name); if (of_driver_match_device(dev, drv)) return 1; if (acpi_driver_match_device(dev, drv)) return 1; if (pdrv->id_table) return platform_match_id(pdrv->id_table, pdev) != NULL; 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
| 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, };
|
1 2 3 4 5 6 7 8 9 10 11 12
| static const struct acpi_device_id my_acpi_match[] = { { "VEND1234", 0 }, {} };
static struct platform_driver my_driver = { .driver = { .name = "my_driver", .acpi_match_table = my_acpi_match, }, .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) }, {} };
static struct pci_driver my_pci_driver = { .name = "my_pci_driver", .id_table = my_pci_ids, .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, };
|
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_table
和name
这两个用于匹配驱动和设备的属性是定义在device_driver
结构体中
在创建了platform_driver
实例后,需要在设备驱动模块的入口注册驱动,在出口解除
1 2
| platform_driver_register(&xxx_driver); platform_driver_unregister(&xxx_driver);
|
系统加载的总线都在/sys/bus/
目录下,cd
到某个总线的目录中ls drivers
可以看到系统现在加载了哪些驱动、ls devices
可以看到现在设备树里加载了哪些设备
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; struct device dev; struct resource *resource; unsigned int num_resources; const struct platform_device_id *id_entry; struct mfd_cell *mfd_cell; void *platform_data; struct pdev_archdata archdata; };
|
实际开发过程中,不需要手动创建该对象,因为==内核启动==时会遍历设备树,然后创建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); return 0; }
static const struct of_device_id xxx_of_match[] = { { .compatible = "xxx-gpio" }, { } };
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");
|
设备树
注意:
举个例子:
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框架的作用:
- 1.统一了驱动开发的模型,通过虚拟总线,让所有设备的驱动开发都是divice–bus–driver形式
- 2.将驱动和板级信息解耦,实际上设备树已经初步实现了这个部分,即使不用platform框架,我们在驱动中也不需要写具体的寄存器地址了,但是此时还要通过设备树的
of
相关API,从设备树里找到具体的节点,万一设备树的节点名字改了,驱动就得全部修改。而如果用了platform框架,通过compatible
属性就可以找到对应节点,并且它还提供了简化的API来获取硬件的资源信息
- 3.可以实现一份驱动支持多个设备实例,只需要在设备树修改一下板级文件就行了。如果用设备树的话,驱动里面得用
of
API写死读哪个设备节点,没办法一个驱动支持多个节点
获取硬件资源的相关简化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)
|
有了这些API,即使调整了设备树,只要compatible
属性不变,驱动照样能用,但是如果直接用设备树的of
相关API,就得改驱动的代码了