在Linux用户空间操作GPIO,经历了从sysfs到libgpiod的演进。
GPIO在内核中的模型
GPIO在Linux内核中由gpio_controller(通过gpiod描述符管理)抽象。每个GPIO控制器(如i.MX的GPIO1~GPIO5)向内核注册一组GPIO,板级设备树描述它们如何被具体硬件使用(LED、按键、复位信号等)。
// 设备树中GPIO的使用示例leds{compatible="gpio-leds";led0{label="user-led";gpios=<&gpio13 GPIO_ACTIVE_LOW>;// GPIO1_3, 低有效default-state="off";};};keys{compatible="gpio-keys";button0{label="user-button";gpios=<&gpio25 GPIO_ACTIVE_LOW>;// GPIO2_5, 低有效linux,code=<KEY_ENTER>;};};
gpios = <&gpio1 3 GPIO_ACTIVE_LOW>中,&gpio1引用GPIO控制器,3是控制器内编号,GPIO_ACTIVE_LOW告诉内核该引脚低电平为"有效"逻辑。应用层调用gpiod_set_value()时只需关心逻辑值,极性由内核自动处理——这和MCU的HAL库HAL_GPIO_WritePin需要自己管极性不同,内核帮屏蔽了这层。
方案一:sysfs接口
sysfs接口是早期的用户空间GPIO操作方式,通过文件系统节点暴露GPIO。
# 导出GPIO(以GPIO1_3为例,内核中全局编号为 1*32+3 = 35)echo 35 > /sys/class/gpio/export# 设置方向echo out > /sys/class/gpio/gpio35/direction# 写值(受active_low影响)echo 1 > /sys/class/gpio/gpio35/value# 读值cat /sys/class/gpio/gpio35/value# 设置低电平有效echo 1 > /sys/class/gpio/gpio35/active_low# 用完取消导出echo 35 > /sys/class/gpio/unexport
sysfs的优点是无需额外库,shell脚本就能操作GPIO,调试时极其方便。缺点也很明显:
全局编号是controller_index * ngpio + offset算出来的,换板子或换内核版本编号可能变,硬编码编号脆弱。每次操作都打开文件、写文件,性能差且有竞态——两个进程同时导出同一个GPIO会冲突。没有引脚状态查询、边沿事件异步通知不方便(要poll value文件,低效)。
// sysfs方式在C程序中的操作(繁琐且易错)intgpio_sysfs_write(int gpio, int value){char path[64];int fd;sprintf(path, "/sys/class/gpio/gpio%d/value", gpio); fd = open(path, O_WRONLY);if (fd < 0) return-1;// 每次写都触发一次系统调用if (write(fd, value ? "1" : "0", 1) < 0) { close(fd);return-1; } close(fd);return0;}
方案二:libgpiod
libgpiod是sysfs的继任者,通过/dev/gpiochipN字符设备操作GPIO,提供了C库、命令行工具和Python绑定。
# 命令行工具(libgpiod提供)# 列出所有GPIO控制器gpiodetect# 输出: gpiochip0 [30200000.gpio] (32 lines)# gpiochip1 [30210000.gpio] (32 lines)# 查看某个控制器的引脚信息gpioinfo gpiochip0# 输出每个pin的编号、名称、方向、使用状态# 读取一个引脚(line 3 of gpiochip0)gpioget gpiochip0 3# 设置一个引脚gpioset gpiochip0 3=1# 监控引脚边沿事件gpiomon gpiochip0 3
libgpiod的核心概念是"line"(引脚)和"line request"(请求)。应用以结构体描述要操作的引脚集合、方向和标志,一次性请求,内核返回文件描述符,后续通过ioctl或poll操作。
// libgpiod C API操作GPIO#include<gpiod.h>intgpio_toggle_demo(void){// 打开GPIO控制器structgpiod_chip *chip = gpiod_chip_open("/dev/gpiochip0");if (!chip) return-1;// 获取引脚3structgpiod_line *line = gpiod_chip_get_line(chip, 3);if (!line) { gpiod_chip_close(chip);return-1; }// 请求输出模式structgpiod_line_request_configcfg = { .consumer = "my-app", // 消费者名,出现在gpioinfo中 .request_type = GPIOD_LINE_REQUEST_DIRECTION_OUTPUT, .flags = GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW, // 低有效 };if (gpiod_line_request(line, &cfg, 0) < 0) { gpiod_chip_close(chip);return-1; }// 设置值 gpiod_line_set_value(line, 1); // 逻辑1(物理低电平,因ACTIVE_LOW) gpiod_line_release(line); gpiod_chip_close(chip);return0;}
libgpiod的优势:
- • 通过
/dev/gpiochipN而非全局编号,引脚标识稳定(控制器名+偏移) - • 支持批量操作多个引脚(一次请求一组line,原子设置)
- • 原生支持边沿事件(
GPIOD_LINE_REQUEST_EVENT_*)和异步通知(返回fd,可poll/select) - • 消费者名可追踪,多个进程操作时
gpioinfo能显示谁在用 - •
gpiod_line_request失败后内核保证不留下半初始化状态
边沿事件处理
按键、中断信号这类需要响应边沿变化的场景,libgpiod比sysfs优雅得多。
// 监控引脚边沿事件(上升沿/下降沿)intgpio_edge_wait(void){structgpiod_chip *chip = gpiod_chip_open("/dev/gpiochip0");structgpiod_line *line = gpiod_chip_get_line(chip, 3);structgpiod_line_request_configcfg = { .consumer = "edge-monitor", .request_type = GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES, // 双沿 }; gpiod_line_request(line, &cfg, 0);// 等待事件发生(可配合poll/select做超时)structgpiod_line_eventevent;int ret = gpiod_line_event_wait(line, NULL); // NULL=阻塞if (ret > 0) { gpiod_line_event_read(line, &event);// event.event_type: GPIOD_LINE_EVENT_RISING_EDGE / FALLING_EDGE// event.ts: 事件时间戳 handle_edge(event.event_type); } gpiod_line_release(line); gpiod_chip_close(chip);return0;}
sysfs要实现同样功能,必须poll/sys/class/gpio/gpioN/value文件,每次边沿触发后还要重新读取确认,效率低且容易丢失快速事件。libgpiod的边沿事件由内核层面缓冲,应用可批量读取,不会漏事件。
方案一 vs 方案二
用户空间与内核态协作
一个常见模式:内核驱动处理底层GPIO逻辑(如按键去抖、边沿检测),通过input子系统上报按键事件;用户空间应用程序通过/dev/input/eventX读取,无需关心具体GPIO编号。这种方式把硬件细节隔离在内核,应用更干净。
// 用户空间读取input事件(按键通过gpio-keys驱动上报)#include<linux/input.h>intread_key_event(void){int fd = open("/dev/input/event0", O_RDONLY);structinput_eventev;while (read(fd, &ev, sizeof(ev)) == sizeof(ev)) {if (ev.type == EV_KEY) {if (ev.value == 1) {// 按键按下(KEY_ENTER等) handle_key_press(ev.code); } elseif (ev.value == 0) { handle_key_release(ev.code); } } } close(fd);return0;}
gpio-keys驱动在设备树中声明按键引脚,内核负责中断、去抖、边沿处理,用户空间只拿到标准化的input_event。这种分工让应用逻辑和板级硬件解耦——换引脚只改设备树,应用代码不变。