基于platform总线的驱动分析

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

...
/*该数组很长,后面就不贴了*/
  • 然后我们还可以尝试添加多个设备,让一个驱动对应多个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");         
  • 驱动方面要注意的地方是传参的问题,对于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);
        }
}

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
    }
};
  • 不难发现,里面并没有填充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,
};
  • 说到底,厂商搞的这一出,其实就是绕了几个圈子,把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>;
    };

/*后面一堆代码就省略了*/

  • 驱动程序将直接和设备树里的设备节点进行配对,是通过设备节点中的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),
    },
};

猜你喜欢

转载自blog.csdn.net/stoic163/article/details/80548220
今日推荐