3. GPIO 操作基础:GPIO 申请与释放、方向设置、电平读写、sysfs 接口与 libgpiod

本章是 Linux GPIO 驱动开发的基石。在嵌入式 Linux 系统中,GPIO 是最常用的外设接口之一,用于控制 LED、读取按键、驱动继电器等。掌握 GPIO 的申请与释放、方向设置、电平读写等核心操作,以及用户空间的 sysfs 和 libgpiod 接口,是编写可靠驱动程序的必备技能。

3.1 GPIO 操作核心概念

GPIO 操作在 Linux 内核中遵循“申请 → 配置 → 使用 → 释放”的生命周期模型。内核提供了标准的 GPIO 子系统框架,屏蔽了底层硬件差异,为驱动开发者提供统一的 API。

关键原则:GPIO 资源必须在使用前显式申请,使用后及时释放。未申请的 GPIO 操作会导致内核警告或系统崩溃。

3.1.1 GPIO 操作生命周期

GPIO 操作生命周期 GPIO 申请 方向设置 电平读写 GPIO 释放 生命周期必须完整:申请 → 配置 → 使用 → 释放,缺一不可

3.2 内核空间 GPIO 操作 API

Linux 内核从 2.6.21 开始引入通用 GPIO 子系统,提供了一套标准化的 API。以下是最常用的内核 GPIO 操作函数。

3.2.1 GPIO 申请与释放

函数 原型 说明
gpio_request int gpio_request(unsigned gpio, const char *label) 申请 GPIO 资源,返回 0 成功
gpio_free void gpio_free(unsigned gpio) 释放 GPIO 资源
gpio_request_one int gpio_request_one(unsigned gpio, unsigned long flags, const char *label) 申请并同时设置方向和初始电平
gpio_request_array int gpio_request_array(const struct gpio *array, size_t num) 批量申请多个 GPIO
最佳实践:推荐使用 gpio_request_one 替代 gpio_request + gpio_direction_output 的组合,减少代码量和出错概率。

3.2.2 方向设置

函数 原型 说明
gpio_direction_input int gpio_direction_input(unsigned gpio) 设置为输入方向
gpio_direction_output int gpio_direction_output(unsigned gpio, int value) 设置为输出方向,并设置初始电平

3.2.3 电平读写

函数 原型 说明
gpio_get_value int gpio_get_value(unsigned gpio) 读取 GPIO 电平,返回 0 或 1
gpio_set_value void gpio_set_value(unsigned gpio, int value) 设置 GPIO 电平,value 为 0 或 1
gpio_cansleep int gpio_cansleep(unsigned gpio) 检查 GPIO 操作是否可能睡眠
注意:在中断上下文或自旋锁保护的临界区中,不能使用可能睡眠的 GPIO 操作函数。使用前应通过 gpio_cansleep 检查。

3.2.4 完整示例代码

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

#define LED_GPIO  17
#define KEY_GPIO  18

static int __init gpio_demo_init(void)
{
    int ret;

    /* 申请 LED GPIO,设置为输出,初始低电平 */
    ret = gpio_request_one(LED_GPIO, GPIOF_OUT_INIT_LOW, "led");
    if (ret < 0) {
        pr_err("Failed to request LED GPIO %d\n", LED_GPIO);
        return ret;
    }

    /* 申请按键 GPIO,设置为输入 */
    ret = gpio_request_one(KEY_GPIO, GPIOF_IN, "key");
    if (ret < 0) {
        pr_err("Failed to request KEY GPIO %d\n", KEY_GPIO);
        gpio_free(LED_GPIO);
        return ret;
    }

    /* 读取按键状态并控制 LED */
    if (gpio_get_value(KEY_GPIO)) {
        gpio_set_value(LED_GPIO, 1);
    } else {
        gpio_set_value(LED_GPIO, 0);
    }

    return 0;
}

static void __exit gpio_demo_exit(void)
{
    gpio_set_value(LED_GPIO, 0);
    gpio_free(LED_GPIO);
    gpio_free(KEY_GPIO);
}

module_init(gpio_demo_init);
module_exit(gpio_demo_exit);

3.3 用户空间 GPIO 操作接口

Linux 提供了两种用户空间 GPIO 操作方式:传统的 sysfs 接口和现代的 libgpiod 库。sysfs 接口在较新内核中已被标记为弃用,libgpiod 是推荐方案。

3.3.1 sysfs 接口(传统方式)

sysfs 接口通过 /sys/class/gpio 目录暴露 GPIO 操作。基本流程如下:

  1. 导出 GPIO:向 /sys/class/gpio/export 写入 GPIO 编号
  2. 设置方向:向 /sys/class/gpio/gpioN/direction 写入 "in" 或 "out"
  3. 读写电平:读取或写入 /sys/class/gpio/gpioN/value
  4. 取消导出:向 /sys/class/gpio/unexport 写入 GPIO 编号
# 导出 GPIO 17
echo 17 > /sys/class/gpio/export

# 设置为输出方向
echo out > /sys/class/gpio/gpio17/direction

# 设置高电平
echo 1 > /sys/class/gpio/gpio17/value

# 读取电平
cat /sys/class/gpio/gpio17/value

# 取消导出
echo 17 > /sys/class/gpio/unexport
sysfs 弃用说明:自 Linux 4.8 起,sysfs GPIO 接口被标记为弃用(DEPRECATED),建议新项目使用 libgpiod。部分嵌入式 BSP 仍支持 sysfs,但长期维护应迁移至 libgpiod。

3.3.2 libgpiod 库(现代方式)

libgpiod 是 GPIO 字符设备接口的用户空间库,提供更安全、更高效的 GPIO 操作方式。它通过 /dev/gpiochipN 字符设备与内核交互。

命令行工具 功能 示例
gpiodetect 列出系统中的 GPIO 控制器 gpiodetect
gpioinfo 显示 GPIO 芯片的详细信息 gpioinfo gpiochip0
gpioset 设置 GPIO 电平 gpioset gpiochip0 17=1
gpioget 读取 GPIO 电平 gpioget gpiochip0 18
gpiomon 监控 GPIO 中断事件 gpiomon gpiochip0 18

3.3.3 libgpiod C 语言编程示例

#include <gpiod.h>
#include <stdio.h>
#include <unistd.h>

#define CONSUMER  "gpio-demo"
#define GPIO_CHIP "/dev/gpiochip0"

int main(void)
{
    struct gpiod_chip *chip;
    struct gpiod_line *led_line, *key_line;
    int ret, val;

    /* 打开 GPIO 芯片 */
    chip = gpiod_chip_open_by_name(GPIO_CHIP);
    if (!chip) {
        perror("Open gpiochip failed");
        return -1;
    }

    /* 获取 LED 线(GPIO 17)并设置为输出 */
    led_line = gpiod_chip_get_line(chip, 17);
    if (!led_line) {
        perror("Get LED line failed");
        gpiod_chip_close(chip);
        return -1;
    }

    ret = gpiod_line_request_output(led_line, CONSUMER, 0);
    if (ret < 0) {
        perror("Request LED output failed");
        gpiod_chip_close(chip);
        return -1;
    }

    /* 获取按键线(GPIO 18)并设置为输入 */
    key_line = gpiod_chip_get_line(chip, 18);
    if (!key_line) {
        perror("Get KEY line failed");
        gpiod_chip_close(chip);
        return -1;
    }

    ret = gpiod_line_request_input(key_line, CONSUMER);
    if (ret < 0) {
        perror("Request KEY input failed");
        gpiod_chip_close(chip);
        return -1;
    }

    /* 读取按键并控制 LED */
    val = gpiod_line_get_value(key_line);
    gpiod_line_set_value(led_line, val ? 1 : 0);

    printf("Key value: %d, LED set to %d\n", val, val ? 1 : 0);

    /* 清理 */
    gpiod_line_release(led_line);
    gpiod_line_release(key_line);
    gpiod_chip_close(chip);

    return 0;
}

3.4 sysfs 与 libgpiod 对比

对比维度 sysfs 接口 libgpiod 接口
内核版本支持 2.6.21 ~ 当前(弃用) 4.8+(推荐)
操作方式 文件读写(echo/cat) 字符设备 ioctl
性能 较低(每次操作需打开/关闭文件) 较高(批量操作、事件驱动)
中断支持 有限(poll/select) 完善(事件监控、边沿触发)
批量操作 不支持 支持(一次设置多个 GPIO)
安全性 低(无权限控制) 高(文件权限 + 线请求机制)
长期维护 不推荐 推荐

3.5 GPIO 操作架构图

Linux GPIO 操作架构 用户空间 sysfs 接口 libgpiod 库 用户应用程序 内核空间 GPIO 子系统核心 GPIO 字符设备驱动 GPIO 控制器驱动 硬件层 SoC GPIO 控制器 / 外部 GPIO 扩展芯片 用户空间通过 sysfs 或 libgpiod 与内核 GPIO 子系统交互,最终操作硬件寄存器

3.6 常见问题与避坑指南

  • GPIO 编号冲突:多个驱动同时申请同一个 GPIO 会导致 gpio_request 返回 -EBUSY。使用 gpio_is_valid 检查编号有效性。
  • 中断上下文操作:在中断处理函数中调用 gpio_set_value 可能触发睡眠,应使用 gpio_set_value_cansleep 的原子版本。
  • 电平反转:部分硬件存在电平反转(如高电平灭灯),可通过设备树 gpio-active-low 属性或软件取反处理。
  • sysfs 权限:sysfs 接口默认只有 root 可操作,嵌入式系统可通过 udev 规则或修改权限解决。
  • libgpiod 版本兼容:不同发行版 libgpiod API 略有差异,编译时注意头文件路径和链接库 -lgpiod。
调试技巧:使用 gpioinfo 查看 GPIO 当前状态(方向、电平、使用者),快速定位 GPIO 被谁占用。

3.7 本章小结