• 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


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

    注册/反注册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.

  • 相关阅读:
    dp_Pku1887
    他们实际上控制的定义很easy5/12
    一:redis 的string类型
    我已经写了DAL层的代码生成器
    《Java并发编程实战》第十二章 测试并发程序 读书笔记
    [Pug] Template Engine -- Jade/ Pug
    [Angular] Alternative Themes
    [Angular] Separating Structural Styles From Theme Styles
    [React] Setup 'beforeunload' listener
    [Node] Convert CommonJS Requires to ES6 Imports
  • 原文地址:https://www.cnblogs.com/fortunely/p/16466453.html
Copyright © 2020-2023  润新知