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 操作生命周期
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 操作。基本流程如下:
- 导出 GPIO:向
/sys/class/gpio/export写入 GPIO 编号 - 设置方向:向
/sys/class/gpio/gpioN/direction写入 "in" 或 "out" - 读写电平:读取或写入
/sys/class/gpio/gpioN/value - 取消导出:向
/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 操作架构图
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 被谁占用。