https://blog.csdn.net/qq_28992301/article/details/52385518
基于platform总线的驱动分析
在设备驱动模型中,总线负责将设备和驱动绑定。在系统每注册一个设备的时候,会寻找与之匹配的驱动;相反的,在系统每注册一个驱动的时候,会寻找与之匹配的设备,而匹配由总线完成。
为什么要有这种匹配机制?难道不是很多余吗?原因详见Linux设备驱动模型与sysfs文末
1.platform总线基本概念
- 设备和驱动若基于设备驱动模型,则它们通常都需要挂接在一种总线上。总线相对于设备和驱动,可谓是“媒人”担当
- 对于本身依附于 USB、 I2C、SPI 等的设备而言,这自然不是问题。但是很多的设备(比如led)实际并不依附于总线,没了“媒人”,设备和驱动怎么产生联系呢?于是内核为这些可怜的设备发明了一种虚拟的总线——platform(平台总线)
- 挂接在platform上的设备和驱动,就称之为platform_device,和platform_driver
2.platform总线驱动工作流程
- 提供并注册platform_device/设备节点
- 提供并注册platform_driver
- 当platform总线内的mach函数会不停的匹配driver和device(老内核是根据driver内的id、name元素;新内核是根据of_match_table中的compatible)
- 一旦匹配成功,则调用driver的probe(探测)函数开始正式执行驱动代码
3.platform总线驱动的独立性和适应性
一个platform总线驱动程序可以对应多个设备,并且设备的变化也不会影响驱动。这是如何实现的呢?
- 简单的说,这是一种类似传参的机制。设备将底层信息(比如寄存器信息、使用到的中断号、设备名称等)传递给驱动,驱动本身代码不用变,只需要根据参数操作底层,便可适应设备的变化
- 现代驱动设计理念就是算法和数据分离,驱动源码中不携带数据,只负责算法(对硬件的操作方法),这样最大程度保持驱动的独立性和适应性
- 具体的实现方法是:老内核中,platform_device包含了一个device结构体,其内部有一个 void *platform_data; 这个有点类似于给用户提供的自留地,用户可以在里面存放各种底层信息。当driver的probe(探测)函数执行时,platform_device会作为参数传进去,这样驱动就能够间接的得到这个 void *platform_data,从而据此操作硬件;新内核则直接在设备节点属性中存放数据,驱动通过API读取节点里的数据
4.老内核下platform总线驱动的编写方法
下面,以led驱动为实例,分析怎么使用platform来写驱动
- 根据上图的流程,首先应该进入mach-xxx.c完成platform设备的注册。
- 第一步:创建适用于我们设备的platform_data类型(为自留地设计一种格式)
- 第二步:为一个具体设备实例化一个platform_data,用来存放该类设备的底层信息
- 第三步:创建一个具体platform设备(实例化一个platform_device),并把各种信息和platform_data填充入该设备
- 第四步:把platform设备丢到专门存放platform_device的数组中,开机时系统会注册数组中所有设备
- 先来看看mach-xxx.c中的情况,如果我们要写新的platform_device,要注意mach-xxx.c内有没有重复功能的。在该mach-xxx.c搜寻“platform”,寻找专门存放platform_device的数组,发现里面并没有led,看来我们要自己从头开始写了
/*sjh_add*/
/*第一步:创建一个适用于我们设备的platform_data类型*/
struct s5pv210_led_platdata {
unsigned int gpio;
unsigned int flags;
char *name;
char *def_trigger;
};
/*第二步:为一个具体设备实例化一个platform_data*/
static struct s5pv210_led_platdata x210_led1_pdata = {
.gpio = S5PV210_GPJ0(3),
.flags = NULL,
.name = "led1",
.def_trigger = NULL,
};
/*第三步:实例化一个platform_device,正式创建设备*/
static struct platform_device x210_led1 = {
.name = "s5pv210_led",//要和platform驱动中的名字对应
.id = 1,
.dev = {
.platform_data = &x210_led1_pdata,//底层信息
},
};
/* 第四步:把我们的platform_device添加进数组,开机时系统会注册数组中所有设备*/
static struct platform_device *smdkc110_devices[] __initdata = {
/*sjh_add*/
&x210_led1,
#ifdef CONFIG_FIQ_DEBUGGER
&s5pv210_device_fiqdbg_uart2,
#endif
...
/*该数组很长,后面就不贴了*/
- 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
- 然后我们还可以尝试添加多个设备,让一个驱动对应多个led,我们只需重复第二、第三步即可。值得注意的是,驱动和设备的匹配是靠name元素的,所以务必保证这几个设备的name元素都要和驱动的name元素相同,并且id不同就行了(如果name和id都一样……那就分不清了……)
- 接着开始驱动部分的编写
/*自留地格式,提供给probe和release函数,让它们可以解析platdata*/
extern struct s5pv210_led_platdata {
unsigned int gpio;
unsigned int flags;
char *name;
char *def_trigger;
};
static int s5pv210_led_probe(struct platform_device *pdev)
{
/*导入自留地格式s5pv210_led_platdata,这样我们才能解析参数*/
struct s5pv210_led_platdata *pdata = pdev->dev.platform_data;
int ret = -1;
/*这是一个例子,我们如何通过传入的参数获得led的gpio编号信息*/
gpio_num = pdata->gpio;
/*各种注册、初始化操作*/
...
return 0;
}
/*卸载模块将触发remove函数*/
static int s5pv210_led_remove(struct platform_device *pdev)
{
struct s5pv210_led_platdata *pdata = pdev->dev.platform_data;
/*各种注销操作*/
return 0;
}
/*定义我们的platform_driver。注意name要和platform_device中相同*/
static struct platform_driver s5pv210_led_driver = {
.probe = s5pv210_led_probe,
.remove = s5pv210_led_remove,
.driver = {
.name = "s5pv210_led",
.owner = THIS_MODULE,
},
};
/*模块与卸载加载函数,在里面分别添加platform驱动的注册和卸载函数*/
static int __init s5pv210_led_init(void)
{
return platform_driver_register(&s5pv210_led_driver);
}
static void __exit s5pv210_led_exit(void)
{
platform_driver_unregister(&s5pv210_led_driver);
}
module_init(s5pv210_led_init);
module_exit(s5pv210_led_exit);
//模块描述信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("taurenking");
MODULE_DESCRIPTION("S5PV210 LED driver");
MODULE_ALIAS("S5PV210 LED driver");
- 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
- 驱动方面要注意的地方是传参的问题,对于probe函数,传进来的是指向platform_device类型实例的指针,而我们需要的底层信息在platform_device中的dev内的platform_data中,前面的那幅图可以很好的表明这个结构
- 为了解析这个参数,还需要extern一个s5pv210_led_platdata到本文件,它是我们led设备的platform_data类型(因为这个类型是我们自己写在mach-xxx.c中的,没有定义在头文件中,故编译器找不到),让编译器知道格式,才能够解析
- 在解析参数前,先让导入自留地的格式
struct s5pv210_led_platdata *pdata = pdev->dev.platform_data;
,这样编译器才能知道格式,我们才能解析参数 - 那么其他硬件操作函数怎么获得底层信息呢?它们又没有platform_data这种参数,我们其实在文件头部定义几个全局变量就能解决,在probe函数种解析底层信息,然后赋值给全局变量,比如前面代码中的
gpio_num = pdata->gpio;
,硬件操作函数调用全局变量即可获得设备的底层信息,例如
static void s5pv210_led_set(struct led_classdev *led_cdev,
enum led_brightness value)
{
if (value == 0) {
gpio_set_value(gpio_num, 1);
}else{
gpio_set_value(gpio_num, 0);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
5.老内核下厂商风格的platform总线驱动
有时,我们需要去分析一些由厂商实现的platform总线驱动,比如framebuffer。此类platform总线驱动本质上和我们之前分析的完全相同,只是在细节上略有区别罢了
- 以framebuffer为例,在mach-xxx中,我们发现在专门存放platform_device的数组中,有一个和fb有关的platform设备
s3c_device_fb
,它被定义在arch/arm/plat-xxx/devs.c内:
struct platform_device s3c_device_fb = {
.name = "s3cfb",
.id = -1,
.num_resources = ARRAY_SIZE(s3cfb_resource),
.resource = s3cfb_resource,
.dev = {
.dma_mask = &fb_dma_mask,
.coherent_dma_mask = 0xffffffffUL
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 不难发现,里面并没有填充platform_data。这意味着该设备没有platform_data吗?显然不是的,就在s3c_device_fb的下面,定义了一个函数
s3cfb_set_platdata
,该函数明显是用来为platform设备填充platform_data的
- 看一下该函数的reference,发现在mach-xxx里的smdkc110_machine_init函数中被调用了,并且传进去一个貌似是LCD硬件信息的参数:
搜索ek070tn93_fb_data,发现这个结构体内果然包括了LCD所有的硬件信息。看来,这个结构体就是platform_data的灵魂
static struct s3c_platform_fb ek070tn93_fb_data __initdata = {
.hw_ver = 0x62,
.nr_wins = 5,
.default_win = CONFIG_FB_S3C_DEFAULT_WINDOW,
.swap = FB_SWAP_WORD | FB_SWAP_HWORD,
.lcd = &ek070tn93,
.cfg_gpio = ek070tn93_cfg_gpio,
.backlight_on = ek070tn93_backlight_on,
.backlight_onoff = ek070tn93_backlight_off,
.reset_lcd = ek070tn93_reset_lcd,
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 说到底,厂商搞的这一出,其实就是绕了几个圈子,把platform设备和platform_data分开定义了,本质和我们之前的那种直接填充的方法没有什么不同
6.新内核下platform总线驱动的编写方法
- 对于驱动本身来说,新内核下的变化倒不大;主要是platform设备不再需要在mach-xxx中注册,而是直接以节点形式定义在设备树中。platform设备可以直接定义在dts的根节点内,比如imx6dl-hummingboard.dts内的ir-receiver设备
/ {
model = "SolidRun HummingBoard DL/Solo";
compatible = "solidrun,hummingboard", "fsl,imx6dl";
ir_recv: ir-receiver {
compatible = "gpio-ir-receiver";
gpios = <&gpio1 2 1>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hummingboard_gpio1_2>;
};
/*后面一堆代码就省略了*/
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 驱动程序将直接和设备树里的设备节点进行配对,是通过设备节点中的compatible(兼容性)来与设备节点进行配对的。具体方法是定义一个of_match_table,只要里面的compatible与设备节点里的compatible相同,那么就触发probe函数
/*驱动中定义的of_match_table*/
static struct of_device_id gpio_ir_recv_of_match[] = {
{ .compatible = "gpio-ir-receiver", },
{ },
};
/*of_match_table被绑定到driver结构体内*/
static struct platform_driver gpio_ir_recv_driver = {
.probe = gpio_ir_recv_probe,
.remove = gpio_ir_recv_remove,
.driver = {
.name = GPIO_IR_DRIVER_NAME,
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(gpio_ir_recv_of_match),
},
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 有关设备的私有数据,新内核不再使用plat_data了,而是直接在节点中定义各种属性,然后在驱动中用特定的API获取,详见设备树详解