返回

如何在Linux下高效监听GPIO中断?

Linux

抛弃轮询,拥抱高效:使用 Select 在 Linux 下监听 GPIO 中断

在嵌入式 Linux 开发中,GPIO 的使用司空见惯。而如何高效地响应 GPIO 中断,则是开发者们常常需要面对的挑战。传统的轮询方式,如同无头苍蝇般反复检查状态,效率低下,白白浪费 CPU 资源。中断处理程序虽然能够及时响应,但编写起来相对复杂,容易出错。有没有一种方法,既能兼顾效率,又能简化代码呢?答案是肯定的,Linux 系统提供的 select 系统调用,为我们提供了一种优雅的解决方案。

Select:不止是文件符的管理者

select,很多开发者只是将其视为一种管理文件符的机制,实际上,它的强大远超你的想象。select 允许进程同时监听多个文件描述符,一旦其中有任何一个文件描述符的状态发生变化,select 就会立即返回,并告知进程哪些文件描述符发生了变化。这种机制,使得程序可以同时监控多个 I/O 操作,而无需阻塞在某一个操作上,极大地提高了程序的并发处理能力。

select 函数的原型如下:

int select(int nfds, fd_set *readfds, fd_set *writefds,
              fd_set *exceptfds, struct timeval *timeout);
  • nfds:所有文件描述符中最大值加 1。
  • readfds:用于监听可读事件的文件描述符集合。
  • writefds:用于监听可写事件的文件描述符集合。
  • exceptfds:用于监听异常事件的文件描述符集合。
  • timeout:指定 select 调用的超时时间,可以设置为 NULL 表示永久阻塞,也可以设置为特定的时间值。

GPIO:Linux 世界中的“一等公民”

在 Linux 系统中,一切皆文件,GPIO 也不例外。通过 /sys/class/gpio 文件系统接口,我们可以方便地访问和控制每一个 GPIO 端口。每个 GPIO 端口在 /sys/class/gpio 目录下都有一个对应的子目录,例如 gpio23 就代表 GPIO 端口 23。

为了使用 select 监听 GPIO 中断,我们需要获取 GPIO 端口对应的文件描述符。操作步骤如下:

  1. 导出 GPIO 端口:echo 23 > /sys/class/gpio/export
  2. 设置 GPIO 端口方向:echo in > /sys/class/gpio/gpio23/direction
  3. 设置 GPIO 端口中断触发方式:echo rising > /sys/class/gpio/gpio23/edge (可选,可以选择 rising, falling 或 both)
  4. 打开 GPIO 值文件:int fd = open("/sys/class/gpio/gpio23/value", O_RDONLY);

获取到文件描述符后,我们就可以使用 select 对其进行监听了。

实战演练:使用 Select 监听 GPIO 上升沿中断

下面,我们将通过一个具体的例子,演示如何使用 select 监听 GPIO 端口 23 上的上升沿中断。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include <fcntl.h>

int main() {
  // 导出 GPIO 端口
  system("echo 23 > /sys/class/gpio/export");
  system("echo in > /sys/class/gpio/gpio23/direction");
  system("echo rising > /sys/class/gpio/gpio23/edge");

  // 打开 GPIO 值文件
  int gpio_fd = open("/sys/class/gpio/gpio23/value", O_RDONLY);
  if (gpio_fd == -1) {
    perror("open");
    exit(1);
  }

  // 使用 select 监听 GPIO 中断
  while (1) {
    fd_set readfds;
    FD_ZERO(&readfds);
    FD_SET(gpio_fd, &readfds);

    // 阻塞等待 GPIO 中断
    int ret = select(gpio_fd + 1, &readfds, NULL, NULL, NULL);
    if (ret == -1) {
      perror("select");
      exit(1);
    } else if (ret) {
      if (FD_ISSET(gpio_fd, &readfds)) {
        // GPIO 中断发生
        char buf[2];
        lseek(gpio_fd, 0, SEEK_SET); // 将文件指针移至文件开头
        read(gpio_fd, buf, sizeof(buf));
        printf("GPIO interrupt detected! Value: %c\n", buf[0]);
        // 处理 GPIO 中断逻辑
      }
    } 
  }

  // 关闭 GPIO 文件
  close(gpio_fd);

  // 释放 GPIO 端口
  system("echo 23 > /sys/class/gpio/unexport");

  return 0;
}

这段代码首先导出了 GPIO 端口 23,并将其配置为输入模式,设置中断触发方式为上升沿。接着,打开 GPIO 值文件,获取其文件描述符。在主循环中,使用 select 监听 GPIO 文件描述符的可读事件。当 GPIO 端口 23 上发生上升沿中断时,select 调用会返回,并将 readfds 集合中对应的位设置为 1。程序检测到 GPIO 中断发生后,就可以读取 GPIO 的值,并执行相应的处理逻辑了。

注意事项

  • 使用 select 监听 GPIO 中断时,需要将 GPIO 端口配置为中断模式,并设置合适的触发方式。
  • 每次 select 调用返回后,都需要重新初始化 fd_set 集合,并将需要监听的文件描述符添加进去。
  • 在处理 GPIO 中断逻辑时,应该尽量避免长时间阻塞操作,以免影响其他程序的运行。
  • 程序结束时,需要关闭 GPIO 文件,并释放 GPIO 端口。

常见问题解答

1. 为什么 select 比轮询效率高?

轮询方式需要程序不断地主动查询 GPIO 状态,即使没有中断发生,也会占用 CPU 资源。而 select 则可以让程序阻塞等待中断发生,只有在中断发生时才会被唤醒,大大降低了 CPU 的占用率,提高了程序的效率。

2. select 是否可以同时监听多个 GPIO 端口?

当然可以。select 可以同时监听多个文件描述符,因此也可以同时监听多个 GPIO 端口。只需要将所有需要监听的 GPIO 文件描述符添加到 fd_set 集合中即可。

3. 如何设置 GPIO 中断的触发方式?

可以通过写入 /sys/class/gpio/gpio{gpio_number}/edge 文件来设置 GPIO 中断的触发方式。例如,要将 GPIO 23 的触发方式设置为下降沿触发,可以使用以下命令:

echo falling > /sys/class/gpio/gpio23/edge

4. 如何在程序中读取 GPIO 的值?

可以通过读取 /sys/class/gpio/gpio{gpio_number}/value 文件来获取 GPIO 的当前值。例如,要读取 GPIO 23 的值,可以使用以下代码:

int fd = open("/sys/class/gpio/gpio23/value", O_RDONLY);
char value;
read(fd, &value, 1);

5. 使用 select 监听 GPIO 中断需要注意哪些问题?

需要注意的是,每次 select 调用返回后,都需要重新初始化 fd_set 集合,并将需要监听的文件描述符添加进去。此外,在处理 GPIO 中断逻辑时,应该尽量避免长时间阻塞操作,以免影响其他程序的运行。

总结

使用 select 监听 GPIO 中断是一种高效且灵活的方法,它不仅可以避免轮询的低效,而且比中断处理程序更容易实现。本文详细介绍了 select 的工作原理、使用方法以及注意事项,并提供了完整的代码示例。希望读者能够通过本文掌握使用 select 监听 GPIO 中断的方法,并在实际项目中加以应用。