2. platform驱动框架:platform_driver与platform_device结构体,注册与注销流程
好,咱们今天聊聊platform驱动框架。说实话,这个框架在Linux内核里太常见了。我刚开始接触嵌入式Linux时,看到一堆platform_xxx的函数,心里直犯嘀咕——这玩意儿到底是干嘛的?后来做项目做多了,才慢慢摸清楚它的门道。
说白了,platform驱动框架就是内核为了统一管理那些“不挂靠在标准总线(比如PCI、USB、I2C)上的设备”而设计的一套机制。你想想看,像GPIO控制器、SPI控制器、中断控制器这些,它们直接挂在CPU的地址总线上,没有标准总线去枚举它们。那怎么办?内核就搞了个虚拟总线——platform总线,把这些设备都挂上去。
2.1 platform_driver结构体
先看驱动这边。platform_driver结构体,是每个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);
int (*resume)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
bool prevent_deferred_probe;
};
这里最核心的是probe和remove两个回调函数。probe函数什么时候被调用?当内核发现一个platform_device与这个驱动匹配成功时,就会调用probe。我在项目中遇到过好几次,probe函数里忘了初始化互斥锁,结果多线程访问时直接崩了。嗯,这个坑踩得挺疼的。
关键点:probe函数是驱动的入口,所有硬件初始化、资源申请、设备文件创建,都应该放在这里。remove函数则是probe的逆操作,负责释放资源。
另外,id_table字段也很重要。它定义了驱动支持的设备列表。内核通过这个表来匹配设备和驱动。
2.2 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;
/* ... 省略其他字段 */
};
name字段是匹配驱动的关键。驱动和设备通过这个名字来“牵手”。resource字段则描述了设备的硬件资源,比如内存地址范围、中断号等。我记得有一次调试,发现设备的中断号没注册上,查了半天,原来是resource数组里中断资源的flag写错了。
小技巧:定义platform_device时,resource数组的结束标志是IORESOURCE_END。别漏了,否则内核解析资源时会越界。
2.3 注册与注销流程
好,结构和概念都清楚了。那注册和注销到底怎么走?我画了个流程图,你看一眼就明白了。
从图上你能看到,注册流程其实有两种触发方式:
- 先注册驱动,后注册设备:驱动注册时,内核遍历platform总线上的设备列表,找到匹配的就调用probe。
- 先注册设备,后注册驱动:设备注册时,内核遍历驱动列表,找到匹配的同样调用probe。
说白了,不管谁先谁后,最终都会走到probe函数里。这个设计挺巧妙的,对吧?
2.4 代码示例:注册与注销
光说不练假把式。我写个简单的示例,你看看实际代码怎么写。
/* 驱动注册示例 */
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my_device",
.of_match_table = my_of_match,
},
};
/* 模块入口 */
static int __init my_driver_init(void)
{
return platform_driver_register(&my_driver);
}
/* 模块出口 */
static void __exit my_driver_exit(void)
{
platform_driver_unregister(&my_driver);
}
module_init(my_driver_init);
module_exit(my_driver_exit);
注意:我曾经在项目里犯过一个低级错误——在module_init里调用了platform_driver_register,但忘记在module_exit里调用platform_driver_unregister。结果模块卸载后,驱动还在总线上挂着,下次加载时直接报错。嗯,这种错误一次就够了。
设备注册也类似,不过设备通常在内核启动时通过板级文件或设备树来注册。如果你要动态注册设备,可以这样:
static struct platform_device my_device = {
.name = "my_device",
.id = -1,
.num_resources = ARRAY_SIZE(my_resources),
.resource = my_resources,
};
platform_device_register(&my_device);
/* 用完后注销 */
platform_device_unregister(&my_device);
2.5 匹配机制
驱动和设备怎么匹配?内核有一套优先级规则:
| 匹配方式 | 优先级 | 说明 |
|---|---|---|
| 设备树 compatible 匹配 | 最高 | 通过 of_match_table 匹配设备树节点 |
| ACPI 匹配 | 次高 | 用于 x86/ARM64 的 ACPI 表 |
| id_table 匹配 | 中等 | 通过 platform_device_id 结构体匹配 |
| name 字段匹配 | 最低 | 直接比较 driver.name 和 device.name |
我个人习惯优先使用设备树匹配,因为可维护性好。但如果你在做一些快速原型验证,直接用name字段匹配也挺方便的。
经验之谈:匹配失败时,别急着怀疑代码。先检查设备树里compatible属性写对了没,或者驱动里name字段拼写有没有错。我至少有三次debug经历,最后发现是少打了个字母。
好了,platform驱动框架的核心内容就这些。结构体、注册注销流程、匹配机制,这三块搞明白了,写platform驱动基本就没什么大问题了。记住,probe函数是你的主战场,所有初始化都在那里完成。remove函数则是善后工作,别忘了释放你申请的资源。