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/

如下图:

未找到图片2|

该目录下的主要内容说明如下:

  • 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

如下图:

未找到图片4|

常用的属性文件介绍如下:

  • direction:表示GPIO引脚的方向,它的可取值如下:

    1. in:引脚为输入模式。

    2. out:引脚为输出模式,且默认输出电平为低。

    3. low:引脚为输出模式,且默认输出电平为低

    4. high:引脚为输出模式,且默认输出电平为高

  • value:表示GPIO的电平,1表示高电平,0表示低电平。GPIO被配置为输出模式, 那么修改该文件的内容可以改变引脚的电平。

  • edge:用于配置GPIO的中断触发方式,当GPIO被配置为中断时,可以通过系统 的poll函数监听。edge文件可取如下的属性值:

    1. none:没有使用中断模式。

    2. rising:表示引脚为中断输入模式,上升沿触发。

    3. falling:表示引脚为中断输入模式,下降沿触发。

    4. 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->引脚号):

未找到图片6|

反向计算(引脚号->GPIO):

未找到图片4|

超过最大引脚数量报错:

未找到图片4|

注意

并不是所有的引脚都能通过export文件导出到用户空间的,正在使用的引脚是不能被导出的

9.4. GPIO sysfs接口控制gpio

9.4.1. 命令行

在Linux中,最常见的读写GPIO方式就是用GPIO sysfs interface, 是通过操作 /sys/class/gpio 目录下的 exportunexportgpio{N}/direction, gpio{N} /value (用实际引脚号替代{N})等文件实现的,经常出现shell脚本里面。 在kernel 4.8开始,加入了libgpiod的支持;而原有基于sysfs的访问方式,将被逐渐放弃。

GPIO举例计算

引脚

控制器

索引号

计算结果

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

如下图:

未找到图片6|

命令执行的原理非常简单:

  • 把GPIO的编号写入到export文件,导出GPIO设备。

  • 修改GPIO设备属性direction文件值为out,把GPIO设置为输出方向。

  • 修改GPIO设备属性文件value的值为1或0,控制GPIO高电平或低电平。

9.4.2. 程序编写

base_linux/gpio/gpio_sys/gpio_sys.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
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

如下图:

未找到图片8|

程序执行后会提示输入,在终端输入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使用两个数据确定引脚

GPIO举例计算

引脚

控制器

索引号

计算结果

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

如下图:

未找到图片6|

三列数据分别是 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

如下图:

未找到图片6|

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

如下图:

未找到图片6|

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

如下图:

未找到图片6|

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

如下图:

未找到图片6|

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. 程序编写

base_linux/gpio/gpio_lib/gpio_lib.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
#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编程

base_linux/gpio/gpio_system/gpio_system.c
 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,速度更快,效率更高

现象:和上面的程序一样