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/

如下图:

未找到图片2|

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

  • 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

如下图:

未找到图片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. 引脚编号转换

算能 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

如下图:

未找到图片6|

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

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

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

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

9.3.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
#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

如下图:

未找到图片8|

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

GPIO举例计算

引脚

控制器

索引号

计算结果

GPIOA_6

A

6

0 6

GPIOB_5

B

5

2 5

PWR_GPIO5

PWR

4

4 5

9.4.1. 命令行控制

常用的命令行如下,可使用 -h 查看命令相对应的使用说明(以GPIOA_6为例)

libgpiod命令

命令

作用

使用举例

说明

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

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/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编程

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 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,速度更快,效率更高

现象:和上面的程序一样。