Linux设备驱动模型之platform设备
上一章节介绍了Linux字符设备驱动,它是比较基础的,让大家理解Linux内核的设备驱动是如何注册、使用的。但在工作中,个人认为完全手写一个字符设备驱动的机会比较少,更多的都是基于前人的代码修修补补过三年。在内核驱动中,你会看到比较多的platform相关的字样,他们具体是什么呢,下面我们一起来看看。
platform bus
在嵌入式Linux中,可能会听到过I2C总线、SPI总线、USB总线等,但是platform总线是什么呢,它有什么规范呢?platform总线是一个虚拟的总线,它没有像上述介绍的总线有通信协议规范,它仅仅是为了更好的管理Linux内核设备驱动而虚化出来的。
项目开发过程中,会有很多的设备驱动需要注册,但是这些设备都会具有很多共同点,比如休眠唤醒时需要注意的操作,每个设备都会有差异,但是都会有需要,开关机时也是如此。为了方便管理这些设备驱动,Linux增加了platform设备和驱动,方便驱动在开发过程做到统一管理,将设备和驱动都挂载在总线上,当有新的设备或者驱动加入时,都会进行比较,当设备和驱动信息一致时,则进行相应的操作,完成资源申请和设备注册。
Linux内核提供了注册总线的接口,下面我们来看看platform的总线都提供了那些信息:
/* 从platform_bus的定义来看,虽然我们说platform总线,实际上它还是一个设备 */
struct device platform_bus = {.init_name = "platform",
};
EXPORT_SYMBOL_GPL(platform_bus);/* 定义platform设备的默认属性 */
static struct attribute *platform_dev_attrs[] = {&dev_attr_modalias.attr,&dev_attr_driver_override.attr,NULL,
};
ATTRIBUTE_GROUPS(platform_dev);/* 提供设备的休眠唤醒接口函数,platform设备的休眠唤醒都通过这个接口完成调用 */
static const struct dev_pm_ops platform_dev_pm_ops = {.runtime_suspend = pm_generic_runtime_suspend,.runtime_resume = pm_generic_runtime_resume,USE_PLATFORM_PM_SLEEP_OPS
};/* 定义platform总线的一些总要接口 */
struct bus_type platform_bus_type = {.name = "platform",.dev_groups = platform_dev_groups, /* 总线上设备的默认属性 */.match = platform_match, /* 前面我们说到注册分别注册设备和驱动之后,将会进行设备驱动匹配,platform设备就是通过该函数实现的 */.uevent = platform_uevent, /* 当添加、删除设备的时候,生成uevent添加到环境变量,实际上可以理解为有设备添加、删除时发送广播 */.pm = &platform_dev_pm_ops, /* 这个就是上面的休眠唤醒时的调用接口了 */
};
EXPORT_SYMBOL_GPL(platform_bus_type);int __init platform_bus_init(void)
{int error;early_platform_cleanup();/* 注册platform设备 */error = device_register(&platform_bus);if (error)return error;/* 向系统注册platform总线 */error = bus_register(&platform_bus_type);if (error)device_unregister(&platform_bus);of_platform_register_reconfig_notifier();return error;
}
bus_register函数主要进行下面几个操作:
- 将驱动自探测标志drivers_autoprobe设置为1,这样后续只要有device或driver加入bus,都将会触发设备驱动探测;
- 创建bus的默认属性节点;
- 创建bus的probe等文件节点;
向系统注册platform总线之后,又是怎么使用呢?下面我们一起看看platform设备和驱动的操作。
platform device
platform device一般在内核驱动中都是添加到各个SOC接口或者外设中出现的,作为一个对接内核设备的接口,使各自的设备驱动可更具灵活性。我们先看看内核是怎么定义这个platform device。
struct platform_device {const char *name; /* 设备属于什么名字的 */int id; /* 设备的索引,比如有多个类似的设备 */struct device dev; /* 内核设备的基础 */...u32 num_resources;struct resource *resource; /* 用于这个设备的资源保存了,比如中断和寄存器地址等 */...
};
上面基本了解了platform device的类型之后,那么它又是怎么登记注册到Linux内核的呢?主要是通过下面的两个接口函数。
/* 登记注册platform device */
int platform_device_register(struct platform_device *pdev);
/* 登记注册num个platform device */
int platform_add_devices(struct platform_device **devs, int num);
那么platform_device_register()函数的操作主要是什么呢?
- 通过 device_initialize 初始化 pdev->dev,也就是初始化上述的struct device;
- 设置platform device的 DMA mask;
- 初始化pdev->dev的parent为platform_bus以及bus为platform_bus_type;
- 更新platform device资源;
- 通过device_add()向内核添加pdev->dev设备;
似乎完成了注册,现在只有设备,还没有驱动,接着看驱动又是怎样的。
platform driver
内核就是将设备与驱动分离的,设备可以是看得见、摸得着的实体,也可以是一些虚拟的、远端的设备,而驱动则是使设备可以正常工作的代码。platform driver的定义如下:
struct platform_device_id {char name[PLATFORM_NAME_SIZE];kernel_ulong_t driver_data;
};struct platform_driver {/* probe函数用于探测是否存在和该驱动一致的platform device,由各自driver实现,驱动加载时调用 */int (*probe)(struct platform_device *);/* remove函数则是在卸载模块时调用,释放相关资源 */int (*remove)(struct platform_device *);/* 系统关机时将会调用shutdown */void (*shutdown)(struct platform_device *);/* suspend和resume则是系统进入休眠和唤醒时调用 */int (*suspend)(struct platform_device *, pm_message_t state);int (*resume)(struct platform_device *);/* 内核驱动的基础 */struct device_driver driver;/* platform device和driver匹配信息 */const struct platform_device_id *id_table;
};
上面罗列了platform driver的基本成员,各自驱动将会实现,基础需要完成probe、remove和id_table信息的填充。而向内核注册驱动则是通过platform_driver_register完成驱动的注册,而platform_driver_register又主要是进行几个操作:
- 将driver的bus设置为platform_bus_type;
- 而driver为platform driver,所以device_driver的probe、remove、shutdown函数设置为plaform driver的函数;
- 通过driver_register函数向内核注册驱动;
probe的意义
单纯只有硬件机器而没有软件,或许它就是一堆废铜烂铁;而只有软件驱动,也是毫无用处,没有载体,发挥不出它们的作用。虽然我们有注册device、driver,但是如果没有匹配,也是不行,就像用一个ARM64的固件放在mips架构的芯片上运行失败一样,所以probe的意义就是通过driver确认当前的device是否与自身一致,一致则可进行系统节点的注册等。
上面在介绍platform device时有name成员,而platform driver中有id_table,什么时候会触发设备和驱动进行匹配呢?
回头看通过platform_driver_register()函数注册驱动时,driver_register()中会调用bus_add_driver()向总线注册driver,在bus_add_driver()中由因为总线已经设置了drivers_autoprobe,将会通过driver_attach()进行device、driver的匹配,这个过程如下:
platform_driver_registerdriver_registerbus_add_driverdriver_attach__driver_attachdriver_match_devicebus->match
而driver_attach中,将从bus的klist_devices(总线上的所有设备信息都在该链表上)逐个device与driver进行匹配,匹配是通过__driver_attach()函数完成,在__driver_attach中,通过driver_match_device()调用bus的match函数,platform bus的match函数为platform_match()。platform_match将会依次进行以下的对比:
- 如果platform device的driver_override置位,则对比pdev的driver_override和drv的name是否一致;
- 进行设备树的匹配,将对比drv的of_match_table和dev的of_node信息是否一致;
- 进行acpi的信息对比,这点有点不是很清晰,平常没有使用;
- 进行pdrv->id_table与pdev->name的对比,平常这个使用较多;
- 最后则是直接对比pdev->name和drv->name;
如果上述对比下来都不一致,则没有匹配,否则匹配成功。设备驱动匹配成功,将会通过device_driver_attach()函数进行匹配。
device_driver_attach将会进行以下的操作:
- 获取device所属的引脚配置信息;
- 在/sys目录下建立driver与device的链接;
- 优先调用设备所在bus的probe函数,如果没有则调用driver的probe函数;
- 将设备所有的引脚配置为默认状态;
像platform bus是没有实现probe函数的,所以将会调用在注册驱动时赋值的platform_drv_probe()函数,而在这里,实际上就是调用platform driver的probe函数,也就是驱动自己实现的probe函数了。
当调用到probe函数时,证明整个Linux内核的设备驱动模型已经跑通,但实际上这个设备是否存在,需要probe函数实现,probe函数确认device存在之后,将会可以进行资源的申请以及相关设备节点的注册操作。