上一篇:《 字符设备号的静态 / 动态分配 》 | 下一篇: |
目录
目录
一、Platform 的作用
类似于 i2c / spi 等总线,SoC 中虽然很多外设没有总线这个概念,但仍想用总线、驱动和设备的模型方便控制。Linux 提出了 platform 这个虚拟总线概念,于是也就有了 platform 驱动 / platform 设备:platform_drive / platform_device.
二、Platform 设备驱动
当写到 Platform 设备驱动,便是很完善的驱动了,需要把大部分格式、数据规范化一下。
1. 私有数据结构体化
之前的驱动中,没有涉及私有数据或者直接定义了全局变量,而全局变量毕竟不安全,所以一般使用结构体来存储该驱动所对应设备的私有数据。一般以 xxx_data / xxx_priv / xxx_dev 为名,写在最前面,后面要用到时基本是用指针来调取使用。
在这个私有数据结构体里,一般最先定义好 platform 设备结构体:
struct demo_priv {
struct platform_device *pdev;
platform_device 是封装好的 platform 设备的相关数据结构体,只需简单了解即可,深入探究可以在论坛上继续搜索,在此定义好的 platform_device 类型的 *pdev 最大作用,就是在后面 probe 函数中获取设备数据。其原型定义如下:
struct platform_device {
const char *name;
int id;
struct device dev;
u32 num_resources;
struct resource *resource;
struct platform_device_id *id_entry;
/* arch specific additions */
struct pdev_archdata archdata;
};
接下来可根据自己驱动所需定义,加入这个 demo 需要 input_dev,以及 kobject 来创建设备节点来读写数据,就需要在这里定义好相关结构体指针:
struct demo_priv {
struct platform_device *pdev;
struct input_dev *input;
struct kobject *kobj;
/* 其他所需数据,仅作为例子
* 这里先不用初始化,用到时再初始化 */
int gpio;
int time;
};
其实在这里定义好结构体类型,在后面就可以用来存储数据了,就已经算定义好了数据结构体。不过规范起见,我们可以多写一行明确定义好结构体,这样更安全、数据脉络更清晰、更不容易出 bug
struct demo_priv *demo;
这样后面通过指针操控结构体指针 demo,便可以很方便的读写私有数据啦!
2. probe 函数 / remove 函数
probe 函数可以说是 platform 设备驱动功能的初始化函数。一般内存的分配、其他函数或功能的定义初始化、节点的注册等等,一切和这个设备驱动功能初始建立相关的事,都在这里做,probe就是这个驱动中,所有 “实质性” 函数最先运作的(这个实质性是我自己定义的,后面会说什么是所谓的 “非实质性” )。其定义应如下:
static int demo_porbe(struct platform_device *pdev)
{
/* 一些局部变量 */
int ret;
/* 分配内存空间 */
demo = kzalloc(sizeof(struct demo_priv), GFP_KERNEL);
/* 其他工作 */
return 0;
}
函数的参数基本固定,就是我们之前私有数据中的 platform_device 结构体。我这个实例里先列了几个比较基本的功能:检验和内存分配。
ret : 用于后续的一些注册函数检验返回值,如果验证出错,就要全部注销掉;
kzlloc() : 定义在 <linux/slab.h> 下,封装好的分配内存函数,include 后直接调用。它的作用就是给最开始定义的私有数据结构体分配一个空间存放,所以函数参数是对 demo_priv 的操作,后方参数 GFP_KERNEL 是指该分配是内核内存的正常分配,可能睡眠。关于其他参数和具体讲解,可以参考 kzalloc 函数详解 ,这位博主讲解的很透彻。值得注意,既然这里才分配空间,也就意味着在此之前自己定义的私有数据结构体其实还没有实际成型,只是个虚化概念,所以不要在这一步之前初始化结构体中的数据,分配好内存空间后再去给数据赋值或者进行其他操作,否则会报错或者引起 panic 。
static int demo_probe(struct platform_device *pdev)
{
int ret = 0;
/* 输出log用于debug */
printk(KERN_ERR"[demo] get in the demo probe!\n");
/* 内存空间分配 */
demo = kzalloc(sizeof(struct demo_priv), GFP_KERNEL);
if (!demo) { // 返回值出现错误,说明函数执行错误,没有分配好内存
ret = -ENOMEM;
printk(KERN_ERR"[demo] kzalloc fail\n");
goto exit; // 跳转至后方处理部分
}
/* 给dev赋值 */
demo->pdev = pdev;
dev_set_drvdata(&pdev->dev, data);
/* 自己所需的功能,如生成设备节点等 */
/* gpio 申请中断 */
/* 原代码中这个函数是自己在前文写的,因为申请中断又要申请 gpio 复用
* 又得申请中断还得使能,而且原驱动涉及了多个引脚,直接写在 probe 函数中冗长乏味
* 所以将这些操作自己打包成专用函数使用,在这里可以简单看做申请了一个 gpio 口
* probe 中推荐将代码写的清晰易懂,可以长但不要繁琐冗长,方便 debug 或者后人 */
ret = gpio_to_irq_init(data);
if (ret) { // 申请 gpio 失败
printk(KERN_ERR"[demo] init gpio failed!\n");
goto gpio_exit; // 跳转至处理阶段
}
/* 以上阶段都没有出错,才会进入到 return 0 */
return 0;
/* 处理错误阶段 */
/* 这里要按照以上初始化顺序的倒序来释放
* 因为中间有一个出错,就要将从这开始往前注册的所有内容释放掉
* 这里没有 gpio_free 是因为 gpio 的申请在最后一步
* 如果出错的话压根就不会有,也就无法注销了
* 所以这个也需要考虑逻辑上的顺序,跳转过来后是应该注销当前步骤的,还是上一步的 */
gpio_exit:
kfree(demo);
exit:
return ret; // 返回出错时的 ret 值
}
remove 函数即与 probe 函数相反,用于注销、释放所用到的函数、节点、gpio等资源,一般来说,大部分内容与 probe 里处理错误部分相似,还需要将一些去全局结构体中的指针部分清空之类,与上文实例相对应的 remove 函数:
static int demo_remove(struct platform_device *pdev)
{
gpio_free(demo->gpio);
/* 将之前的 drvdata 和 pdev 设为 NULL */
dev_set_drvdata(&pdev->dev, NULL);
demo->pdev = NULL;
kfree(demo);
return 0;
}
3. probe 函数的启用—— of_match
在之前的字符设备驱动中,整个驱动启用的流程是:
module_init(chrtest_init); // 设置为入口函数
module_init(chrtest_exit); // 设置为出口函数
/* ↓ */ // 上层调用入口函数
static int __init chrtest_init(void) // 执行 init 函数
/* ↓ */
alloc_chrdev_region(&devid, 0, 1, CHRDEVTEST_NAME); // init 内注册字符设备驱动
/* ↓ */ // 上层调用出口函数
static void __exit chrtest_exit(void) //执行 exit 函数
/* ↓ */
unregister_chrdev_region(devid, 1); // exit 内注销字符设备驱动
那么现在用到了 platform 设备驱动,很大程度上因为需要有条理地控制多个设备,并且在所有驱动需要控制的设备中,找到自己需要控制的设备,这就和传说中因为 Linus 一句脏话而引入的设备树完美契合
Gaah! Guys, this whole ARM thing is a fucking pain in the ass.
—— Linus Benedict Torvalds
那么我们先了解一下 platform 设备中基本的启用流程再去具体学习什么是设备树。其实相较于字符设备驱动,只是多了两个部分:struct of_device_id / struct platform_driver,示例:
/* 与设备树进行设备匹配的 of_match_table */
static struct of_device_id demo_of_match[] = {
/* 用于匹配的 compatible 项 */
{ .compatible = "demodemo", },
{ }, // 必不可少的一行
};
/* platform 驱动结构体 */
static struct platform_driver demo_driver = {
/* 定义本驱动中 probe 函数是 demo_probe */
.probe = demo_probe,
/* 定义本驱动中 remove 函数是 demo_probe */
.remove = demo_remove,
/* 定义本驱动中的 owner 和 name
* 以及接下来的重点,用于匹配设备树的 of_match_table 是 demo_of_match */
.driver = {
.owner = THIS_MODULE,
.name = "demo",
.of_match_table = of_match_ptr(demo_of_match),
}
};
/* 入口函数 */
static int __init demo_init(void)
{
/* 注册 platform 设备驱动 */
return platform_driver_register(&demo_driver);
}
/* 出口函数 */
static void __exit demo_exit(void)
{
/* 注销 platform 设备驱动 */
platform_driver_unregister(&demo_driver);
}
/* 声明入口、出口函数 */
module_init(demo_init);
module_exit(demo_exit);
compatible : 此项内容 "demodemo" 要与设备树中对应设备下的compatible项完全一致,这个就是匹配的依据,而它的具体内容完全自定义,不过严谨些会以 "公司名,产品名" 命名;
demo_driver :在这里面定义本驱动的 probe / remove / owner / name / of_match_table;
platform_driver_register : 注册 platform 设备驱动的函数;
platform_driver_unregister :注销 platform 设备驱动的函数。
那么,在此 platform 驱动中,启用的流程是这样的:
module_init(demo_init); // 设置为入口函数
module_init(demo_exit); // 设置为出口函数
/* ↓ */ // 上层调用入口函数
static int __init demo_init(void) // 执行 init 函数
/* ↓ */
platform_driver_register(&demo_driver); // init 内注册 platform 设备驱动
/* ↓ */
static struct platform_driver demo_driver // 按照此结构体数据注册
/* ↓ */
static struct of_device_id demo_of_match[]; // 调用此结构体数组
/* ↓ */
{ .compatible = "demodemo", }, // compatible 项与设备树中比对
/* ↓ */ // 匹配成功
static int demo_probe(struct platform_device *pdev) // 执行 probe 函数
/* ↓ */ // 上层调用出口函数
static void __exit demo_exit(void) // 执行 exit 函数
/* ↓ */
platform_driver_unregister(&demo_driver); // exit 内注销 platform 设备驱动
/* ↓ */
static struct platform_driver demo_driver // 按照此结构体数据注销
/* ↓ */
static int demo_remove(struct platform_device *pdev) // 执行 remove 函数
看着繁琐了很多,但实际上编写时我们只需要额外编写 of_match_table / platform_driver 即可,剩下的流程就不用我们管啦,而且这样,可以在项目中很好地联系驱动与设备。
三、设备树与 platform driver 的关联
现在,联系着驱动与设备的 of_match_table 已经完美建立起来,驱动.c这边的工作全部完成,接下来就需要在设备上完善体系,这就要用到一直提到的设备树,以及设备节点中的 compatible 项。
对于设备树,详细的配置过程和各个节点之间的联系含义,推荐《 设备树详解 》这篇博客,十分的全面。这里只写下配置好设备树后与 platform 驱动关联的方法。
在设备数种配置好的设备节点,开头一般都是如下:
/ {
compatible = "xxxx";
module = "xxxxx";
/* 其他节点 */
demo-dts {
compatible = "ti, tps4h000bq";
/* 其他配置项 */
status = "okay";
};
/* 其他节点 */
};
第二行的 compatible 项是整个根节点的,暂时不用去管,除非你负责撰写整个项目的源设备树文件。对于我们的 platform demo 驱动所需的设备节点 demo-dts,在第 7 行定义,并初始化相关配置内容。
compatible : 此项内容要与驱动源码中,of_device_id 结构体下的 .compatible 项完全一致,这个就是匹配的依据。设备节点很多时候以一个 ic 或者某个功能的整体实现为单位来撰写,例子中以 TI 公司的 tps4h000bq 供电诊断芯片为单位,很普遍的命名法是 "公司, 型号",方便自己或他人理解与维护;
status : 顾名思义就是该设备节点的状态 "okay" 即为开启使用,"disabled" 即为关闭使用。
这里的 compatible 对应上文驱动文件的 demo_of_match[] 中的 .compatible,所以上文驱动需要和这里设备节点所关联,用到该设备节点中的一些设备参数信息,则需要将 .compatible 项改成相同字符串:
static struct of_device_id demo_of_match[] = {
/* 用于匹配的 compatible 项 */
{ .compatible = "ti, tps4h000bq", },
{ },
};
这样,platform 设备驱动就与 dts 中设备节点相关联。加载的时候会先去匹配设备树中 compatible 项与之相同的设备节点,如果匹配上了,会继续进入 probe 函数,否则这篇驱动将不会继续执行,直接进入 exit 函数退出。值得一提的是,这样的匹配机制就注定驱动中的 .compatible 可能是多个字符串。事实上也正是如此,在 of_match 结构体中,如果 .compatible 所含有的字符串项不止一个,那么驱动将按顺序进行匹配,一旦有一个匹配上了,立即关联这个设备节点并进入 probe 函数,后续的选项不会再去验证,如果都没有匹配则进入 exit 函数退出。
static struct of_device_id demo_of_match[] = {
/* 用于匹配的多个 .compatible 项 */
{ .compatible = "ti, tps4h000bq", "maxim, max20086", },
{ },
};
以上便是本篇全部内容,感谢阅读。