Linux 设备驱动的软件架构总结学习笔记

2022-09-29

Linux 设备驱动的软件架构总结学习笔记。

Linux驱动的软件架构

基本思想:将驱动与设备分离,具体来说,驱动只管驱动,设备只管设备,总线负责匹配设备和驱动,而驱动以标准途径拿到板级信息。

设备指与项目有关的板级信息,项目用到的板级资源;驱动指对芯片、对硬件的操作。

Linux字符设备驱动需要编写file_operations成员函数,并负责处理阻塞、非阻塞、多路复用、SIGIO等。但我们面对一个真实硬件驱动时,并不像干所有事情,只需要实现我们关注的事情。这样就需要一个分层思想,因为所有输入设备都是一样的,可以提炼一个中间层,将软件进行分层:提炼一个input的核心层,把跟Linux接口以及整个一套input事件的汇报机制都在这里实现。

驱动与设备分离示意图:

Linux驱动分层:

在单片机中,我们可能有多个 SPI控制器,如YYY1,YYY2。如果对每个SPI控制器都单独设一个驱动API,那么会有:

spi_client_yyy1_work1()
spi_client_yyy2_work2()

如果有数个spi控制器,那么就会多个驱动API。类似地,其他主机控制器可能也有多个,这样驱动API数量就会爆炸性增长,而它们的操作是类似的,有没有什么办法解决这个问题? 可以将Linux设备驱动的主机控制器与外设驱动分离,主机控制器不用关心外设,而外设也不用关心主机,外设只是访问核心层的通用API进行数据传输,主机和外设间可以进行任意组合。

Linux设备驱动的主机与外设驱动分离:

platform_device 总线设备

Linux2.6后,驱动模型只用关心3个实体:总线、设备、驱动。

  • 总线 总线将设备和驱动绑定。系统每注册一个设备时,会寻找与之匹配的驱动;相反,系统中每注册一个驱动时,会寻找与之匹配的设备。 Linux并没有实体的总线,而是发明了虚拟总线,也称为platform总线。一个设备、驱动都挂接在一种总线上。
  • 设备 一个platform_device对象代表一个总线设备。platform_device并不是与字符设备、块设备、网络设备并列概念,而是Linux系统提供的一种附加手段,如SoC内部集成的I2C,RTC、LCD,Watchdog等控制器都归纳为platform_device,而这些本身就是字符设备。
  • 驱动 一个platform_driver对象代表一个总线驱动。

引入platform概念的好处: 1)使得设备被挂接在一个总线上,符合Linux2.6以后内核的设备模型。使配套的sysfs节点、设备电源管理都成为可能。

2)隔离BSP和驱动。BSP中调用platform设备和设备使用的资源、具体配置,而驱动中只需要通过通用API获取资源和数据。如此,板相关代码和驱动代码分离,驱动具有更好的可扩展性和跨平台性。

3)让一个驱动支持多个设备实例。

总线设备、驱动相关结构体

platform_device结构体:

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;
};

platform_driver 结构体:

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;
};

platform_driver 包含probe、remove、一个device_driver实例、电源管理函数suspend、resume。

电源管理可以用suspend, resume,但已经过时,更好的办法是实现device_driver中的dev_pm_ops结构体成员。

device_driver定义

struct device_driver {
    const char        *name;
    struct bus_type        *bus;

    struct module        *owner;
    const char        *mod_name;    /* used for built-in modules */

    bool suppress_bind_attrs;    /* disables bind/unbind via sysfs */
    enum probe_type probe_type;

    const struct of_device_id    *of_match_table;
    const struct acpi_device_id    *acpi_match_table;

    int (*probe) (struct device *dev);
    int (*remove) (struct device *dev);
    void (*shutdown) (struct device *dev);
    int (*suspend) (struct device *dev, pm_message_t state);
    int (*resume) (struct device *dev);
    const struct attribute_group **groups;

    const struct dev_pm_ops *pm;

    struct driver_private *p;
};

跟platform_driver 地位对等的i2c_driver、spi_driver、usb_driver、pci_driver中都包含了device_driver实例成员。描述了各种xxx_driver(xxx代表总线名)在驱动意义上的一些共性。

系统为platform总线定义了一个bus_type 实例platform_bus_type,位于drivers/base/platform.c。

struct bus_type platform_bus_type = {
    .name          = "platform",
    .dev_groups    = platform_dev_groups,
    .match         = platform_match,
    .uevent        = platform_uevent,
    .pm            = &platform_dev_pm_ops,
};
EXPORT_SYMBOL_GPL(platform_bus_type);

重点是match(),该函数确定了platform_device和platform_driver之间如何匹配。

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) // 匹配platform_device的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)) // 基于ACPI风格匹配
        return 1;

    /* Then try to match against the id table */
    if (pdrv->id_table) // 匹配ID表
        return platform_match_id(pdrv->id_table, pdev) != NULL;

    /* fall-back to driver name match */
    return (strcmp(pdev->name, drv->name) == 0); // 匹配platform_device设备名和驱动名
}

platform_device(总线设备)和platform_driver(总线驱动)匹配有5种方式,按顺序排列: 1)匹配platform_device的driver_override和驱动名;(不推荐) 2)基于设备树风格的匹配;(推荐) 3)基于ACPI风格匹配; 4)匹配ID表,即platform_device设备名是否出现在platform_driver的ID表内;(推荐) 5)匹配platform_device设备名和驱动名;(推荐)

如何添加一个platform_device? Linux 2.6 ARM,platform_device定义为一个数组,通过platform_add_devices()注册总线设备。它内部调用platform_device_register() 注册单个设备。

int platform_add_devices(struct platform_device **devs, int num);

Linux 3.x后,根据设备树内容自动展开platform_device,而不再推荐通过手动编码填写platform_device和注册。

platform 设备资源和数据

platform 设备资源

platform_device 定义中,由resource结构体描述资源。resource定义如下:

struct resource {
    resource_size_t start; // 资源开始值
    resource_size_t end;   // 资源结束值
    const char *name;
    unsigned long flags;   // 资源类型, 值可为 IORESOURCE_IO/IORESOURCE_MEM/IORESOURCE_IRQ/IORESOURCE_DMA
    unsigned long desc;
    struct resource *parent, *sibling, *child;
};

通常关注start、end、flags 这3个字段,分别表示资源开始值、资源结束值、资源类型。start、end含义随flags不同而不同,例如:

  • flags为IORESOURCE_MEM,start、end表示该设备占据的内存起始地址、结束地址;
  • flags为IORESOURCE_IRQ,start、end表示该设备使用的中断号开始值和结束值(左闭右闭);

多份同类型资源,可停用多个start、end、flags。

如何获取设备资源?可通过platform_get_resource(),获得该dev中某种类型(type)的第num个资源。num从0开始计数。

#include <linux/platform_device.h>

struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num);

对于IRQ类型资源,还有一个变体platform_get_irq()。

#include <linux/platform_device.h>

int platform_get_irq(struct platform_device *dev, unsigned int num);

例,在arch/arm/mach-at91/board-sam9261ek.c板文件中,为DM9000网卡定义了如下resource:

static struct resource dm9000_resource[] = {
    [0] = {
        .start = AT91_CHIPSELECT_2,
        .end   = AT91_CHIPSELECT_2 + 3,
        .flags = IORESOURCE_MEM
    },
    [1] = {
        .start = AT91_CHIPSELECT_2 + 0x44,
        .end   = AT91_CHIPSELECT_2 + 0xFF,
        .flags = IORESOURCE_MEM
    },
    [2] = {
        .flags = IORESOURCE_IRQ | IORESOURCE_IRQLOWEDGE | IORESOURCE_IRQ_HIGHEDGE,
    },
};

在DM9000网卡驱动中,通过下面办法获得这3份资源:

db->addr_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
db->addr_res = platform_get_resource(pdev, IORESOURCE_MEM, 1);
db->addr_res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);

platform_data 设备数据

设备除了在BSP中定义资源(resource)外,还能附加一些数据信息,因为对设备的硬件描述除了中断、内存等标准资源外,可能还有一些配置信息,而这些配置信息依赖于板,不适宜放在设备驱动上。因此,platform提供platform_data支持。

platform_data 由每个驱动自定义。定义时,可以只存放一个结构体指针;解析时,根据结构体类型来解析。 例如,对于DM9000网卡,为platform_data定义一个dm9000_plat_data结构体,定义完后,可以将MAC地址、总线宽度、板上有无EEPROM信息等放入platform_data中。

static struct dm9000_plat_data dm9000_platdata = {
    .flags = DM9000_PLATF_16BITONLY | DM9000_PLATF_NO_EEPROM,
};

static struct platform_device dm9000_device = {
    .name = "dm9000",
    .id = 0,
    .num_resources = ARRAY_SIZE(dm9000_resource),
    .resource = dm9000_resource,
    .dev = {
        .platform_data = &dm9000_platdata, // 自定义设备数据
    }
};

如何获取设备数据? 可使用dev_get_platdata()。例如,在DM9000网卡驱动的probe()中,可这样拿到platform_data:

struct dm9000_plat_data *pdata = dev_get_platdata(&pdev->dev);

如何基于总线设备、驱动写程序?

以100ask使用2个GPIO驱动2个led灯的简单例子进行说明。主要分为两类工作:定义设备和驱动。

总线设备

总线设备指定项目用到的板级资源,因此放到与板配置相关目录board_A_led.c中,主要任务是定义并注册platform_device对象。

注意:现在通常不手动定义总线设备,而是通过设备树(dts)文件来定义设备使用的资源,因此实际项目中可能看不到这部分代码。

1)定义资源对象(resource) 定义2个GPIO口,为IORESOURCE_IRQ类资源。

/* 定义资源 */
static struct resource resources[] = {
    {
        .start = GROUP_PIN(3, 1), /* GPIO3_1 */
        .flags = IORESOURCE_IRQ,  /* flags 表示哪一类资源 */
    },
    {
        .start = GROUP_PIN(5, 8), /* GPIO5_8 */
        .flags = IORESOURCE_IRQ,
    },
};

2)定义设备对象(platform_device)

/* 把定义的资源添加到总线设备中
*
* 还需要定义一个与platform_device匹配的platform_dirver
*/
static struct platform_device board_A_led_dev = {
    .name = "100ask_led",                   /*总线设备名称  */
    .num_resources = ARRAY_SIZE(resources), /* 资源个数 */
    .resource = resources,
};

3)注册设备 通过board_A_led模块加载函数,注册总线设备(platform_device对象)。

static int led_dev_init(void)
{
    int err;
    /* register device */
    err = platform_device_register(&board_A_led_dev);
    return 0;
}

4)反注册设备 利用board_A_led模块卸载函数,反注册总线设备(platform_device对象)。

static void led_dev_exit(void)
{
    int err;
    /* unregister device */
    err = platform_device_unregister(&board_A_led_dev);
}

5)其他工作:指定入口函数、出口函数

module_init(led_dev_init);
module_exit(led_dev_exit);

MODULE_LICENSE("GPL");

其中,pdev为platform_device指针。

总线驱动

与板子无关,与具体的芯片和用到的外设相关,因此放到与芯片相关的chip_demo_gpio.c。主要任务是定义并注册platform_driver对象。

1)定义总线驱动结对象 需要与总线设备配对,即名称相同。

/*
* 总线驱动
* 需要与platform_device(总线设备)配对
*/
static struct platform_driver chip_demo_gpio_drv = {
    .driver = {
        .name = "100ask_led", /* 总线驱动名字, 要与总线设备名字配对 */
    },
    .probe    = chip_demo_gpio_led_probe,  /* 当发现配对的platform_device时, 就会调用probe函数 */
    .remove    = chip_demo_gpio_led_remove, /* 当把platform device去掉时, 就会掉调用remove函数 */
};

2)在probe函数中,获取资源,并创建逻辑设备(device_create)

static int chip_demo_gpio_led_probe(struct platform_device *dev)
{
    int i = 0;
    struct resource *res;
    
    while (1) { /* 因为用到了多个引脚资源, 所有用while循环获取 */
        res = platform_get_resource(dev, IORESOURCE_IRQ, i++);
        if (!res)
            break;
        /* save pin */
        g_ledpins[g_ledcnt] = res->start;

        /* device_create */
        led_device_create(g_ledcnt);
        g_ledcnt++
    }
    
    return 0;
}

led_device_create和led_device_destroy是上层leddrv.c中定义的,分别用于创建和销毁逻辑设备的函数。

为什么要在上层leddrv.c中定义? 因为调用device_create需要led_class(设备逻辑类 class_create创建),是static类型,只在leddrv.c中可见。

void led_device_create(int minor)
{
    device_create(led_class, NULL, MKDEV(major, minor), NULL, "100ask_led%d", minor);
}

void led_device_destroy(int minor)
{
    device_destroy(led_class, MKDEV(major, minor));
}
EXOPRT_SYMBOL(led_device_create);
EXOPRT_SYMBOL(led_device_destroy);

3)在remove函数中,销毁逻辑设备(device_destroy)

static int chip_demo_gpio_led_remove(struct platform_device *pdev)
{
    int i = 0;
    /* device_destroy */
    for (i = 0; i < g_ledcnt; i++) {
        led_device_destroy(i);
    }
    g_ledcnt = 0;

    return 0;
}

4)注册总线驱动 利用chip_demo_gpio模块的加载函数chip_demo_gpio_drv_init,来注册总线驱动。

static int chip_demo_gpio_drv_init(void)
{
    int err;
    err = platform_driver_register(&chip_demo_gpio_drv);   /* 注册总线驱动 */
    register_led_operation(&board_demo_led_opr);
    return 0;
}

5)反注册总线驱动 利用chip_demo_gpio模块的卸载函数chip_demo_gpio_drv_exit,来反注册总线驱动。

static void chip_demo_gpio_drv_exit(void)
{
    int err;
    err = platform_driver_unregister(&chip_demo_gpio_drv); /* 反注册总线驱动 */
}

6)登记chip_demo_gpio模块的入口函数、出口函数

module_init(chip_demo_gpio_drv_init);
module_exit(chip_demo_gpio_drv_exit);
MODULE_LICENSE("GPL");
完整源码示例参见:[04_led_drv_template_bus_dev_drv gitee](https://gitee.com/fortunely/imx6study/tree/master/source/02_led_drv/04_led_drv_template_bus_dev_drv)

附:与总线设备、驱动有关的常用函数

注册/反注册platform_device

platform_device_register 注册总线设备; platform_device_unregister 反注册总线设备;

#include <linux/platform_device.h>

int platform_device_register(struct platform_device *);
void platform_device_unregister(struct platform_device *);

注册/反注册platform_driver

platform_driver_register 注册总线驱动; platform_driver_unregister 反注册总线驱动。

#include <linux/platform_device.h>

#define platform_driver_register(drv) \
    __platform_driver_register(drv, THIS_MODULE)

int __platform_driver_register(struct platform_driver *,
                    struct module *);
void platform_driver_unregister(struct platform_driver *);

注册多个device

platform_add_devices 一次注册多个device

int platform_add_devices(struct platform_device **, int);

获得设备资源

platform_get_resource 获得该dev中某类型(type)的第num个资源。num从0开始计数。

#include <linux/platform_device.h>

struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num);

platform_get_irq 获得该dev中某类型(type)的第num个中断。num从0开始计数。

#include <linux/platform_device.h>

int platform_get_irq(struct platform_device *dev, unsigned int num);

platform_get_resource_byname 通过名字获得该dev的某类型(type)资源。

#include <linux/platform_device.h>

struct resource *platform_get_resource_byname(struct platform_device *dev,
                          unsigned int type,
                          const char *name)

通过名字(name)返回该dev的中断号。

#include <linux/platform_device.h>

int platform_get_irq_byname(struct platform_device *dev, const char *name);

参考

[1]宋宝华. Linux设备驱动开发详解[M]. 人民邮电出版社, 2010.