[Linux] 通过sysfs使用GPIO中断事务(代码示例)

通过sysfs使用GPIO中断事务

《[Linux] Linux GPIO应用编程深度解析与实践指南(代码示例)》一文中,详细介绍了GPIO使用中断事务的方法,但是全都是基于libgpiod的,libgpiod比起sysfs来说要强大很多,使用方便,性能好。但是我们卑微的嵌入式人,由于硬件器件选型和工具链版本问题,很多时使用的都是较老的内核,在其基础上增加libgpiod支持,则显得非常麻烦,还是经常需要在sysfs下进行高效的GPIO操作,本文专门对此进行介绍,全当《[Linux] Linux GPIO应用编程深度解析与实践指南(代码示例)》的补充吧。

一、代码示例

较新的Linux内核推荐使用libgpiod,但某些旧系统或特定场景可能仍需使用sysfs。由于sysfs的GPIO接口已被标记为过时(deprecated),但在一些旧版本内核中仍然可用,我们这里提供一种实现方式。

1. 具体步骤步骤:

  1. 导出GPIO

  2. 设置方向为输入

  3. 设置中断触发边沿(上升沿、下降沿、双边沿等)

  4. 使用poll()或select()监听value文件的变化(因为sysfs不支持epoll,所以我们用poll)

  5. 当有事件发生时,读取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接口操作流程

导出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. 中断处理机制

  1. 边沿触发配置:通过写入edge文件设置触发方式

    • rising:上升沿触发(0→1)
    • falling:下降沿触发(1→0)
    • both:双边沿触发(任何变化)
  2. 事件通知机制

    • 当配置的边沿事件发生时,内核会更新value文件
    • 使用poll()监听value文件的POLLPRI事件
    • 事件发生时必须重新读取value文件以清除事件状态
  3. 电平读取

    • 事件发生后读取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); // 读取事件
}

五、应用场景

  1. 物理按键检测:监听用户按键操作
  2. 传感器触发:处理PIR运动传感器、光电传感器等事件
  3. 旋转编码器:检测旋转方向及步数
  4. 限位开关:机械位置检测
  5. 低功耗设备:唤醒休眠系统

通过这个sysfs实现方案,您可以在不使用额外库的情况下,实现基本的GPIO中断事件监听功能,适合嵌入式Linux系统的快速原型开发。


研究学习不易,点赞易。
工作生活不易,收藏易,点收藏不迷茫 :)


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值