3、GPIO驱动开发:GPIO子系统框架、设备树配置、GPIO控制LED实战

好,咱们今天聊点实在的——GPIO驱动开发。

很多刚入行的朋友觉得GPIO太简单了,不就是拉高拉低嘛。嗯,从应用层看确实是这样。但到了底层驱动,你会发现事情没那么简单。我当年第一次在嵌入式Linux上点灯,折腾了整整两天,最后发现是设备树里一个引脚复用配置写错了。

所以这一节,咱们把GPIO子系统掰开揉碎了讲。从框架到设备树,再到实战,一条线串下来。

3.1 GPIO子系统框架:别被“子系统”三个字吓到

Linux的GPIO子系统,说白了就是一套标准化的接口。它把芯片厂商千奇百怪的GPIO操作方式,统一成一套API给驱动开发者用。

你想想看,不同厂家的芯片,寄存器地址不一样,控制位不一样,甚至电平极性都可能相反。如果没有子系统,每个驱动都得自己操作寄存器,那代码就没法看了。

GPIO子系统的分层结构大致是这样的:

  • GPIO 控制器驱动层:芯片厂商提供,负责操作具体寄存器。比如设置方向、读取电平、设置中断触发方式等。
  • GPIO 核心层:内核自带,提供统一的接口。比如 gpio_request()gpio_direction_output()gpio_set_value() 等。
  • GPIO 消费层:我们写驱动时用的那一层。通过核心层提供的API,操作具体的GPIO引脚。

核心要点:你写驱动时,永远不要直接操作寄存器。用 gpio_set_value() 这类API,内核会帮你搞定底层差异。

我个人习惯把GPIO子系统想象成一个“中间人”。你告诉它“我要让GPIO1_2输出高电平”,它负责找到对应的寄存器,把正确的位写进去。至于寄存器地址是0x1234还是0x5678,你不用关心。

3.2 设备树配置:GPIO的“身份证”

设备树,就是用来描述硬件信息的。GPIO引脚在设备树里怎么描述?我直接给你看一个典型例子:

// 设备树中定义一个LED节点
leds {
    compatible = "gpio-leds";
    
    led_red {
        label = "red-led";
        gpios = &gpio1 2 GPIO_ACTIVE_HIGH;
        default-state = "off";
    };
    
    led_green {
        label = "green-led";
        gpios = &gpio2 5 GPIO_ACTIVE_LOW;
        default-state = "on";
    };
};

这里有几个关键点,我一个个说:

  • gpios = &gpio1 2 GPIO_ACTIVE_HIGH:意思是“使用gpio1控制器,第2号引脚,高电平有效”。
  • GPIO_ACTIVE_HIGHGPIO_ACTIVE_LOW:定义电平极性。我曾经踩过一个坑——硬件设计上LED是低电平点亮,但我配成了高电平有效,结果灯死活不亮。查了半天才发现是极性搞反了。
  • default-state:上电后的默认状态。这个在系统启动时很有用,比如电源指示灯一上电就亮。

注意:设备树里的GPIO编号不是绝对的物理引脚号。它是相对于GPIO控制器的偏移量。比如 gpio1 2 表示gpio1控制器的第2个引脚,不是芯片的第2号引脚。这个一定要搞清楚,否则你会配错。

3.3 GPIO控制LED实战:从设备树到驱动代码

理论说完了,咱们动手。目标:通过GPIO控制一个LED闪烁。

3.3.1 设备树配置

假设我们的LED接在GPIO3_12上,高电平点亮。设备树里这样写:

/ {
    my_led {
        compatible = "my-company,my-led";
        led-gpio = &gpio3 12 GPIO_ACTIVE_HIGH;
    };
};

注意这里我用了自定义的compatible字符串 "my-company,my-led"。驱动里会通过这个字符串匹配到设备节点。

3.3.2 驱动代码实现

驱动代码分三步走:获取GPIO、申请GPIO、操作GPIO。我直接上代码:

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

struct my_led_data {
    struct gpio_desc *led_gpio;
};

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

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

    // 第一步:从设备树获取GPIO描述符
    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);
    }

    platform_set_drvdata(pdev, data);

    // 第二步:让LED闪烁几次
    for (int i = 0; i < 5; i++) {
        gpiod_set_value(data->led_gpio, 1);  // 亮
        msleep(500);
        gpiod_set_value(data->led_gpio, 0);  // 灭
        msleep(500);
    }

    dev_info(dev, "LED probe success\n");
    return 0;
}

static int my_led_remove(struct platform_device *pdev)
{
    struct my_led_data *data = platform_get_drvdata(pdev);
    
    // 关闭LED
    gpiod_set_value(data->led_gpio, 0);
    
    dev_info(&pdev->dev, "LED removed\n");
    return 0;
}

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

static struct platform_driver my_led_driver = {
    .probe  = my_led_probe,
    .remove = my_led_remove,
    .driver = {
        .name = "my_led",
        .of_match_table = my_led_of_match,
    },
};

module_platform_driver(my_led_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple GPIO LED driver");

这段代码里,我用了 devm_gpiod_get() 而不是老式的 gpio_request()。为什么?因为 devm_ 系列API会自动管理资源,驱动卸载时自动释放GPIO,省得你忘了写清理代码。我建议你也用这套接口,少踩坑。

小技巧GPIOD_OUT_LOW 表示申请GPIO时初始化为输出低电平。如果你想要高电平,用 GPIOD_OUT_HIGH。这个参数能省掉你后续的一次 gpiod_set_value() 调用。

3.3.3 编译与测试

把驱动编译进内核或者编译成模块都行。如果是模块,加载后你会看到内核日志打印“LED probe success”,同时LED闪烁5次。

我曾经遇到一个问题:驱动加载后LED没反应。查了半天,发现是设备树里 led-gpio 属性名写成了 led_gpio(下划线 vs 短横线)。设备树里属性名只能用短横线,不能用下划线。这个细节,坑过不少人。

3.4 避坑指南:GPIO驱动开发常见问题

做GPIO驱动开发,有几个坑我几乎每次都会遇到:

问题 原因 解决方法
GPIO被其他驱动占用 引脚复用冲突 检查设备树中pinctrl配置,确保引脚没有被其他外设使用
电平极性反了 设备树中GPIO_ACTIVE_HIGH/LOW配置错误 对照原理图确认硬件电平逻辑
GPIO编号不对 混淆了物理引脚号和控制器偏移量 查看芯片手册,确认GPIO控制器的基地址和偏移
驱动probe不执行 compatible字符串不匹配 cat /proc/device-tree/ 检查设备树节点内容

嗯,这里要特别说一下引脚复用。很多SoC的GPIO引脚同时具备UART、I2C、SPI等功能。如果你在设备树里配了GPIO功能,但某个外设驱动也占用了同一个引脚,就会冲突。我建议你在设备树里显式配置pinctrl,明确指定引脚功能。

3.5 总结

GPIO驱动开发,说白了就是三板斧:设备树描述硬件、驱动获取GPIO、API操作电平。但每一板斧都有细节,稍不注意就翻车。

我个人建议,刚开始做的时候,先用 gpio-leds 这个内核自带的驱动试试水。它不需要你写任何代码,只要配好设备树,LED就能工作。等熟悉了流程,再自己写驱动。

下一节,我们会聊中断驱动开发。到时候你会发现,GPIO和中断是密不可分的——很多中断源就是GPIO引脚。好,今天就到这里,有问题咱们群里聊。