您现在的位置是:首页 >技术杂谈 >【Linux驱动开发】023 platform设备驱动网站首页技术杂谈

【Linux驱动开发】023 platform设备驱动

Kashine 2023-05-18 04:00:01
简介【Linux驱动开发】023 platform设备驱动

一、前言 

驱动分离目的:提高Linux代码重用性和可移植性。


二、驱动的分隔与分离

百度看了很多,大多都没讲清楚为什么使用platform驱动,为什么驱动分隔与分离可以提高代码重用性,只是在讲实现的结构体、函数接口等等,现在我们就来分析一下:

先拿stm32单片机举个例子,如果使用I2C驱动的MPU6050,我们需要写一个mpu6050.c文件对其进行初始化,包括I2C初始化和读写函数、mpu6050初始化函数,如下图所示,其中对I2C的操作是stm32的I2C主机驱动,而对于mpu6050的初始化时设备的驱动。

如果现在我们改用MSP430驱动MPU6050,那么需要重新编写上面的mpu6050.c文件,也就是需要同时对主机驱动和设备驱动进行更改,总结一下上面的操作如下:

对于不同的主机(这里是单片机),更改主机的驱动文件是必要的,因为不同的主机I2C驱动文件肯定不一样,但是对于同一个MPU6050设备而言,重新编写相应的驱动文件是没有必要的!

为了保持设备驱动文件的可重用性,我们将主机驱动和设备驱动文件进行隔离,如下图所示:

并且我们统一命名所有主机(单片机)的I2C主机驱动中函数(I2C操作init、read、write函数),也就是stm32和MSP430中的I2C初始化函数、读写函数名字相同,这样在对mpu6050_init函数中使用统一的I2C读写操作进行初始化操作。

这样从stm32移植到MSP430中就无需重写mpu6050.c,也就是只需要重写I2C主机驱动,无需重写MPU6050设备驱动,重写主机驱动是可以理解的,毕竟平台不同。主机和设备驱动分隔示意图如下:

如果有多个使用I2C的设备,每个设备驱动对于不同的平台只需要写一次即可,这样代码的重用性就非常好。多个I2C设备的主机驱动与设备驱动隔离示意图如下:

类比到Linux,传统的驱动是这样的:

驱动分隔之后是这样的:

对于多个I2C设备的驱动分隔示意图是这样的:

相当于通过驱动分隔提高了代码的重用性! (分离、分隔都是一个意思,也就是分离)

上面说的主机驱动一般由半导体厂家开发,设备驱动由设备器件厂家开发,我们只需要提供设备的信息即可,比如I2C设备连接到哪个I2C接口上,速度是多少等等。也即是说将设备信息从设备驱动中分离出来,也就是驱动只负责驱动,设备只负责设备,使用总线对两者进行匹配。这个就是linux中的驱动-总线-设备模型也就是所谓的驱动分离通过着则这种方式,我们可以进一步分离,使代码重用性变得更高!

当某个注册某个驱动的时候,总线就会在右侧设备查找匹配项,匹配成功则将两者联系起来。

OK到此为止,Linux驱动分离讲解完成!


三、驱动的分层

分层就是将一个复杂的工作分成了4层, 分而做之,降低难度。每一层只专注于自己的事情, 系统已经将其中的核心层和事件处理层写好了,所以我们只需要来写硬件相关的驱动层代码即可。


四、platform平台驱动

SOC中的某些的某些外设可能没有总线这个概念,但是又需要使用驱动-总线-设备模型,因此,提出了platform虚拟总线,对应的驱动为platform_driver,对应的设备为platforn_device。

1、platform总线

struct bus_type {
     const char  *name;
     const char  *dev_name;
     struct device  *dev_root;
     struct device_attribute *dev_attrs; /* use dev_groups instead */
     const struct attribute_group **bus_groups;
     const struct attribute_group **dev_groups;
     const struct attribute_group **drv_groups;
    
     int (*match)(struct device *dev, struct device_driver *drv);//匹配函数
     int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
     int (*probe)(struct device *dev);
     int (*remove)(struct device *dev);
     void (*shutdown)(struct device *dev);

     int (*online)(struct device *dev);
     int (*offline)(struct device *dev);

     int (*suspend)(struct device *dev, pm_message_t state);
     int (*resume)(struct device *dev);

     const struct dev_pm_ops *pm;

     const struct iommu_ops *iommu_ops;

     struct subsys_private *p;
     struct lock_class_key lock_key;
};

使用上面的结构体struct bus_type表示总线,match函数完成驱动和设备之间的匹配。

platform是结构体struct 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, 
}; 

platform_match就是匹配函数,其定义如下:

static int platform_match(struct device *dev, struct device_driver *drv) //(设备,设备驱动)
{ 
    struct platform_device *pdev = to_platform_device(dev); 
    struct platform_driver *pdrv = to_platform_driver(drv); 
   
    /*When driver_override is set,only bind to the matching driver*/ 
    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 */ 
    if (acpi_driver_match_device(dev, drv)) 
        return 1; 

    /* Then try to match against the id table */ 
    if (pdrv->id_table) 
        return platform_match_id(pdrv->id_table, pdev) != NULL; 
  
    /* fall-back to driver name match */ 
    return (strcmp(pdev->name, drv->name) == 0); 
} 

我们需要学习一下驱动和设备是如何匹配的,这样才能学会配置两者匹配的方式,好的,现在看上面的代码:

第二个if语句:OF匹配形式,即设备树匹配方式of_driver_match_device(dev, drv)函数形参drv表示platform驱动,也就是一个形参变量,该变量中有个of_match_table的成员变量,此成员变量保存着驱动的compatible匹配表,设备树节点compatible属性值和此匹配表进行比较,查看是否匹配,成功匹配probe函数(platform设备驱动中的一个函数,下面讲解)就会执行。

第三个if语句:ACPI匹配方式。

第四个if语句:id_table匹配,每个platform_driver(platform驱动结构体)有一个id_table成员变量,保存着这个platform所支持的设备类型。

最后一个return语句:如果id_table不存在的话,直接比较驱动和设备的name字段,相当表明匹配成功。

2、platform驱动

platform_driver结构体表示platform驱动,定义如下:

struct platform_driver { 
    int (*probe)(struct platform_device *); //重要函数
    int (*remove)(struct platform_device *); 
    void (*shutdown)(struct platform_device *); 
    int (*suspend)(struct platform_device *, pm_message_t state); 
    int (*resume)(struct platform_device *); 
    struct device_driver driver; //相当于一个基类
    const struct platform_device_id *id_table; //上面讲解的第三种驱动和设备匹配方法
    bool prevent_deferred_probe; 
}; 

probe函数:当驱动和设备匹配成功之后,probe函数就会执行。非常重要的函数!!

driver成员:device_driver 结构体变量,相当于C++中的基类, device_driver 结构体是驱动最基础的框架。

id_table:上面讲解第三种驱动和设备匹配方法(非设备树)。

上方 struct device_driver 结构体是 struct platform_driver 的基类结构体,其中:

  • name 是前面说的第四种方式,直接比较驱动和设备的name字段,就是这里基类里面的name。
  • of_match_table 为采用设备树的时候使用的匹配表。
struct device_driver {
    const char          *name;// 非设备树匹配方式
    ...

    const struct of_device_id      *of_match_table;// 设备树匹配方式
    ...
}

上面的设备树匹配方式 struct of_device_id  结构体如下,其中 compatible 非常重要,因为对于设备树而言,就是通过设备节点的 compatible 属性值和 of_match_table 中每个项目的 compatible 成员变量进行比较,如果有相等的就表示设备和此驱动匹配成功。 

struct of_device_id {
    char        name[32];
    char        type[32];
    char        compatible[128];
    const void   *data;
};

3、platform 驱动API 函数

当我们定义并初始化好 platform_driver 结构体变量以后,需要在驱动入口函数里面调用
platform_driver_register 函数向 Linux 内核注册一个 platform 驱动:

int platform_driver_register (struct platform_driver    *driver) 

driver:要注册的 platform 驱动。 
返回值:负数,失败;0,成功。

还需要在驱动卸载函数中通过 platform_driver_unregister 函数卸载 platform 驱动,

void platform_driver_unregister(struct platform_driver *drv) 
drv:要卸载的 platform 驱动。 
返回值:无。 

/* 设备结构体 */
struct xxx_dev{
    struct cdev cdev;
    /* 设备结构体其他具体内容 */
};

struct xxx_dev xxxdev;   /* 定义个设备结构体变量 */

static int xxx_open(struct inode *inode, struct file *filp)
{
    /* 函数具体内容 */
    return 0;
}

static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    /* 函数具体内容 */
    return 0;
}

/*
* 字符设备驱动操作集
*/
static struct file_operations xxx_fops = {
    owner = THIS_MODULE,
    open = xxx_open,
    write = xxx_write,
};

/*
* platform 驱动的 probe 函数
* 驱动与设备匹配成功以后此函数就会执行
*/
static int xxx_probe(struct platform_device *dev)
{
    cdev_init(&xxxdev.cdev, &xxx_fops); /* 注册字符设备驱动 */
    /* 函数具体内容 */
    return 0;
}

static int xxx_remove(struct platform_device *dev)
{

    cdev_del(&xxxdev.cdev);/*  删除 cdev */
    /* 函数具体内容 */
    return 0;
}

/* 匹配列表 */
static const struct of_device_id xxx_of_match[] = {
{     .compatible = "xxx-gpio" },
    { /* Sentinel */ }
};

/*
* platform 平台驱动结构体
*/
static struct platform_driver xxx_driver = {
    driver = {
    name       = "xxx",
    of_match_table = xxx_of_match,
    },
    probe      = xxx_probe,
    remove     = xxx_remove,
};

/* 驱动模块加载 */
static int __init xxxdriver_init(void)
{
    return platform_driver_register(&xxx_driver);
}

/* 驱动模块卸载 */
static void __exit xxxdriver_exit(void)
{
    platform_driver_unregister(&xxx_driver);
}

module_init(xxxdriver_init);
module_exit(xxxdriver_exit);
MODULE_LICENSE("GPL");

4、不使用设备树的 platform 设备

platform 驱动已经准备好了,我们还需要 platform 设备,驱动写了一个文件了,设备还要写一个文件嘛?是的,不使用设备树是要将设备信息写为一个文件的,总要描述设备信息吧,还不能和驱动写在同一个文件中,只能单独写一个文件喽。

platform_device 这个结构体表示 platform 设备:

struct platform_device {
    const char  *name;
    int         id;
    bool        id_auto;
    struct device   dev;
    u32     num_resources;
    struct resource *resource;

    const struct platform_device_id *id_entry;
    char *driver_override; /* Driver name to force a match */

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

    /* arch specific additions */
    struct pdev_archdata    archdata;
};

name:设备名字,要和所使用的 platform 驱动的 name 字段相同,否则的话设备就无法匹配到对应的驱动;

num_resources 表示资源数量,一般为第 resource 资源的大小; 

resource 表示资源,也就是设备信息,比如外设寄存器等。

Linux 内核使用 struct resource 表示资源:

struct resource {
    resource_size_t   start;// 资源的起始和终止信息
    resource_size_t   end;
    const char        *name;// 资源名字
    unsigned long     flags;// 资源类型, 可选的资源类型都定义在了文件 include/linux/ioport.h 里面
    struct resource   *parent, *sibling, *child;
};

5、不使用设备树的 platform 设备API 函数

在以前不支持设备树的Linux 版本中,用户需要编写platform_device 变量来描述设备信息,然后使用 platform_device_register 函数将设备信息注册到 Linux 内核中:

int platform_device_register(struct platform_device *pdev) 

pdev:要注册的 platform 设备。 
返回值:负数,失败;0,成功。 

如果不再使用 platform 可以通过 platform_device_unregister 函数注销掉相应的 platform 设备:

void platform_device_unregister(struct platform_device *pdev) 
pdev:要注销的 platform 设备。 
返回值:无。 

platform 设备信息框架如下所示:

/* 寄存器地址定义*/
#define PERIPH1_REGISTER_BASE    (0X20000000) /* 外设 1 寄存器首地址 */
#define PERIPH2_REGISTER_BASE    (0X020E0068) /* 外设 2 寄存器首地址 */
#define REGISTER_LENGTH           4

/* 资源 */
static struct resource xxx_resources[] = {
    [0] = {
        start  = PERIPH1_REGISTER_BASE,
        end    = (PERIPH1_REGISTER_BASE + REGISTER_LENGTH - 1),
        flags  = IORESOURCE_MEM,
    },
    [1] = {
        start  = PERIPH2_REGISTER_BASE,
        end    = (PERIPH2_REGISTER_BASE + REGISTER_LENGTH - 1),
        flags  = IORESOURCE_MEM,
    },
};

/* platform 设备结构体 */
static struct platform_device xxxdevice = {
    name = "xxx-gpio",
    id = -1,
    num_resources = ARRAY_SIZE(xxx_resources),
    resource = xxx_resources,
};

/* 设备模块加载 */
static int __init xxxdevice_init(void)
{
    return platform_device_register(&xxxdevice);
}

/* 设备模块注销 */
static void __exit xxx_resourcesdevice_exit(void)
{
    platform_device_unregister(&xxxdevice);
}

module_init(xxxdevice_init);
module_exit(xxxdevice_exit);
MODULE_LICENSE("GPL");

6、设备树下的platform驱动

在没有设备树的 Linux 内核下,我们需要分别编写并注册 platform_device 和 platform_driver,分别代表设备和驱动。

在使用设备树的时候,设备的描述被放到了设备树中,因此 platform_device 就不需要我们去编写了,我们只需要实现 platform_driver 即可。

Linux 内核启动的时候会从设备树中读取设备信息,然后将其组织成platform_device 形式,至于设备树到 platform_device 的具体过程就不去详细的追究了。

1、在设备树中创建设备节点 

毫无疑问,肯定要先在设备树中创建设备节点来描述设备信息,重点是要设置好 compatible 属性的值,因为 platform 总线需要通过设备节点的 compatible 属性值来匹配驱动!

gpioled {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "atkalpha-gpioled";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_led>;
    led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
    status = "okay";
};

注意第 4 行的 compatible 属性值为“atkalpha-gpioled”,因此一会在编写 platform 驱动的时候 of_match_table 属性表中要有“atkalpha-gpioled”。 

2、编写 platform 驱动的时候要注意兼容属性 

上一章已经详细的讲解过了,在使用设备树的时候 platform 驱动会通过 of_match_table 来保存兼容性值,也就是表明此驱动兼容哪些设备。所以,of_match_table 将会尤为重要:

static const struct of_device_id leds_of_match[] = {
    { .compatible = "atkalpha-gpioled" },  /* 兼容属性 */
    { /* Sentinel */ } // 最后一个必须为空
};

MODULE_DEVICE_TABLE(of, leds_of_match);

static struct platform_driver leds_platform_driver = {
    driver = {
        name       = "imx6ul-led",
        of_match_table = leds_of_match,
    },
    .probe          = leds_probe, 
    .remove         = leds_remove, 
}; 
  • 在编写 of_device_id 的时候最后一个元素一定要为空!
  • 通过 MODULE_DEVICE_TABLE 声明一下 leds_of_match 这个设备匹配表。

3、编写 platform 驱动 

基于设备树的 platform 驱动和上一章无设备树的 platform 驱动基本一样,都是当驱动和设备匹配成功以后就会执行 probe 函数。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。