文章目录
- Linux 内核 GPIO 接口
- 旧版本方式:sysfs 接口
- 新版本方式:chardev 接口
- gpiod 库及其命令行
- gpiod 库的命令行
- gpiod 库函数的应用
GPIO(General Purpose Input/Output,通用输入/输出接口),是微控制器或微处理器上的引脚,可以被编程为输入或输出,用于与外部设备进行通信。在 Linux 系统中,通过内核提供的用户空间接口,开发者能够轻松地读取、设置 GPIO 的状态,实现对外部设备的控制和监测。
本文将基于 Orangepi ZERO 2开发板,探讨 Linux 内核(kernel 4.8 版本起)基于字符设备的新接口,用于访问和管理用户空间中的 GPIO 线路。
Linux 内核 GPIO 接口
在 Linux 系统内部,Linux 内核通过生产者/消费者模型实现对 GPIO 的访问。 有生产 GPIO 线路的驱动程序(GPIO控制器驱动程序)和消耗 GPIO 线路的驱动程序(键盘、触摸屏、传感器等)。
[!IMPORTANT]
生产 GPIO 线路的驱动程序相关链接:GPIO Driver Interface — The Linux Kernel documentation
消耗 GPIO 线路的驱动程序相关链接:GPIO Descriptor Consumer Interface — The Linux Kernel documentation
为了管理 GPIO 注册和分配,Linux 内核中有一个名为 gpiolib
的框架。 该框架为在内核空间和用户空间应用程序中运行的设备驱动程序提供了一个 API。
旧版本方式:sysfs 接口
在 Linux kernel 4.7 版本之前,管理用户空间中 GPIO 线路的接口一直通过在 /sys/class/gpio
导出的文件在 sysfs
中。 因此,如果想要将某个 GPIO 设置成输出,且让这个 GPIO 输入高电平,整体步骤如下:
- 确定GPIO线路的编号;
- 将 GPIO 编号写入到
/sys/class/gpio/export
; - 将 GPIO 线路配置为输出,对应设置的文件为
/sys/class/gpio/gpioX/direction
; - 将 1 写入到
/sys/class/gpio/gpioX/value
,使 GPIO 输出高电平。
以 Orangepi ZERO 2 为例子,要从用户空间设置 GPIO 69 输出高电平,要执行以下命令(root 用户下执行):
echo 69 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio69/direction
echo 1 > /sys/class/gpio/gpio69/value
执行前后效果如下图所示(gpio readall
命令用于查看物理引脚的状态):
从命令的运行界面来看,效果很好,执行也简单,但还是有一些缺陷:
- GPIO 的分配不与任何进程绑定,如果使用 GPIO 的进程结束执行或崩溃,GPIO 可能会继续保持当前状态;
- 如果多个进程访问同一 GPIO 线路,并发可能是一个问题;
- 写入多个引脚需要对大量文件(export / direction / value等)进行
open
、read
、write
和close
等操作; - 捕获事件(GPIO 线路的中断)的轮询过程不可靠;
- 没有接口来配置 GPIO 线路(开源、开漏等);
- 分配给 GPIO 线路的编号不确定。
新版本方式:chardev 接口
从 Linux kernel 4.8 版本开始,GPIO sysfs 接口被弃用,现在有一个基于字符设备的新 API,可以从用户空间访问 GPIO 线路。
每个 GPIO 控制器(gpiochip)在 /dev
中都会有一个字符设备,可以使用文件操作(open、read、write、ioctl、poll、close)来管理 GPIO 行并与之交互,输入 ls /dev/gpiochip*
,可以看到 GPIO 的所有字符设备:
尽管这个新的字符设备接口可以防止使用 echo
和 cat
等标准命令行操作 GPIO,但与 sysfs 接口相比,它有一些优点:
- GPIO 的分配与正在使用它的进程相关联,从而改进了对用户空间进程使用哪些 GPIO 线路的控制;
- 以一次读取或写入多条 GPIO 线路;
- 可以按名称找到 GPIO 控制器和 GPIO 线路;
- 可以配置引脚的状态(开源、开漏等);
- 捕获事件(来自 GPIO 线路的中断)的轮询过程是可靠的。
gpiod 库及其命令行
使用 chardev 接口,可以安装 libgpiod 项目来使用,这是个与 Linux GPIO 字符设备交互的 C 库和工具。
还是以 Orangepi ZERO 2 开发板为例,该板运行的操作系统为 Ubuntu22.04,kernel 版本为 5.16。安装 libgpiod 库和工具的命令如下(如不是在 root 用户下操作,需要命令前加上 sudo
扩充命令的权限):
apt update
apt install libgpiod-dev
apt install gpiod
gpiod 库的命令行
安装好库和工具后,可以使用一些命令来确认是否安装成功。例如,检查可用的 GPIO 芯片:
gpiodetect
Orangepi ZERO 2 开发板的 GPIO 芯片如下图所示,有两个 gpiochip
,GPIO 线路分别是 288 和 32。
[300b000.pinctrl]
和 [7022000.pinctrl]
是GPIO芯片的名称或标签,通常与芯片的硬件地址或设备树节点名称相关联。这些名称由内核设备驱动程序指定,用于标识和区分不同的GPIO芯片。
查看 GPIO 芯片的信息命令(以 gpiochip0
为例):
gpioinfo /dev/gpiochip0
这个命令显示的信息很多,line
是GPIO 线的编号(从 0 到 287),后面是 GPIO 线的名称,未命名的线显示为 unnamed
,unused
表示该 GPIO 线目前未被使用,input
表示该 GPIO 线配置为输入模式,output
表示该 GPIO 线配置为输出模式,active-high
表示该 GPIO 线的活动电平为高电平。
在上图可以看出,有几个 GPIO 引脚被使用,被使用的 GPIO 会被命名为其他的名字,后面还会出 [used]
的字样。
用此前 wiringPi 库的命令 gpio readall
查看当前物理引脚的状态。其中物理引脚 12 号对应的 GPIO 线路号为 75,当前值为 0。
[!NOTE]
wiringPi 库是开发 Orangepi ZERO 2 开发板应用层程序的中间件,安装教程在我之前的博客《OrangePi ZERO 2 外设应用程序开发之接口与 wiringOP 库》提及。
用 gpioset
命令可以设置 GPIO 线路,假设现在要改变 75 号 GPIO 的输出状态(输出为 1),具体命令如下:
gpioset 0 75=1
命令中的 0
表示 gpiochip0
,75
为 GPIO 线路号,1
为写入值。
执行后,GPIO 75 的输出值立刻变为 1,相较于 sysfs 接口的调用,方便了非常多。
gpioget
命令将读取 GPIO 线路的值。例如,读取 GPIO 65 的值,命令如下:
gpioget 0 65
执行结果如下图,表示 GPIO 65 号引脚当前值为 0。
所有这些命令的源代码都可以在 libgpiod 仓库中找到。
gpiod 库函数的应用
gpiod 库提供了很多 API,可以直接对 GPIO 线路进行操作:
struct gpiod_chip *gpiod_chip_open(const char *path);
struct gpiod_line *gpiod_chip_get_line(struct gpiod_chip *chip, unsigned int offset);
int gpiod_line_request_input(struct gpiod_line *line, const char *consumer);
int gpiod_line_get_value(struct gpiod_line *line);
下面是 libgpiod
提供的几个函数的解释:
-
gpiod_chip_open
- 功能: 打开一个 GPIO 芯片。
- 参数:
const char *path
- 设备文件的路径,例如/dev/gpiochip0
。
- 返回值:
- 成功时返回一个指向
gpiod_chip
结构体的指针。 - 失败时返回
NULL
。
- 成功时返回一个指向
- 用途: 这是使用 GPIO 芯片的第一步,通过指定的路径打开一个 GPIO 芯片。
struct gpiod_chip *chip; chip = gpiod_chip_open("/dev/gpiochip0"); if (!chip) { perror("gpiod_chip_open"); exit(1); }
-
gpiod_chip_get_line
- 功能: 获取 GPIO 芯片上的一条 GPIO 线。
- 参数:
struct gpiod_chip *chip
- 一个指向打开的 GPIO 芯片的指针。unsigned int offset
- 要获取的 GPIO 线的编号(从 0 开始)。
- 返回值:
- 成功时返回一个指向
gpiod_line
结构体的指针。 - 失败时返回
NULL
。
- 成功时返回一个指向
- 用途: 获取特定编号的 GPIO 线,以便对其进行操作。
struct gpiod_line *line; line = gpiod_chip_get_line(chip, 4); // 获取第 4 条 GPIO 线 if (!line) { perror("gpiod_chip_get_line"); gpiod_chip_close(chip); exit(1); }
-
gpiod_line_request_input
- 功能: 请求将 GPIO 线配置为输入模式。
- 参数:
struct gpiod_line *line
- 一个指向要配置的 GPIO 线的指针。const char *consumer
- 请求者的名称(通常是应用程序的名称,用于调试和日志记录)。
- 返回值:
- 成功时返回 0。
- 失败时返回 -1。
- 用途: 将指定的 GPIO 线配置为输入模式,以便读取其电平值。
int ret; ret = gpiod_line_request_input(line, "my_consumer"); if (ret) { perror("gpiod_line_request_input"); gpiod_line_release(line); gpiod_chip_close(chip); exit(1); }
-
gpiod_line_get_value
- 功能: 获取 GPIO 线的电平值。
- 参数:
struct gpiod_line *line
- 一个指向配置为输入模式的 GPIO 线的指针。
- 返回值:
- 成功时返回 0 或 1,分别表示低电平和高电平。
- 失败时返回 -1。
- 用途: 读取 GPIO 线的当前电平值。
int value; value = gpiod_line_get_value(line); if (value < 0) { perror("gpiod_line_get_value"); } else { printf("GPIO line value: %d\n", value); }
总结来说,这几个函数的主要作用是打开 GPIO 芯片、获取 GPIO 线路、将 GPIO 线路配置为输入模式并读取其电平值。通过这些步骤,可以实现对 GPIO 线状态的监测和响应。
以下 C 程序使用 libgpiod
读取 GPIO 70 的例子:
// gpio_line.c
#include <stdio.h>
#include <string.h>
#include <gpiod.h>
#include <errno.h>
int main()
{
struct gpiod_chip *chip;
struct gpiod_line *line;
int req, value;
chip = gpiod_chip_open("/dev/gpiochip0");
if (!chip) {
perror("Failed to open GPIO chip");
return -1;
}
line = gpiod_chip_get_line(chip, 70);
if (!line) {
gpiod_chip_close(chip);
perror("Failed to get line");
return -1;
}
req = gpiod_line_request_input(line, "gpio_state");
if (req) {
gpiod_chip_close(chip);
fprintf(stderr, "Failed to request line as input: %s\n", strerror(errno));
return -1;
}
value = gpiod_line_get_value(line);
if (value < 0) {
gpiod_chip_close(chip);
fprintf(stderr, "Failed to get line value: %s\n", strerror(errno));
return -1;
}
printf("GPIO value is: %d\n", value);
gpiod_chip_close(chip);
return 0;
}
编译和运行命令如下:
gcc -o gpio_line gpio_line.c -lgpiod
./gpio_line
运行结果如下:
新的 Linux 内核 GPIO 用户空间接口是一个非常简单、优雅和健壮的 API,从现在开始应该用在嵌入式Linux开发上吧!