Linux GPIO Sysfs 中断监听:从 /sys/class/gpio 到 poll 的 3 步实战
Linux GPIO Sysfs 中断监听从 /sys/class/gpio 到 poll 的 3 步实战嵌入式Linux开发中GPIO中断监听是硬件交互的基础操作。相比内核驱动开发用户态通过Sysfs接口操作GPIO具有快速验证、灵活配置的优势。本文将手把手带你实现一个健壮的C语言中断监听程序涵盖从GPIO导出到事件监听的完整流程。1. 环境准备与GPIO配置在开始编码前我们需要确认系统环境支持GPIO Sysfs接口。现代Linux内核默认启用该功能可通过以下命令验证ls /sys/class/gpio若目录存在且包含export、unexport等文件说明支持用户态GPIO操作。接下来是GPIO配置的三个关键步骤1.1 GPIO引脚导出通过向/sys/class/gpio/export写入GPIO编号来申请控制权。以GPIO 19为例int export_gpio(int gpio_num) { int fd open(/sys/class/gpio/export, O_WRONLY); if (fd 0) { perror(Failed to open export file); return -1; } char buf[10]; snprintf(buf, sizeof(buf), %d, gpio_num); if (write(fd, buf, strlen(buf)) 0) { perror(Failed to export GPIO); close(fd); return -1; } close(fd); return 0; }常见错误处理权限问题需root或gpio用户组权限GPIO已被占用检查/sys/kernel/debug/gpio无效GPIO编号确认SoC文档的GPIO映射1.2 方向与触发设置配置GPIO为输入模式并设置中断触发方式int configure_gpio(int gpio_num, const char *edge) { char path[50]; snprintf(path, sizeof(path), /sys/class/gpio/gpio%d/direction, gpio_num); int fd open(path, O_WRONLY); if (fd 0) { perror(Failed to open direction file); return -1; } if (write(fd, in, 2) 0) { perror(Failed to set direction); close(fd); return -1; } close(fd); snprintf(path, sizeof(path), /sys/class/gpio/gpio%d/edge, gpio_num); fd open(path, O_WRONLY); if (fd 0) { perror(Failed to open edge file); return -1; } if (write(fd, edge, strlen(edge)) 0) { perror(Failed to set edge); close(fd); return -1; } close(fd); return 0; }触发方式可选none无中断rising上升沿falling下降沿both双边沿1.3 文件描述符准备打开value文件并预读以清除可能存在的旧状态int prepare_gpio_fd(int gpio_num) { char path[50]; snprintf(path, sizeof(path), /sys/class/gpio/gpio%d/value, gpio_num); int fd open(path, O_RDONLY | O_NONBLOCK); if (fd 0) { perror(Failed to open value file); return -1; } // 预读清除初始状态 char buf; read(fd, buf, 1); lseek(fd, 0, SEEK_SET); return fd; }注意必须使用O_NONBLOCK标志否则后续poll可能阻塞2. 中断监听实现2.1 poll系统调用基础poll允许进程监控多个文件描述符的事件其原型为#include poll.h int poll(struct pollfd *fds, nfds_t nfds, int timeout);关键参数说明参数类型说明fdsstruct pollfd*监控的文件描述符数组nfdsnfds_t监控的文件描述符数量timeoutint超时时间(ms)-1表示无限等待2.2 GPIO中断监听实现完整的中断监听循环实现void monitor_gpio_interrupt(int gpio_fd) { struct pollfd fds { .fd gpio_fd, .events POLLPRI, // 高优先级数据可读 .revents 0 }; while (1) { int ret poll(fds, 1, -1); // 无限等待 if (ret 0) { perror(poll failed); break; } if (fds.revents POLLPRI) { char buf[10]; lseek(gpio_fd, 0, SEEK_SET); read(gpio_fd, buf, sizeof(buf)); printf(GPIO interrupt detected! Value: %c\n, buf[0]); } // 清除事件标志 fds.revents 0; } }关键点解析POLLPRI用于监听高优先级事件如GPIO中断每次中断后必须lseek重置文件位置必须读取value值以清除中断状态2.3 错误处理增强实际项目中需要增加以下错误处理if (fds.revents POLLERR) { fprintf(stderr, Error condition on GPIO fd\n); break; } if (fds.revents POLLHUP) { fprintf(stderr, GPIO fd hung up\n); break; } if (fds.revents POLLNVAL) { fprintf(stderr, GPIO fd invalid\n); break; }3. 完整示例与优化3.1 完整可运行代码#include stdio.h #include stdlib.h #include unistd.h #include fcntl.h #include string.h #include poll.h #include errno.h #define GPIO_NUM 19 #define EDGE_TYPE rising int main() { // 1. GPIO导出 if (export_gpio(GPIO_NUM) 0) { fprintf(stderr, GPIO export failed\n); return EXIT_FAILURE; } // 2. 配置GPIO if (configure_gpio(GPIO_NUM, EDGE_TYPE) 0) { fprintf(stderr, GPIO configure failed\n); goto cleanup; } // 3. 准备文件描述符 int gpio_fd prepare_gpio_fd(GPIO_NUM); if (gpio_fd 0) { fprintf(stderr, GPIO fd preparation failed\n); goto cleanup; } printf(Monitoring GPIO %d for %s edge interrupts...\n, GPIO_NUM, EDGE_TYPE); // 4. 开始监听 monitor_gpio_interrupt(gpio_fd); // 5. 清理 cleanup: close(gpio_fd); unexport_gpio(GPIO_NUM); return EXIT_SUCCESS; }3.2 性能优化技巧批处理操作对多个GPIO使用poll同时监控struct pollfd fds[2]; fds[0].fd gpio_fd1; fds[1].fd gpio_fd2; poll(fds, 2, -1);信号处理添加优雅退出机制volatile sig_atomic_t stop 0; void sig_handler(int signo) { stop 1; } signal(SIGINT, sig_handler); while (!stop) { // poll循环 }日志记录记录中断时间戳#include time.h struct timespec ts; clock_gettime(CLOCK_MONOTONIC, ts); printf([%ld.%09ld] Interrupt\n, ts.tv_sec, ts.tv_nsec);3.3 资源清理程序退出前必须释放资源void unexport_gpio(int gpio_num) { int fd open(/sys/class/gpio/unexport, O_WRONLY); if (fd 0) { perror(Failed to open unexport file); return; } char buf[10]; snprintf(buf, sizeof(buf), %d, gpio_num); write(fd, buf, strlen(buf)); close(fd); }4. 高级应用与问题排查4.1 实际项目中的应用模式在工业控制系统中GPIO中断通常用于紧急停止按钮配置为下降沿触发最高优先级传感器触发光电传感器使用边沿触发硬件看门狗定时检测脉冲信号4.2 常见问题解决方案问题1poll立即返回但无中断检查edge设置是否正确确认硬件连接正常验证GPIO是否被其他进程占用问题2中断丢失// 增加事件缓冲队列 #define EVENT_BUF_SIZE 10 struct gpio_event { struct timespec timestamp; int value; } event_buf[EVENT_BUF_SIZE]; int event_idx 0;问题3高CPU占用避免在中断处理中进行复杂计算考虑使用epoll替代poll需内核支持4.3 替代方案比较方案优点缺点适用场景Sysfs无需内核模块简单性能较低原型开发、简单应用字符设备性能较好需要驱动支持生产环境libgpiod官方推荐功能丰富依赖较新内核现代Linux系统// libgpiod示例需安装开发包 #include gpiod.h struct gpiod_line *line gpiod_line_get(gpiochip0, GPIO_NUM); gpiod_line_request_rising_edge_events(line, example); struct gpiod_line_event event; gpiod_line_event_wait(line, NULL); gpiod_line_event_read(line, event);通过本文介绍的方法开发者可以快速实现用户态的GPIO中断监听。在实际项目中建议根据具体需求选择最适合的技术方案并充分考虑错误处理和系统稳定性。