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驱动注册与注销流程 platform_driver_register() 驱动加入platform总线 匹配已注册的device 调用probe()函数 platform_device_register() 设备加入platform总线 匹配已注册的driver 调用probe()函数 注销:调用remove() + unregister() 注:driver和device的注册顺序无关,谁先注册都会触发匹配

从图上你能看到,注册流程其实有两种触发方式:

  1. 先注册驱动,后注册设备:驱动注册时,内核遍历platform总线上的设备列表,找到匹配的就调用probe。
  2. 先注册设备,后注册驱动:设备注册时,内核遍历驱动列表,找到匹配的同样调用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函数则是善后工作,别忘了释放你申请的资源。


可可精品资料库