4. GPIO子系统驱动开发:从框架到实战

GPIO,说白了就是芯片的“手脚”。你要控制LED亮灭、检测按键按下,都得靠它。在Linux里,GPIO驱动已经封装得很好了,我们不需要直接操作寄存器——除非你想自己造轮子。

我个人习惯,拿到一个新板子,先看GPIO的用法。因为这是最基础的外设,也是验证硬件是否正常的第一步。今天我们就从框架到实战,把GPIO子系统彻底搞明白。

4.1 GPIO子系统框架:三层结构

Linux的GPIO子系统分三层,我画了个图,你看一眼就懂了:

GPIO子系统三层架构 用户空间 应用程序 / sysfs接口 / libgpiod 内核空间 - GPIO子系统核心 gpiolib核心层:gpio_chip注册、pin控制、中断映射 硬件层 - SoC GPIO控制器 全志T113 GPIO寄存器 / 中断控制器 / pinctrl 驱动开发者主要工作:实现gpio_chip结构体,注册到gpiolib

用户空间通过/sys/class/gpio或libgpiod操作GPIO。内核空间里,gpiolib负责管理所有GPIO。硬件层就是芯片原厂提供的驱动,全志T113的GPIO控制器驱动已经写好了,我们只需要配置设备树,然后调用API就行。

我的经验:刚开始学的时候,别急着看gpiolib源码。先学会用API,等遇到性能瓶颈了再回头研究。我当年就是一头扎进源码,结果越看越晕。

4.2 设备树中GPIO的配置

设备树是Linux描述硬件的“语言”。全志T113的GPIO引脚,在设备树里用gpios属性来描述。格式是:

gpios = <&gpio_bank pin_index flags>;

举个例子,控制一个LED:

leds {
    compatible = "gpio-leds";
    led_red {
        label = "red-led";
        gpios = <&pio 1 2 GPIO_ACTIVE_HIGH>;
        default-state = "off";
    };
};

这里&pio是GPIO控制器的引用,1是bank号(PB组),2是引脚号(PB2),GPIO_ACTIVE_HIGH表示高电平点亮。

参数 说明 全志T113示例
gpio_bank GPIO组控制器节点引用 &pio (PA/PB/PC...)
pin_index 组内引脚编号,从0开始 2 表示该组第3个引脚
flags 电平极性:高有效/低有效 GPIO_ACTIVE_HIGH / LOW
注意:我曾经在项目里把bank号和引脚号搞反了,结果LED死活不亮。查了两天才发现是设备树写错了。全志T113的bank编号:PA=0, PB=1, PC=2, PD=3,以此类推。

4.3 使用GPIO子系统API控制LED

设备树配好了,驱动里怎么用?核心API就几个:

// 获取GPIO描述符
struct gpio_desc *gpiod_get(struct device *dev, const char *con_id, 
                            enum gpiod_flags flags);

// 设置方向
int gpiod_direction_output(struct gpio_desc *desc, int value);

// 设置/读取电平
void gpiod_set_value(struct gpio_desc *desc, int value);
int gpiod_get_value(struct gpio_desc *desc);

// 释放
void gpiod_put(struct gpio_desc *desc);

写一个LED驱动的核心代码:

#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>

struct led_data {
    struct gpio_desc *led_gpio;
};

static int led_probe(struct platform_device *pdev)
{
    struct led_data *data;
    struct device *dev = &pdev->dev;

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

    // 从设备树获取GPIO,con_id对应设备树里的"led-gpios"
    data->led_gpio = devm_gpiod_get(dev, "led", GPIOD_OUT_LOW);
    if (IS_ERR(data->led_gpio)) {
        dev_err(dev, "Failed to get LED GPIO\n");
        return PTR_ERR(data->led_gpio);
    }

    // 设置输出方向,初始低电平
    gpiod_direction_output(data->led_gpio, 0);

    platform_set_drvdata(pdev, data);
    return 0;
}

static int led_remove(struct platform_device *pdev)
{
    // devm_* 系列会自动释放,这里可以留空
    return 0;
}

static const struct of_device_id led_of_match[] = {
    { .compatible = "mycompany,led-driver" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, led_of_match);

static struct platform_driver led_driver = {
    .probe  = led_probe,
    .remove = led_remove,
    .driver = {
        .name = "my-led",
        .of_match_table = led_of_match,
    },
};
module_platform_driver(led_driver);

你想想看,用devm_gpiod_get的好处是什么?自动管理生命周期,probe里申请,remove时自动释放。我早期写驱动都是手动gpiod_get/gpiod_put配对,后来发现devm_系列函数太香了,省心不少。

4.4 按键输入与中断驱动

按键检测有两种方式:轮询和中断。轮询浪费CPU,中断才是正经做法。全志T113的GPIO可以配置为中断源,设备树里这样配:

key {
    compatible = "mycompany,key-driver";
    key-gpios = <&pio 3 5 GPIO_ACTIVE_LOW>;  // PD5,低电平表示按下
    interrupts-extended = <&pio 3 5 IRQ_TYPE_EDGE_FALLING>;
};

驱动里申请中断的API:

// 将GPIO转为中断号
int gpiod_to_irq(const struct gpio_desc *desc);

// 申请中断
int devm_request_irq(struct device *dev, unsigned int irq,
                     irq_handler_t handler, unsigned long flags,
                     const char *name, void *dev_id);

完整按键中断驱动示例:

struct key_data {
    struct gpio_desc *key_gpio;
    int irq;
};

static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
    struct key_data *data = dev_id;
    int val;

    // 读取当前电平,确认按键状态
    val = gpiod_get_value(data->key_gpio);
    if (val == 0) {
        pr_info("Key pressed!\n");
    } else {
        pr_info("Key released!\n");
    }

    return IRQ_HANDLED;
}

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

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

    // 获取按键GPIO
    data->key_gpio = devm_gpiod_get(dev, "key", GPIOD_IN);
    if (IS_ERR(data->key_gpio)) {
        dev_err(dev, "Failed to get key GPIO\n");
        return PTR_ERR(data->key_gpio);
    }

    // 设置为输入
    gpiod_direction_input(data->key_gpio);

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

    // 申请中断,下降沿触发
    ret = devm_request_irq(dev, data->irq, key_irq_handler,
                           IRQF_TRIGGER_FALLING, "my-key", data);
    if (ret) {
        dev_err(dev, "Failed to request IRQ\n");
        return ret;
    }

    dev_info(dev, "Key driver loaded, IRQ=%d\n", data->irq);
    platform_set_drvdata(pdev, data);
    return 0;
}

static const struct of_device_id key_of_match[] = {
    { .compatible = "mycompany,key-driver" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, key_of_match);

static struct platform_driver key_driver = {
    .probe  = key_probe,
    .remove = key_remove,
    .driver = {
        .name = "my-key",
        .of_match_table = key_of_match,
    },
};
module_platform_driver(key_driver);
关键点:中断处理函数里不要做耗时操作。我见过有人直接在中断里调用msleep,结果系统直接卡死。中断里只做标记,真正的处理放到工作队列或tasklet里。

4.5 避坑指南:我踩过的那些坑

  • GPIO复用冲突:全志T113的引脚功能很多,同一个引脚可能是GPIO,也可能是UART或I2C。设备树里配了GPIO,但pinctrl没配对,结果引脚没反应。检查/sys/kernel/debug/gpio可以看当前GPIO状态。
  • 中断触发方式:按键有抖动,下降沿触发可能一次按下触发多次中断。我一般加一个10ms的去抖定时器,或者用IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING双边沿触发,然后在软件里判断。
  • 设备树编译:改完设备树记得编译成dtb,然后更新到板子上。我经常改了dts忘了编译,折腾半天发现还是旧配置。
  • GPIO编号混淆:内核里GPIO编号是全局的,全志T113的PA0可能是0号,PB0可能是32号。用gpiod_get按名字获取最安全,别自己算编号。

4.6 调试技巧

调试GPIO驱动,我常用的几个方法:

  1. 查看GPIO状态:cat /sys/kernel/debug/gpio,能看到所有GPIO的申请者和方向。
  2. 手动操作GPIO:如果驱动还没写好,可以用echo 32 > /sys/class/gpio/export先手动测试硬件是否正常。
  3. 测量波形:示波器或逻辑分析仪直接量引脚,看电平变化。我遇到过设备树配对了,但硬件虚焊的情况,示波器一量就发现了。
  4. 打印中断次数:在中断处理函数里加一个计数器,cat /proc/interrupts看中断有没有触发。
小技巧:调试按键中断时,我习惯先写一个简单的字符设备驱动,把中断次数暴露给用户空间。这样不用每次改代码重新编译,直接cat /dev/mykey就能看到按键次数。

好了,GPIO子系统就讲到这里。从框架到设备树,从LED到按键中断,这些代码你都可以直接拿到全志T113上跑。记住,驱动开发的核心是“配好设备树,调好API”,剩下的就是耐心调试了。