3、GPIO驱动实战:从框架到LED点亮

GPIO驱动,说白了就是嵌入式Linux里最基础也最常用的外设操作。你想想看,一个嵌入式板子上,控制LED、读取按键、驱动蜂鸣器,哪个离得开GPIO?我刚开始做驱动时,总觉得GPIO太简单,结果在项目里踩了不少坑。嗯,这一章咱们就把GPIO子系统彻底讲透。

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

Linux内核的GPIO子系统,其实就三层。我习惯把它想象成一个「三层蛋糕」:

  • 最底层:硬件相关层,直接操作GPIO寄存器
  • 中间层:GPIO核心层,提供统一的接口管理
  • 最上层:驱动使用层,也就是我们写驱动时调用的API

为什么会这样设计?说白了就是为了解耦。你换了一个芯片,只需要改底层,上层代码完全不用动。我在项目中遇到过,从i.MX6换到i.MX8,LED驱动代码一行没改,只改了设备树。

核心要点:GPIO子系统通过gpiolib实现,提供gpio_request()、gpio_direction_output()、gpio_set_value()等标准接口。驱动开发者只需要调用这些API,不需要关心底层寄存器。

下面这张图,是我画的三层架构关系,你看一眼就明白了:

GPIO子系统三层架构 驱动使用层 gpio_request() / gpio_direction_output() / gpio_set_value() GPIO核心层 (gpiolib) gpio_chip注册 / 中断映射 / 引脚控制 硬件相关层 GPIO寄存器操作 / 引脚复用配置 驱动开发者只需要关注最上层API,底层由芯片厂商实现

3.2 设备树中的GPIO描述

设备树里描述GPIO,其实就几个关键属性。我刚开始学的时候,总搞混gpios和gpio的区别。后来踩了一次坑才记住——gpios是复数,表示可以有多个GPIO;gpio是单数,只表示一个。

来看一个典型的LED节点描述:

/ {
    leds {
        compatible = "gpio-leds";
        
        led0: led-red {
            label = "red-led";
            gpios = &gpio1 3 GPIO_ACTIVE_LOW;
            default-state = "off";
        };
        
        led1: led-green {
            label = "green-led";
            gpios = &gpio1 4 GPIO_ACTIVE_HIGH;
            default-state = "on";
        };
    };
};

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

  • compatible:匹配驱动,内核里已经有gpio-leds驱动,可以直接用
  • gpios:三个参数——GPIO控制器、引脚号、有效电平标志
  • GPIO_ACTIVE_LOW:低电平有效,意味着写0时灯亮,写1时灯灭
  • GPIO_ACTIVE_HIGH:高电平有效,写1亮,写0灭

我的习惯:设备树里尽量用GPIO_ACTIVE_LOW或GPIO_ACTIVE_HIGH宏定义,不要直接写0或1。这样代码可读性高,别人一看就知道电平逻辑。

3.3 GPIO操作核心API

写GPIO驱动,最常用的就这几个API。我列个表格,方便你对照:

API函数 功能说明 返回值
gpio_request(unsigned gpio, const char *label) 申请GPIO资源,防止被其他模块占用 0成功,负值失败
gpio_direction_output(unsigned gpio, int value) 设置GPIO为输出方向,并设置初始电平 0成功,负值失败
gpio_direction_input(unsigned gpio) 设置GPIO为输入方向 0成功,负值失败
gpio_set_value(unsigned gpio, int value) 设置GPIO输出电平(0或1) 无返回值
gpio_get_value(unsigned gpio) 读取GPIO当前电平 0或1
gpio_free(unsigned gpio) 释放GPIO资源 无返回值

注意:gpio_request()一定要在操作GPIO之前调用。我曾经在项目里忘了调用,结果两个驱动同时操作同一个引脚,导致LED忽明忽暗,查了两天才找到原因。

3.4 实现LED驱动:完整代码

好了,理论说完了,咱们直接上代码。这是一个完整的LED驱动,我加了详细的注释:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/gpio.h>
#include <linux/delay.h>

#define LED_GPIO 3  // 假设使用GPIO1_3

static int __init led_driver_init(void)
{
    int ret;
    
    // 第一步:申请GPIO资源
    ret = gpio_request(LED_GPIO, "my-led");
    if (ret) {
        pr_err("Failed to request GPIO %d\n", LED_GPIO);
        return ret;
    }
    
    // 第二步:设置输出方向,初始为低电平
    ret = gpio_direction_output(LED_GPIO, 0);
    if (ret) {
        pr_err("Failed to set GPIO direction\n");
        gpio_free(LED_GPIO);
        return ret;
    }
    
    pr_info("LED driver initialized successfully\n");
    
    // 测试:闪烁3次
    int i;
    for (i = 0; i < 3; i++) {
        gpio_set_value(LED_GPIO, 1);  // 亮
        mdelay(500);
        gpio_set_value(LED_GPIO, 0);  // 灭
        mdelay(500);
    }
    
    return 0;
}

static void __exit led_driver_exit(void)
{
    gpio_set_value(LED_GPIO, 0);  // 关闭LED
    gpio_free(LED_GPIO);          // 释放GPIO
    pr_info("LED driver exited\n");
}

module_init(led_driver_init);
module_exit(led_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple LED GPIO Driver");

3.5 驱动加载与测试

代码写完了,怎么验证?我一般按这个步骤来:

  1. 编译驱动:把代码放到内核源码的drivers目录下,或者作为外部模块编译
  2. 加载模块:insmod led_driver.ko
  3. 查看日志:dmesg | tail,看有没有错误信息
  4. 检查GPIO状态:cat /sys/kernel/debug/gpio,查看引脚是否被正确申请
  5. 卸载模块:rmmod led_driver,确认LED熄灭

调试技巧:如果LED不亮,先检查设备树里GPIO号是否正确。我常用的方法是,在/sys/class/gpio目录下手动操作一下,确认硬件没问题,再查驱动代码。

3.6 常见故障与定位

做GPIO驱动,难免遇到问题。我总结了几种常见情况:

现象 可能原因 排查方法
gpio_request返回-EBUSY GPIO已被其他驱动占用 检查/sys/kernel/debug/gpio,看谁在用
LED不亮 GPIO号错误或电平逻辑反了 用gpio_set_value手动切换,万用表测电压
LED亮度异常 引脚复用配置错误 检查设备树pinctrl设置
卸载后LED仍亮 没有在exit函数里关闭 检查gpio_set_value和gpio_free调用顺序

我曾经遇到过一个特别坑的问题:LED在加载驱动时正常闪烁,但卸载后再加载就不亮了。查了半天,发现是gpio_free之后,引脚被复用成了其他功能。解决办法是在设备树里加上pinctrl-0属性,强制指定引脚功能。

我的建议:写GPIO驱动时,一定要在init函数里检查每个API的返回值。很多新手觉得gpio_request不会失败,结果在量产时出了问题。宁可多写几行错误处理,也别偷懒。