目录
一、Linux 设备驱动分层和分离
1.设备驱动的分层思想
在面向对象的程序设计中, 可以为某一类相似的事物定义一个
基类
, 而具体的事物可以
继承
基类的成员。 如果对于继承的这个事物而言, 其某函数的实现与基类一致, 那它就可以直接继承基类的成员; 若不一致,它可以重载之。
这种面向对象的设计思想极大地提高了代码的可重用能力, 是对现实世界事物间关系的一种良好呈现
。
Linux 内核完全由 C 语言和汇编语言写成, 但是却频繁用到了面向对象的设计思想。 在设备驱动方面,
为同类的设备设计了一个框架, 框架中的核心层则实现了该设备通用的一些功能
。 同样的, 如果具体的设备不想使用核心层的函数, 它可以重载之。
return_type core_funca(xxx_device * bottom_dev, param1_type param1, param1_type param2)
{
if(bottom_dev->funca)
return bottom_dev->funca(param1, param2);
/* 核心层通用的 funca 代码 */
...
}
上述 core_funca 的实现中,
会检查底层设备是否重载
了 funca(), 如果重载了, 就调用底层的代码, 否则直接使用通用层的。 这样做的好处是, 核心层的代码可以处理绝大多数该类设备的funca()对应的功能,只有少数特殊设备需要重新实现 funca()。
copyreturn_type core_funca(xxx_device * bottom_dev, param1_type param1, param1_type param2)
{
/*通用的步骤代码 A */
...
bottom_dev->funca_ops1();
/*通用的步骤代码 B */
...
bottom_dev->funca_ops2();
/*通用的步骤代码 C */
...
bottom_dev->funca_ops3();
}
上述代码假定为了实现 funca(), 对于同类设备而言, 操作流程一致, 都要经过通用代码 A、 底层 ops1、通用代码 B、 底层 ops2、 通用代码 C、 底层 ops3这几步, 分层设计明显带来的好处是, 对于通用代码 A、B、 C, 具体的底层驱动不需要再实现, 而仅仅只关心其底层的操作ops1、 ops2、 ops3。
在 Linux 的分层化设计 ,input 子系统负责管理所有跟输入有关的驱动,包括键盘、鼠标、触摸等,最底层的就是设备原始驱动,负责获取输入设备的原始值,获取到的输入事件上报给 input 核心层。 input 核心层会处理各种 IO 模型,并且提供 file_operations 操作集合。
分层的目的也是为了在不同的层处理不同的内容
。
2.主机驱动和外设驱动分离思想
在 Linux 设备驱动框架的设计中, 除了有分层设计实现以外, 还有分隔的思想。
假如现在有三个平台 A、 B 和 C,这三个平台上都有 MPU6050 ,I2C 接口的六轴传感器
每种平台下都有一个
主机驱动和设备驱动
,主机驱动是必须要的,不同的平台其
I2C 控制器不同
。但是设备驱动没必要每个平台都写一个,因为不管对于不同的SOC ,MPU6050是一样,都是通过 I2C 接口读写数据,只需要一个 MPU6050 的驱动程序即可。
所以,每个平台的 I2C 控制器都提供一个统一的接口(也叫做主机驱动),每个设备的话也只提供一个驱动程序(设备驱动),每个设备通过统一的 I2C接口驱动来访问,这样就可以大大简化驱动文件
以上就是驱动的分隔,也就是
将主机驱动和设备驱动分隔开来。
比如 I2C、 SPI会采用驱动分隔的方式来简化驱动的开发。在实际的驱动开发中,一般
I2C 主机控制器驱动
由半导体厂家编写好了,设备驱动一般由设备器件的厂家编写好了,开发人员只需要提供设备信息即可。
将设备信息从设备驱动中剥离开来,驱动使用标准方法去获取到设备信息
(比如从设备树中获
取到设备信息),
根据获取到的设备信息来初始化设备
。
驱动只负责驱动,设备只负责设备,总线法将两者进行匹配
。这就是
Linux 中的总线(bus)、驱动(driver)和设备(device)模型
,即驱动分离。
当向系统注册一个驱动的时,总线会在设备中查找与之匹配的设备,如果有,就将两者联系起来。同样的,当向系统中注册一个设备的时候,总线就会在驱动中查找与之匹配的设备,如果有,也联系起来。
Linux 内核中大量的驱动程序都采用总线、驱动和设备模式
。
二、Platform 平台驱动模型
在 Linux 2.6 以后的设备驱动模型中, 需关心
总线、 设备和驱动
这 3 个实体, 总线将设备和驱动绑定。在系统每注册一个设备的时候, 会寻找与之匹配的驱动;同样的,在系统每注册一个驱动的时候, 会寻找与之匹配的设备, 而匹配由总线完成。
Linux 设备和驱动通常都需要挂接在一种总线上, 对于本身依附于 PCI、 USB、 I2C、 SPI 等
的设备而言, 这自然不是问题。但是在嵌入式系统里面, 在 SoC 系统中集成的独立外设控制器、 挂接在 SoC内存空间的外设等却不依附于此类总线。 基于这一背景, Linux 发明了一种虚拟的总线, 称为
platform 总线
, 相应的设备称为
platform_device
, 驱动称为
platform_driver
。
通过这种方式实现了此类设备和驱动的分离, 增强设备驱动的可移植性
。平台总线模型也称为 platform 总线模型,是 Linux 内核虚拟出来的一条总线, 不是真实的导线。 平台总线模型就是把原来的驱动C文件给分成了俩个 C 文件,一个是 device.c, 一个是 driver.c 。把稳定不变的放在 driver.c 里面, 需要改变的就放在device.c 里面。
平台总线模型好处:
①可以提高代码的重用性
②减少重复性代码
平台总线模型将设备代码和驱动代码分离,
将与硬件设备相关的都放到 device.c 文件里面
,
驱动部分代码放到 driver.c 文件里面
。所以对于大量的同类设备而言,
只需要修改设备文件信息,驱动文件不用修改,可以提高代码的重用性,减少重复性代码。
1.platform 设备
在 platform 平台下用
platform_device结构体
表示
platform设备
,
如果内核支持设备树的话就不用使用 platform_device 来描述设备, 使用设备树去描述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;
};
name
:设备名字,
要和所使用的 platform 驱动的 name 字段相同
,
否则设备就无法匹配到对应的驱动
。比如对应的 platform 驱动的name字段为xxx-gpio,则此name字段也要设置为xxx-gpio。
id :用来区分如果设备名字相同的时,通过在后面添加一个数字来代表不同的设备
dev:内置的device结构体
num_resources
:资源数量,一般为resource 资源的大小(个数),
ARRAY_SIZE 来测量一个数组的元素个数
。
resource
:指向一个资源结构体数组,即设备信息,比如外设寄存器等。 Linux 内核使用 resource结构体表示资源
struct resource {
resource_size_t start;
resource_size_t end;
const char *name;
unsigned long flags;
struct resource *parent, *sibling, *child;
};
start 和 end 分别表示资源的起始和终止信息
,对于
内存类的资源
,表示内存起始和终止地址, name 表示资源名字, flags 表示资源类型,可选的资源类型定义在文件include/linux/ioport.h 里面
#define IORESOURCE_BITS 0x000000ff /* Bus-specific bits */
#define IORESOURCE_TYPE_BITS 0x00001f00 /* Resource type */
#define IORESOURCE_IO 0x00000100 /* PCI/ISA I/O ports */
#define IORESOURCE_MEM 0x00000200
#define IORESOURCE_REG 0x00000300 /* Register offsets */
#define IORESOURCE_IRQ 0x00000400
#define IORESOURCE_DMA 0x00000800
#define IORESOURCE_BUS 0x00001000
......
/* PCI control bits. Shares IORESOURCE_BITS with above PCI ROM. */
#define IORESOURCE_PCI_FIXED (1<<4) /* Do not move resource */
常用 flags 宏定义如下所示:
#define IORESOURCE_IO IO 的内存
#define IORESOURCE_MEM 表述一段物理内存
#define IORESOURCE_IRQ 表示中断
在
不支持设备树的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_device来描述设备,
使用设备树去描述platform_device。
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] =
{
14 .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");
MODULE_AUTHOR("mingfei");
2.platform 驱动
在 Linux 内核中, 用
platform_driver结构体
表示
platform驱动,
platform_driver 结构体定义指定名称的平台设备驱动注册函数和平台设备驱动注销函数,定义在include/linux/platform_device.h
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 函数就会执行, 一般驱动的提供者会编写
remove函数
:当 driver 和 device 任意一个 remove 的时, 就会执行该函数
driver:
device_driver 结构体变量, Linux 内核里面大量使用到了面向对象的思维,
device_driver相当于基类,提供了最基础的驱动框架。 plaform_driver 继承了这个基类,在此基础上又添加了一些特有的成员变量
id_table 表保存 id 信息。 这些 id 信息
存放着platformd 驱动所支持的驱动类型
。 id_table是个表(数组), 每个元素的类型为 platform_device_id, platform_device_id 结构体内容如下:
struct platform_device_id {
char name[PLATFORM_NAME_SIZE];
kernel_ulong_t driver_data;
};
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 */
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;
};
of_match_table表就是采用设备树时驱动使用的匹配表,也是数组,每个匹配项都为 of_device_id 结构体类型
,此结构体定义在文件 include/linux/mod_devicetable.h 中
struct of_device_id {
char name[32];
char type[32];
char compatible[128];
const void *data;
};
compatible: 在支持设备树的内核中, 就是通过
设备节点的compatible属性值
和
of_match_table
中每个项目的 compatible 成员变量进行比较
,
如果有相等的就表示设备和此驱动匹配成功
。
在编写 platform 驱动的时, 首先定义一个 platform_driver 结构体变量, 然后实现结构体中的各个
成员变量, 重点是
实现匹配方法以及 probe 函数
。 当驱动和设备匹配成功以后 probe 函数就会执行, 驱动程序具体功能的实现在 probe 函数里面编写。
驱动和设备匹配成功后,驱动会从
设备里面获得硬件资源
, 因为平台总线将驱动拆成了俩部分, 第一部分是 device.c, 另一部分是 driver.c。 匹配成功了后, driver.c 要从 device.c 中获得硬件资源, 那么 driver.c 就是在 probe 函数中获得的。
获得硬件资源有两种方法
①直接获取,不推荐
/*
* platform 驱动的 probe 函数
* 驱动与设备匹配成功以后此函数就会执行
*/
static int xxx_probe(struct platform_device *dev)
{
......
dev_init(&xxxdev.cdev, &xxx_fops); /* 注册字符设备驱动 */
/* 函数具体内容 */
return 0;
}
xxx_probe 函数里面有一个
形参 pdev
, 指向了 platform_device,
可以直接通过指针访问结构体 platform_device 的成员变量
。
pdev->resource[0].name; //获取资源下标为0的名字
②使用函数来获取资源
extern struct resource *platform_get_resource(struct platform_device *,unsigned int, unsigned int);
第一个参数:平台设备名
第二个参数:资源类型
第三个参数:索引号, 资源处在同类资源的哪个位置上, 同类资源是指 flags 一样
获得硬件资源之后, 就可以在 probe 函数中注册杂项/字符设备, 完善 file_operation 结构体, 并生成设备节点
定义并初始化好 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 驱动
返回值: 无
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");
MODULE_AUTHOR("mingfei");
paltform_driver 中的 device_driver 成员变量的
name 和 of_match_table
这两个属性。其中name 属性用于传统的驱动与设备匹配,检查驱动和设备的 name 字段是不是相同。of_match_table 属性就是用于设备树下的驱动与设备检查。对于一个完整的驱动程序,必须提供有设备树和无设备树两种匹配方法。
platform 驱动并不是独立于字符设备驱动、块设备驱动和网络设备驱动之外的其他种类的驱动。
platform 只是为了驱动的分离与分层而提出来的一种框架,其驱动的具体实现还是需要字符设备驱动、块设备驱动或网络设备驱动
。
3.platform 总线
platform设备和platform驱动,相当于把设备和驱动分离了,
需要 platform 总线进行配
,
platform 设备和 platform 驱动进行内核注册时, 都是注册到总线上
。
当内核中有驱动注册时, 总线就会在右侧的设备中查找, 是否有匹配的设备, 同样的, 当有设备注册到内核中时, 也会在总线左侧查找是否有匹配的驱动。
在 Linux 内核中使用 bus_type 结构体表示总线, 此结构体定义在文件 include/linux/device.h
struct bus_type {
const char *name; /* 总线名字 */
const char *dev_name;
struct device *dev_root;
struct device_attribute *dev_attrs;
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;
};
match 函数:
完成设备和驱动之间匹配的
,总线使用 match 函数来根据注册的设备来查找对应的驱动,或者根据注册的驱动来查找相应的设备,因此每一条总线都必须实现此函数。 match 函数有两个参数:
dev 和 drv
,这两个参数分别为 device 和 device_driver 类型,
即设备和驱动
。
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,
};
其中
platform_bus_type 结构体实例就表示 platform 总线
,
platform_match 匹配函数, 用来匹配注册到 platform 总线的设备和驱动
。
查看platform_match函数,如何匹配驱动和设备的,定义在文件 drivers/base/platform.c 中
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);
}
驱动和设备的匹配有四种方法。
①OF 类型的匹配
设备树采用的匹配方式,of_driver_match_device 函数定义在文件 include/linux/of_device.h 中。 device_driver 结构体(设备驱动)中有个名为
of_match_table的成员变量
,此成员变量保存着驱动的
compatible匹配表
,设备树中的每个设备节点的 compatible 属性会和 of_match_table 表中的所有成员比较,查看是否有相同的条目,如果有的话就表示设备和此驱动匹配,设备和驱动匹配成功以后 probe 函数就会执行。
②ACPI 匹配方式
③id_table 匹配
每个 platform_driver 结构体有一个 id_table成员变量,保存了很多 id 信息。这些 id 信息存放着这个 platformd 驱动所支持的驱动类型
④名字匹配
如果第三种匹配方式的 id_table 不存在的话就
直接比较驱动和设备的 name 字段,
如果相等的话就匹配成功。
对于支持设备树的 Linux 版本号,一般设备驱动为了兼容性都支持设备树和无设备树两种匹配方式
。即第一种匹配方式一般都会存在,第三种和第四种只要存在一种就可以,一般用的最多的还是第四种,直接比较驱动和设备的 name 字段。