GPIO
通用输入输出(GPIO)是微控制器(MCU)或中央处理单元(CPU)上可编程的引脚,它们能够进行基本的数字信号输入和输出操作。除了简单的电平检测和输出功能外,某些GPIO引脚还可能与主控器的内置外设相连,用于串行通信、I2C、网络接口或电压检测等。在Linux系统中,通过GPIO子系统和相应的驱动框架,用户可以灵活地控制硬件板上的GPIO引脚。
注解
继电器输出,光耦输入、LED指示灯、蜂鸣器、按键等都可以通过 GPIO 来控制,因此归为一类,均可参考本文。
1. GPIO PIN ID
Rockchip PIN的ID 按照控制器(bank)+端口(port)+索引序号(PIN)组成,
控制器和GPIO控制器数量一致
端口固定为A/B/C/D,序号从0开始,0/1/2/3
索引导号固定0/1/2/3/4/5/6/7
具体计算公式如下:
PIN-ID=32*BANK+8×PORT+1*PIN
例如:GPIO1_A2的PIN-ID=32×1+8×0+1×2=34
2. 查看 pinmux-pins:gpio全部信息
cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins
3. 查看 GPIO 占用情况
cat /sys/kernel/debug/gpio
4. 使用GPIO sysfs接口控制IO
4.1 简介
在Linux系统中,操作GPIO(通用输入输出)引脚的一种常见方法是通过GPIO sysfs接口。这种方式涉及到对/sys/class/gpio
目录下的文件进行操作,比如export
、unexport
、gpio{N}/direction
和gpio{N}/value
(其中{N}
代表实际的引脚编号)。这种操作经常在shell脚本中见到。
4.2 操作步骤
假设要控制GPIO3_A0,那么对应的PIN-ID=41,通过下面的操作步骤来控制GPIO3_A0:
计算PIN ID
PIN-ID=32*BANK+8×PORT+1*PIN
PIN-ID=32*3+8×0+1×0=96
导出GPIO
echo 96 > /sys/class/gpio/export # gpio_request 申请导出相应的gpio
设置GPIO方向
echo out > /sys/class/gpio/gpio96/direction # 设置gpio为输出模式,可选in/out
设置GPIO值
echo 1 > /sys/class/gpio/gpio96/value # 设置gpio输出高电平,可选1/0
查看 GPIO 当前值
cat /sys/class/gpio/gpio96/value # 查看gpio当前值
释放 GPIO
echo 96 > /sys/class/gpio/unexport # gpio_free 释放gpio
注解
如果驱动程序已经使用了该引脚,那么将会export失败,会提示下面的错误:
Device or resource busy
5. 使用libgpiod控制IO
libgpiod
是一个现代的 GPIO 访问库,提供了一种通过字符设备文件控制 GPIO 的方式。这种接口在 Linux 内核 4.8 及以后的版本中引入,旨在替代原有的 sysfs GPIO 接口。libgpiod
提供了一系列命令行工具和编程库,包括C 库和 Python 封装,使得 GPIO 控制更加灵活和方便。
5.1 安装 libgpiod 命令行工具
在基于 Debian 的系统上,您可以使用以下命令安装 gpiod
工具:
sudo apt update
sudo apt install gpiod
5.2 libgpiod 的使用方法
与 sysfs 接口不同,libgpiod
以控制器为单位进行操作,然后指定端口号和索引号来确定具体的引脚。以下是如何使用 libgpiod
来控制 GPIO:
5.2.1 GPIO 引脚编号计算
5.2.2 常用的 libgpiod 命令行工具
gpiodetect:列出所有的 GPIO 控制器。
bash
gpiodetect
此命令会显示系统中所有可用的 GPIO 控制器信息。
gpioinfo:列出指定 GPIO 控制器的引脚情况。
bash
gpioinfo 1
此命令会显示第一组控制器的引脚信息。
gpioset:设置 GPIO 引脚的状态。
bash
gpioset 1 20=0
此命令将第一组控制器的第 20 号引脚设置为低电平。
gpioget:获取 GPIO 引脚的状态。
bash
gpioget 1 20
此命令获取第一组控制器的第 20 号引脚的当前状态。
gpiomon:监控 GPIO 引脚的状态变化。
bash
gpiomon 1 20
此命令监控第一组控制器的第 20 号引脚的状态变化。
5.3 注意事项
Rockchip Pin 的 ID 是按照控制器(bank)+ 端口(port)+ 索引序号(pin)组成的。端口号和索引号会合并成一个数值传入到
gpiod
里去。并不是所有的引脚都能够使用
libgpiod
控制,例如已经被用作 LED 或其他功能的引脚。尝试控制这些已经被定义的引脚时,可能会出现设备繁忙的错误,导致无法使用。
6. GPIO C语言编程实例
通过sysfs方式控制GPIO,先访问/sys/class/gpio目录,向export文件写入GPIO编号,使得该GPIO的操作接口从内核空间暴露到用户空间,GPIO的操作接口包括direction和value等,direction控制GPIO方向,而value可控制GPIO输出或获得GPIO输入。文件IO方式操作GPIO,使用到了4个函数open、close、read、write。
6.1 GPIO 输出实例
以下是一个简单的基于 C 语言的流水灯和呼吸灯效果的示例代码。这个示例代码使用的是 Linux 上的用户空间 GPIO 控制,你需要适配代码以使用正确的 GPIO 引脚和路径。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#define LED_NUM 4
// GPIO 控制的相关路径
#define SYSFS_GPIO_EXPORT "/sys/class/gpio/export"
#define SYSFS_GPIO_DIR_PREFIX "/sys/class/gpio/gpio"
#define SYSFS_GPIO_VALUE_SUFFIX "/value"
// 设置 GPIO 方向
void set_gpio_direction(int gpio, const char *dir) {
char gpio_path[50];
sprintf(gpio_path, "%s%d/direction", SYSFS_GPIO_DIR_PREFIX, gpio);
int fd = open(gpio_path, O_WRONLY);
if (fd == -1) {
perror("Error opening direction file");
exit(EXIT_FAILURE);
}
write(fd, dir, strlen(dir));
close(fd);
}
// 控制 GPIO 输出
void set_gpio_value(int gpio, int value) {
char gpio_path[50];
sprintf(gpio_path, "%s%d/value", SYSFS_GPIO_DIR_PREFIX, gpio);
int fd = open(gpio_path, O_WRONLY);
if (fd == -1) {
perror("Error opening value file");
exit(EXIT_FAILURE);
}
char str_value = value ? '1' : '0';
write(fd, &str_value, 1);
close(fd);
}
int main() {
int leds[LED_NUM] = {17, 18, 19, 20}; // 假设使用的 GPIO 引脚编号
int i, j;
for (i = 0; i < LED_NUM; i++) {
// 导出 GPIO
int export_fd = open(SYSFS_GPIO_EXPORT, O_WRONLY);
if (export_fd == -1) {
perror("Error opening export file");
return EXIT_FAILURE;
}
char gpio_str[3];
sprintf(gpio_str, "%d", leds[i]);
write(export_fd, gpio_str, strlen(gpio_str));
close(export_fd);
// 设置 GPIO 方向为输出
set_gpio_direction(leds[i], "out");
}
while (1) {
// 流水灯效果
for (i = 0; i < LED_NUM; i++) {
set_gpio_value(leds[i], 1);
usleep(200000);
set_gpio_value(leds[i], 0);
}
// 呼吸灯效果
for (j = 0; j < 100; j++) {
for (i = 0; i < LED_NUM; i++) {
set_gpio_value(leds[i], 1);
usleep(j * j);
}
for (i = 0; i < LED_NUM; i++) {
set_gpio_value(leds[i], 0);
usleep(j * j);
}
}
}
return 0;
}
6.2 GPIO 输入实例
以下是一个将 GPIO 配置为输入状态以检测外部按键状态变化的 C 程序。这个程序会监听指定 GPIO 引脚的电平变化,并在检测到变化时输出相应信息。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <poll.h>
#define BUTTON_GPIO 17 // 假设使用 GPIO17 连接按键
// GPIO 控制的相关路径
#define SYSFS_GPIO_EXPORT "/sys/class/gpio/export"
#define SYSFS_GPIO_UNEXPORT "/sys/class/gpio/unexport"
#define SYSFS_GPIO_DIR_PREFIX "/sys/class/gpio/gpio"
#define SYSFS_GPIO_DIRECTION_SUFFIX "/direction"
#define SYSFS_GPIO_EDGE_SUFFIX "/edge"
#define SYSFS_GPIO_VALUE_SUFFIX "/value"
// 导出 GPIO
void export_gpio(int gpio) {
int fd = open(SYSFS_GPIO_EXPORT, O_WRONLY);
if (fd == -1) {
perror("Error opening export file");
exit(EXIT_FAILURE);
}
char gpio_str[10];
sprintf(gpio_str, "%d", gpio);
write(fd, gpio_str, strlen(gpio_str));
close(fd);
}
// 取消导出 GPIO
void unexport_gpio(int gpio) {
int fd = open(SYSFS_GPIO_UNEXPORT, O_WRONLY);
if (fd == -1) {
perror("Error opening unexport file");
exit(EXIT_FAILURE);
}
char gpio_str[10];
sprintf(gpio_str, "%d", gpio);
write(fd, gpio_str, strlen(gpio_str));
close(fd);
}
// 设置 GPIO 方向
void set_gpio_direction(int gpio, const char *dir) {
char gpio_path[50];
sprintf(gpio_path, "%s%d%s", SYSFS_GPIO_DIR_PREFIX, gpio, SYSFS_GPIO_DIRECTION_SUFFIX);
int fd = open(gpio_path, O_WRONLY);
if (fd == -1) {
perror("Error opening direction file");
exit(EXIT_FAILURE);
}
write(fd, dir, strlen(dir));
close(fd);
}
// 设置 GPIO 边沿触发方式
void set_gpio_edge(int gpio, const char *edge) {
char gpio_path[50];
sprintf(gpio_path, "%s%d%s", SYSFS_GPIO_DIR_PREFIX, gpio, SYSFS_GPIO_EDGE_SUFFIX);
int fd = open(gpio_path, O_WRONLY);
if (fd == -1) {
perror("Error opening edge file");
exit(EXIT_FAILURE);
}
write(fd, edge, strlen(edge));
close(fd);
}
// 读取 GPIO 值
int read_gpio_value(int gpio) {
char gpio_path[50];
sprintf(gpio_path, "%s%d%s", SYSFS_GPIO_DIR_PREFIX, gpio, SYSFS_GPIO_VALUE_SUFFIX);
int fd = open(gpio_path, O_RDONLY);
if (fd == -1) {
perror("Error opening value file");
exit(EXIT_FAILURE);
}
char value_str[2];
if (read(fd, value_str, 1) == -1) {
perror("Error reading value");
exit(EXIT_FAILURE);
}
close(fd);
return (value_str[0] == '1') ? 1 : 0;
}
int main() {
// 导出 GPIO
export_gpio(BUTTON_GPIO);
printf("GPIO%d 已导出\n", BUTTON_GPIO);
// 设置 GPIO 方向为输入
set_gpio_direction(BUTTON_GPIO, "in");
printf("GPIO%d 已设置为输入模式\n", BUTTON_GPIO);
// 设置 GPIO 边沿触发方式为双边沿
set_gpio_edge(BUTTON_GPIO, "both");
printf("GPIO%d 已设置为双边沿触发\n", BUTTON_GPIO);
// 打开 GPIO 值文件用于轮询
char gpio_path[50];
sprintf(gpio_path, "%s%d%s", SYSFS_GPIO_DIR_PREFIX, BUTTON_GPIO, SYSFS_GPIO_VALUE_SUFFIX);
int fd = open(gpio_path, O_RDONLY);
if (fd == -1) {
perror("Error opening value file for polling");
return EXIT_FAILURE;
}
// 清除初始值
char buf[10];
lseek(fd, 0, SEEK_SET);
read(fd, buf, 1);
// 配置 poll 结构
struct pollfd fds[1];
fds[0].fd = fd;
fds[0].events = POLLPRI | POLLERR;
printf("开始监听 GPIO%d 的状态变化...\n", BUTTON_GPIO);
printf("按 Ctrl+C 退出\n");
while (1) {
// 等待事件发生
int result = poll(fds, 1, -1);
if (result == -1) {
perror("Poll error");
break;
} else if (result > 0) {
// 检测到事件
if (fds[0].revents & POLLPRI) {
// 清除文件指针
lseek(fd, 0, SEEK_SET);
// 读取新值
char value_str[2];
if (read(fd, value_str, 1) == -1) {
perror("Error reading value after poll");
continue;
}
int value = (value_str[0] == '1') ? 1 : 0;
printf("检测到 GPIO%d 状态变化: %s\n", BUTTON_GPIO, value ? "高电平" : "低电平");
}
}
}
// 关闭文件描述符
close(fd);
// 取消导出 GPIO
unexport_gpio(BUTTON_GPIO);
printf("GPIO%d 已取消导出\n", BUTTON_GPIO);
return 0;
}