9. GPIO子系统¶
本章配套视频介绍:
本章讲解Linux GPIO子系统驱动相关应用层程序的控制原理。
本章的示例代码目录为:base_linux/gpio/
LubanCat-sg200x系列板卡引出了22pin排针引脚, 部分引脚可以复用为I2C、uart、pwm等功能。
其中LubanCat-P1板卡22pin引脚图如下:
9.1. 简介¶
GPIO是General Purpose I/O的缩写,即通用输入输出端口, 简单来说就是MCU/CPU可控制的引脚,这些引脚通常有多种功能, 最基本的是高低电平输入检测和输出, 部分引脚还会与主控器的片上外设绑定, 如作为串口、I2C、网络、电压检测的通讯引脚。
Linux提供了GPIO子系统驱动框架, 使用该驱动框架可以把CPU的GPIO引脚导出到用户空间, 用户通过访问/sys文件系统进行控制, GPIO子系统支持把引脚用于基本的输入输出功能, 其中输入功能还支持中断检测。 在Linux内核源码的“Documentation/gpio”目录可找 到关于GPIO子系统的说明。
9.1.1. GPIO设备目录¶
GPIO驱动子系统导出到用户空间的目录是“/sys/class/gpio”。
可使用如下命令查看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #需要切换到root用户执行下列命令
sudo -s
输入密码
#导出GPIO到用户空间
echo 486 > /sys/class/gpio/export
#查看目录的变化,增加了gpio486目录
ls /sys/class/gpio/
#把gpio486从用户空间中取消导出
echo 486 > /sys/class/gpio/unexport
#查看目录变化,gpio486目录消失了
ls /sys/class/gpio/
|
如下图:
该目录下的主要内容说明如下:
export文件:导出GPIO,该文件只能写不能读,用户向该文件写 入GPIO的编号N可以向内核申请将该编号的GPIO导出到用户空间,若内核本 身没有把该GPIO用于其它功能,那么在/sys/class/gpio目录下会新增一 个对应编号的gpioN目录,如上图一导出了gpio486。
unexport文件:export的相反操作,取消导出GPIO,该文件同样只能 写不能读。上图演示了往unexport写入486后,gpio486目录消失了。
gpiochipX目录:该目录是指GPIO控制器外设.
gpioN目录:通过export导出的具体GPIO引脚的控制目录, 如上图中的gpio486目录下会包含有控制该引脚的相应文件。
9.1.2. GPIO设备属性¶
gpioN目录下相关的设备文件,可以使用以下命令查看:
1 2 3 4 5 6 7 8 9 | #在板卡的终端使用以下命令
#导出编号为486的GPIO
echo 486 > /sys/class/gpio/export
#切换到gpiox的目录中
cd /sys/class/gpio/gpio486
#查看gpio486目录下的内容
ls -lh
|
如下图:
常用的属性文件介绍如下:
direction:表示GPIO引脚的方向,它的可取值如下:
in:引脚为输入模式。
out:引脚为输出模式,且默认输出电平为低。
low:引脚为输出模式,且默认输出电平为低
high:引脚为输出模式,且默认输出电平为高
value:表示GPIO的电平,1表示高电平,0表示低电平。GPIO被配置为输出模式, 那么修改该文件的内容可以改变引脚的电平。
edge:用于配置GPIO的中断触发方式,当GPIO被配置为中断时,可以通过系统 的poll函数监听。edge文件可取如下的属性值:
none:没有使用中断模式。
rising:表示引脚为中断输入模式,上升沿触发。
falling:表示引脚为中断输入模式,下降沿触发。
both:表示引脚为中断输入模式,边沿触发。
如果该引脚会被设备占用,它的功能在用户空间是无法再被修改的, 而使用GPIO子系统的设备则可以在用户空间灵活配置作为输入、输出或中断模式。
9.2. 引脚编号转换¶
算能 Pin的ID按照 控制器(bank)+索引序号(pin) 组成。
控制器和GPIO控制器数量⼀致
控制器固定 A、B、C、D、PWR,对应GPIO控制器0、1、2、3、4
索引序号固定 0、1、2、3、4、5、6、7…
sg200x具有5个控制器,作为GPIO功能时,端口⾏为由GPIO控制器寄存器配置。
GPIO 编号 = GPIO 组号值 + 偏移值
各控制器的GPIO值如下:
控制器 |
组号值 |
---|---|
A |
480 |
B |
448 |
C |
416 |
D |
384 |
PWR |
352 |
GPIOA_1表达的意思为控制器号为A,索引号为1。该引脚号的计算公式为 480+1=481
9.3. GPIO sysfs接口控制gpio¶
9.3.1. 命令行¶
在板卡上执行以下命令测试,测试前需确保当前用户为root用户:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #以下所有操作均需要打开管理者权限使用
#使能引脚
echo 486 > /sys/class/gpio/export
#设置引脚为输入模式
echo in > /sys/class/gpio/gpio486/direction
#读取引脚的值
cat /sys/class/gpio/gpio486/value
#设置引脚为输出模式
echo out > /sys/class/gpio/gpio486/direction
#设置引脚为低电平
echo 0 > /sys/class/gpio/gpio486/value
#设置引脚为高电平
echo 1 > /sys/class/gpio/gpio486/value
#复位引脚
echo 486 > /sys/class/gpio/unexport
|
如下图:
命令执行的原理非常简单:
把GPIO的编号写入到export文件,导出GPIO设备。
修改GPIO设备属性direction文件值为out,把GPIO设置为输出方向。
修改GPIO设备属性文件value的值为1或0,控制GPIO高电平或低电平。
9.3.2. 程序编写¶
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 118 119 120 121 122 | #include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
static char gpio_path[75];
int gpio_init(char *name)
{
int fd;
//index config
sprintf(gpio_path, "/sys/class/gpio/gpio%s", name);
if (access("gpio_path", F_OK)){
fd = open("/sys/class/gpio/export", O_WRONLY);
if(fd < 0)
return 1 ;
write(fd, name, strlen(name));
close(fd);
//direction config
sprintf(gpio_path, "/sys/class/gpio/gpio%s/direction", name);
fd = open(gpio_path, O_WRONLY);
if(fd < 0)
return 2;
write(fd, "out", strlen("out"));
close(fd);
}
return 0;
}
int gpio_deinit(char *name)
{
int fd;
fd = open("/sys/class/gpio/unexport", O_WRONLY);
if(fd < 0)
return 1;
write(fd, name, strlen(name));
close(fd);
return 0;
}
int gpio_high(char *name)
{
int fd;
sprintf(gpio_path, "/sys/class/gpio/gpio%s/value", name);
fd = open(gpio_path, O_WRONLY);
if(fd < 0){
printf("open %s wrong\n",gpio_path);
return -1;
}
if(2 != write(fd, "1", sizeof("1")))
printf("wrong set \n");
close(fd);
return 0;
}
int gpio_low(char *name)
{
int fd;
sprintf(gpio_path, "/sys/class/gpio/gpio%s/value", name);
fd = open(gpio_path, O_WRONLY);
if(fd < 0){
printf("open %s wrong\n",gpio_path);
return -1;
}
if(2 != write(fd, "0", sizeof("0")))
printf("wrong set \n");
close(fd);
return 0;
}
int main(int argc, char *argv[])
{
char buf[10];
int res;
/* 校验传参 */
if (2 != argc) {
printf( "usage: %s <PinNum>\n",argv[0]);
return -1;
}
res = gpio_init(argv[1]);
if(res){
printf("gpio init error,code = %d",res);
return 0;
}
while(1){
printf("Please input the value : 0--low 1--high q--exit\n");
scanf("%10s", buf);
switch (buf[0]){
case '0':
gpio_low(argv[1]);
break;
case '1':
gpio_high(argv[1]);
break;
case 'q':
gpio_deinit(argv[1]);
printf("Exit\n");
return 0;
default:
break;
}
}
return 0;
}
|
该代码说明如下:
gpio_init函数:它使用了open、write、close等函数修改export和gpioN/direction文件, 初始化gpio使用的引脚为输出模式。
bgpio_deinit函数:向unexport文件写入编号,取消导出。
gpio_high和gpio_low函数:往gpioN/value文件写入1和0,控制引脚输出高低电平。
scanf检测用户输入,根据用户输入调用对应的函数控制GPIO。
本代码要特别注意的是export和unexport文件是只有写权限的,所以通过open打开时要 使用“O_WRONLY”标志以写入方式打开,不能使用“O_RDWR”等带读模式的标志。
9.3.3. 编译&运行¶
方法1:
1 2 3 4 5 6 7 8 9 | #编译
make
#运行
sudo ./gpio_sys 引脚编号
#例子
#如果要控制GPIOA_6
sudo ./gpio_sys 486
|
方法2:
1 2 3 4 5 6 7 8 9 | #编译
gcc gpio_sys.c -o gpio_sys
#运行
sudo ./gpio_sys 引脚编号
#例子
#如果要控制GPIOA_6
sudo ./gpio_sys 486
|
如下图:
程序执行后会提示输入,在终端输入1并回车后GPIO会高电平,输入0并回车后GPIO会低电平。
9.4. 使用libgpiod控制IO¶
libgpiod是一种字符设备接口,GPIO访问控制是通过操作字符设备文件(比如 /dev/gpiodchip0 )实现的, 并通过libgpiod提供一些命令工具、c库以及python封装。
想要使用libgpiod,需要在板卡上安装libgpiod库。
1 2 3 4 5 | #安装libgpiod库及头文件
sudo apt install libgpiod-dev
#安装gpiod 命令行工具
sudo apt install gpiod
|
gpiod工具的使用方法与sysfs接口的不同,gpiod是以控制器为单位,然后再详细到端口号和索引号,即gpiod使用两个数据确定引脚
引脚 |
控制器 |
索引号 |
计算结果 |
---|---|---|---|
GPIOA_6 |
A |
6 |
0 6 |
GPIOB_5 |
B |
5 |
2 5 |
PWR_GPIO5 |
PWR |
4 |
4 5 |
9.4.1. 命令行控制¶
常用的命令行如下,可使用 -h 查看命令相对应的使用说明(以GPIOA_6为例)
命令 |
作用 |
使用举例 |
说明 |
---|---|---|---|
gpiodetect |
列出所有的GPIO控制器 |
gpiodetect(无参数) |
列出所有的GPIO控制器 |
gpioinfo |
列出gpio控制器的引脚情况 |
gpioinfo 0 |
列出第一组控制器引脚组情况 |
gpioset |
设置gpio |
gpioset 0 6=0 |
设置第一组控制器编号6引脚为低电平 |
gpioget |
获取gpio引脚状态 |
gpioget 0 6 |
获取第一组控制器编号6的引脚状态 |
gpiomon |
监控gpio的状态 |
gpiomon 0 6 |
监控第一组控制器编号6的引脚状态 |
重要
算能 Pin的ID按照控制器(bank)+索引序号(pin) 组成。其中端口号和索引号会合并成一个数值传入到gpiod里去 并不是所有的引脚都能够使用libgpiod控制,例如心跳灯、wifi供电使能之类的一些已经被使用的引脚。
9.4.2. 使用libgpiod编程¶
板卡安装好ligpiod-dev后,可以通过以下命令找到具体的头文件和库文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | # 在板卡上查找libgpiod库
dpkg -L libgpiod-dev
#信息输出如下
/.
/usr
/usr/include
/usr/include/gpiod.h
/usr/include/gpiod.hpp
/usr/lib
/usr/lib/riscv64-linux-gnu
/usr/lib/riscv64-linux-gnu/libgpiod.a
/usr/lib/riscv64-linux-gnu/libgpiodcxx.a
/usr/lib/riscv64-linux-gnu/pkgconfig
/usr/lib/riscv64-linux-gnu/pkgconfig/libgpiod.pc
/usr/lib/riscv64-linux-gnu/pkgconfig/libgpiodcxx.pc
/usr/share
/usr/share/doc
/usr/share/doc/libgpiod-dev
/usr/share/doc/libgpiod-dev/README.gz
/usr/share/doc/libgpiod-dev/copyright
/usr/lib/riscv64-linux-gnu/libgpiod.so
/usr/lib/riscv64-linux-gnu/libgpiodcxx.so
/usr/share/doc/libgpiod-dev/changelog.Debian.gz
|
查找结果中的gpiod.h、libgpiod.so和libgpiod.a就是板卡使用的头文件、动态和静态链接库, 它是Ubuntu默认apt安装的版本。
常用的libgpiod API(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 | //成员变量
struct gpiod_chip; //GPIO组句柄
struct gpiod_line; //GPIO引脚句柄
//获取GPIO控制器(GPIO组)
struct gpiod_chip *gpiod_chip_open(const char *path);
//获取GPIO引脚
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_request_output(struct gpiod_line *line,const char *consumer, int default_val)
//设置引脚的高低电平
int gpiod_line_set_value(struct gpiod_line *line, int value);
//读取引脚状态
int gpiod_line_get_value(struct gpiod_line *line);
//释放GPIO引脚
void gpiod_line_release(struct gpiod_line *line);
//关闭GPIO组句柄并释放所有分配的资源。
void gpiod_chip_close(struct gpiod_chip *chip);
|
9.4.2.1. 程序编写¶
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 | #include <gpiod.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
int i;
int ret;
char buf[10];
struct gpiod_chip * chip; //GPIO控制器句柄
struct gpiod_line * line; //GPIO引脚句柄
/*获取GPIO控制器*/
chip = gpiod_chip_open("/dev/gpiochip0");
if(chip == NULL){
printf("gpiod_chip_open error\n");
return -1;
}
/*获取GPIO引脚*/
line = gpiod_chip_get_line(chip, 6);
if(line == NULL){
printf("gpiod_chip_get_line error\n");
goto release_line;
}
/*设置GPIO为输出模式*/
ret = gpiod_line_request_output(line,"led",0);
if(ret < 0){
printf("gpiod_line_request_output error\n");
goto release_chip;
}
for(i = 0;i<10;i++)
{
gpiod_line_set_value(line,1);
usleep(500000); //延时0.5s
gpiod_line_set_value(line,0);
usleep(500000);
}
release_line:
/*释放GPIO引脚*/
gpiod_line_release(line);
release_chip:
/*释放GPIO控制器*/
gpiod_chip_close(chip);
return 0;
}
|
9.4.2.2. 编译&运行¶
在运行前可以根据自己想要控制的引脚修改代码
这里以控制GPIOA_6为例
1 | chip = gpiod_chip_open("/dev/gpiochip0");
|
该函数用来确定gpio的控制器,gpiochip0,1,2,3,4
1 | line = gpiod_chip_get_line(chip, 6);
|
控制器固定 A、B、C、D、PWR,对应GPIO控制器0、1、2、3、4
索引序号固定 0、1、2、3、4、5、6、7…
方法1:
1 2 3 4 5 | #编译
make
#运行
sudo ./gpio_lib
|
现象:gpio外接灯,可以看到灯间歇性亮灭,闪了十下就灭了
方法2:
1 2 3 4 5 | #编译,需要借助外来的库
gcc gpio_lib.c -o gpio_lib `pkg-config --cflags libgpiod` `pkg-config --libs libgpiod`
#运行
sudo ./gpio_lib
|
现象:gpio外接灯,可以看到灯间歇性亮灭,闪了十下就灭了
9.5. systerm编程¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
pid_t result;
int i;
for(i=0;i<10;i++){
/*调用 system()函数*/
result = system("gpioset 0 6=1");
usleep(500000);
result = system("gpioset 0 6=0");
usleep(500000);
}
return result;
}
|
该代码的原理是在C程序中使用”system()” 相当于在shell终端里使用命令, 需要root权限执行。
缺点:这种方法控制io会涉及到内核的上下文切换,会影响到内核的处理。 因此,不推荐使用这种方法在短时间内多次操作GPIO。如果对GPIO有多次操作, 可以使用gpio_lib.c,速度更快,效率更高
现象:和上面的程序一样。