Linux platform驱动模型

时间:2022-07-20 17:53:36

/************************************************************************************

*本文为个人学习记录,如有错误,欢迎指正。

*本文参考资料: 

*        http://www.cnblogs.com/xiaojiang1025/p/6367061.html

*        http://www.cnblogs.com/xiaojiang1025/p/6367910.html

*        http://www.cnblogs.com/xiaojiang1025/p/6369065.html

*        https://www.cnblogs.com/lifexy/p/7569371.html

*        https://www.cnblogs.com/biaohc/p/6667529.html

************************************************************************************/

1. platform总线

1.1 platform总线简介

在Linux2.6以后的设备驱动模型中,需关心总线,设备和驱动这三种实体,总线将设备和驱动绑定。Linux内核中的总线主要负责管理挂接在该总线下的设备与驱动,将设备信息与驱动程序分类管理,提高驱动程序的可移植性。在系统每注册一个设备的时候,会寻找与之匹配的驱动;相同地,在系统每注册一个驱动的时候,会寻找与之匹配的设备,而匹配由总线完成。 
对于依附在USB、PCI、I2C、SPI等物理总线来 这些都不是问题。但是在嵌入式系统里面,在SoC中集成的独立外设控制器,挂接在SoC内存空间的外设等却不依附在此类总线。基于这一背景,Linux发明了一种总线,称为platform。相对于USB、PCI、I2C、SPI等物理总线来说,platform总线是一种虚拟、抽象出来的总线,实际中并不存在这样的总线。 

1.2 platform总线核心代码

(1)platform总线相关代码:/kernel/driver/base/platform.c 文件;
(2)相关数据结构体定义:/kernel/include/linux/platform_device.h 文件中。

2. platform总线机制

platform总线在sysfs下的目录为/sys/bus/platform,该目录下有两个子目录和相关的platform属性文件;/platform/devices目录下存放的是platform总线下的所有设备,/platform/drivers目录下存放的是platform总线下的所有驱动程序。

Linux platform驱动模型

platform总线的驱动与设备的管理与匹配机制如下图所示。

 Linux platform驱动模型

2.1 platform_device对象

在设备树出现之前,设备信息只能使用C语言的方式进行编写,在Linux3.0之后,设备信息就开始同时支持两种编写方式:设备树、C语言。对于ARM平台,使用设备树封装设备信息是将来的趋势,但是由于历史原因,当下的内核中这两种种方式并存。

1)设备树

使用设备树,手动将设备信息写到设备树中之后,内核就可以自动从设备树中提取相应的设备信息并将其封装成相应的platform_device对象,并注册到相应的总线中。从而,我们就不需要对设备信息再进行编码。

2)C语言

使用C语言,我们需要将使用内核提供的结构将设备信息进行手动封装,这种封装又分为两种形式,一种是使用平台文件(静态),将整个板子的所有设备都写在一个文件中并编译进内核。另一种是使用模块(动态),将我们需要的设备信息编译成模块在insmod进内核。

本文主要讨论C语言的方式实现设备信息的填充。

(1)struct platform_device

Linux内核中,使用struct platform_device来描述一个注册在platform总线上的设备。对设备信息进行编码,其实就是创建一个struct platform_device对象,platform_device和其他设备一样,都是device的子类。

struct platform_device 
{
const char * name;//设备的名称,是设备和驱动match的方法之一 int id; //表示这个platform_device对象表征了几个设备,当多个设备有共用资源的时候(MFD),里面填充相应的设备数量,如果只是一个,填-1 struct device dev; //父类对象 u32 num_resources;//资源的数量,即resource数组中元素的个数,我们用ARRAY_SIZE()宏来确定数组的大小 struct resource * resource;//资源指针,如果是多个资源就是struct resource[]数组名 const struct platform_device_id *id_entry;//设备和驱动match的方法之一 /* arch specific additions */ struct pdev_archdata archdata; };

struct platform_device的父类struct device。我们通常关心里面的platform_data和release,前者是用来存储私有设备信息的,后者是供当这个设备的最后引用被删除时被内核回调,注意和rmmod没关系。

Linux platform驱动模型Linux platform驱动模型
struct device 
{
    struct device          *parent;
    struct device_private  *p;
    struct kobject kobj;
    const char          *init_name;  /* initial name of the device */
    struct device_type  *type;
    struct mutex        mutex;       /* mutex to synchronize calls to its driver.*/

    struct bus_type    *bus;         /* type of bus device is on */
    struct device_driver *driver;    /* which driver has allocated this device */
    void   *platform_data;           /* Platform specific data, device core doesn't touch it */
    struct dev_pm_info    power;

#ifdef CONFIG_NUMA
    int        numa_node;    /* NUMA node this device is close to */
#endif
    u64        *dma_mask;    /* dma mask (if dma'able device) */
    u64        coherent_dma_mask;
    struct device_dma_parameters *dma_parms;
    struct list_head    dma_pools;    /* dma pools (if dma'ble) */
    struct dma_coherent_mem    *dma_mem; /* internal for coherent mem override */
    struct dev_archdata    archdata;
#ifdef CONFIG_OF
    struct device_node    *of_node;
#endif

    dev_t    devt;    /* dev_t, creates the sysfs "dev" */
    spinlock_t        devres_lock;
    struct list_head    devres_head;
    struct klist_node    knode_class;
    struct class        *class;
    const struct attribute_group **groups;    /* optional groups */
    void    (*release)(struct device *dev);
};

struct device_private {
    struct klist klist_children;
    struct klist_node knode_parent;
    struct klist_node knode_driver;
    struct klist_node knode_bus;
    void *driver_data;
    struct device *device;
};
struct device

下面是一个platform_device的实例。

static struct platform_device demo_device = 
{ .name
= "demo", .id = -1, .dev =
  { .platform_data
= &priv, .release = dev_release, }, .num_resources = ARRAY_SIZE(res), .resource = res, };

(2)struct resource

Linux内核中,使用struct resource来描述一个设备所用到的资源(地址资源或中断资源)。

struct resource 
{
    resource_size_t start;//表示资源开始的位置,如果是IO地址资源,就是起始物理地址;如果是中断资源,就是中断号
    resource_size_t end;  //表示资源结束的位置,如果是IO地址地址,就是映射的最后一个物理地址;如果是中断资源,就不用填
    const char *name;     //资源的名字
    unsigned long flags;  //资源类型
    struct resource *parent, *sibling, *child;//用于组成管理资源的链表
};

Linux内核中定义了相关的宏来表示resource的类型。

#define IORESOURCE_TYPE_BITS  0x00001f00    
#define IORESOURCE_IO         0x00000100
#define IORESOURCE_MEM        0x00000200
#define IORESOURCE_IRQ        0x00000400
#define IORESOURCE_DMA        0x00000800
#define IORESOURCE_BUS        0x00001000

Linux内核中定义了相关的宏来帮助我们快速的创建resource对象。(一些版本较低的内核可能不支持resource的相关宏)

#define DEFINE_RES_IO(_start, _size)   
#define DEFINE_RES_MEM(_start, _size)   
#define DEFINE_RES_IRQ(_irq)  
#define DEFINE_RES_DMA(_dma) 

一个设备一般会用到多种不同类型的资源,因此一般用一个struct  resource数组来描述一个设备所需的资源。下面是一个resource的实例。该实例用到了resource的两种写法,一般推荐使用内核中的宏来实现。

struct resource res[] = 
{ [
0] =
  { .start
= 0x10000000, .end = 0x20000000-1, .flags = IORESOURCE_MEM }, [1] = DEFINE_RES_MEM(0x20000000, 1024), [2] =
  { .start
= 10, //中断号 .flags = IORESOURCE_IRQ|IRQF_TRIGGER_RISING//include/linux/interrupt.h }, [3] = DEFINE_RES_IRQ(11), };

(3)设备对象的注册与注销

/*
 *注册:把指定设备添加到内核中平台总线的设备列表,等待匹配,匹配成功则回调驱动中probe;
 */
int platform_device_register(struct platform_device *pdev);
/*
 *注销:把指定设备从设备列表中删除,如果驱动已匹配则回调驱动方法和设备信息中的release;
 */
void platform_device_unregister(struct platform_device *pdev);

通常,我们会将platform_device_register写在模块加载的函数中,将platform_device_unregister写在模块卸载函数中。我们可以模仿内核的宏写一个注册、注销的快捷方式。

#define module_platform_device (xxx)       \
static int __init xxx##_init(void)         \
{                           \
    return platform_device_register(&xxx); \
}                           \
static void __exit xxx##_exit(void)        \
{                           \
    platform_device_unregister(&xxx);      \
}                           \
module_init(xxx##_init);                   \
module_exit(xxx##_exit);

2.2 platform_driver对象

(1)struct platform_driver

Linux内核中,使用struct platform_driver来描述一个注册在platform总线上的驱动程序。对驱动信息进行编码,其实就是创建一个struct platform_driver对象,platform_driver是device_driver的子类。

struct platform_driver 
{
    int (*probe)(struct platform_device *);   //探测函数,如果驱动匹配到了目标设备,总线会自动回调probe函数,由驱动工程师实现(必须实现)
    int (*remove)(struct platform_device *);  //释放函数,如果匹配到的设备从总线移除了,总线会自动回调remove函数,由驱动工程师实现(必须实现)
    void (*shutdown)(struct platform_device *);
    int (*suspend)(struct platform_device *, pm_message_t state);
    int (*resume)(struct platform_device *);
    struct device_driver driver;              //platform_driver的父类
    const struct platform_device_id *id_table;//设备信息,设备与驱动match的方法之一
};

platform_driver里面有些内容需要在其父类driver中实现。

struct device_driver {
    const char        *name;     //驱动名称,设备和驱动的match方法之一
    struct bus_type   *bus;      //总线类型,这个成员由内核填充
    struct module     *owner;    //owner,通常就写THIS_MODULE
    const char        *mod_name; /* used for built-in modules */
    bool suppress_bind_attrs;    /* disables bind/unbind via sysfs */
#if defined(CONFIG_OF)
    const struct of_device_id    *of_match_table;  //设备树表示的驱动信息,设备和驱动match的方法之一
#endif
    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的实例。

static struct platform_driver drv = 
{
    .probe  = demo_probe,
    .remove = demo_remove,

    .driver = 
    {
        .name = "demo",
    },
};

(2)probe与remove函数

probe即探测函数,如果驱动匹配到了目标设备,总线会自动回调probe函数,并把匹配到的设备信息platform_device对象传入。probe函数的主要工作如下:

  1)申请资源;

  2)初始化;

  3)提供接口(cdev/sysfs/proc)。

显然,remove主要完成与probe相反的操作,这两个接口都是我们必须实现的。remove函数的主要工作如下: 

  1)释放资源;

  2)释放接口(cdev/sysfs/proc)。

在probe的工作中,最常见的就是提取设备信息,虽然总线会将设备信息封装成一个platform_device对象并传入probe函数,我们可以很容易的得到关于这个设备的所有信息,但是更好的方法就是直接使用内核API中相关的函数。

/**
 * platform_get_resource - 获取资源
 * @dev: 平台总线设备
 * @type:资源类型,include/linux/ioport.h中有定义
 * @num: 资源索引,即第几个此类型的资源,从0开始
 */
struct resource *platform_get_resource(struct platform_device *dev,unsigned int type, unsigned int num)

注意,通过内核API(eg,上下这两个API)获取的resource如果是中断,那么只能是软中断号,而不是芯片手册/C语言设备信息/设备树设备信息中的硬中断号,但是此时获取的resource的flag是可以正确的反映该中断的触发方式的,只需要 " flag & IRQF_TRIGGER_MASK " 即可获取该中断的触发方式。

/**
 * platform_get_irq - 获取一个设备的中断号
 * @dev: 平台总线设备
 * @num: 中断号索引,即想要获取的第几个中断号,从0开始
 */
int platform_get_irq(struct platform_device *dev, unsigned int num)
/**
 * dev_get_platdata - 获取私有数据
 */
static inline void *dev_get_platdata(const struct device *dev)
{       
        return dev->platform_data;
}

(3)驱动对象的注册与注销

/*
 * platform_driver_register - 注册
 */
int platform_driver_register(struct platform_driver *drv);

/*
 * platform_driver_unregister - 注销
 */
int platform_driver_unregister(struct platform_driver *drv);

通常,我们会将platform_driver_register写在模块加载的函数中,将platform_driver_unregister写在模块卸载函数中。我们可以模仿内核的宏写一个注册、注销的快捷方式。

#define module_platform_driver (xxx)       \
static int __init xxx##_init(void)         \
{                           \
    return platform_driver_register(&xxx); \
}                           \
static void __exit xxx##_exit(void)        \
{                           \
    platform_driver_unregister(&xxx);      \
}                           \
module_init(xxx##_init);                   \
module_exit(xxx##_exit);

2.3 platform总线

(1)struct bus_type

platform总线由内核工程师注册好的,驱动工程师无需修改platform总线的相关代码,只需调用其接口即可。了解platform总线的数据结构,有助于驱动工程师对platform总线的总体理解与应用。

struct bus_type platform_bus_type = 
{
    .name        = "platform",           //总线名称
    .dev_attrs    = platform_dev_attrs,  //总线属性
    .match        = platform_match,      //总线下的设备与驱动的匹配函数
    .uevent        = platform_uevent,
    .pm        = &platform_dev_pm_ops,   //电源管理函数
};

(2)device与driver的匹配(match)

从platform_match函数中可知,它支持三种匹配方式,这三种匹配方式的优先级为:of_match_table > id_table > name。

1)of_match_table 

platform_driver->device_driver->of_device_id(of_match_table )包含了该驱动所支持的设备(使用设备树编码的设备信息)。of_match_table 与设备树进行匹配,查看设备树中是否有相应的设备。

struct of_device_id
{
    char    name[32];       //设备名
    char    type[32];       //设备类型
    char    compatible[128];//用于与设备树compatible属性值匹配的字符串
#ifdef __KERNEL__
    void    *data;
#else
    kernel_ulong_t data;    //私有数据
#endif
};

对于一个驱动匹配多个设备的情况,使用struct of_device_id tbl[]来表示。

struct of_device_id of_tbl[] = 
{
    {.compatible = "x210,demo0",},
    {.compatible = "x210,demo1",},
    {},  //表示结束,必须要有
};

2)id_table 

将设备中的platform_device -> platform_device_id与驱动中的platform_driver -> platform_device_id进行比对,看该设备与驱动是否匹配。

struct platform_device_id 
{
    char name[PLATFORM_NAME_SIZE];  //设备名
    kernel_ulong_t driver_data
            __attribute__((aligned(sizeof(kernel_ulong_t))));
};

对于一个驱动匹配多个设备的情况,使用struct platform_device_id tbl[]来表示。

static struct platform_device_id tbl[] = 
{
    {"demo0"},
    {"demo1"},
    {},//表示结束,必须要有
};

3)name

如果platform_driver和C语言编码的platform_device是一 一匹配,则将设备中的platform_device -> name与驱动中的platform_driver -> name进行比对,看该设备与驱动是否匹配。 

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);
 
    /* Attempt an OF style match first */
    if (of_driver_match_device(dev, drv))                      //of_match_table方式
      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; //platform_device_id方式
  /* fall-back to driver name match */ 
   return (strcmp(pdev->name, drv->name) == 0);               //platform_device->name方式
}

3. platform总线应用实例

驱动程序实例(一):LED设备驱动程序( platform + cdev)

驱动程序实例(二):LED设备驱动程序( platform + /sys接口)