本系列导航
(一)初识Linux驱动
(二)Linux设备驱动的模块化编程
(三)写一个完整的Linux驱动程序访问硬件并写应用程序进行测试
(四)Linux设备驱动之多个同类设备共用一套驱动
(五)Linux设备驱动模型介绍
(六)Linux驱动子系统-I2C子系统
(七)Linux驱动子系统-SPI子系统
(八)Linux驱动子系统-PWM子系统
(九)Linux驱动子系统-Light子系统
(十)Linux驱动子系统-背光子系统
(十一)Linux驱动-触摸屏驱动
我们知道,同一个LED灯,在不同的硬件平台上,可能接在不同的GPIO上面,那么你针对某个平台写的LED驱动就不适应于另外一个平台,这个时候从新移植驱动到新的平台还是很麻烦的。但是对于这两个平台的LED驱动基本都是相同的,唯有操作的GPIO不同,比如控制方法一样(亮/灭/闪烁等),硬件的gpio接口不一样。所以为了减小移植驱动的工作量,就提出了总线、设备、驱动的这种设备模型,把驱动相同的部分代码提取出来,作为公共部分(平台驱动driver),而与平台相关的单独放在一起(叫做平台设备device),然后通过一个叫做平台总线的东西将他们关联在一起这样,对于同样的设备在不同平台上的驱动只需要修改平台相关的部分即可。
Linux设备驱动模型之bus、device、driver模型,将驱动划分为三个部分–总线(bus)、设备(device)、驱动(driver),总线可以是物理上存在的总线,比如i2c_bus、spi_bus等,也可以是物理上不存在而虚拟出来的总线(platform_bus),设备代码和驱动代码都挂在(通过指针)对应的总线上面,由总线来统一管理他们,总线如果发现它下面挂的驱动和挂在它下面的设备相匹配,那么就会调用驱动里面相应的函数,来获取设备里面相应的硬件信息,从而来初始化硬件设备。简而言之,这三个东西就是对应三个结构体,那么按照这个模型,写驱动,其实就是填充对应的结构体:
struct bus_type {
const char *name; //bus的名字
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); //device和driver的匹配函数
...
};
struct device {
struct device *parent;
struct device_private *p;
struct kobject kobj;
const char *init_name; /* initial name of the device */
const struct device_type *type;
struct bus_type *bus; /* type of bus device is on */ //表示这个device挂在哪个bus下面
struct device_driver *driver; /* which driver has allocated this device */
void *platform_data;
struct device_node *of_node; /* associated device tree node */
...
};
struct device_driver {
const char *name; //driver的名字
struct bus_type *bus; //表示这个driver挂在哪个bus下面
struct module *owner;
const char *mod_name; /* used for built-in modules */
const struct of_device_id *of_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 dev_pm_ops *pm;
...
};
bus_dev_drv模型是一个泛型的概念,根据具体的总线,就会衍生出具体的总线模型,比如i2c总线模型,spi总线模型,platform虚拟总线模型,那么接下来具体讲的就是platform总线模型,后续再说I2C等总线模型:
有些驱动(LED、PWM等)没有挂在具体的总线上面,但是也想使用bus_dev_drv模型,那么就需要虚拟一个bus,我们称为platform_bus_type,那么对应的设备称之为platform_device,继承于或者说派生于struct device结构体(面向对象思想),对应的驱动称之为platform_driver,继承于或者说派生于struct device_driver结构体:
struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match, //用来匹配platform_device和对应的platform_driver
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};
struct platform_device {
const char *name; //platform_device的名字
int id;
bool id_auto;
struct device dev; //表示platform_driver是继承于(用面向对象思想,不严谨,但便于理解)device_driver
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;
};
struct platform_driver {
int (*probe)(struct platform_device *); //如何platform总线探测到它下面挂的某个driver和device匹配,那么就会调用对应的drv下面的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; //表示platform_driver是继承于(用面向对象思想,不严谨,但便于理解)device_driver
const struct platform_device_id *id_table;
bool prevent_deferred_probe;
};
下面我们重点分析下platform_match函数,看bus是如何进行device和driver的匹配的:
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);
//如果探测到是设备树的格式,那么就按照设备树的方式匹配(后续再说设备树)
/* Attempt an OF style match first */
if (of_driver_match_device(dev, drv))
return 1;
//如果drv里面id_table选项初始化了,那么就按照match_id的方式进行匹配,后面再讲
/* Then try to match against the id table */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;
//如果上面的匹配方式都没有探测到,那么就用最简单的名字进行匹配,其实就是比较 platform_device->name和platform_driver->drv->name
//如果这两个name一样,那么就代表传进来的device和driver是对应同一个设备的,接下来就会调用driver的probe函数去操作device了
/* fall-back to driver name match */
return (strcmp(pdev->name, drv->name) == 0);
}
编写代码,从两个部分入手–device和driver
device:
driver:
4. 分配并设置一个platform_driver结构体
5. 将这个platform_driver结构体注册到platform_bus上面 – platform_driver_register
6. 注册成功后,platform_bus会去遍历这个bus上面的所以platform_device,如果某个platform_device里面的name和当前注册的这个platform_driver的name一样(其实就是执行platform_match函数),那么就调用这个platform_driver里面的probe函数。
paltform_device代码:
#include
#include
#include
static struct platform_device hello_device = {
.name = "hello device",
};
static int hello_init(void)
{
printk("%s : %d platform_device_registered\n", __func__, __LINE__);
platform_device_register(&hello_device);
return 0;
}
static void hello_exit(void)
{
printk("%s : %d platform_device_unregistered\n", __func__, __LINE__);
platform_device_unregister(&hello_device);
}
MODULE_LICENSE("GPL");
module_init(hello_init);
module_exit(hello_exit);
paltform_driver代码:
#include
#include
#include
#include
int hello_probe(struct platform_device * pdev)
{
printk("%s : %d match ok, probed\n", __func__, __LINE__);
return 0;
}
int hello_remove(struct platform_device * pdev)
{
printk("%s : %d platform_driver has been removed\n", __func__, __LINE__);
return 0;
}
static struct platform_driver hello_driver = {
.driver = {
.name = "hello device",
},
.probe = hello_probe,
.remove = hello_remove, //device和driver匹配成功后,当device或者driver中的任意一个被从bus中卸载后,就会立马执行这个remove函数
};
static int hello_init(void)
{
printk("%s : %d platform_driver_has been registered\n", __func__, __LINE__);
platform_driver_register(&hello_driver);
return 0;
}
static void hello_exit(void)
{
printk("%s : %d platform_driver_has been unregistered\n", __func__, __LINE__);
platform_driver_unregister(&hello_driver);
}
MODULE_LICENSE("GPL");
module_init(hello_init);
module_exit(hello_exit);
测试:
paltform_device结构体中有两个重要的成员,这两个成员就是用来描述硬件资源信息的:
u32 num_resources; //用来描述硬件资源的数量
struct resource *resource; //结构体数组,用来描述具体的硬件资源
我们要操作的硬件资源,无非是寄存器地址,内存地址,或者是引脚号中断号等,用这个resource结构体即可描述:
struct resource {
resource_size_t start;
resource_size_t end;
const char *name;
unsigned long flags; //IORESOURCE_MEM(地址资源) IORESOURCE_IO(IO ports资源) IORESOURCE_IRQ(中断资源)
unsigned long desc;
struct resource *parent, *sibling, *child;
};
实例,描述了两个资源(一个寄存器地址资源和一个中断号资源,资源的类型是通过flags成员来确定的)的hello_res结构体:
struct resource hello_src[] = {
{
.start = 0x18000000,
.end = 0x18000004,
.flags = IORESOURCE_MEM
},
{
.start = 0x11,
.end = 0x11,
.flags = IORESOURCE_IRQ
},
{},
};
那么driver如何获取这些资源呢?在driver里面调用下面的函数即可:
device:
#include
#include
#include
#include
struct resource hello_res[] = {
{
.start = 0x18000000,
.end = 0x18000004,
.flags = IORESOURCE_MEM,
},
{
.start = 0x18000010,
.end = 0x18000014,
.flags = IORESOURCE_MEM,
},
{
.start = 0x9,
.end = 0x9,
.flags = IORESOURCE_IRQ,
},
};
struct platform_device hello_device = {
.name = "hello device",
.num_resources = ARRAY_SIZE(hello_res),
.resource = hello_res,
};
static int hello_init(void)
{
printk("%s : %d platform_device_registered\n", __func__, __LINE__);
platform_device_register(&hello_device);
return 0;
}
static void hello_exit(void)
{
printk("%s : %d platform_device_unregistered\n", __func__, __LINE__);
platform_device_unregister(&hello_device);
}
MODULE_LICENSE("GPL");
module_init(hello_init);
module_exit(hello_exit);
driver:
#include
#include
#include
#include
int hello_probe(struct platform_device * pdev)
{
struct resource * res;
printk("%s : %d match ok, probed\n", __func__, __LINE__);
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (NULL == res)
{
printk("%s : %d get resource err\n", __func__, __LINE__);
return -1;
}
printk("%s : %d res.start:0x%x res.end:0x%x\n", __func__, __LINE__, (unsigned int)res[1].start, (unsigned int)res->end); //注意观察log
res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
if (NULL == res)
{
printk("%s : %d get resource err\n", __func__, __LINE__);
return -1;
}
printk("%s : %d res.start:0x%x res.end:0x%x\n", __func__, __LINE__, (unsigned int)res->start, (unsigned int)res->end);
return 0;
}
int hello_remove(struct platform_device * pdev)
{
printk("%s : %d platform_driver has been removed\n", __func__, __LINE__);
return 0;
}
struct platform_driver hello_driver = {
.driver = {
.name = "hello device",
},
.probe = hello_probe,
.remove = hello_remove,
};
static int hello_init(void)
{
printk("%s : %d platform_driver_has been registered\n", __func__, __LINE__);
platform_driver_register(&hello_driver);
return 0;
}
static void hello_exit(void)
{
printk("%s : %d platform_driver_has been unregistered\n", __func__, __LINE__);
platform_driver_unregister(&hello_driver);
}
MODULE_LICENSE("GPL");
module_init(hello_init);
module_exit(hello_exit);
那么对于4.x以后的Linux内核,不再使用platform_device来描述设备信息了,而改用的设备树来进行描述(这里暂时对设备树不做详细解释),更为简洁清晰:
设备树一般存放在kernel的arch/arm/boot/dts这个目录下,高通的代码通常放在arch/arm64/boot/dts/qcom目录下,设备树是一个描述设备信息的文件(dts),由一系列的节点构成的,而节点里面使用键值对(key-val)的形式来描述硬件信息的,通过节点中compatible和驱动进行匹配要想使用设备树来对一个硬件设备进行描述,无非就是在相应的dts文件或者dtsi文件中加入一个节点,例如:
xxx @0x12345678{
compatible = "qcom, test";
a = "hello world";
b = <2>;
c = <0x40000000 4>;
...
};
注意,当我们加入新的节点时,一定要包含在根节点的某个大的节点里面。其中xxx为你加入的新的节点的名字,@0x12345678是一个可有可无的东西,当有多个重名节点时,@后面加数字可以区分不同的节点,compatible属性是节点中必须存在的,用来和driver进行匹配,一旦匹配成功,就会执行driver中的probe函数。其中a、b、c为三个自定义属性的名字,分别表示字符串,单个int数据,数组。开发时可以根据需要在对应的节点中加入需要的属性。
最后,我们的driver如何描述,才能匹配这个节点呢?那么就必须要初始化platform_driver中的of_match_table成员了,这个在前面分析bus总线中的match匹配函数时说过:
struct of_device_id hello_table_id[] = {
{.compatible = "qcom, test"},
{},
};
struct platform_driver hello_driver = {
.driver = {
.name = "hello device",
.of_match_table = hello_table_id,
},
.probe = hello_probe,
.remove = hello_remove,
};
前面分析bus的match函数时说过,match的时候有一定的顺序,先进行设备树的匹配,如果探测到of_match_table被设置,那么就去遍历dts寻找是否有节点的compatible属性的值和of_match_table中设置的compatible值一样,如何一样就代表匹配成功,并执行probe函数,如果有多个节点的compatible属性值都一样,那么probe就会被执行多次,直至将整个dts中的节点遍历完毕,同时,driver中的name也不会去寻找device结构体去匹配了。这个就是优先级的问题(看match函数即可知)。
然后就是有童鞋有疑问,我们的platform_driver不管是匹配bus的dev链表中的platform_device结构体,还是匹配设备树,最终都会执行同一个probe函数,那么设备树节点的硬件参数是如何传进probe函数的?看我们的probe函数
int hello_probe(struct platform_device * pdev)
首先要看probe的参数platform_device结构体,它里面有一个成员:struct device dev; 前面说过,可以用继承的思想来理解,platform_device继承了device结构体,而device结构体中有一个成员:struct device_node *of_node; 这个of_node成员就对应匹配到的设备树的节点,然后我们可以在probe函数中根据dts的of系列函数来获取资源:
int of_get_named_gpio(struct device_node *np, const char *propname, int index)
int of_property_read_u32(const struct device_node *np, const char *propname, u32 *out_value)
int of_property_read_string(const struct device_node *np, const char *propname, const char **out_string);
...