4、GPIO驱动模型:platform driver与GPIO的结合、设备树中GPIO的描述

好,咱们接着聊。前面几章我们把GPIO的基本操作和中断机制都捋了一遍,现在该进入一个更实际的环节了——如何把GPIO驱动塞进Linux内核的驱动模型里

说白了,你不能写个驱动就直接去操作寄存器。内核有它自己的规矩,你得遵守。这个规矩就是 platform driver 模型。我个人习惯把 platform driver 理解为「内核给那些不挂靠在PCI、USB等标准总线上的设备,搭的一个虚拟总线」。嗯,嵌入式设备里那些直接焊在板子上的外设,十有八九都用它。

4.1 为什么是platform driver?

你想想看,一个GPIO按键,它挂在哪个总线上?I2C?SPI?都不是。它就是SoC内部的一个引脚。内核为了统一管理这类设备,搞了个 platform bus。你的驱动只需要注册一个 platform_driver,内核就会自动去匹配设备树里描述的设备节点。

我在项目中遇到过好几次,新手直接把GPIO操作写在init函数里,完全不考虑驱动模型。结果呢?设备树一改,驱动就废了。所以,一定要用platform driver来封装你的GPIO逻辑

核心思想: platform driver 负责「驱动」部分,设备树负责「设备」部分。两者通过 name 字段匹配。

4.2 设备树中GPIO的描述

设备树,说白了就是一块描述硬件信息的「数据板」。内核启动时解析它,然后创建对应的 platform_device。GPIO在设备树里怎么描述?我直接给你看个典型例子:

// 设备树片段:描述一个GPIO按键
gpio_keys {
    compatible = "gpio-keys";
    #address-cells = <1>;
    #size-cells = <0>;

    button@0 {
        label = "power_key";
        gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
        linux,code = <KEY_POWER>;
        debounce-interval = <10>;
    };
};

这里的关键是 gpios 属性。它有三个参数:

  • 第一个:GPIO控制器节点引用(比如 &gpio1)
  • 第二个:引脚编号(这里是3号引脚)
  • 第三个:标志位,比如 GPIO_ACTIVE_LOW 表示低电平有效

嗯,这里要注意:GPIO编号是相对于控制器内部的,不是全局的。你写驱动时,得用 of_get_named_gpio() 来解析这个属性。

我的习惯: 在设备树里给每个GPIO加一个 label 属性,方便调试时识别。比如 "power_key"、"led_red" 这种,一看就懂。

4.3 platform driver 与 GPIO 的结合

好,设备树写好了,驱动怎么写?我给你一个精简的模板,这是我实际项目中用过的:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/interrupt.h>

struct my_gpio_data {
    int gpio_num;
    int irq_num;
    struct platform_device *pdev;
};

static irqreturn_t my_gpio_irq_handler(int irq, void *dev_id)
{
    struct my_gpio_data *data = dev_id;
    int val = gpio_get_value(data->gpio_num);

    pr_info("GPIO %d interrupt, value = %d\n", data->gpio_num, val);
    return IRQ_HANDLED;
}

static int my_gpio_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_gpio_data *data;
    int ret;

    data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    data->pdev = pdev;

    // 从设备树获取GPIO编号
    data->gpio_num = of_get_named_gpio(dev->of_node, "gpios", 0);
    if (data->gpio_num < 0) {
        dev_err(dev, "Failed to get gpio\n");
        return data->gpio_num;
    }

    // 申请GPIO
    ret = devm_gpio_request(dev, data->gpio_num, "my_gpio");
    if (ret) {
        dev_err(dev, "Failed to request gpio\n");
        return ret;
    }

    // 设置为输入
    gpio_direction_input(data->gpio_num);

    // 获取中断号
    data->irq_num = gpio_to_irq(data->gpio_num);
    if (data->irq_num < 0) {
        dev_err(dev, "Failed to get irq\n");
        return data->irq_num;
    }

    // 注册中断
    ret = devm_request_irq(dev, data->irq_num, my_gpio_irq_handler,
                           IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
                           "my_gpio_irq", data);
    if (ret) {
        dev_err(dev, "Failed to request irq\n");
        return ret;
    }

    platform_set_drvdata(pdev, data);
    dev_info(dev, "GPIO driver probed, gpio=%d, irq=%d\n",
             data->gpio_num, data->irq_num);

    return 0;
}

static int my_gpio_remove(struct platform_device *pdev)
{
    dev_info(&pdev->dev, "GPIO driver removed\n");
    return 0;
}

static const struct of_device_id my_gpio_of_match[] = {
    { .compatible = "my-company,my-gpio-device" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_gpio_of_match);

static struct platform_driver my_gpio_driver = {
    .probe  = my_gpio_probe,
    .remove = my_gpio_remove,
    .driver = {
        .name = "my_gpio",
        .of_match_table = my_gpio_of_match,
    },
};

module_platform_driver(my_gpio_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Platform driver with GPIO and interrupt");

这段代码里,我用了 devm_ 系列函数。为什么?因为 devm_ 是设备资源管理,你不需要手动释放内存、GPIO、中断。probe 失败或 remove 时,内核自动帮你清理。我曾经在项目里因为忘了释放中断,导致模块卸载后系统崩溃,从那以后我再也不敢不用 devm_ 了。

4.4 匹配机制:设备树与驱动的桥梁

驱动和设备树怎么匹配?靠的是 compatible 属性。设备树里写 compatible = "my-company,my-gpio-device",驱动里定义 of_device_id 表,两者一致就匹配上了。

我建议你养成一个习惯:compatible 字符串用「厂商,设备名」的格式,避免冲突。内核里成千上万的驱动,万一重名了,你的驱动可能被别人的设备节点匹配走,那可就乱套了。

避坑指南: 我曾经遇到一个bug,设备树里 compatible 写成了 "my-gpio-device"(少了个逗号),驱动里写的是 "my-company,my-gpio-device"。结果 probe 函数死活不调用。查了两天才发现是字符串不匹配。所以,一定要逐字符核对

4.5 知识体系图

下面这张图,我把本章的核心逻辑画出来了。你看一眼就能明白:设备树、platform driver、GPIO、中断,它们是怎么串起来的。

设备树 (DTS) compatible = "xxx" gpios = <&gpio1 3 0> interrupts = <...> 解析 Platform Bus 匹配 platform_driver 调用 probe() 触发 Platform Driver probe(): 申请GPIO gpio_to_irq() request_irq() 操作 GPIO 子系统 gpio_get_value() gpio_set_value() 触发 中断子系统 中断处理函数 返回 IRQ_HANDLED 硬件描述层 驱动匹配层 GPIO操作层 中断响应层

从这张图你能看到,整个流程是:设备树 → Platform Bus → Platform Driver → GPIO子系统 → 中断子系统。每一层各司其职,耦合度很低。这也是为什么Linux驱动模型这么设计——方便复用、方便移植。

4.6 几个实用技巧

最后,分享几个我在实际项目中积累的小技巧:

  • 调试时多用 dev_dbg():别用 printk,dev_dbg 会带上设备名,一看就知道是哪个设备在打印。
  • GPIO编号别硬编码:永远从设备树获取。我见过有人直接在代码里写死 GPIO 17,换一块板子就废了。
  • 中断触发方式要确认:设备树里可以指定,但驱动里也可以覆盖。我建议在驱动里明确写出来,避免设备树配置错误。
  • probe 失败要干净:用 devm_ 系列函数,失败时内核自动回滚。别自己搞一堆 goto 标签,容易漏。

一个小建议: 如果你在调试时发现 probe 没被调用,先检查 /sys/bus/platform/devices/ 下有没有你的设备节点。如果没有,说明设备树没解析成功。如果有,检查 compatible 是否匹配。这是最快的排查路径。

嗯,这一章的内容就到这里。platform driver 和 GPIO 的结合,说白了就是「设备树描述硬件,驱动处理逻辑」。你把这个模型吃透了,后面写任何外设驱动都会顺手很多。


专注资料整理