第三章 GPIO驱动适配:从控制器到用户空间
GPIO,说白了就是芯片的「手脚」。你要让LED亮、检测按键按下、或者跟某个外设握个手,都得靠它。这一章我带你走一遍Tina Linux下GPIO适配的完整路径——从控制器驱动框架,到设备树描述,再到用户空间怎么玩,最后搞一个中断配置实战。
本章核心脉络:GPIO控制器驱动框架 → 设备树GPIO描述 → 用户空间操作 → 中断配置实战
3.1 GPIO控制器驱动框架
Linux内核的GPIO子系统,其实分了三层:
- 底层:芯片厂商实现的GPIO控制器驱动,操作寄存器
- 中间层:gpiolib核心代码,提供统一的API接口
- 上层:消费者驱动或用户空间,通过标准接口使用GPIO
我在项目中遇到过好几次,有人直接在板级文件里写死GPIO号,换了个内核版本就崩了。原因就是没理解这个分层——你该操作的是gpiolib提供的句柄,而不是硬编码的物理引脚号。
来看一个典型的GPIO控制器驱动注册流程:
static const struct gpio_chip my_gpio_chip = {
.label = "my-gpio",
.direction_input = my_gpio_direction_input,
.direction_output = my_gpio_direction_output,
.get = my_gpio_get,
.set = my_gpio_set,
.request = my_gpio_request,
.free = my_gpio_free,
.base = -1, // 动态分配基号
.ngpio = 32, // 32个引脚
.can_sleep = false,
};
static int my_gpio_probe(struct platform_device *pdev)
{
struct gpio_chip *chip = &my_gpio_chip;
chip->parent = &pdev->dev;
return gpiochip_add(chip); // 注册到gpiolib
}
这里有个关键点:base = -1。我建议你永远用动态分配,别自己指定基号。为什么?因为多个GPIO控制器可能冲突,你指定了100,别人也指定了100,就打架了。动态分配让内核帮你协调,省心。
| 回调函数 | 作用 | 注意事项 |
|---|---|---|
| direction_input | 设置引脚为输入 | 需要配置内部上拉/下拉 |
| direction_output | 设置引脚为输出并赋初值 | 初值要在函数内完成 |
| get | 读取引脚电平 | 注意去抖处理 |
| set | 设置引脚电平 | 输出值要缓存 |
| request | 申请引脚时的钩子 | 可以做时钟使能 |
| free | 释放引脚时的钩子 | 记得关时钟 |
3.2 设备树中GPIO描述
设备树是Tina Linux的灵魂。GPIO在设备树里怎么描述?我直接给你看一个实际例子:
// GPIO控制器节点
gpio0: gpio@2000000 {
compatible = "allwinner,sun8i-gpio";
reg = <0x02000000 0x1000>;
interrupts = <GIC_SPI 64 IRQ_TYPE_LEVEL_HIGH>;
#gpio-cells = <3>; // 三个参数:bank, pin, 标志
gpio-controller;
#interrupt-cells = <3>;
interrupt-controller;
};
// 消费者节点——一个LED
led {
compatible = "gpio-leds";
led-power {
label = "power";
gpios = <&gpio0 1 12 GPIO_ACTIVE_HIGH>; // PB12
default-state = "on";
};
};
你注意看#gpio-cells = <3>这个属性。它告诉内核:每个GPIO引用需要3个参数。Allwinner的方案里,第一个是bank序号,第二个是pin序号,第三个是标志位。不同芯片可能不一样,有的只用2个cell。
我的习惯:写设备树时,先确认芯片的GPIO cell格式。去内核文档里翻Documentation/devicetree/bindings/gpio/,或者直接看同系列芯片的dtsi文件。别自己猜,猜错了板子起不来。
GPIO标志位常用的几个宏:
GPIO_ACTIVE_HIGH(0):高电平有效GPIO_ACTIVE_LOW(1):低电平有效GPIO_PULL_UP:使能上拉GPIO_PULL_DOWN:使能下拉
我曾经踩过一个坑:设备树里写的是GPIO_ACTIVE_LOW,但驱动里没做电平反转。结果LED亮的时候,逻辑电平反而是低的。排查了半天,最后发现是设备树和驱动对「有效电平」的理解不一致。
3.3 用户空间GPIO操作
Tina Linux支持两种用户空间GPIO操作方式:老的sysfs接口和新的chardev接口。我建议你直接用chardev,也就是libgpiod。为什么?sysfs在5.x内核里已经标记为废弃了,新内核随时可能砍掉。
安装libgpiod工具:
# make menuconfig 中选中
# Utilities ---> gpio-utils ---> libgpiod-tools
常用命令一览:
| 命令 | 功能 | 示例 |
|---|---|---|
| gpiodetect | 列出所有GPIO控制器 | gpiodetect |
| gpioinfo | 查看控制器详细信息 | gpioinfo gpiochip0 |
| gpioset | 设置引脚输出电平 | gpioset gpiochip0 12=1 |
| gpioget | 读取引脚输入电平 | gpioget gpiochip0 12 |
| gpiomon | 监控引脚中断事件 | gpiomon gpiochip0 12 |
写个简单的C程序操作GPIO:
#include <gpiod.h>
#include <stdio.h>
int main(void)
{
struct gpiod_chip *chip;
struct gpiod_line *line;
int ret;
chip = gpiod_chip_open_by_name("gpiochip0");
if (!chip) {
perror("open chip failed");
return -1;
}
line = gpiod_chip_get_line(chip, 12); // 获取PB12
if (!line) {
perror("get line failed");
gpiod_chip_close(chip);
return -1;
}
ret = gpiod_line_request_output(line, "myapp", 0);
if (ret < 0) {
perror("request output failed");
gpiod_chip_close(chip);
return -1;
}
gpiod_line_set_value(line, 1); // 输出高电平
sleep(1);
gpiod_line_set_value(line, 0); // 输出低电平
gpiod_line_release(line);
gpiod_chip_close(chip);
return 0;
}
编译时链接-lgpiod。嗯,这里要注意:gpiod_chip_open_by_name里的名字,得跟gpiodetect看到的一致。有的芯片叫gpiochip0,有的叫pinctrl-gpio,别写死了。
3.4 GPIO中断配置实战
终于到实战环节了。我们做一个按键中断的例子——按键按下时,打印一条日志。
先看设备树怎么配:
key {
compatible = "my-company,my-key";
gpios = <&gpio0 1 14 GPIO_ACTIVE_LOW>; // PB14
interrupts-extended = <&gpio0 1 14 IRQ_TYPE_EDGE_FALLING>;
};
驱动代码核心部分:
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
struct my_key_dev *dev = dev_id;
int val;
val = gpiod_get_value(dev->gpiod);
dev_info(dev->dev, "key pressed, value=%d\n", val);
return IRQ_HANDLED;
}
static int key_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct my_key_dev *key_dev;
int ret, irq;
key_dev = devm_kzalloc(dev, sizeof(*key_dev), GFP_KERNEL);
if (!key_dev)
return -ENOMEM;
key_dev->dev = dev;
// 获取GPIO描述符
key_dev->gpiod = devm_gpiod_get(dev, NULL, GPIOD_IN);
if (IS_ERR(key_dev->gpiod))
return PTR_ERR(key_dev->gpiod);
// 获取中断号
irq = gpiod_to_irq(key_dev->gpiod);
if (irq < 0)
return irq;
// 申请中断
ret = devm_request_irq(dev, irq, key_irq_handler,
IRQF_TRIGGER_FALLING, "my-key", key_dev);
if (ret)
return ret;
return 0;
}
我曾经踩过的坑:中断处理函数里调用gpiod_get_value,但忘了引脚配置了内部上拉。结果按键没按时,引脚浮空,中断疯狂触发。后来加上GPIOD_IN | GPIOD_PULL_UP才稳定。记住:外部中断一定要处理好引脚状态,不然你会被假中断搞疯。
中断触发类型怎么选?我一般这么判断:
- 按键:用下降沿或上升沿,配合去抖
- 电平敏感设备:用高电平或低电平触发
- 编码器:双边沿触发
去抖处理,我建议在硬件上加RC滤波,软件里再加10-20ms的延时判断。别只靠软件,硬件滤波能省你很多调试时间。
最后说一句:GPIO中断处理函数里,别做复杂操作。打印日志、调度工作队列、或者用bottom half机制。你想想看,中断上下文里调msleep?那系统直接死给你看。
我的调试习惯:先用gpiomon在用户空间验证中断能不能触发,确认硬件没问题了,再写内核驱动。这样能快速定位问题是出在硬件还是软件。