3、Platform驱动核心结构:struct platform_driver与struct platform_device详解,probe与remove回调

好,咱们今天来聊聊Platform驱动里最核心的两个结构体——platform_driverplatform_device。说白了,整个驱动框架就是围着这两个家伙转的。你写驱动,本质上就是在填充这两个结构体,然后告诉内核:嘿,我这儿有个驱动,你看着办。

我个人习惯,每次写一个新驱动,都会先把这两个结构体的骨架搭好。就像盖房子,先立柱子,再砌墙。今天我们就来拆开看看,这两根柱子到底长什么样。

3.1 struct platform_driver:驱动的“身份证”

先看驱动侧。每个Platform驱动都必须有一个struct platform_driver实例。它告诉内核:我这个驱动能管哪些设备,怎么初始化,怎么卸载。

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;
};

这里最关键的,就是proberemove这两个回调函数。嗯,咱们重点讲它们。

核心要点:probe是驱动的入口,remove是驱动的出口。一个负责“生”,一个负责“死”。

3.1.1 probe回调——驱动的“出生证明”

什么时候会调用probe?当内核发现一个platform_device与你的platform_driver匹配成功时,就会调用你的probe函数。说白了,就是内核说:“嘿,你的设备来了,快去初始化吧!”

我在项目中遇到过好几次,新手写probe时恨不得把所有代码都塞进去。其实probe只应该做三件事:

  • 资源获取:从device tree或platform_device中拿到内存地址、中断号、时钟等
  • 硬件初始化:复位设备、配置寄存器、启动时钟
  • 注册子系统:比如注册字符设备、input设备、net设备等

来看一个我实际项目中用过的probe示例:

static int my_uart_probe(struct platform_device *pdev)
{
    struct resource *res;
    void __iomem *base;
    int irq, ret;

    // 1. 获取IO内存资源
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(base))
        return PTR_ERR(base);

    // 2. 获取中断号
    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    // 3. 分配并初始化驱动私有数据
    // ... 这里省略具体实现

    // 4. 注册字符设备
    ret = register_chrdev(0, "my_uart", &my_uart_fops);
    if (ret < 0) {
        dev_err(&pdev->dev, "Failed to register char device\n");
        return ret;
    }

    dev_info(&pdev->dev, "my_uart probed successfully\n");
    return 0;
}

我的小技巧:尽量多用devm_系列函数(比如devm_ioremap_resource、devm_kzalloc)。这些函数会在驱动卸载或probe失败时自动帮你释放资源,省得你写一堆错误处理代码。我曾经因为忘记释放中断,导致系统重启后设备无法使用,排查了半天才发现是资源泄漏。

3.1.2 remove回调——驱动的“临终遗言”

remove是probe的逆操作。当驱动被卸载,或者设备被移除时,内核会调用remove。它的任务很简单:把probe里做的事情,一件一件撤销掉。

static int my_uart_remove(struct platform_device *pdev)
{
    struct my_uart_data *data = platform_get_drvdata(pdev);

    // 1. 注销字符设备
    unregister_chrdev(data->major, "my_uart");

    // 2. 关闭硬件(比如关时钟、关中断)
    // ...

    // 3. 释放私有数据(如果用devm_分配,这步可以省略)
    // ...

    dev_info(&pdev->dev, "my_uart removed\n");
    return 0;
}

你想想看,如果probe里申请了5种资源,remove里只释放了4种,那就是一颗定时炸弹。我建议你写probe的时候,就把remove的框架写好,一一对应。

避坑指南:我曾经在remove里忘记调用free_irq,结果驱动卸载后,中断还在触发,系统直接panic。记住:probe里申请了什么,remove里就要释放什么,一个都不能少。

3.2 struct 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;
    /* ... 省略一些不常用的字段 */
};

这里最重要的就是resource数组。它描述了设备需要的所有硬件资源。在设备树时代,这些资源由内核自动解析并填充。但在老式的板级文件(board file)中,你需要手动填写。

3.2.1 资源类型与获取

资源类型用宏定义区分,最常见的有:

宏定义 含义 获取函数
IORESOURCE_MEM 内存映射IO地址 platform_get_resource(pdev, IORESOURCE_MEM, 0)
IORESOURCE_IRQ 中断号 platform_get_irq(pdev, 0)
IORESOURCE_DMA DMA通道 platform_get_resource(pdev, IORESOURCE_DMA, 0)
IORESOURCE_IO IO端口地址 platform_get_resource(pdev, IORESOURCE_IO, 0)

嗯,这里要注意:platform_get_irq是专门获取中断的快捷函数,它比platform_get_resource更智能,能处理一些特殊情况。我个人习惯,拿中断一律用platform_get_irq,拿内存用platform_get_resource

3.3 匹配机制:驱动和设备如何“牵手”

驱动和设备是怎么匹配上的?说白了,就是内核拿着你的platform_driver,去遍历所有platform_device,看谁的name字段能对上。

匹配顺序是这样的:

  1. 设备树匹配:如果设备树中节点的compatible属性与driver的of_match_table匹配
  2. ID表匹配:如果platform_driver有id_table,则与platform_device的name比较
  3. 名字直接匹配:如果以上都不行,直接比较driver的name和device的name

我画了一张图,帮你理清这个流程:

Platform驱动与设备匹配流程 platform_driver .driver.name = "my_uart" .id_table = my_uart_ids .of_match_table = my_uart_of_match platform_device .name = "my_uart" .dev.of_node = {...} .num_resources = 2 开始匹配 匹配优先级 ① 设备树匹配: 检查 compatible 属性是否匹配 of_match_table ② ID表匹配: 检查 device.name 是否在 id_table 中 ③ 名字直接匹配: 直接比较 driver.name 和 device.name 匹配成功 → 调用probe

3.4 实战经验:probe/remove的常见陷阱

最后,分享几个我踩过的坑,希望能帮你少走弯路。

陷阱1:probe失败后资源泄漏

如果你在probe中途失败返回,之前申请的资源必须全部释放。用devm_系列函数可以自动处理,但如果你用了request_irqioremap等非devm函数,一定要在错误路径上手动释放。

陷阱2:remove被调用时设备还在工作

我曾经在remove里直接释放了DMA缓冲区,但DMA传输还没完成,结果系统直接挂死。正确的做法是:先停止设备的所有活动(关中断、停DMA、等待传输完成),再释放资源。

陷阱3:probe和remove的对称性

我见过一个驱动,probe里申请了3个中断,remove里只释放了2个。这种bug很难查,因为不是每次卸载都会触发。我的习惯是:写probe时,每申请一个资源,就在旁边注释上对应的释放代码。这样写remove时直接抄作业就行。

好了,关于platform_driver和platform_device的核心内容,今天就聊到这儿。这两个结构体是Platform驱动的地基,地基打牢了,上面的房子才稳。下一节我们会深入设备树,看看这些资源是怎么从dts文件里解析出来的。


专注资料整理