在编写设备驱动代码时,若将设备信息如寄存器地址都写在驱动代码中,当换一个硬件平台时就需要重新修改驱动代码进行适配。是否有办法将设备信息从驱动中剥离出来,并让驱动通过某种标准方法获取设备相关的平台信息?
Linux总线、设备和驱动模型:驱动只负责驱动,设备只管设备,总线负责匹配设备和驱动,驱动可以标准方式获取板级信息。如下:
Linux设备驱动模型的分层设计:
所有的设备的file_operations
结构体的实现、I/O模型处理等部分的代码实现几乎都一样,可以将这些代码提炼出来作为一个中间层(核心层),开发人员只需完成一些简单的工作即可,如输入事件的获取和报告。
Linux设备驱动模型的分隔设计:
假设通过SPI总线访问某外设,CPU的名字叫xxx1,SPI外设的名字叫yyy1。当访问yyy1时,需通过操作CPU xxx1上的SPI控制器实现。当操作CPU xxx2上的SPI控制器访问外设xxx1时,又需要再实现一套驱动代码。即任何一个SPI外设的驱动代码都与CPU相关,如果又N个不同的YYY外设在M个不同的CPU XXX上运行,就需要M*N份代码。
对主机控制器驱动和外设驱动进行分隔后,主动控制器驱动就不需要关心外设,而外设也不需要关心主机,外设与主机之间通过核心层的通用API进行数据传输,并且可任意组合。
platform总线框架
platform总线的实现主要在以下几个文件中完成:
drivers/base/platform.c
:实现了总线、对外接口的操作include/linux/platform_device.h
:定义了platform
设备结构体,声明一系列对外接口
总线
内核专门定义了一个总线类型的全局变量platform_bus_type
,在该变量中定义了设备和驱动的匹配规则、电源管理、设备属性等操作。
struct bus_type platform_bus_type = {
.name = "platform",
.dev_attrs = platform_dev_attrs, /* 设备属性结构体 */
.match = platform_match, /* 设备与驱动匹配函数 */
.uevent = platform_uevent, /* */
.pm = &platform_dev_pm_ops, /* 电源管理结构体 */
};
主要说下platform_match()
函数:
从这里可以看出,设备和驱动的匹配方式有三种:
- 设备树匹配
- id_table匹配
- 驱动和设备的名字匹配
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))
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);
}
在完成总线的类型定义后,还需将该总线注册到系统中,下面这个函数应该是在内核启动时就会被调用,从而完成总线的初始化工作。
int __init platform_bus_init(void)
{
int error;
early_platform_cleanup();
error = device_register(&platform_bus);
if (error)
return error;
error = bus_register(&platform_bus_type);
if (error)
device_unregister(&platform_bus);
return error;
}
驱动
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;
};
其中:
probe
接口会在驱动和设备匹配成功后被调用,主要完成设备号申请、设备资源获取、各类文件节点的创建等。remove
接口则会在卸载驱动时被调用,完成的工作刚好与probe
相反。driver
字段主要关心设备树表of_match_table
,虽然该字段中也定义了probe
、remove
等接口,但这些接口最终都会调用驱动platform_driver
中的接口id_table
字段用于设备和驱动的匹配。
在完成上述驱动结构体的定义后,可通过这两个函数向总线注册/卸载驱动:
/* 向总线注册驱动 */
int platform_driver_register(struct platform_driver *drv)
{
drv->driver.bus = &platform_bus_type;
if (drv->probe)
drv->driver.probe = platform_drv_probe;
if (drv->remove)
drv->driver.remove = platform_drv_remove;
if (drv->shutdown)
drv->driver.shutdown = platform_drv_shutdown;
return driver_register(&drv->driver);
}
/* 从总线卸载驱动 */
void platform_driver_unregister(struct platform_driver *drv)
{
driver_unregister(&drv->driver);
}
设备
向总线注册设备时,可以使用设备树方式,也可以通过硬编码的方式。
硬编码的方式是指在内核代码中实现设备信息的注册,如下所示。
设备结构体:
struct platform_device {
const char * name; /* 设备名称,可用于设备和驱动的匹配 */
int id;
struct device dev;
u32 num_resources; /* 指示resource字段的数量 */
struct resource * resource; /* 设备的资源信息 */
const struct platform_device_id *id_entry; /* 用于id_table匹配方式 */
/* MFD cell pointer */
struct mfd_cell *mfd_cell;
/* arch specific additions */
struct pdev_archdata archdata;
};
开发者需要实现struct resource
结构体,并完成struct platform_device
结构体的定义。
然后,可通过这两个函数完成设备的注册和卸载:
int platform_device_register(struct platform_device *);
void platform_device_unregister(struct platform_device *);
platform
总线的整体框架如下:
MISC
驱动
misc
意思是混合、杂项的,也就是说MISC
驱动专门负责一些杂项工作,像有些外设无法进行分类时可以使用MISC
驱动框架。实际上,MISC
驱动框架完全基于platform
总线框架实现,只是将一些只有在驱动和设备匹配成功后probe
函数中完成的设备号申请、文件节点创建等工作给提前完成了。
MISC
驱动框架提前申请了主设备号10
和256个次设备号,任何使用该框架的设备其主设备号都是10
,只需要通过MISC
框架提供的接口申请次设备号即可。
MISC
框架就不详细介绍了,直接根据下图学习即可:
I2C驱动框架
Linux的I2C驱动框架分为三层:
- 主机适配器层:基于
platform
总线,完成I2C
控制器的驱动,比如I2C总线时序、启动、停止、应答等。 - API接口层:为设备驱动层提供统一的接口以完成数据的收发,使得具体设备和
I2C
控制器分离。 - 设备驱动层:基于
i2c_bus_type
总线实现具体设备的驱动,如MPU6050、EEPROM等。
I2C驱动框架如下:
SPI驱动框架
与I2C驱动框架一样,SPI驱动框架也分为三层:
- 主机适配器层:基于
platform
总线,完成SPI
控制器的驱动,比如总线时序等。 - API接口层:为设备驱动层提供统一的接口以完成数据的收发,使得具体设备和
SPI
控制器分离。 - 设备驱动层:基于
spi_bus_type
总线实现具体设备的驱动。
SPI驱动框架如下:
UART驱动框架
UART驱动基于platform
总线实现,大致框架如下: