重要提示:sysfs GPIO 接口自 Linux 4.8 起已被标记为已弃用,并在 Linux 5.3 之后开始逐步移除。

基本原理

sysfs 是一个虚拟文件系统,它将内核中的设备、驱动等信息映射到用户空间,以文件和目录的形式呈现。

对于 GPIO,其控制接口位于 /sys/class/gpio/

将 gpio 写入 /sys/class/gpio/export ,就可将此 gpio 从内核空间导出到用户空间。例如:

1
echo 12 > /sys/class/gpio/export  

导出后,在 /sys/class/gpio/ 目录下就会生成一个 gpio12 目录。其目录结构如下:

gpio12
├── active_low
├── device -> ../../../gpiochip0
├── direction
├── edge
├── power
│ ├── autosuspend_delay_ms
│ ├── control
│ ├── runtime_active_time
│ ├── runtime_status
│ └── runtime_suspended_time
├── subsystem -> ../../../../../../../class/gpio
├── uevent
└── value

GPIO Sysfs 接口文件功能详解:

  • value (最重要的文件)

    • 功能:读取或设置GPIO引脚的电平状态。

    • 读取 (cat value):

      • 返回引脚的当前电平状态。
      • 0: 代表低电平。
      • 1: 代表高电平。
    • 写入 (echo 1 > valueecho 0 > value):

      • 当引脚被设置为输出模式时,向此文件写入可以控制引脚输出高电平或低电平。
      • 注意:在写入之前,必须先将 direction 设置为 out
  • direction

    • 功能:设置GPIO引脚的方向(输入或输出)。

    • 读取 (cat direction):

      • 返回引脚当前的方向,通常是 inout
    • 写入:

      • echo in > direction: 将引脚设置为输入模式。
      • echo out > direction: 将引脚设置为输出模式。
      • 注意:出于安全考虑,将方向从 in 改为 out 时,引脚的初始输出电平是未定义的。有些系统允许使用 echo low > directionecho high > direction 来同时设置方向为输出并指定初始输出电平(low为低,high为高)。
  • edge

    • 功能:设置引脚的中断触发模式。这个文件仅当引脚设置为输入模式时有效。

    • 读取 (cat edge): 返回当前设置的中断触发模式。

    • 写入:

      • echo none > edge默认值。禁止中断触发。表示忽略该引脚上的边沿变化。
      • echo rising > edge: 设置为上升沿触发。当引脚电平从低变高时,会产生中断事件。
      • echo falling > edge: 设置为下降沿触发。当引脚电平从高变低时,会产生中断事件。
      • echo both > edge: 设置为双边沿触发。只要引脚电平发生变化(无论是上升还是下降),都会产生中断事件。
  • active_low

    • 功能:反转引脚的逻辑电平。这是一个非常有用的功能,用于处理低电平有效的设备(例如,低电平触发的LED或按键)。

    • 读取 (cat active_low):

      • 0: 默认值。正常逻辑。value 文件中的 0 代表低电平,1 代表高电平。
      • 1: 反转逻辑。value 文件中的 0 现在代表高电平,1 代表低电平。
    • 写入:

      • echo 0 > active_low: 使用正常逻辑。
      • echo 1 > active_low: 使用反转逻辑。
  • device

    • 功能:一个符号链接(Symbolic Link),指向这个GPIO引脚所属于的硬件设备(通常是GPIO控制器芯片)在 /sys/devices/ 目录下的路径。

    • 作用: 主要用于在sysfs文件系统中维护设备之间的层次关系,对于普通用户控制GPIO来说,一般不需要关心这个文件。

  • subsystem

    • 功能:一个符号链接,指向包含它的子系统目录,即 /sys/class/gpio

    • 作用: 同样用于维护sysfs的内部结构,表明这个 gpio12 目录属于 gpio 子系统。用户通常无需操作。

  • uevent

    • 功能:内核事件接口。主要用于通知设备管理器(如udev)关于设备的热插拔事件。

    • 作用: 当你向这个文件写入 add,它会强制内核发送一个“新增设备”的事件,通知用户空间的设备管理工具。对于GPIO,这个文件通常由系统自动管理,用户一般不需要手动操作。

  • power

    • 功能:电源管理相关目录。它包含一些与设备电源状态管理相关的接口文件,如 async, runtime_status, control 等。

    • 作用: 用于高级的电源管理功能,例如挂起或恢复设备以节省功耗。对于简单的GPIO操作,完全可以忽略这个目录及其内容。

控制步骤

要控制一个GPIO,通常遵循以下步骤:

  1. 导出引脚echo 12 > /sys/class/gpio/export
  2. 设置方向
    • 输出: echo out > /sys/class/gpio/gpio12/direction
    • 输入: echo in > /sys/class/gpio/gpio12/direction
  3. 操作引脚
    • 如果是输出
      • 设置高电平: echo 1 > /sys/class/gpio/gpio12/value
      • 设置低电平: echo 0 > /sys/class/gpio/gpio12/value
    • 如果是输入
      • 读取电平: cat /sys/class/gpio/gpio12/value
      • (可选)如果需要中断通知,设置中断模式: echo rising > /sys/class/gpio/gpio12/edge
  4. (完成后)取消导出echo 12 > /sys/class/gpio/unexport (这会移除 gpio12 目录)

GPIO 输出

操作步骤

导出 GPIO

将 GPIO 控制从内核空间导出到用户空间

1
echo 12 > /sys/class/gpio/export  

设置成输出模式

1
echo out > /sys/class/gpio/gpio12/direction

设置高低电平

1
2
echo 1 > /sys/class/gpio/gpio12/value
echo 0 > /sys/class/gpio/gpio12/value

shell 编程示例

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
if [ ! -e /sys/class/gpio/gpio12 ]; then
echo 12 > /sys/class/gpio/export
fi

echo out > /sys/class/gpio/gpio12/direction

if [ "$1" == "1" ]; then
echo 1 > /sys/class/gpio/gpio12/value
elif [ "$1" == "0" ]; then
echo 0 > /sys/class/gpio/gpio12/value
fi

C 系统编程示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <unistd.h> // access, close, write
#include <fcntl.h> // open
#include <string.h> // strcmp

int main(int argc, char *argv[])
{
int ret, fd;

if (argc != 2)
{
printf("%s 0/1\n", argv[0]);
return -1;
}

ret = access("/sys/class/gpio/gpio12", F_OK); // 判断文件是否存在
if (0 != ret)
{
fd = open("/sys/class/gpio/export", O_WRONLY);
write(fd, "12", 2);
close(fd);
}

fd = open("/sys/class/gpio/gpio12/direction", O_WRONLY);
write(fd, "out", 3);
close(fd);


if (0 == strcmp(argv[1], "0"))
{
fd = open("/sys/class/gpio/gpio12/value", O_WRONLY);
write(fd, "0", 1);
close(fd);
}
else
{
fd = open("/sys/class/gpio/gpio12/value", O_WRONLY);
write(fd, "1", 1);
close(fd);
}

return 0;
}

GPIO 输入

操作步骤

导出 GPIO

将 GPIO 控制从内核空间导出到用户空间

1
echo 12 > /sys/class/gpio/export  

设置成输入模式

1
echo in > /sys/class/gpio/gpio12/direction

读取电平

1
cat /sys/class/gpio/gpio12/value

C 系统编程示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <unistd.h> // access, close, write, read
#include <fcntl.h> // open

int main(int argc, char *argv[])
{
int ret, fd;
char buf[8] = {};

ret = access("/sys/class/gpio/gpio13", F_OK); // 判断文件是否存在
if (0 != ret)
{
fd = open("/sys/class/gpio/export", O_WRONLY);
write(fd, "13", 2);
close(fd);
}

fd = open("/sys/class/gpio/gpio13/direction", O_WRONLY);
write(fd, "in", 2);
close(fd);

fd = open("/sys/class/gpio/gpio13/value", O_RDONLY);
read(fd, buf, 1);
close(fd);
printf("gpio13 val=%s\n", buf);

return 0;
}

GPIO 中断

实现步骤

导出 GPIO

首先需要导出要使用的 GPIO 引脚:

1
2
# 以 GPIO 13 为例
echo 13 > /sys/class/gpio/export

设置方向为输入

1
echo "in" > /sys/class/gpio/gpio13/direction

设置边沿触发模式

这是关键步骤,设置中断触发方式:

1
2
3
4
5
6
7
8
# 设置为下降沿触发(从高到低电平变化)
echo "falling" > /sys/class/gpio/gpio13/edge

# 其他可选值:
# "none" - 无中断
# "rising" - 上升沿触发
# "falling" - 下降沿触发
# "both" - 双边沿触发

使用 poll 监测中断

poll 函数

函数原型和头文件

1
2
3
#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数详解

struct pollfd *fds

指向 pollfd 结构体数组的指针,每个结构体描述一个要监视的文件描述符。

1
2
3
4
5
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 请求的事件 */
short revents; /* 返回的事件 */
};

nfds_t nfds

fds 数组中的元素数量。

int timeout

等待的超时时间(毫秒):

  • -1:无限期阻塞,直到有事件发生
  • 0:立即返回,不阻塞(轮询)
  • > 0:等待指定的毫秒数

返回值

  • > 0:返回就绪的文件描述符数量
  • = 0:超时,没有文件描述符就绪
  • -1:发生错误,并设置 errno

events 和 revents 标志

常用事件标志:

标志 描述 用途
POLLIN 有数据可读 普通文件、socket、管道
POLLPRI 有紧急数据可读 带外数据、GPIO中断
POLLOUT 可写,不会阻塞 普通文件、socket
POLLERR 发生错误 自动设置,无需请求
POLLHUP 挂起 对端关闭连接
POLLNVAL 文件描述符未打开 自动设置

C 编程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
#include <signal.h>
#include <errno.h>

#define GPIO_PIN "13"
#define SYSFS_GPIO_DIR "/sys/class/gpio"
#define MAX_BUF 64

volatile sig_atomic_t stop = 0;

void signal_handler(int sig) {
stop = 1;
}

int main(int argc, char *argv[]) {
int fd, ret;
struct pollfd pfd;
char buf[MAX_BUF];
int gpio_fd;

// 注册信号处理,用于优雅退出
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);

// 导出 GPIO
fd = open(SYSFS_GPIO_DIR "/export", O_WRONLY);
if (fd == -1) {
perror("GPIO export open failed");
// 可能已经导出,继续尝试
} else {
write(fd, GPIO_PIN, strlen(GPIO_PIN));
close(fd);
}

// 设置方向为输入
snprintf(buf, sizeof(buf), SYSFS_GPIO_DIR "/gpio%s/direction", GPIO_PIN);
fd = open(buf, O_WRONLY);
if (fd == -1) {
perror("GPIO direction open failed");
return 1;
}
write(fd, "in", 2);
close(fd);

// 设置边沿触发为下降沿
snprintf(buf, sizeof(buf), SYSFS_GPIO_DIR "/gpio%s/edge", GPIO_PIN);
fd = open(buf, O_WRONLY);
if (fd == -1) {
perror("GPIO edge open failed");
return 1;
}
write(fd, "falling", 7);
close(fd);

// 打开 value 文件用于 poll
snprintf(buf, sizeof(buf), SYSFS_GPIO_DIR "/gpio%s/value", GPIO_PIN);
gpio_fd = open(buf, O_RDONLY);
if (gpio_fd == -1) {
perror("GPIO value open failed");
return 1;
}

// 第一次读取,清除可能存在的旧状态
read(gpio_fd, buf, MAX_BUF);
lseek(gpio_fd, 0, SEEK_SET);

printf("开始监测 GPIO %s 的下拉事件...\n", GPIO_PIN);
printf("按 Ctrl+C 退出\n");

// 配置 poll
pfd.fd = gpio_fd;
pfd.events = POLLPRI | POLLERR;

while (!stop) {
ret = poll(&pfd, 1, -1); // 无限期等待

if (ret < 0) {
if (errno == EINTR) continue; // 被信号中断
perror("poll failed");
break;
}

if (pfd.revents & POLLPRI) {
// GPIO 状态变化,读取当前值
lseek(gpio_fd, 0, SEEK_SET);
read(gpio_fd, buf, MAX_BUF);

int value = atoi(buf);
printf("检测到下拉事件! GPIO 当前值: %d\n", value);

// 如果是下拉,值应该为 0
if (value == 0) {
printf("✅ 确认下拉: GPIO 被拉低\n");
} else {
printf("⚠️ 异常: 触发但值不为0\n");
}
}
}

// 清理
close(gpio_fd);

// 取消导出 GPIO
fd = open(SYSFS_GPIO_DIR "/unexport", O_WRONLY);
if (fd != -1) {
write(fd, GPIO_PIN, strlen(GPIO_PIN));
close(fd);
}

printf("程序退出\n");
return 0;
}