第三章 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中断配置完整流程 设备树描述GPIO devm_gpiod_get 获取描述符 gpiod_to_irq 获取中断号 devm_request_irq 注册中断 中断处理函数执行 注意事项 • 引脚状态要配置正确 • 中断触发类型要匹配 • 处理函数要快进快出

最后说一句:GPIO中断处理函数里,别做复杂操作。打印日志、调度工作队列、或者用bottom half机制。你想想看,中断上下文里调msleep?那系统直接死给你看。

我的调试习惯:先用gpiomon在用户空间验证中断能不能触发,确认硬件没问题了,再写内核驱动。这样能快速定位问题是出在硬件还是软件。