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);关键参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
| fds | struct pollfd* | 监控的文件描述符数组 |
| nfds | nfds_t | 监控的文件描述符数量 |
| timeout | int | 超时时间(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 常见问题解决方案
问题1:poll立即返回但无中断
- 检查
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中断监听。在实际项目中,建议根据具体需求选择最适合的技术方案,并充分考虑错误处理和系统稳定性。