找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

6139

积分

0

好友

752

主题
发表于 3 天前 | 查看: 3| 回复: 0

写 platform 驱动的时候,你多半也想过这个问题:我们并没有直接调用 probe(),它究竟是怎么被执行的?

驱动开发中,我们通常会填好匹配表和回调,最后用 module_platform_driver() 完成注册。那么,内核怎么知道这份驱动能处理哪些设备?又是在什么时候开始查找和匹配的?

之前在开源的 QEMU 工程里,我已经用 GPIO 演示过平台总线驱动的开发过程,这篇文章继续用这个例子。没看过之前的文章也没关系,用到的设备树和驱动代码都会贴在下面。

这个驱动只做寄存器映射和一次读取,代码量很小,可以直接跟着读。例子虽然简单,但走的却是真实控制器驱动同样在用的 platform 框架。GPIO、I2C、SPI 等控制器都建立在这套框架上,共用同样的注册、匹配和探测主干。因此,把这个 GPIO 搞明白之后,再去读其他驱动,就会有可以对照的关系。

不过这一节我不会只把代码调用流程过一遍,也不建议大家这样学内核。那等于背词典,看完之后留不下什么印象,也没有形成真正的理解,遇到自己的驱动还是不知道从哪下手分析。而且现在梳理调用栈完全可以交给 AI,你不必死记函数的调用流程。

所以,我更想带大家理解设备、驱动和总线之间的结构关系,以及匹配与绑定背后的机制。这篇文章就从注册入口开始,看内核怎样找到 GPIO、怎样把正确的设备交给 cc_gpio_probe(),以及走到哪一步才算真正完成绑定。

1. GPIO 设备、驱动与总线的关系

注册流程从 module_platform_driver(cc_gpio_driver) 开始。先看它怎样进入内核的驱动注册函数,以下源码以 Linux 6.12.28 为准:

module_platform_driver(cc_gpio_driver)
    │ 宏展开,生成 cc_gpio_driver_init()
    ▼
insmod 加载模块,执行 cc_gpio_driver_init()
    └─ platform_driver_register(&cc_gpio_driver)
       └─ __platform_driver_register(&cc_gpio_driver, THIS_MODULE)
          └─ driver_register(&cc_gpio_driver.driver)

这条注册路径从我们定义的 cc_gpio_driver 开始。要看它怎样与 GPIO 设备匹配,得先把驱动里填写的信息和设备树中的描述对应起来。

1.1 设备描述与驱动声明

QEMU 设备树中适配后的 GPIO 节点如下:

gpio0: pl061@9030000 {
    phandle = <0x8004>;
    clock-names = "apb_pclk";
    clocks = <&apb_pclk>;
    interrupts = <0x00 0x07 0x04>;
    gpio-controller;
    #gpio-cells = <0x02>;
    compatible = "corechip,cc-gpio";
    reg = <0x00 0x9030000 0x00 0x1000>;
};

内核根据这个节点创建的 platform 设备名是 9030000.pl061。节点中的 compatible 描述设备的兼容类型,reg 等属性描述当前这个 GPIO 实例的资源。

驱动一侧通过匹配表声明支持的类型,并在 platform_driver 中填写初始化回调:

static int cc_gpio_probe(struct platform_device *pdev);
static void cc_gpio_remove(struct platform_device *pdev);

static const struct of_device_id cc_gpio_of_match[] = {
    { .compatible = "corechip,cc-gpio" },
    { }
};
MODULE_DEVICE_TABLE(of, cc_gpio_of_match);

static struct platform_driver cc_gpio_driver = {
    .probe = cc_gpio_probe,
    .remove = cc_gpio_remove,
    .driver = {
        .name = "cc-gpio",
        .of_match_table = cc_gpio_of_match,
    },
};
module_platform_driver(cc_gpio_driver);

把上面的设备树和驱动放在一起看,就能看出它们各自负责什么:设备树说明系统里有什么设备,驱动说明自己能处理什么设备,以及该怎么初始化它。

设备树没有指定要调用 cc_gpio_probe(),驱动也没有写死这个 GPIO 的寄存器地址。 两者通过共同的兼容类型建立联系,具体设备的资源则留在设备一侧,等回调处理这个实例时再取出来。

1.2 注册入口与内嵌对象

现在,匹配表和回调已经填进了 cc_gpio_driver,我们还需要让内核知道这份驱动的存在,最后一行注册宏做的就是这件事。

module_platform_driver() 内部使用另一个宏 module_driver(),后者通过参数替换和标识符拼接生成模块入口,初始化部分的定义如下:

/* include/linux/platform_device.h */
#define module_platform_driver(__platform_driver) \
    module_driver(__platform_driver, platform_driver_register, \
                  platform_driver_unregister)

/* include/linux/device/driver.h:初始化部分 */
#define module_driver(__driver, __register, __unregister, ...) \
static int __init __driver##_init(void) \
{ \
    return __register(&(__driver), ##__VA_ARGS__); \
} \
module_init(__driver##_init);

## 把 cc_gpio_driver 和 _init 拼成 cc_gpio_driver_init,生成的入口相当于下面这段代码:

static int __init cc_gpio_driver_init(void)
{
    return platform_driver_register(&cc_gpio_driver);
}
module_init(cc_gpio_driver_init);

加载模块时,内核执行这个入口。platform_driver_register() 再补入模块信息,调用 __platform_driver_register():

/* drivers/base/platform.c */
int __platform_driver_register(struct platform_driver *drv,
                               struct module *owner)
{
    drv->driver.owner = owner;
    drv->driver.bus = &platform_bus_type;

    return driver_register(&drv->driver);
}

这里要注意最后一行的参数,driver_register() 接收的是 &drv->driver,也就是 cc_gpio_driver 内部那个 struct device_driver 成员的地址。

为什么要把这个成员单独传进去?

因为内核的通用设备管理代码需要统一组织不同类型的设备与驱动,它使用的是 struct device 和 struct device_driver 这些公共结构。平台层把自己的资源和回调放在外层结构里,同时把公共结构嵌在其中。

驱动一侧是 platform_driver 内嵌 device_driver,设备一侧也是同样的做法,platform_device 内嵌一个 device:

platform_device 与 platform_driver 内嵌结构体关系图

图中的 dev 和 driver 都是成员名,内外两层框表示的正是这种包含关系。我们注册时把里面的公共成员传进去,后面需要平台层的信息时,又可以根据这个成员找到外层对象。

这样再看设备、驱动和总线,后面要用的信息就各有位置了:

对象 保存的信息 后续用途
platform_device 当前实例的 resource[],以及内嵌 dev 中的设备树节点指针 确定这是哪个设备,取得它的资源
platform_driver 内嵌 driver 中的匹配表,以及外层的 probe 回调 声明支持哪些设备,提供初始化方法
platform_bus_type platform 的匹配与探测回调 为同属 platform 总线的设备和驱动执行匹配、调用回调

有了这些信息,内核还要确定查找范围。注册 platform 驱动时,它需要从 platform 设备中寻找匹配对象,这个范围由双方的 bus 成员确定。在 __platform_driver_register() 中,driver.bus 被设置为 &platform_bus_type,GPIO 设备创建时,dev.bus 也指向它。

这里的 platform 总线,就是内核用来组织这两类对象的一套软件机制。双方指向同一条总线,后面的查找才有共同的范围,匹配也才有共同的规则。

2. 注册触发与类型匹配

前面我们把设备和驱动联系到了同一条总线上,可在实际系统里,我们不能假定它们总会按固定的顺序注册。设备注册和驱动加载是相对独立的两件事,各有自己的时机。写驱动时,我们不能要求设备一定先准备好,但无论谁先谁后,只要双方满足匹配条件,内核都应该能让它们找到对方。

比如我们这个 GPIO,系统已经启动,设备也已经注册好了,之后才通过 insmod 加载驱动,这就是设备在前、驱动在后。反过来,也可能是驱动已经随系统启动完成注册,对应的 platform 设备却还没有注册进来,过一段时间,内核才有这个设备可以交给驱动处理。

这两种情况我们都来看一下,先从当前 GPIO 的情况入手:驱动还没加载时,已经注册的设备会留在哪里?

2.1 总线中的设备与驱动列表

系统启动时,GPIO 节点已经转换成了 platform_device,设备注册经过 of_device_add()、device_add(),进入 bus_add_device()。我们要找的登记位置就在这里,这个函数根据 dev->bus 找到 platform 总线的数据,再把当前设备加入它的设备列表:

/* drivers/base/bus.c */
int bus_add_device(struct device *dev)
{
    struct subsys_private *sp = bus_to_subsys(dev->bus);
    /* ... */
    klist_add_tail(&dev->p->knode_bus, &sp->klist_devices);
    /* ... */
}

GPIO 设备已经登记在这份列表中,后续查找就可以从总线上取得它。因此,设备可以先完成注册,等驱动加载时再进行匹配。

等我们执行 insmod 加载 GPIO 驱动,前面看到的 driver_register() 就会继续调用 bus_add_driver()。这次除了把驱动加入总线的驱动列表,还多了一个值得注意的动作,就是调用 driver_attach():

/* drivers/base/bus.c */
int bus_add_driver(struct device_driver *drv)
{
    struct subsys_private *sp = bus_to_subsys(drv->bus);
    struct driver_private *priv;
    int error = 0;
    /* ... */
    klist_add_tail(&priv->knode_bus, &sp->klist_drivers);

    if (sp->drivers_autoprobe) {
        error = driver_attach(drv);
    /* ... */
    }
    /* ... */
}

/* drivers/base/dd.c */
int driver_attach(const struct device_driver *drv)
{
    return bus_for_each_dev(drv->bus, NULL, (void *)drv, __driver_attach);
}

真正触发这次设备查找的,就是这里的 driver_attach(),在默认自动探测的配置下,它会遍历 platform 总线上的设备。遍历时,驱动作为 data 保持不变,每次取出的设备作为 dev 传给 __driver_attach()。

也就是说,内核拿着同一份 cc_gpio_driver 逐个检查设备,轮到 9030000.pl061 时,就判断这个 GPIO 是不是它能处理的设备。

那如果顺序反过来,驱动先注册了,后来才有设备呢?这时候就该由新注册的设备发起查找了。设备加入总线后,device_add() 中的 bus_probe_device() 会启动设备一侧的查找,经过 device_initial_probe()、__device_attach(),由 bus_for_each_drv() 遍历已有驱动。

前一种情况固定驱动、逐个检查设备,这里则固定设备、逐个检查驱动,两个方向可以放在一起看:

platform 总线设备与驱动匹配流程图

注册在这里同时承担了两件事:登记自己,并查找已经存在的另一方。 先注册的一方已经保存在列表中,后注册的一方再触发查找,因此设备和驱动不必按固定顺序出现。

2.2 当前 GPIO 的 OF 匹配

现在,内核已经能从列表里找到另一方了,不过,platform 总线上还有其他设备,找到一个设备,并不等于当前驱动就能处理它。我们还得回到双方最初填写的信息,看看这个设备的类型,是否在驱动声明的支持范围内。

__driver_attach() 通过 driver_match_device() 调用总线的 match 回调。对于 platform 总线,这个回调就是 platform_match():

/* drivers/base/platform.c */
static int platform_match(struct device *dev, const struct device_driver *drv)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct platform_driver *pdrv = to_platform_driver(drv);

    if (pdev->driver_override)
        return !strcmp(pdev->driver_override, drv->name);

    if (of_driver_match_device(dev, drv))
        return 1;

    if (acpi_driver_match_device(dev, drv))
        return 1;

    if (pdrv->id_table)
        return platform_match_id(pdrv->id_table, pdev) != NULL;

    return (strcmp(pdev->name, drv->name) == 0);
}

这里能看到好几种匹配方式,是因为 platform 设备可以来自不同的硬件描述方式,内核需要识别这些描述,才能找到合适的驱动。OF 对应我们正在使用的设备树接口,ACPI 则是固件向操作系统描述硬件和电源管理信息的一套规范,采用 ACPI 描述的设备,可以通过相应的设备标识与驱动匹配。除此之外,platform 还提供 ID 表和名称等匹配方式,driver_override 则用于显式指定要匹配的驱动。

回到我们这个 GPIO,它来自设备树,所以走的是 OF 分支。前面保存在设备和驱动中的两份信息,现在正好派上用场,内核沿 dev->of_node 找到设备树节点,沿 drv->of_match_table 找到驱动的匹配表:

OF 匹配核心过程示意图

设备节点声明 compatible = "corechip,cc-gpio",驱动的匹配表也包含这一项,OF 匹配因此成功,platform_match() 在这里返回 1。

再回头看这个例子,设备名是 9030000.pl061,驱动名却是 cc-gpio,两者名字并不相同,为什么仍然能匹配?原因就在这里:这次决定匹配结果的是双方的 compatible,它确认的是设备是否属于驱动声明支持的类型。

驱动支持哪一类设备,现在已经确认了,接下来还要落实到当前这个 GPIO 上:内核怎样从匹配到的驱动中取出回调,又怎样把当前设备传给它?

3. 从匹配对象到具体设备的 probe

OF 匹配成功后,内核处理的仍然是刚才参与比较的两个对象:当前 GPIO 内嵌的 device 和 cc_gpio_driver 中内嵌的 device_driver。后续探测会继续使用这两个对象,找到驱动的回调,并把当前 GPIO 传进去。

下面这张图展示了它们怎样进入平台层,最终调用到 cc_gpio_probe(),以及回调结果如何影响后续的绑定:

platform_probe 到 cc_gpio_probe 的探测流程架构图

图中有两个 probe 回调字段,一个属于 platform_bus_type,指向 platform_probe(),另一个属于 cc_gpio_driver,指向我们实现的 cc_gpio_probe()。要让这两层回调接起来,后面的代码必须知道当前设备准备使用哪份驱动,这就需要先在设备上记录两者的关联。

3.1 把匹配的驱动记录到当前设备

我们先回到 __driver_attach(),driver_attach() 把固定的驱动和每次遍历到的设备传进来之后,匹配和探测就围绕同一对参数继续进行:

/* drivers/base/dd.c */
static int __driver_attach(struct device *dev, void *data)
{
    const struct device_driver *drv = data;
    int ret;
    /* ... */
    ret = driver_match_device(drv, dev);
    if (ret == 0)
        return 0;
    /* ... */
    __device_driver_lock(dev, dev->parent);
    driver_probe_device(drv, dev);
    __device_driver_unlock(dev, dev->parent);

    return 0;
}

匹配成立后,这一对 drv 和 dev 经 driver_probe_device()、__driver_probe_device() 进入 really_probe(),这里会先记录驱动关联,再调用探测回调:

/* drivers/base/dd.c */
static int really_probe(struct device *dev, const struct device_driver *drv)
{
    int ret, link_ret;
    /* ... */
    device_set_driver(dev, drv);
    /* ... */
    ret = call_driver_probe(dev, drv);
    /* ... */
    driver_bound(dev);
    /* ... */
}

device_set_driver(dev, drv) 把这次匹配到的驱动记录在 dev->driver 中。对于当前 GPIO,这个指针指向 cc_gpio_driver.driver,后面的平台代码就能沿着它找到要使用的驱动。

这里要注意,dev->driver 已经有值,还不能说明设备绑定成功了。 此时只是为接下来的探测建立关联,设备能否初始化成功,还要等回调执行,图中的 driver_bound() 位于回调成功和后续处理完成之后。

3.2 平台层还原对象并调用驱动

dev->driver 指向的是内嵌的 device_driver,而我们填写的 cc_gpio_probe() 保存在外层 platform_driver 中。要调用这个回调,还需要找回外层的平台驱动,同时取得回调需要的 platform_device。

这部分工作由平台层的 platform_probe() 完成,call_driver_probe() 通过总线的 probe 字段进入它:

/* drivers/base/dd.c */
static int call_driver_probe(struct device *dev, const struct device_driver *drv)
{
    int ret = 0;

    if (dev->bus->probe)
        ret = dev->bus->probe(dev);
    else if (drv->probe)
        ret = drv->probe(dev);

    /* ... */
    return ret;
}

platform 总线设置了 .probe = platform_probe,所以这里实际执行的是 platform_probe(dev),传入的仍然是那个内嵌的 struct device 成员的地址。

platform_probe() 中,_dev 表示传入的通用设备,局部变量 dev 和 drv 分别指向还原后的平台设备与平台驱动,对应图中的 pdev 和 pdrv:

/* drivers/base/platform.c */
static int platform_probe(struct device *_dev)
{
    struct platform_driver *drv = to_platform_driver(_dev->driver);
    struct platform_device *dev = to_platform_device(_dev);
    int ret;
    /* ... */
    if (drv->probe) {
        ret = drv->probe(dev);
    /* ... */
    }
    /* ... */
    return ret;
}

前面讲的内嵌关系,到这里就真正用上了:注册时,我们把外层对象中的公共成员传进去,现在平台层又根据这些成员找回原来的外层对象。完成这个转换的就是 to_platform_device() 和 to_platform_driver(),它们内部通过 container_of(),根据成员地址找到包含它的结构体。

_dev 原本就是当前 GPIO 平台设备内部的 dev,因此从它找回的外层对象,仍然是这个 GPIO。_dev->driver 指向 cc_gpio_driver.driver,因此从它找回的外层驱动,就是我们注册的 cc_gpio_driver。

外层驱动的 .probe 保存着 cc_gpio_probe,于是源码中的 drv->probe(dev),在当前例子里就相当于 cc_gpio_probe(pdev)。这样一来,回调函数和设备参数就对应上了:cc_gpio_probe() 来自匹配到的驱动,传进去的 pdev 则是当前正在处理的 GPIO 设备。

这里最值得理解的是,同一个设备经过这些调用,身份始终没有变化。通用层使用它内部的 dev,平台层再找回外面的 platform_device,一路传下来的都是这个 GPIO,回调自然就能拿到它自己的资源。

3.3 同一份驱动处理不同实例

有了当前 GPIO 的 pdev,驱动就能从这个对象里取得寄存器资源,我们在回调中用的正是这种方式:

#define CC_GPIO_DIR 0x400

static int cc_gpio_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    void __iomem *base;
    u8 dir;

    base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(base))
        return PTR_ERR(base);

    dir = readb(base + CC_GPIO_DIR);
    dev_info(dev, "register resource ready: GPIODIR=0x%02x\n", dir);
    return 0;
}

static void cc_gpio_remove(struct platform_device *pdev)
{
    dev_info(&pdev->dev, "remove\n");
}

devm_platform_ioremap_resource(pdev, 0) 使用的是传入设备的第一个内存资源。前面设备树中的 reg 已经转换为这个平台设备的资源,所以驱动通过 pdev 就能映射当前 GPIO 的寄存器,再读取方向寄存器。

注意这个写法中的分工,驱动知道方向寄存器的偏移是 CC_GPIO_DIR,但当前 GPIO 的寄存器窗口在哪里,要从传入的 pdev 中取得。

假如系统里还有另一个相同类型的 GPIO,内核处理到它时,也会调用同一个 cc_gpio_probe(),但这次传入的是另一个 platform_device。回调从各自的设备对象中取得资源,寄存器访问就会落到对应的 GPIO 上。同一份驱动能够复用,是因为处理方法保存在驱动中,实例差异保存在设备中,probe(pdev) 把两者结合起来。

在实际控制器驱动中,我们会继续围绕这个 pdev 申请实例所需的资源、保存私有数据,并完成各自的初始化。这也解释了为什么驱动一侧的设备遍历,在成功处理一个设备以后还要继续,后面可能还有同样需要这份驱动的其他实例。

4. 从初始化结果到绑定状态

不过,代码能够进入 cc_gpio_probe(),就可以认为设备已经绑定成功了吗?我们还得看这次初始化能不能做完。

比如刚才的 GPIO 回调,需要先映射寄存器才能继续访问硬件,如果映射失败,它就会返回错误。这时,设备与驱动的类型虽然已经匹配,本次初始化却没有成功。

如果 cc_gpio_probe() 返回 0,并且 really_probe() 中后续处理成功,内核会调用 driver_bound(),把当前设备加入驱动的已绑定设备列表:

/* drivers/base/dd.c */
static void driver_bound(struct device *dev)
{
    /* ... */
    klist_add_tail(&dev->p->knode_driver,
                   &dev->driver->p->klist_devices);
    /* ... */
}

bool device_is_bound(struct device *dev)
{
    return dev->p && klist_node_attached(&dev->p->knode_driver);
}

这里又出现了一份设备列表,注意它和前面总线的设备列表有一个区别:总线记录的是自己这里有哪些设备,这份列表则属于某个具体驱动,记录它已经成功绑定了哪些设备。device_is_bound() 检查的正是设备有没有加入这份已绑定列表。

前面给 dev->driver 赋值,是为了让探测过程找到要调用的驱动,而这里的列表登记,才记录了这次探测已经成功。

如果当前 GPIO 的探测失败,really_probe() 的错误路径会进入 device_unbind_cleanup(),释放这次探测中申请的托管资源,并清除驱动关联:

/* drivers/base/dd.c */
static void device_unbind_cleanup(struct device *dev)
{
    devres_release_all(dev);
    /* ... */
    device_set_driver(dev, NULL);
    dev_set_drvdata(dev, NULL);
    /* ... */
}

驱动自行管理的资源仍然需要在自己的失败路径中处理,我们这个示例使用的 devm_ 资源则由上述托管清理机制释放。

所以,这三个阶段要分开理解:匹配确认支持关系,probe() 尝试初始化当前实例,绑定记录成功接管的结果。 以后遇到“明明已经匹配,设备却没有绑定”的情况,就可以沿探测路径检查 probe() 是否执行、返回了什么,以及后续是否完成绑定登记。

5. 总结

现在再回头看驱动末尾的 module_platform_driver(cc_gpio_driver),这一行之所以能让后面执行到 cc_gpio_probe(),是因为注册过程会主动查找已经存在的设备,匹配成功后,再围绕当前这一对设备与驱动继续执行。

其中最关键的关系,我们已经在源码中看到了:设备保存当前实例的信息,驱动保存支持类型和处理方法,总线把双方组织起来。到了调用回调的时候,从驱动中取得函数,从当前设备中取得参数,同一份驱动也就能够处理不同实例。等当前实例初始化成功、后续处理完成,内核再记录绑定关系,这时驱动才算成功接管了设备。

以后再读一个复杂的控制器驱动,我们可以先把这些关系找出来,看看信息存在哪个对象里,回调从哪个字段取出,传进去的又是哪一个设备。有了这层认识,再顺着源码往下读,函数之间的调用才会变成有来由的动作,而不只是一串需要记住的名字。如果你在实际 BSP 开发中遇到匹配与绑定的具体疑难,也欢迎到云栈社区和更多同行一起讨论。




上一篇:Keil MDK内存调试实战:STM32程序RAM运行配置指南
下一篇:Claude Code 2.1.277 终于读 AGENTS.md 了:内置 mod 插件机制首次亮相
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-3 00:34 , Processed in 0.497512 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表