9. GPIO子系统¶
本章讲解Linux GPIO子系统驱动相关应用层程序的控制原理。
本章的示例代码目录为:base_linux/gpio
LubanCat-AW系列的板卡引脚不是完全一样,以下为引脚图
LubanCat-H618系列
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 | #需要切换到root用户执行下列命令
su
输入密码
#导出GPIO到用户空间
echo 67 > /sys/class/gpio/export
#查看目录的变化,增加了gpio67目录
ls /sys/class/gpio/
#把gpio67从用户空间中取消导出
echo 67 > /sys/class/gpio/unexport
#查看目录变化,gpio67目录消失了
ls /sys/class/gpio/
|
如下图:
该目录下的主要内容说明如下:
export文件:导出GPIO,该文件只能写不能读,用户向该文件写入GPIO的编号N可以向内核申请将该编号的GPIO导出到用户空间, 若内核本身没有把该GPIO用于其它功能,那么在/sys/class/gpio目录下会新增一个对应编号的gpioN目录, 如上图一导出了gpio67。
unexport文件:export的相反操作,取消导出GPIO,该文件同样只能写不能读。 上图演示了往unexport写入67后,gpio67目录消失了。
gpiochipX目录:该目录是指GPIO控制器外设.
gpioN目录:通过export导出的具体GPIO引脚的控制目录, 如上图中的gpio67目录下会包含有控制该引脚的相应文件。
9.1.2. GPIO设备属性¶
gpioN目录下相关的设备文件,可以使用以下命令查看:
1 2 3 4 5 6 7 8 9 | #在板卡的终端使用以下命令
#导出编号为67的GPIO
echo 67 > /sys/class/gpio/export
#切换到gpiox的目录中
cd /sys/class/gpio/gpio67
#查看gpio67目录下的内容
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. 引脚编号转换¶
Allwinner Pin的ID按照 控制器端口(port)+索引序号(pin) 组成。 其中端口号和索引号会合并成一个数值传入到gpiod里去, 并不是所有的引脚都能够使用libgpiod控制,因为有些引脚已经被系统占用。
控制器端口有 PC、PF、PG、PH、PI、PL
控制器端口与具体控制器端口有关,比如PG就有20个索引序号
h618具有6个GPIO控制器,每个控制器控制不同数量的IO,如下表:
控制器端口 |
索引序号(索引号) |
PC |
0,1,2,3,······,15,16 |
PF |
0,1,2,3,4,5,6 |
PG |
0,1,2,3,······,18,19 |
PH |
0,1,2,3,······,9,10 |
PI |
0,1,2,3,······,15,16 |
PL |
0,1 |
作为GPIO功能时,端口⾏为由GPIO控制器寄存器配置。
9.3. 引脚号计算¶
引脚号的计算公式为:32 x (控制器端口) + (索引号)
如:PH2表达的意思为GPIO控制器为PH,索引号为2。该引脚号的计算公式为32 x 7 + 2 = 226
AllwinnerPin是一款全志芯片引脚计算器软件。
获取方式: :red:`网盘资料下载链接 -> 6-开发软件 -> AllwinnerPin.zip`
正向计算(GPIO->引脚号):
反向计算(引脚号->GPIO):
超过最大引脚数量报错:
注意
并不是所有的引脚都能通过export文件导出到用户空间的,正在使用的引脚是不能被导出的
9.4. GPIO sysfs接口控制gpio¶
9.4.1. 命令行¶
在Linux中,最常见的读写GPIO方式就是用GPIO sysfs interface, 是通过操作 /sys/class/gpio 目录下的 export 、 unexport 、gpio{N}/direction, gpio{N} /value (用实际引脚号替代{N})等文件实现的,经常出现shell脚本里面。 在kernel 4.8开始,加入了libgpiod的支持;而原有基于sysfs的访问方式,将被逐渐放弃。
引脚 |
控制器 |
索引号 |
计算结果 |
---|---|---|---|
PC3 |
PC |
3 |
67 (32 x 2 + 3) |
PG16 |
PG |
16 |
208 (32 x 6 + 16) |
PH7 |
PH |
7 |
231 (32 x 7 + 7) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #以下所有操作均需要打开管理者权限使用
#使能引脚PC3
echo 67 > /sys/class/gpio/export
#设置引脚为输入模式
echo in > /sys/class/gpio/gpio67/direction
#读取引脚的值
cat /sys/class/gpio/gpio67/value
#设置引脚为输出模式
echo out > /sys/class/gpio/gpio67/direction
#设置引脚为低电平
echo 0 > /sys/class/gpio/gpio67/value
#设置引脚为高电平
echo 1 > /sys/class/gpio/gpio67/value
#复位引脚
echo 67 > /sys/class/gpio/unexport
|
如下图:
命令执行的原理非常简单:
把GPIO的编号写入到export文件,导出GPIO设备。
修改GPIO设备属性direction文件值为out,把GPIO设置为输出方向。
修改GPIO设备属性文件value的值为1或0,控制GPIO高电平或低电平。
9.4.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 123 | #include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#define GPIO_INDEX "42"
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, "0", sizeof("0")))
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, "1", sizeof("1")))
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使用的引脚为输出模式。
gpio_deinit函数:向unexport文件写入编号,取消导出。
gpio_high和gpio_low函数:往gpioN/value文件写入1和0,控制引脚输出高低电平。
scanf检测用户输入,根据用户输入调用对应的函数控制GPIO。
注意
本代码要特别注意的是export和unexport文件是只有写权限的,所以通过open打开时要使用“O_WRONLY”标志以写入方式打开,不能使用“O_RDWR”等带读模式的标志。
9.4.3. 编译&运行¶
方法1:
1 2 3 4 5 6 7 8 9 | #编译
make
#运行
sudo ./gpio_sys 引脚编号
#例子
#如果要控制PC3
sudo ./gpio_sys 67
|
方法2:
1 2 3 4 5 6 7 8 9 | #编译
gcc gpio_sys.c -o gpio_sys
#运行
sudo ./gpio_sys 引脚编号
#例子
#如果要控制PC3
sudo ./gpio_sys 67
|
如下图:
程序执行后会提示输入,在终端输入1并回车后GPIO会高电平,输入0并回车后GPIO会低电平。
9.5. 使用libgpiod控制IO¶
从Linux 4.8版本开始,Linux引入了新的gpio操作方式,GPIO字符设备。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使用两个数据确定引脚
引脚 |
控制器 |
索引号 |
计算结果 |
---|---|---|---|
PC3 |
PC |
3 |
67 (32 x 2 + 3) |
PG16 |
PG |
16 |
208 (32 x 6 + 16) |
PH7 |
PH |
7 |
231 (32 x 7 + 7) |
重要
Allwinner Pin的ID按照 控制器端口(port)+索引序号(pin) 组成。其中端口号和索引号会合并成一个数值传入到gpiod里去,并不是所有的引脚都能够使用libgpiod控制,因为有些引脚已经被系统占用。
9.5.1. 命令行控制¶
常用的命令行如下,可使用 -h 查看命令相对应的使用说明,-h 查看版本信息。
9.5.1.1. gpiodetect¶
gpiodetect 命令用于列出系统上存在的所有 gpiochip,以及它们的名称、标签和 GPIO lines。
语法:
1 | gpiodetect [OPTIONS]
|
选项[OPTIONS]:
-h, –help :查看帮助并退出
-v, –version :查看版本信息并退出
示例:
1 2 | #在主机上执行如下命令
sudo gpiodetect
|
如下图:
三列数据分别是 gpiochip 的名称(name)、标签(label)和行数(lines)。
其中 gpiochip1
为后面需要用到的GPIO 控制器组。
9.5.1.2. gpioinfo¶
gpioinfo 命令用于列出指定 gpiochip 的所有 line,以及它们的名称、使用者、方向、活动状态和其他标志。
语法:
1 | gpioinfo [OPTIONS] <gpiochip> ...
|
选项[OPTIONS]:
-h, –help :查看帮助并退出
-v, –version :查看版本信息并退出
参数:
<gpiochip>:指定 gpiochip,如 gpiochip0、gpiochip1,可同时输入多个参数。
如果没有参数,则查询所有 gpiochip 的所有 line 的信息。
示例:
1 2 3 4 5 | #在主机上执行如下命令
#查询gpiochip1
sudo gpioinfo gpiochip1
#将查询gpiochip1简化成sudo gpioinfo 1
sudo gpioinfo 1
|
如下图:
9.5.1.3. gpioset¶
gpioset 命令用于设置指定的 GPIO line 的值。
语法:
1 | gpioset [OPTIONS] <chip name/number> <offset1>=<value1> <offset2>=<value2> ...
|
选项[OPTIONS]:
-l, –active-low :设置低电平为有效电平
-B, –bias=[as-is|disable|pull-down|pull-up] :设置 bias(默认使用 as-is)
-D, –drive=[push-pull|open-drain|open-source] :设置驱动模式(默认使用 push-pull)
-m, –mode=[exit|wait|time|signal] :设置完成后的动作模式
-s, –sec=SEC :当使用 –mode=time 选项时,指定等到的时间(单位:秒)
-u, –usec=USEC :当使用 –mode=time 选项时,指定等到的时间(单位:微秒)
-b, –background :设置完成后与控制终端分离
-h, –help :查看帮助并退出
-v, –version :查看版本信息并退出
参数:
<chip name/number>:指定 gpiochip,要使用板卡GPIO时需指定为gpiochip1/1。
<offset>:行内偏移量,这里参数应该为上述GPIO的计算结果,如PC3引脚为67。
<value>:参数为0和1,当高电平为有效电平(默认)时,1为高,0为低。
示例:
1 2 3 4 5 | #设置PF6、PC3、PH7为高电平
cat@lubancat:~$ sudo gpioset 1 166=1 67=1 231=1
#设置PF6、PC3、PH7为低电平
cat@lubancat:~$ sudo gpioset 1 166=0 67=0 231=0
|
如下图:
9.5.1.4. gpioget¶
gpioget 命令用于读取指定 GPIO line 的值。
语法:
1 | gpioget [OPTIONS] <chip name/number> <offset 1> <offset 2> ...
|
选项[OPTIONS]:
-l, –active-low :设置低电平为有效电平
-B, –bias=[as-is|disable|pull-down|pull-up] :设置 bias(默认使用 as-is)
-h, –help :查看帮助并退出
-v, –version :查看版本信息并退出
参数:
<chip name/number>:指定 gpiochip,要使用板卡GPIO时需指定为gpiochip1/1。
<offset>:行内偏移量,这里参数应该为上述GPIO的计算结果,如PC3引脚为67。
示例:
1 2 | #查看PF6、PC3、PH7引脚电平
cat@lubancat:~$ sudo gpioget 1 166 67 231
|
如下图:
9.5.1.5. gpiomon¶
gpiomon 命令用于等待指定 GPIO line 上的事件,或指定要监视的事件。
语法:
1 | gpiomon [OPTIONS] <chip name/number> <offset 1> <offset 2> ...
|
选项[OPTIONS]:
-l, –active-low :设置低电平为有效电平
-B, –bias=[as-is|disable|pull-down|pull-up] :设置 bias(默认使用 as-is)
-n, –num-events=NUM :处理完 NUM 个事件后退出
-s, –silent :不打印事件信息
-r, –rising-edge :只处理上升沿事件
-f, –falling-edge :只处理下降沿事件
-b, –line-buffered :将标准输出设置为行缓冲
-F, –format=FMT :指定输出格式(%o 为 GPIO 行内偏移量,%e 为事件类型,%s 为事件时间戳秒数部分,%n 为事件时间戳纳秒部分)
-h, –help :查看帮助并退出
-v, –version :查看版本信息并退出
参数:
<chip name/number>:指定 gpiochip,要使用板卡GPIO时需指定为gpiochip1/1。
<offset>:行内偏移量,这里参数应该为上述GPIO的计算结果,如PC3引脚为67。
示例:
1 2 | #监控PF6、PC3、PH7引脚
cat@lubancat:~$ sudo gpiomon 1 166 67 231
|
如下图:
9.6. C程序点灯¶
下面点灯程序以调用libgpiod库为例,演示板卡为鲁班猫A1。
9.6.1. 使用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 24 25 26 27 | # 在板卡上查找libgpiod库
dpkg -L libgpiod-dev
# 以下是输出
dpkg -L libgpiod-dev
/.
/usr
/usr/include
/usr/include/gpiod.h
/usr/include/gpiod.hpp
/usr/lib
/usr/lib/aarch64-linux-gnu
/usr/lib/aarch64-linux-gnu/libgpiod.a
/usr/lib/aarch64-linux-gnu/libgpiodcxx.a
/usr/lib/aarch64-linux-gnu/pkgconfig
/usr/lib/aarch64-linux-gnu/pkgconfig/libgpiod.pc
/usr/lib/aarch64-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/changelog.Debian.gz
/usr/share/doc/libgpiod-dev/copyright
/usr/lib/aarch64-linux-gnu/libgpiod.so
/usr/lib/aarch64-linux-gnu/libgpiodcxx.so
/usr/include/gpiod.h
/usr/lib/arm-linux-gnueabihf/libgpiod.a
/usr/lib/arm-linux-gnueabihf/libgpiod.so
|
查找结果中的gpiod.h、libgpiod.so和libgpiod.a就是板卡使用的头文件、动态和静态链接库, 它是Debian 10 buster默认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.6.1.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/gpiochip1");
if(chip == NULL){
printf("gpiod_chip_open error\n");
return -1;
}
/*获取GPIO引脚*/
line = gpiod_chip_get_line(chip, 8);
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.6.1.2. 编译&运行¶
在运行前可以根据自己想要控制的引脚修改代码
这里以控制GPIO1_A4为例
1 | chip = gpiod_chip_open("/dev/gpiochip1");
|
该函数用来确定gpio的控制器,gpiochip0,1,2,3,4
1 | line = gpiod_chip_get_line(chip, 4);
|
端口固定 A、B、C和D,每个端口仅有8个索引号,(A=0,B=1,C=2,D=3)
索引序号固定 0、1、2、3、4、5、6、7
计算方法:端口*8+索引序号,例0*8+4=4
方法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.7. 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 1 8=1");
usleep(500000);
result = system("gpioset 1 8=0");
usleep(500000);
}
return result;
}
|
该代码的原理是在C程序中使用”system()” 相当于在shell终端里使用命令, 需要root权限执行。
缺点:这种方法控制io会涉及到内核的上下文切换,会影响到内核的处理。 因此,不推荐使用这种方法在短时间内多次操作GPIO。如果对GPIO有多次操作, 可以使用gpio_lib.c,速度更快,效率更高
现象:和上面的程序一样