第4章 GPIO驱动开发:GPIO子系统介绍、设备树中GPIO的配置、基于GPIO的LED驱动实战

各位同学,今天我们来聊聊GPIO驱动开发。说实话,GPIO是嵌入式开发里最基础也最常用的外设了。我刚开始做驱动时,第一个练手的项目就是点灯。你别笑,能把灯点得稳、点得准,说明你对整个驱动框架已经摸透了。

4.1 GPIO子系统:内核里的“万能接口”

GPIO子系统,说白了就是Linux内核给开发者提供的一套标准API。你想想看,不同芯片的GPIO寄存器地址、控制方式都不一样,要是每个驱动都直接操作寄存器,那代码就没法移植了。

内核的GPIO子系统分三层:

  • 底层:芯片厂商实现的gpio_chip,负责读写寄存器
  • 中间层:gpiolib,提供统一的接口封装
  • 上层:驱动开发者调用的gpiod_xxx系列API

我在项目中遇到过一件事:有个同事直接操作GPIO寄存器来实现按键检测,结果换了颗芯片后,整个驱动都得重写。这就是没用好子系统的教训。

核心API速查表

函数作用
gpiod_get()获取GPIO描述符
gpiod_direction_output()设置为输出模式
gpiod_set_value()设置输出电平
gpiod_direction_input()设置为输入模式
gpiod_get_value()读取输入电平

4.2 设备树中GPIO的配置:把硬件描述交给DTS

设备树,就是用来描述硬件拓扑的。GPIO的配置都在这里完成。我个人习惯把设备树里的GPIO节点想象成“硬件接线图”——哪个引脚接了什么东西,一目了然。

来看一个典型的LED节点配置:

leds {
    compatible = "gpio-leds";
    led0: led-red {
        label = "red-led";
        gpios = &gpio1 15 GPIO_ACTIVE_LOW;
        default-state = "off";
    };
};

这里有几个关键点:

  • gpios属性:第一个参数是GPIO控制器,第二个是引脚号,第三个是有效电平
  • GPIO_ACTIVE_LOW:表示低电平点亮。嗯,这里要注意,很多新手会搞反
  • default-state:上电后的默认状态

我的小技巧:调试设备树时,可以在内核启动参数里加gpio_debug,这样系统会打印出所有GPIO的申请和释放信息。我曾经靠这个定位过一个GPIO被两个驱动同时占用的bug。

4.3 基于GPIO的LED驱动实战:从零开始点灯

好,理论说完了,咱们直接上手写代码。这个LED驱动,我建议你用gpio-leds框架来做,内核已经封装好了,你只需要提供设备树节点就行。

但为了让你真正理解底层原理,我带你手写一个简单的LED驱动:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.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
    data->led_gpio = devm_gpiod_get(dev, NULL, GPIOD_OUT_LOW);
    if (IS_ERR(data->led_gpio))
        return PTR_ERR(data->led_gpio);

    gpiod_set_value(data->led_gpio, 1);  // 点亮LED
    dev_info(dev, "LED is on!\n");

    platform_set_drvdata(pdev, data);
    return 0;
}

static int led_remove(struct platform_device *pdev)
{
    struct led_data *data = platform_get_drvdata(pdev);
    gpiod_set_value(data->led_gpio, 0);  // 熄灭LED
    return 0;
}

static const struct of_device_id led_of_match[] = {
    { .compatible = "my-led-driver" },
    { }
};
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);

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

这段代码里,我用了devm_gpiod_get这个函数。为什么用带devm_前缀的版本?因为它会自动管理资源,驱动卸载时自动释放GPIO。你想想看,要是忘了释放GPIO,下次加载驱动就会报错。

避坑指南:我曾经在probe函数里直接调用gpio_requestgpio_direction_output,结果在设备树里改了引脚号后,驱动就崩溃了。后来改用gpiod_系列API,问题就解决了。因为gpiod_会自动解析设备树里的gpios属性,不用你手动传引脚号。

4.4 调试技巧:让LED“说话”

驱动写完了,怎么验证?我一般用这几个方法:

  • 看内核日志dmesg | grep my_led,看probe有没有成功
  • 操作sysfs:如果用了gpio-leds框架,/sys/class/leds/下会有对应节点,直接echo 1 > brightness就能控制
  • 示波器测量:实在不行就上示波器,看引脚电平有没有变化

我记得有一次,LED死活不亮。查了半天,发现是设备树里GPIO_ACTIVE_LOW写成了GPIO_ACTIVE_HIGH。电平反了,灯当然不亮。这种低级错误,犯过一次就记住了。

好了,这一章的内容就到这里。GPIO驱动开发,说白了就是“获取描述符、设置方向、读写电平”这三步。但每一步里都有坑,多动手、多踩坑,你才能真正成为老手。