(3.7)一个按键所能涉及的:设备驱动分层分离的概念

/* AUTHOR: Pinus

* Creat on : 2018-10-29

* KERNEL : linux-4.4.145

* BOARD : JZ2440(arm9 s3c2440)

* REFS : 韦东山视频教程第二期

               Linux设备驱动模型之platform(平台)总线详解

               详解Linux2.6内核中基于platform机制的驱动模型

*/

概述

        分层即把硬件相关和相对稳定的东西给抽出来,并向上提供统一的接口,每一层专注于自己的事情,比如前文的输入子系统。分离即把硬件相关和相对稳定的东西给分离开来,实际上即是bus-dev-drv模型(平台总线、平台设备、平台驱动)。以最简单的led驱动为例,几乎每个系统都会用到,操作LED的原理是相同的,只是具体LED的物理地址之类的不同,因此不防,把操作之类的放在drv中,把具体的硬件信息放在dev中,这样只要修改Dev的信息就可以轻松移植了,无疑增加的程序的可移植行,节约开发时间,便于修改。

        实际查看内核的一些驱动代码,都是使用了platform bus而不是像前面我们那样直接用file_operation。

1. 什么是platform(平台)总线?

        相对于USB、PCI、I2C、SPI等物理总线来说,platform总线是一种虚拟、抽象出来的总线,实际中并不存在这样的总线。那为什么需要platform总线呢?其实是Linux设备驱动模型为了保持设备驱动的统一性而虚拟出来的总线。因为对于usb设备、i2c设备、pci设备、spi设备等等,他们与cpu的通信都是直接挂在相应的总线下面与我们的cpu进行数据交互的,但是在我们的嵌入式系统当中,并不是所有的设备都能够归属于这些常见的总线,在嵌入式系统里面,SoC系统中集成的独立的外设控制器、挂接在SoC内存空间的外设却不依附与此类总线。所以Linux驱动模型为了保持完整性,将这些设备挂在一条虚拟的总线上(platform总线),而不至于使得有些设备挂在总线上,另一些设备没有挂在总线上。

platform总线相关代码:driver\base\platform.c 文件

相关结构体定义:include\linux\platform_device.h 文件中

2、platform总线管理下的2员大将

(1)两个结构体platform_deviceplatform_driver

对于任何一种Linux设备驱动模型下的总线都由两个部分组成:描述设备相关的结构体和描述驱动相关的结构体

在platform总线下就是platform_device和platform_driver,下面是对两个结构体的各个元素进行分析:

platform_device结构体:(include\linux\platform_device.h)

struct platform_device { // platform总线设备
    const char * name; // 平台设备的名字
    int id; // ID 是用来区分如果设备名字相同的时候(通过在后面添加一个数字来代表不同的设备,因为有时候有这种需求)
    struct device dev; // 内置的device结构体
    u32 num_resources; // 资源结构体数量
    struct resource * resource; // 指向一个资源结构体数组

    const struct platform_device_id *id_entry; // 用来进行与设备驱动匹配用的id_table表
    char *driver_override; /* Driver name to force a match 强制匹配某个驱动 */

    /* MFD cell pointer */
    struct mfd_cell *mfd_cell;

    /* arch specific additions */
    struct pdev_archdata archdata; // 自留地 添加自己的东西
};

platform_device结构体中的struct resource结构体分析

struct resource { // 资源结构体
    resource_size_t start; // 资源的起始值,如果是地址,那么是物理地址,不是虚拟地址
    resource_size_t end;   // 资源的结束值,如果是地址,那么是物理地址,不是虚拟地址
    const char *name;     // 资源名
    unsigned long flags;  // 资源的标示,用来识别不同的资源
    struct resource *parent, *sibling, *child; // 资源指针,可以构成链表
};

platform_driver结构体:(include\linux\platform_device.h)

struct platform_driver {
    int (*probe)(struct platform_device *); // 这个probe函数其实和 device_driver中的是一样的功能,但是一般是使用device_driver中的那个
    int (*remove)(struct platform_device *); // 卸载平台设备驱动的时候会调用这个函数,但是device_driver下面也有,具体调用的是谁这个就得分析了
    void (*shutdown)(struct platform_device *);
    int (*suspend)(struct platform_device *, pm_message_t state);
    int (*resume)(struct platform_device *);
    struct device_driver driver;  // 内置的device_driver 结构体
    const struct platform_device_id *id_table; // 该设备驱动支持的设备的列表 他是通过这个指针去指向 platform_device_id 类型的数组
    bool prevent_deferred_probe;
};

(2)两组接口函数(driver\base\platform.c)

int platform_driver_register(struct platform_driver *);       // 用来注册我们的设备驱动    

void platform_driver_unregister(struct platform_driver *);  // 用来卸载我们的设备驱动

int platform_device_register(struct platform_device *);      // 用来注册我们的设备      

void platform_device_unregister(struct platform_device *); // 用来卸载我们的设备

 

实验

目的:实际体验platform bus 驱动的编写架构以LED为例

驱动部分 led_drv.c

/* 分配、设置、注册一个 platform_driver */
#include <linux/module.h>
#include <linux/version.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/sched.h>
#include <linux/pm.h>
#include <linux/sysctl.h>
#include <linux/proc_fs.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/input.h>
#include <linux/console.h>
#include <linux/err.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/device.h> //class_create
#include <linux/fs.h>

#include <asm/io.h>
#include <asm/irq.h>
#include <asm/uaccess.h>

static struct class *led_class;
static struct device *led_dev;

static unsigned int major;

static volatile unsigned long *gpio_con;
static volatile unsigned long *gpio_dat;

static int pin;

static int led_drv_open(struct inode *inode, struct file *file)
{
    *gpio_con &= ~(0x3<<(pin*2)); //现将对应位清零
    *gpio_con |= (0x1<<(pin*2)); // 01 输出
    
    return 0;
}

static ssize_t led_drv_write(struct file *file, const char *buf, size_t count, loff_t *pos)
{
    char val;
    
    copy_from_user(&val, buf, count); //将数据从用户空间传到内核空间 反之copy_to_user
    printk("/dev/led: %d\n", val);

    if(val==1) {
        *gpio_dat &= ~(1<<pin);
    }
    else {
        *gpio_dat |= (1<<pin);
    }

    return 0;
}


static const struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_drv_open,
    .write = led_drv_write,
};

static int led_probe(struct platform_device *pdev)
{
    /* 根据device的资源进行 ioremap */
    struct resource *res;
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    gpio_con = ioremap(res->start, res->end-res->start+1);
    gpio_dat = gpio_con + 1;

    res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
    pin = res->start;

    /* 注册字符设备驱动 */
    major = register_chrdev(0, "myled", &led_fops);
    led_class = class_create(THIS_MODULE, "myled"); // 创建一个类
    led_dev = device_create(led_class, NULL, MKDEV(major, 0), NULL, "led"); // 创建设备节点"dev/led"

    printk("led_probe : find led \n");
    return 0;
}

static int led_remove(struct platform_device *pdev)
{
    /* 卸载字符设备驱动 */
    device_destroy(led_class, MKDEV(major, 0));
    class_destroy(led_class);
    unregister_chrdev(major, "myled");

    /* iounmap */
    iounmap(gpio_con);
    printk("led_remove : remove led \n");
    return 0;
}

static struct platform_driver led_drv = {
    .probe = led_probe,
    .remove = led_remove,
    .driver = {
        .name = "myled", //依靠名字来检索,要和dev保持一致
    },
};

static int __init led_drv_init(void)
{
    return platform_driver_register(&led_drv);
}

static void __exit led_drv_exit(void)
{
    platform_driver_unregister(&led_drv);
}

module_init(led_drv_init);
module_exit(led_drv_exit);
MODULE_LICENSE("GPL");

原理分析

在过去我们在入口函数init里都会进行初始化注册设备之类,现在只调用一个函数

platform_driver_register(&led_drv);

其中

static struct platform_driver led_drv = {
    .probe = led_probe,
    .remove = led_remove,
    .driver = {
        .name = "myled", //依靠名字来检索,要和dev保持一致
    },
};

上面代码中实际应用部分其实与以前一样,也要有file_operation,在前面插了一步platform_register的注册,下面就是分析一下内核的代码,大概的流程,代码太多不适合全放上来,跟着流程走

// 通过宏定义调用另一个程序

platform_driver_register(drv)

    |

    __platform_driver_register(drv, THIS_MODULE)

        |

        drv->driver.bus = &platform_bus_type; // 设置设备驱动 挂接在 platform平台总线下

        driver_register(&drv->driver); // 注册设备驱动

// 其中platform_bus_type是一个bus_type 结构体,其中的匹配函数会在接下来分析时用到

struct bus_type platform_bus_type = {
    .name = "platform",
    .dev_groups = platform_dev_groups,
    .match = platform_match, // 匹配驱动和设备
    .uevent = platform_uevent,
    .pm = &platform_dev_pm_ops,
};

driver_register(&drv->driver);

    |

    other = driver_find(drv->name, drv->bus); // 这个函数其实进行了一个校验 比对当前的 总线下是否存在名字和现在需要注册的设备驱动的名字相同的设备驱动

    bus_add_driver(drv); // 在总线挂接设备驱动 就是将设备驱动对应的kobj对象与组织建立关系

        |

        struct bus_type *bus; // 定义一个bus_type 结构体指针

        struct driver_private *priv; // 定义一个 driver_private 指针

        priv->driver = drv; // 使用 priv->driver 指向 drv

        drv->p = priv; // 使用drv->p 指向 priv 这两步见多了 ,跟之前分析的是一样的意思 就是建立关系

        priv->kobj.kset = bus->p->drivers_kset; // 设置设备驱动对象的父对象( 也就是指向一个 kset ) 父对象就是 /sys/bus/bus_type/drivers/ 这个目录对应的对象

        kobject_init_and_add(&priv->kobj, &driver_ktype, NULL,  "%s", drv->name);// 添加kobject 对象到目录层次中就能够在 /sys/bus/bus_type/drivers/ 目录中看到设备驱动对应的文件了

        klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers); // 链表挂接:priv->knode_bus 挂接到 bus->p->klist_drivers 链表头上去

        driver_attach(drv); // 尝试将驱动绑定到设备 也就是通过这个函数进行设备

        module_add_driver(drv->owner, drv);

        driver_create_file(drv, &driver_attr_uevent); // 建立属性文件: uevent

        driver_add_groups(drv, bus->drv_groups);

前面的的呢大致就是一些初始化,把当前驱动添加进平台驱动链表啦,创建对应的系统文件之类,platform bus 最关键的当然还是如何把驱动和设备关联起来

/**
 * driver_attach - try to bind driver to devices.
   尝试将驱动和设备绑定
 * @drv: driver.
 */

int driver_attach(struct device_driver *drv)
{
    return bus_for_each_dev(drv->bus, NULL, drv, __driver_attach); // 这个函数的功能就是:依次去匹配bus总线下的各个设备
}
int bus_for_each_dev(struct bus_type *bus, struct device *start, void *data, int (*fn)(struct device *, void *))
{
    struct klist_iter i; // 定义一个klist_iter 结构体变量 包含: struct klist 和 struct klist_node
    struct device *dev;
    int error = 0;

    if (!bus || !bus->p)
        return -EINVAL;

    klist_iter_init_node(&bus->p->klist_devices, &i, (start ? &start->p->knode_bus : NULL)); //这个函数的功能就是将 klist_devices 和 knode_bus填充到 i 变量中

    while ((dev = next_device(&i)) && !error) // 依次返回出总线上的各个设备结构体device
        error = fn(dev, data); // 对于每一个设备和设备驱动都调用fn这个函数 直道成功 或者全部都匹配不上

    klist_iter_exit(&i);

    return error;
}

 

/*

* 作为参数传进去,进行驱动和设备匹配识别

*/
 

static int __driver_attach(struct device *dev, void *data)
{
    struct device_driver *drv = data; // 定义一个device_driver 指针

    /*
     * Lock device and try to bind to it. We drop the error
     * here and always return 0, because we need to keep trying
     * to bind to devices and some drivers will return an error
     * simply if it didn't support the device.
     *
     * driver_probe_device() will spit a warning if there
     * is an error.
     */

    if (!driver_match_device(drv, dev)) // 通过这个函数进行匹配 调用总线下的match 函数
        return 0;

    if (dev->parent) /* Needed for USB */
        device_lock(dev->parent);
    device_lock(dev);
    if (!dev->driver)
        driver_probe_device(drv, dev); // 如果匹配成功,调用probe函数

    device_unlock(dev);

    if (dev->parent)
        device_unlock(dev->parent);

    return 0;
}

这才是真正的匹配函数,匹配传入的驱动和设备是否匹配,可见调用的是总线结构体下的match函数

static inline int driver_match_device(struct device_driver *drv, struct device *dev)
{
    return drv->bus->match ? drv->bus->match(dev, drv) : 1;
}

platform->match:就在上文中的platform_bus_type中引用

struct bus_type platform_bus_type = {

        .match = platform_match, // 匹配驱动和设备

};

static int platform_match(struct device *dev, struct device_driver *drv)
{
    struct platform_device *pdev = to_platform_device(dev);//获得platform_device
    struct platform_driver *pdrv = to_platform_driver(drv); //获取 platform_driver

    /* When driver_override is set, only bind to the matching driver 当设置driver_override时,只绑定到匹配驱动程序 */
    if (pdev->driver_override)
        return !strcmp(pdev->driver_override, drv->name);

    /* Attempt an OF style match first 尝试一种风格匹配*/
    if (of_driver_match_device(dev, drv))
        return 1;

    /* Then try ACPI style match 然后尝试ACPI风格匹配 */
    if (acpi_driver_match_device(dev, drv))
        return 1;

    /* Then try to match against the id table 如果id_table存在,就直接匹配id */
    if (pdrv->id_table)
        return platform_match_id(pdrv->id_table, pdev) != NULL;

    /* fall-back to driver name match 还没有就直接匹配 pdev->name drv->name 名字是否形同*/
    return (strcmp(pdev->name, drv->name) == 0);
}

通过上面函数,如果匹配成功那么__driver_attach就会继续向下执行,调用driver_probe_device函数

driver_probe_device

    |

    really_probe

        |

        dev->driver = drv; // 使用 dev->driver 指针去指向 drv 这就使得这两者建立了一种关系

        if (dev->bus->probe) { // 如果总线下的probe函数存在 则调用优先调用这个函数

            ret = dev->bus->probe(dev);

        } else if (drv->probe) { // 否则调用设备驱动中的probe函数

            ret = drv->probe(dev); // 所以由此可知: 总线中的probe函数具有更高的优先级

        }

综上,如果经过驱动初始化过程中和设备经过匹配成功,最终会调用probe函数

 

设备部分 led_dev.c

#include <linux/module.h>
#include <linux/version.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/sched.h>
#include <linux/pm.h>
#include <linux/sysctl.h>
#include <linux/proc_fs.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/input.h>
#include <linux/console.h>
#include <linux/err.h>
#include <linux/types.h>
#include <linux/kernel.h>

#include <asm/io.h>
#include <asm/irq.h>
#include <asm/uaccess.h>

/* 分配、设置、注册一个 platform_device */
static struct resource led_resources[] = {
    [0] = {
        .start = 0x56000050,
        .end = 0x56000050 + 8 - 1,
        .flags = IORESOURCE_MEM,
    },

    [1] = {
        .start = 4,
        .end = 4,
        .flags = IORESOURCE_IRQ,
    },
};


void led_release(struct device *dev)
{
    // 没什么用,但必须存在
}


static struct platform_device myled_dev = {
    .name = "myled",
    .id = -1,
    .num_resources = ARRAY_SIZE(led_resources),
    .resource = led_resources,
    .dev = {
        .release = led_release,
    },
};

static int __init led_dev_init(void)
{
    return platform_device_register(&myled_dev);
}

static void __exit led_dev_exit(void)
{
    platform_device_unregister(&myled_dev);
}


module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL");

原理分析

【流程】

platform_device_register(&myled_dev)

    |

    platform_device_add(pdev);

        |

        pdev->dev.bus = &platform_bus_type;

        device_add(&pdev->dev); // 添加到设备链表里

            |

            bus_probe_device(dev);

                |

                device_initial_probe

                    |

                    __device_attach(dev, true); // 则调用这个函数进行匹配

                       |

                       bus_for_each_drv(dev->bus, NULL, &data, __device_attach_driver); // 遍历总线的链表匹配对应的设备驱动

可以看出,接下来的流程与驱动差不多了,最后也是匹配设备与驱动,如果成功那么也会调用probe函数。

综上,设备和驱动殊途同归,其初始化时都是现将自己添加到对应的链表,然后和另一个链表进行匹配,如果成功那么就会调用probe函数。而因为内核中并没有定义platform的总线probe,因此最终都会调用驱动中的probe

static int led_probe(struct platform_device *pdev)
{
    /* 根据device的资源进行 ioremap */
    struct resource *res;
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    gpio_con = ioremap(res->start, res->end-res->start+1);
    gpio_dat = gpio_con + 1;

    res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
    pin = res->start;

    /* 注册字符设备驱动 */
    major = register_chrdev(0, "myled", &led_fops);
    led_class = class_create(THIS_MODULE, "myled"); // 创建一个类
    led_dev = device_create(led_class, NULL, MKDEV(major, 0), NULL, "led"); // 创建设备节点"dev/led"
    
    printk("led_probe : find led \n");

    return 0;
}

可以看出在probe函数中我们进行了以往在init中的设备注册,及一些初始化工作

注意到其中使用了platform_get_resource(pdev, IORESOURCE_MEM, 0);来获取真正的物理资源,而这些资源都是定义在led_dev.c中的
 

static struct platform_device myled_dev = {
    .name = "myled",
    .id = -1,
    .num_resources = ARRAY_SIZE(led_resources),
    .resource = led_resources, // 资源
    .dev = {
        .release = led_release,
    },
};



/* 分配、设置、注册一个 platform_device */
static struct resource led_resources[] = {
    [0] = {
        .start = 0x56000050,
        .end = 0x56000050 + 8 - 1,
        .flags = IORESOURCE_MEM,
    },
    
    [1] = {
        .start = 4,
        .end = 4,
        .flags = IORESOURCE_IRQ,
    },
};

可以想象,如果我们想让这个led驱动程序在另一板子上跑起来,只要根据板子更改led_resources[] 这一个地方就可以了,不用再整个代码中到处找,可以预见这在其他复杂的驱动里,能极大提高可以执行,缩短开发周期,避免错误。