通过sysfs使用GPIO中断事务
文章目录
在《[Linux] Linux GPIO应用编程深度解析与实践指南(代码示例)》一文中,详细介绍了GPIO使用中断事务的方法,但是全都是基于libgpiod
的,libgpiod
比起sysfs
来说要强大很多,使用方便,性能好。但是我们卑微的嵌入式人,由于硬件器件选型和工具链版本问题,很多时使用的都是较老的内核,在其基础上增加libgpiod
支持,则显得非常麻烦,还是经常需要在sysfs
下进行高效的GPIO操作,本文专门对此进行介绍,全当《[Linux] Linux GPIO应用编程深度解析与实践指南(代码示例)》的补充吧。
一、代码示例
较新的Linux内核推荐使用libgpiod,但某些旧系统或特定场景可能仍需使用sysfs。由于sysfs的GPIO接口已被标记为过时(deprecated),但在一些旧版本内核中仍然可用,我们这里提供一种实现方式。
1. 具体步骤步骤:
-
导出GPIO
-
设置方向为输入
-
设置中断触发边沿(上升沿、下降沿、双边沿等)
-
使用poll()或select()监听value文件的变化(因为sysfs不支持epoll,所以我们用poll)
-
当有事件发生时,读取value文件或直接处理事件
注意:sysfs的GPIO中断事件通过轮询value文件的变化来实现,效率不如libgpiod,但可以用于简单场景。
以下是一个完整的示例程序,通过sysfs监听GPIO引脚的中断事件(以按键为例,使用下降沿触发)。
2. 代码
下面是通过sysfs接口实现GPIO中断事件监听的完整解决方案,包含详细的代码实现、编译方法和使用说明。
/**
* sysfs_gpio_interrupt.c - 通过sysfs接口监听GPIO中断事件
*
* 编译: gcc sysfs_gpio_interrupt.c -o gpio_interrupt
* 运行: sudo ./gpio_interrupt <GPIO编号> [edge]
*
* 参数:
* GPIO编号 - 要监听的GPIO引脚号(如17、24等)
* edge - 可选,触发边沿(rising, falling, both),默认为both
*
* 示例:
* sudo ./gpio_interrupt 24 # 监听GPIO24的双边沿事件
* sudo ./gpio_interrupt 17 falling # 监听GPIO17的下降沿事件
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <signal.h>
#include <time.h>
#include <errno.h>
// 全局变量控制程序运行
static volatile int keep_running = 1;
// 信号处理函数
void signal_handler(int sig) {
(void)sig;
keep_running = 0;
printf("\n接收到终止信号,程序退出...\n");
}
// 获取当前时间字符串
const char *current_time() {
static char buffer[20];
time_t rawtime;
struct tm *timeinfo;
time(&rawtime);
timeinfo = localtime(&rawtime);
strftime(buffer, sizeof(buffer), "%H:%M:%S", timeinfo);
return buffer;
}
// 导出GPIO引脚
int export_gpio(int gpio) {
char buffer[32];
int fd, len;
fd = open("/sys/class/gpio/export", O_WRONLY);
if (fd < 0) {
perror("无法打开export文件");
return -1;
}
len = snprintf(buffer, sizeof(buffer), "%d", gpio);
if (write(fd, buffer, len) < 0) {
perror("无法导出GPIO");
close(fd);
return -1;
}
close(fd);
return 0;
}
// 取消导出GPIO引脚
int unexport_gpio(int gpio) {
char buffer[32];
int fd, len;
fd = open("/sys/class/gpio/unexport", O_WRONLY);
if (fd < 0) {
perror("无法打开unexport文件");
return -1;
}
len = snprintf(buffer, sizeof(buffer), "%d", gpio);
if (write(fd, buffer, len) < 0) {
perror("无法取消导出GPIO");
close(fd);
return -1;
}
close(fd);
return 0;
}
// 设置GPIO方向
int set_gpio_direction(int gpio, const char *direction) {
char path[64];
int fd;
snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/direction", gpio);
fd = open(path, O_WRONLY);
if (fd < 0) {
perror("无法打开direction文件");
return -1;
}
if (write(fd, direction, strlen(direction)) < 0) {
perror("无法设置GPIO方向");
close(fd);
return -1;
}
close(fd);
return 0;
}
// 设置GPIO中断边沿触发
int set_gpio_edge(int gpio, const char *edge) {
char path[64];
int fd;
snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/edge", gpio);
fd = open(path, O_WRONLY);
if (fd < 0) {
perror("无法打开edge文件");
return -1;
}
if (write(fd, edge, strlen(edge)) < 0) {
perror("无法设置中断边沿");
close(fd);
return -1;
}
close(fd);
return 0;
}
// 打开GPIO值文件
int open_gpio_value(int gpio) {
char path[64];
int fd;
snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", gpio);
fd = open(path, O_RDONLY | O_NONBLOCK);
if (fd < 0) {
perror("无法打开value文件");
return -1;
}
return fd;
}
// 主函数
int main(int argc, char *argv[]) {
int gpio;
char *edge_type = "both"; // 默认双边沿触发
int value_fd = -1;
char buf;
struct pollfd fdset;
int ret;
int n;
// 注册信号处理
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
// 检查参数
if (argc < 2) {
fprintf(stderr, "用法: %s <GPIO编号> [edge]\n", argv[0]);
fprintf(stderr, "edge可选值: rising, falling, both\n");
return EXIT_FAILURE;
}
gpio = atoi(argv[1]);
if (gpio <= 0) {
fprintf(stderr, "错误: 无效的GPIO编号\n");
return EXIT_FAILURE;
}
if (argc > 2) {
if (strcmp(argv[2], "rising") == 0 ||
strcmp(argv[2], "falling") == 0 ||
strcmp(argv[2], "both") == 0) {
edge_type = argv[2];
} else {
fprintf(stderr, "警告: 无效的边沿类型 '%s',使用默认值 'both'\n", argv[2]);
}
}
printf("[%s] 开始监听GPIO%d的中断事件 (%s边沿)\n",
current_time(), gpio, edge_type);
// 导出GPIO
if (export_gpio(gpio) {
fprintf(stderr, "错误: 无法导出GPIO%d\n", gpio);
return EXIT_FAILURE;
}
// 设置方向为输入
if (set_gpio_direction(gpio, "in")) {
fprintf(stderr, "错误: 无法设置GPIO%d方向\n", gpio);
unexport_gpio(gpio);
return EXIT_FAILURE;
}
// 设置中断边沿
if (set_gpio_edge(gpio, edge_type)) {
fprintf(stderr, "错误: 无法设置GPIO%d中断边沿\n", gpio);
unexport_gpio(gpio);
return EXIT_FAILURE;
}
// 打开值文件
value_fd = open_gpio_value(gpio);
if (value_fd < 0) {
fprintf(stderr, "错误: 无法打开GPIO%d值文件\n", gpio);
unexport_gpio(gpio);
return EXIT_FAILURE;
}
// 读取初始值(丢弃)
read(value_fd, &buf, 1);
lseek(value_fd, 0, SEEK_SET);
printf("[%s] 准备就绪,等待中断事件... (按Ctrl+C退出)\n", current_time());
// 主事件循环
while (keep_running) {
memset(&fdset, 0, sizeof(fdset));
fdset.fd = value_fd;
fdset.events = POLLPRI | POLLERR;
// 使用poll等待事件,超时时间1秒
ret = poll(&fdset, 1, 1000);
if (ret < 0) {
if (errno == EINTR) continue; // 信号中断
perror("poll错误");
break;
}
if (ret == 0) {
// 超时,继续等待
continue;
}
// 检查是否有中断事件
if (fdset.revents & POLLPRI) {
lseek(value_fd, 0, SEEK_SET);
n = read(value_fd, &buf, 1);
if (n > 0) {
// 根据值判断事件类型
if (buf == '0') {
printf("[%s] 下降沿事件 - GPIO%d值: 0\n", current_time(), gpio);
} else if (buf == '1') {
printf("[%s] 上升沿事件 - GPIO%d值: 1\n", current_time(), gpio);
}
}
}
}
// 清理资源
close(value_fd);
unexport_gpio(gpio);
printf("[%s] 程序退出\n", current_time());
return EXIT_SUCCESS;
}
二、编译与运行说明
1. 编译程序
gcc sysfs_gpio_interrupt.c -o gpio_interrupt
2. 硬件连接
- 将按键/传感器连接到目标GPIO引脚(如GPIO24)
- 按键另一端接地(下降沿触发)或接VCC(上升沿触发)
- 建议使用10KΩ上拉/下拉电阻
3. 运行程序
# 监听GPIO24的双边沿事件
sudo ./gpio_interrupt 24
# 监听GPIO17的下降沿事件
sudo ./gpio_interrupt 17 falling
# 监听GPIO18的上升沿事件
sudo ./gpio_interrupt 18 rising
4. 程序输出示例
[14:35:22] 开始监听GPIO24的中断事件 (both边沿)
[14:35:22] 准备就绪,等待中断事件... (按Ctrl+C退出)
[14:35:25] 下降沿事件 - GPIO24值: 0
[14:35:25] 上升沿事件 - GPIO24值: 1
[14:35:26] 下降沿事件 - GPIO24值: 0
[14:35:26] 上升沿事件 - GPIO24值: 1
^C
接收到终止信号,程序退出...
[14:35:30] 程序退出
三、实现原理详解
1. Sysfs GPIO接口操作流程
2. 关键代码解析
导出GPIO引脚:
int export_gpio(int gpio) {
int fd = open("/sys/class/gpio/export", O_WRONLY);
write(fd, buffer, len); // 写入GPIO编号
}
设置中断触发边沿:
int set_gpio_edge(int gpio, const char *edge) {
char path[64];
snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/edge", gpio);
int fd = open(path, O_WRONLY);
write(fd, edge, strlen(edge)); // 写入触发类型
}
事件监听循环:
while (keep_running) {
struct pollfd fdset;
fdset.fd = value_fd;
fdset.events = POLLPRI | POLLERR; // 设置监听优先级事件
int ret = poll(&fdset, 1, 1000); // 等待事件
if (fdset.revents & POLLPRI) {
lseek(value_fd, 0, SEEK_SET);
read(value_fd, &buf, 1); // 读取当前值
// 根据值判断事件类型
}
}
3. 中断处理机制
-
边沿触发配置:通过写入
edge
文件设置触发方式rising
:上升沿触发(0→1)falling
:下降沿触发(1→0)both
:双边沿触发(任何变化)
-
事件通知机制:
- 当配置的边沿事件发生时,内核会更新
value
文件 - 使用
poll()
监听value
文件的POLLPRI
事件 - 事件发生时必须重新读取
value
文件以清除事件状态
- 当配置的边沿事件发生时,内核会更新
-
电平读取:
- 事件发生后读取
value
文件获取当前电平 - '0’表示低电平,'1’表示高电平
- 事件发生后读取
四、性能优化与注意事项
1. 响应时间优化
- 使用
O_NONBLOCK
标志打开文件避免阻塞 - 设置合理的poll超时时间(示例中为1000ms)
- 事件处理后立即重新定位文件指针
2. 错误处理
- 所有系统调用都有错误检查
- 信号处理确保程序优雅退出
- 资源释放防止GPIO引脚被占用
3. 使用限制
- Sysfs GPIO接口在较新内核中已被标记为过时
- 性能不如libgpiod(适合高频率事件)
- 每个GPIO需要单独的文件操作
4. 替代方案建议
对于新项目,推荐使用libgpiod库:
#include <gpiod.h>
// 配置中断
gpiod_line_request_rising_edge_events(line, "example");
// 事件循环
struct gpiod_line_event event;
while (1) {
gpiod_line_event_wait(line, NULL); // 等待事件
gpiod_line_event_read(line, &event); // 读取事件
}
五、应用场景
- 物理按键检测:监听用户按键操作
- 传感器触发:处理PIR运动传感器、光电传感器等事件
- 旋转编码器:检测旋转方向及步数
- 限位开关:机械位置检测
- 低功耗设备:唤醒休眠系统
通过这个sysfs实现方案,您可以在不使用额外库的情况下,实现基本的GPIO中断事件监听功能,适合嵌入式Linux系统的快速原型开发。
研究学习不易,点赞易。
工作生活不易,收藏易,点收藏不迷茫 :)