Linux驱动开发之平台总线

Posted 我想我会记得你

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux驱动开发之平台总线相关的知识,希望对你有一定的参考价值。

2020-02-14

关键字:Linux驱动中的probe函数是如何执行的


 

1、设备驱动模型

嵌入式 Linux 驱动开发往往都是按照如下的步骤来进行的:

1、实现入口函数 xxx_init() 和卸载函数 xxx_exit()

2、申请设备号 register_chrdev()

3、创建设备文件节点 class_create(), device_create()

4、硬件初始化,io资源映射 ioremap,注册中断等。

5、构建 file_operations 结构体对象

6、实现操作硬件方法 xxx_open(), xxx_read(), xxx_write(), xxx_close()

 

上面这几个步骤其实一套走下来还是比较繁琐的。再加上往往一款设备要适配的驱动数量还不少,如果每新增一款设备都要按照这套流程走一遍,那我们的软件开发效率将会比较低下。而其实在上面的驱动开发步骤中,不同硬件中仅第 4 步会有显著的差异,其余步骤很少有不同。因此,本着实现代码重用/复用的目的,就有了 Linux设备驱动模型 这个概念。这个概念的核心思想就是将驱动开发拆分成几个模块,按需调用拼接以组合成一个完整的驱动程序以减少程序员的开发工作量。

 

设备驱动模型中将设备驱动程序之间的关系按下图定义:

Device 就表示硬件设备的描述。如地址、中断号等。每新增一种设备可能都需要更改一次这里的描述信息,因此这块的代码是量少改动多的类型。

 

Driver 则表示驱动程序,主要是负责申请设备号、创建设备节点、申请中断等事宜。因为在 Linux 中几乎每个设备驱动都需要走这套流程,因此在设备驱动模型中它是属于代码量多,但改动较少的类型。

 

Bus 虽然名叫总线,但其实称它为中介更为合适。Linux的设备驱动模型将驱动与设备进行了分离,Bus这个中介的作用就是为二者牵桥搭线,根据 Device 与 Driver 的名称来匹配,名称相等的就将它们组合起来以实现某款硬件的驱动功能并立即调用 Driver 中的 probe() 函数。同时,Bus既身为中介,那肯定要知道待匹配双方的信息,因此 Device 与 Driver 都需要向 Bus 注册自己的信息以供匹配之用。在Bus内部则是通过一个链表来管理待匹配双方的信息的。

 

Sysfs

在任何一个 Linux 设备中,根目录下都会有一个 /sys 目录。这个目录是专用于告知用户内核的相关信息的。这个 /sys 目录机制被称为 Sysfs,即 sys文件系统。它在设备驱动模型中的关系如下图所示:

 

上图在 /sys 目录下分别对应于 bus , devices, class 三个目录。

 

关于设备驱动信息的记载则主要是在 /sys/class 目录下。例如,在 /dev 目录下往往会有很多设备节点,但这些设备节点所记载的信息往往不足以确定它到底是个什么设备。如:/dev/input/event0,光从字面上无法确定。而在 /sys/class/input 目录下也有一个 event0 目录,这个目录就是记载所有关于 event0 的信息的。在 event0/device 目录下有一个 name 文件,这个文件当中就记载着这个 event0 是一个 "Power Button" 设备。因此,/sys/class 往往能帮助我们确定系统中某个设备节点是什么东西。

 

在 /sys/bus 目录下,每一个总线都会创建一个目录,每个总线目录下又会有 drivers 与 devices 两个目录,这两个子目录表示当前系统中有哪些驱动以及当前系统中有哪些设备。

 

而在 /sys/devices 目录下又记录了系统中所有的设备的信息,每一个设备都有一个目录。在 /sys/bus 下的设备的信息就是通过软链接的方式引到 /sys/devices 下去的。

 

/sys/class 目录则是将系统中的所有设备作一个分类。就以上图中的鼠标为例,一个鼠标它既是一个设备,又是USB通信类型,同时又可以称其为输入型设备。因此就有了上图所示的关系结构。

 

我们也可以构建自己的总线模型,只需要按照下图所示的总线模型结构来创建就可以:

首先在 /sys/bus 目录下创建一个自己的目录,也即创建一个自定义的 Bus 总线类型。

 

Bus 总线在Linux中被抽象成 bus_type 结构体,其原型如下,一般我们只挑选需要关心的成员来赋值:

struct bus_type{
    const char *name; // /sys/bus 下的目录名称。
    int (*match)(struct device *dev, struct device_driver *drv);
};

总线的注册与注销函数签名如下:

int bus_register(struct bus_type *bus);
void bus_unregister(struct bus_type *bus);

bus_register() 函数在正确执行以后就可以发现在 /sys/bus 下有自己定义的名称的目录了,且这个目录下还会有 driver 与 device 两个子目录。但这两个子目录目前还是空目录。接下来就是要分别注册这两个对象。

 

Device 结构体对象是用于描述设备的信息的,一般包括地址、中断号等。该结构体的原型如下,同样我们只挑选需要关心的成员来赋值:

struct device {
    struct kobject kobj;
    const char *init_name; // 用于给Bus用来跟Driver做匹配的名字。即 /sys/bus/xxx/device/<init_name>
    struct bus_type *bus; //要注册到的Bus对象。
    void *platform_data; //自定义的数据。设备的地址、中断号等信息就定义在这里。
};

Device的注册与注销的函数签名如下:

int device_register(struct device *dev);
void device_unregister(struct device *dev);

如此,自定义Bus对象下的Device就注册好了。下面还有 Driver 对象。

 

Driver 结构体对象的原型如下:

struct device_driver {
    const char *name; //与Device类似,是 /sys/bus/xxx/driver/<name> 下的名字。
    struct bus_type *bus;
    int (*probe)(struct device *dev); //在Bus中将Driver与Device匹配上以后要执行的函数。
    int (*remove)(struct device *dev);
};

Driver的注册与注销的函数签名如下:

int driver_register(struct device_driver *drv);
void driver_unregister(struct device_driver *drv);

 

如此,我们自定义的Bus总线就创建好了,且它下面的 Device 对象有了,Driver 对象也有了。那接下来就该考虑让它们二者匹配的实现了。这里必须再强调一下:默认情况下只有 Bus 下的 Device 的 init_name 与 Driver 的 name 一致才能匹配成功。

 

Linux设备驱动模型的目的是为了将驱动开发中的设备与驱动代码在开发时分离解耦。但在系统运行起来以后还是要将二者合并才能正常驱动硬件运行的。那这里就涉及到一个问题:如何将在 Device 中定义的设备信息传递到 Driver 中去呢?

 

答案还是在 Device 结构体对象中的 void *platform_data; 中。

 

我们可以在 Device 的代码中任意设定设备描述信息,只要在最后将这些信息打包赋给这个 platform_data 指针变量即可。Bus在将Device与Driver匹配成功后会自动将记载在Device中platform_data上的设备描述信息传递到Driver的 probe() 中。

 

以下贴出本例中 Bus , Device , Driver 三个模块的源码:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/device.h>
#include <string.h>

int mybus_match(struct device *dev, struct device_driver *drv)
{
    //如果匹配成功,一定要返回1,失败返回0。
    if(!strncmp(drv->name, dev->kobj.name/*不能用init_name,它会被kobject清空。*/, strlen(drv->name)))
    {
        //名称匹配成功。
        printk("match ok.\\n");
        return 1;
    }
    else
    {
        printk("match failed.\\n");
        return 0;
    }
}

//实例化一个bus对象
struct bus_type mybus = {
    .name = "mybus",
    .match = mybus_match,
};

EXPORT_SYMBOL(mybus); //只有这样其它ko才能访问到这个结构体对象。dev.c中的extern仅仅是为了通过编译才加的。ko真正运行起来以后还得靠这个EXPORT_SYMBOL()的开放才能正常使用到。

static int __init mybus_init()
{
    //构建一个总线 /sys/bus/mybus
    
    int ret = bus_register(&mybus);//这段代码运行以后即可在 /sys/bus 下看到\'mybus\'目录被创建。
    
    return 0;
}

static void __exit mybus_exit()
{
    bus_unregister(&mybus);
}

module_init(mybus_init);
module_exit(mybus_exit);
MODULE_LICENSE("GPL");
Bus模块源码bus.c

 

#include <linux/init.h>
#include <linux/module.h>
#include <linux/device.h>

extern struct bus_type mybus; //mybus在bus.c文件中

//设置一个自定义数据,描述设备的特性。用于传给 device 结构体的 platform_data。
struct mydev_desc{
    char *name;
    int irq;
    unsigned long addr;
};

struct mydev_desc devinfo = {
    .name = "testdev",
    .irq = 999,
    .addr = 0x30000000,
};

void mydev_release(struct device *dev)
{
    printk("mydev_release()\\n");
}

//实例化一个bus对象
struct device mydev = {
    .init_name = "fsdev_drv", //要与drv.c中的name一致。
    .bus = &mybus,
    .release = mydev_release,
    .platform_data = &devinfo,
};

static int __init mydev_init()
{
    //将device注册到总线中 /sys/bus/mybus/devices
    
    int ret = device_register(&mydev); //这段代码运行成功后即可在 /sys/bus/mybus/devices 目录下创建一个 mydev 目录。
    
    return 0;
}

static void __exit mydev_exit()
{
    device_unregister(&mydev);
}

module_init(mydev_init);
module_exit(mydev_exit);
MODULE_LICENSE("GPL");
Device模块源码dev.c

 

/*
    这个是驱动代码。
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/device.h>

extern struct bus_type mybus;

struct mydev_desc *pdesc; //这个结构体在 dev.c 中定义,不能在这里直接使用。

int mydrv_probe(struct device *dev)
{
    printk("mydrv_probe()\\n");
    
    pdesc = (struct mydev_desc *)dev->platform_data;
    //将信息打印出来验证数据是否与在dev.c中设定时的一致。
    printk("name:%s\\nirq:%d\\n", pdesc->name, pdesc->irq);
    
    return 0;
}

int mydrv_remove(struct device *dev)
{
    return 0;
}

struct device_driver mydrv = {
    .name = "fsdev_drv", //要与dev.c中的init_name一致。
    .bus = &mybus,
    .probe = mydrv_probe,
    .remove = mydrv_remove,
};

static int __init mydrv_init()
{
    //将driver注册到总线中 /sys/bus/mybus/drivers
    
    int ret = driver_register(&mydrv); //这段代码运行成功后即可在 /sys/bus/mybus/drivers 目录下可以看到有一个 mydrv 目录生成。
    
    return 0;
}

static void __exit mydrv_exit()
{
    driver_unregister(&mydrv);
}

module_init(mydrv_init);
module_exit(mydrv_exit);
MODULE_LICENSE("GPL");
Driver模块源码drv.c

 

 

2、平台总线

前面介绍的设备驱动模型仅仅是为了给平台总线知识作铺垫。当我们掌握了设备驱动模型以后,再来接触平台总线就会比较容易一些。

 

前面的设备驱动模型的目的是为了减少驱动开发过程中的代码冗余,提升软件开发效率。这里的Linux平台总线同样有这个目的,并且还多了一个在对设备平台升级时减少软件重新移植的工作量的目的。因为一般来说不同平台不同版本的芯片或外围硬件其功能上的差异很少,有的仅仅是一些中断号、物理地址等方面的差异。在应用了平台总线模型以后就可以很方便地仅修改少部分代码就完成程序的移植,甚至可以同一款程序同时兼容多个平台。

 

平台总线模型图如下所示:

 

平台总线模型与设备驱动模型一样包含三个组成元素:

1、Bus

platform_bus。平台总线与普通总线不一样的地方在于它完全由系统创建,操作系统在开机时即会自行创建这一总线,不需要用户干预。

这个由系统完成创建的总线对象的结构体对象声明如下:

struct bus_type platform_bus_type = {

    .name = "platform",

    .dev_groups = platform_dev_groups,

    .match = platform_match,

    .uevent = platform_uevent,

    .pm = &platform_dev_pm_ops,

};

当然,这个结构体对象我们了解一下就好了。

而 platform_bus 中的 match 规则则是按以下两个条件来匹配:

1、优先根据 id_table 中记载的描述信息来匹配;

2、若没有 id_table 再去匹配 device 与 driver 的名称;

在 platform_bus 的 match 函数中,依旧是只将 device 与 device_driver 两个结构体对象作为参数传递进来。通过下面 platform_device 与 platform_driver 的结构体原型我们可以知道 device 与 device_driver 仅仅是它们的内部成员而已。不过在 Linux 中却可以根据成员直接拿到外结构体对象,它的拿取方法就靠 to_platform_device()/to_platform_driver() 函数,如 platform_bus 中的 match() 函数中写道:

struct platform_device *pdev = to_platform_device(dev);

struct platform_driver *pdrv = to_platform_driver(drv);

2、Deivce

结构体原型中几个常用的成员定义如下:

struct platform_device {

    const char *name;  //用于匹配的名称。

    int id;  //一般给-1即可,没什么用。

    struct device dev; //表示继承了device父类。

    u32 num_resources;  //资源的个数。

    struct resource *res;  //就是device结构体中的 void *platform_data,只不过在这里将它封装成了一个结构体对象而已。

};

struct resource 的原型如下:

struct resource {

    resource_size_t start;

    resource_size_t end;

    const char *name;

    unsigned long flags; //区分当前资源是内存(IORESOURCE_MEM)还是中断(IORESOURCE_IRQ)

    struct resource *parent, *sibling, *child;

};

platform_device 的注册与注销函数签名如下:

int platform_device_register(struct platfrom_device *pdev);

void platform_device_unregister(struct platform_device *pdev);

3、Driver

结构体原型中几个常用的成员定义如下:

struct platform_driver {

    int (*probe)(struct platform_device *);

    int (*remove)(struct platform_device *);

    struct device_driver driver; //表示继承自 device_driver 父类。

    const struct platfrom_device_id *id_table; //表示这个Driver可以支持的平台的信息列表,选填。

};

platform_driver 中注册与注销函数签名如下:

int platform_driver_register(struct platform_driver *drv);

void platform_driver_unregister(struct platform_driver *drv);

 

以下贴出一个点亮开发板上LED类的平台总线驱动编程源码:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <plat/irqs.h>

#define GPX3_CON 0x1140000
//GPX3寄存器中有6个,每个的地址占4个字节,因此值是24。
#define GPX3_SIZE 24

//一个设备可能有多个资源,因此给个数组。
struct resource led_res[] = {
    [0] = {
        .start = GPX3_CON,
        .end = GPX3_CON + GPX3_SIZE - 1, //地址计算容易错,要搞清楚了。
        .flags = IORESOURCE_MEM,
    },//第0个数组做初始化。
    
    [1] = {
        .start = GPX3_CON,
        .end = GPX3_CON + GPX3_SIZE - 1,
        .flags = IORESOURCE_MEM,
    },
    
    [2] = {
        .start = 24,
        .end = 24,//start与end表示中断号。因为中断号是不连续的,因此开始与结束是一致的。
        .flags = IORESOURCE_IRQ,
    },
};

struct platform_device pdev = {
    .name = "led_dev_test",
    .id = -1,
    .num_resources = ,
    .resource = ,
};

static int __init plt_led_dev_init()
{
    //注册
    int ret = platform_device_register(&led_pdev);
    
    return 0;
}

static void __exit plt_led_dev_exit()
{
    platform_device_unregister(&led_pdev);
}

module_init(plt_led_dev_init);
module_exit(plt_led_dev_exit);
MODULE_LICENSE("GPL");
platform_device的plt_led_dev.c

 

#include <linux/init.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/slab.h>
#include <linux/fs.h>
#include <linux/ioport.h>

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

struct led_dev{
    int dev_major;
    struct class *cls;
    struct device *dev;
    struct resource *res; //获取到的硬件资源描述信息对象。
    void *reg_base; //保存物理地址映射之后的虚拟地址。
};

struct led_dev *sled;

ssize_t led_pdrv_write(struct file *fp, const char __user *buf, size_t count, loff_t *fpos)
{
    int val;
    int ret = copy_from_user(&val, buf, count);
    if(ret == 0)
    {
        if(val)
        {
            //将亮LED灯的指令写到相应的虚拟地址上去。
        }
        else
        {
            //将灭LED灯的指令写到相应的虚拟地址上去。
        }
    }
    
    return count;
}

int led_pdrv_open(struct inode *inode, struct file *fp)
{
    return 0;
}

int led_pdrv_close(struct inode *inode, struct file *fp)
{
    return 0;
}

const struct file_operations fops = {
    .open = led_pdrv_open,
    .release = led_pdrv_close,
    .write = led_pdrv_write,
};

int led_pdrv_probe(struct platform_device *pdev)
{
    printk("%s()\\n", __FUNCTION__);
    
    sled = kzalloc(sizeof(struct led_dev), GFP_KERNEL);
    //1、注册设备号
    sled->dev_major = register_chrdev(0, "sled_drv", &fops);
    
    //2、创建设备节点
    sled->cls = class_create(THIS_MODULE, "led_new_cls");
    sled->dev = device_create(sled->cls, NULL, MKDEV(sled->dev_major, 0), NULL, "sled0");
    
    //3、初始化硬件
    sled->res = platform_get_resource(pdev, IORESOURCE_MEM, 0);//直接调用系统函数来获取资源。取plt_led_dev中的led_res[]数组的第0个元素。
    struct resource *res1 = platform_get_resource(pdev, IORESOURCE_MEM, 1);//取plt_led_dev中的led_res[]数组的第1个元素。
    struct resource *res2 = platform_get_resource(pdev, IORESOURCE_IRQ, 0/*注意这个值,它表示同种资源的第n个。*/);//取plt_led_dev中的led_res[]数组的第2个元素。
    int iiirq = platform_get_irq(pdev, 0);//等同于上面那个函数调用。
    printk("演示,irq in resource:%d,,iiirq:%d\\n", res2->start, iiirq);//两个数是相等的。
    
    ioremap(sled->res->start, sled->res->end = sled->res->start + 1);//为什么要+1,假设地址位为 0 ~ 7,则7-0=7,但实际上地址长度是8。
    sled->reg_base = ioremap(sled->res->start, resource_size(sled->res));//与上一行函数调用是等价的。
    //接下来可以配置相应寄存器了,这个操作就与相应芯片关联了。
    writel((readl(sled->reg_base) & ~(0xff<<16)) | (0x11<<16), sled->reg_base);
    
    //4、实现各种IO接口
    
    
    return 0;
}

int led_pdrv_remove(struct platform_device *pdev)
{
    printk("%s()\\n", __FUNCTION__);
    
    iounmap(sled->reg_base);
    device_destroy(sled->cls, MKDEV(sled->dev_major, 0));
    class_destroy(sled->cls);
    unregister_chrdev(sled->dev_major, "sled_drv");
    kfree(sled);
    
    return 0;
}

const struct platform_device_id led_id_table[] = {
    {"led_dev_test", 0xfff}, //名称一致,表示这个驱动可以与 plt_led_dev.c 匹配上。
    {"led_dev_test2", 0xeee}, //可以匹配其它设备,名叫 led_dev_test2 的设备。
};

struct platform_driver led_pdrv = {
    
    .probe = led_pdrv_probe,
    .remove = led_pdrv_remove,
    .driver = {
        .name = "samsung_led_drv", //这个就是父类 device_driver 中的名字。若定义了 id_table,那这个名称就不用于做匹配,否则就是用来作匹配,必须与 plt_led_dev.c 中的 platform_device 中的名称一致。这里指定的名称还会在 /sys/bus/platform/drivers 目录下创建一个子目录。
    },
    .id_table = led_id_table,
};

static int __init plt_led_drv_init()
{
    int ret = platform_driver_register(&led_pdrv);
    
    return 0;
}

static void __exit plt_led_drv_exit()
{
    platform_driver_unregister(&led_pdrv);
}

module_init(plt_led_drv_init);
module_exit(plt_led_drv_exit);
MODULE_LICENSE("GPL");
platform_driver的plt_led_drv.c

 

以上就是嵌入式Linux中平台总线驱动编程模型的基础知识。

 


 

以上是关于Linux驱动开发之平台总线的主要内容,如果未能解决你的问题,请参考以下文章

linux设备驱动之平台总线实践环节

linux设备驱动之platform平台总线工作原理

Linux——Linux驱动之基于平台总线platform的设备驱动编写实战(手把手教你以platform形式利用GPIO控制蜂鸣器)

Linux——Linux驱动之基于平台总线platform的设备驱动编写实战(手把手教你以platform形式利用GPIO控制蜂鸣器)

linux驱动之platform平台总线工作原理

linux设备驱动之平台总线实践环节