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、中断,它们是怎么串起来的。
从这张图你能看到,整个流程是:设备树 → 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 的结合,说白了就是「设备树描述硬件,驱动处理逻辑」。你把这个模型吃透了,后面写任何外设驱动都会顺手很多。