版权声明

本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/发表。已经授权的媒体、网站,在下载使用时必须注明来源,违者本人将依法追究责任。

  • Copyright (C) 2021 凌云物网智科实验室·郭工

  • Author: GuoWenxue <guowenxue@gmail.com> QQ: 281143292

wechat_pub

2.3 GPIO编程之LED灯设备控制

2.3.1 sysfs方式控制gpio简介

我们igkboard开发板支持sysfs方式控制gpio的电平行为,下面简单介绍一下sysfs方式控制。

首先查看/sys/class/gpio文件夹,如存在该文件夹说明,系统支持sysfs方式控制gpio

root@igkboard:/sys/class/gpio# ls
export  gpiochip0  gpiochip128  gpiochip32  gpiochip64  gpiochip96  unexport
  • 关于sysfs是什么?

sysfs 是最初基于 ramfs 的基于 ram 的文件系统。它提供了一种将内核数据结构、它们的属性以及它们之间的链接导出到用户空间的方法。 可以理解为驱动程序将一些驱动设备在内核程序的属性,通过sysfs的方式,导出到用户空间,最终以文本文件的方式显示。上一节ds18b20的驱动正是此方式,后面驱动编程时候,我们可以利用该机制个性化将我们自己的去驱动程序的属性导出到用户空间。

  • 下面我们简单介绍/sys/class/gpio中文件的作用

  1. /sys/class/gpio/export文件用于通知系统需要导出控制的GPIO引脚编号

  2. /sys/class/gpio/unexport 用于通知系统注销已导出的GPIO

  3. /sys/class/gpio/gpiochipX目录保存系统中GPIO寄存器的信息,包括每个寄存器控制引脚的起始编号base,寄存器名称,引脚总数 导出一个引脚的操作步骤

2.3.1.1 sysfs中gpio编号计算方法

  • 查看igkboard开发板扩展的gpio管脚

PiHeader

该40pin扩展图中,已经标好每个gpio对应的/sys/class/gpio 导出时候的编号。但是再此还是需要知道是如何计算的,计算公式如下

假设需要导出的gpio是GPIO0X_IOY
计算其编号为 NUM = (X - 1) * 32 + Y
示例 :GPIO01_IO29 GPIO05_IO09
NUM_GPIO01_IO29 = 0 + 29 = 29
NUM_GPIO05_IO09 = 4 * 32 + 9 = 137

2.3.1.2 sysfs常用接口使用

下面将GPIO05_IO09作为操作对象,进行示例测试。

  • 导出GPIO05_IO09到用户空间

root@igkboard:~# echo 137 > /sys/class/gpio/export 
root@igkboard:/sys/class/gpio# ls
export  gpio137  gpiochip0  gpiochip128  gpiochip32  gpiochip64  gpiochip96  unexport
root@igkboard:/sys/class/gpio# ls gpio137/
active_low  device  direction  edge  power  subsystem  uevent  value

可以看到/sys/class/gpio文件夹下增加了gpio137文件夹,查看该文件夹。下面讲解三个常用属性接口 active_low、direction、value

  • direction:gpio的输入输出属性,可以为in或out。

  • active_low:gpio的有效电平为低使能属性,可以为1或0(一般为0)。active_low为0时,高电平为有效电平,value为1时,gpio电平为高电平,0时为低电平;active_low为1时,低电平为有效电平,value为1时,gpio电平为低电平,0时为高电平。为了符合软件编程习惯,一般设置active_low=0。

  • value:gpio的电平值,实际电平高低和有效电平属性相关。可以为1或0。

  • 将GPIO05_IO09设置为输出或输入示例

root@igkboard:/sys/class/gpio/gpio137# echo out > direction 
root@igkboard:/sys/class/gpio/gpio137# echo in > direction 
  • 输出状态设置gpio的电平为低或高。(使用杜邦线连接 GPIO05_IO09 和 LED灯,可以看到灯亮灭效果)

root@igkboard:/sys/class/gpio/gpio137# echo out > direction 
root@igkboard:/sys/class/gpio/gpio137# echo 1 > value 
root@igkboard:/sys/class/gpio/gpio137# echo 0 > value
  • 输如状态读取gpio的电平。(使用杜邦线短接GPIO05_IO09和gnd或3.3V)

root@igkboard:/sys/class/gpio/gpio137# echo in > direction  
root@igkboard:/sys/class/gpio/gpio137# cat value 
0
root@igkboard:/sys/class/gpio/gpio137# cat value 
1
  • 注销已导出的gpio

root@igkboard:/sys/class/gpio# echo 137 > unexport     
root@igkboard:/sys/class/gpio# ls
export  gpiochip0  gpiochip128  gpiochip32  gpiochip64  gpiochip96  unexport

2.3.2 libgpiod库简介

libgpiod是用于与linux GPIO交互的C库和工具,从 linux 4.8 后,官方不推荐使用 GPIO sysfs 接口,libgpiod库封装了 ioctl 调用和简单的API接口。以下是libgpiod的仓库https://github.com/brgl/libgpiod。 与sysfs方式相比,libgpiod可以保证所有分配的资源,在关闭文件描述符后得到完全释放,并且拥有sysfs方式接口中不存在的功能(如时间轮询,一次设置/读取多个gpio值)。此外libgpiod还包含一组命令行工具,允许用户使用脚本对gpio进行个性化操作。

当前igkboard开发板的linux内核支持libgpiod的库使用,登录开发板后在/usr/lib文件夹之中查看的该动态库的存在,查看gpiodetect工具版本。

root@igkboard:/usr/lib# ls libgpiod.so.2* -l
lrwxrwxrwx 1 root root    17 Mar  9  2018 libgpiod.so.2 -> libgpiod.so.2.2.2
-rwxr-xr-x 1 root root 25896 Mar  9  2018 libgpiod.so.2.2.2
root@igkboard:~# gpiodetect -v
gpiodetect (libgpiod) v1.6.3
Copyright (C) 2017-2018 Bartosz Golaszewski
License: LGPLv2.1
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

下面首先介绍libgpiod提供的命令行工具。

2.3.2.1 gpiod命令行工具

目前有六个命令行工具可用,实验室树莓派上也可以使用(需要sudo权限)

  • gpiodetect - 列出系统上存在的所有 gpiochips,它们的名称、标签和 GPIO管脚数量。

  • gpioinfo - 列出指定 gpio 的所属chip、它们的名称、被使用者名字、方向、激活状态和附加标志。

  • gpioget<gpiochip_name + OFFSET> - 读取指定 GPIO 的值

  • gpioset<gpiochip_name + gpio_line_number> - 设置指定 GPIO 的值

  • gpiofind<gpio line number?> - 获取 gpiochip 名称和给定行名称的行偏移

  • gpiomon - 监听 GPIO 上的特定事件

下面列出几个常用的功能示例,同样以点亮led灯和读取gpio管脚电平作为示例。

  • gpiodetec和gpioinfo使用,用于查看信息。

root@igkboard:~# gpiodetect 
gpiochip0 [209c000.gpio] (32 lines)
gpiochip1 [20a0000.gpio] (32 lines)
gpiochip2 [20a4000.gpio] (32 lines)
gpiochip3 [20a8000.gpio] (32 lines)
gpiochip4 [20ac000.gpio] (32 lines)
root@igkboard:~# gpioinfo 
gpiochip0 - 32 lines:
        line   0:      unnamed       unused   input  active-high 
...
        line   8:      unnamed       unused  output  active-high 
        line   9:      unnamed "regulator-sd1-vmmc" output active-high [used]
...
        line  18:      unnamed         "w1"  output  active-high [used open-drain]
        line  19:      unnamed         "cd"   input   active-low [used]
...
gpiochip4 - 32 lines:
        line   0:      unnamed       unused   input  active-high 
        line   1:      unnamed       unused   input  active-high 
        line   2:      unnamed        "irq"   input  active-high [used]
        line   3:      unnamed      "reset"   input  active-high [used]
        line   4:      unnamed  "phy-reset"  output  active-high [used]
...
  • gpioset设置**GPIO05_IO09(gpiochip4 9)GPIO05_IO08(gpiochip4 8)**电平。命令行中可以将gpiochip4使用最后数字编号进行输入,示例如下

root@igkboard:~# gpioset gpiochip4 8=1 9=1 
root@igkboard:~# gpioset gpiochip4 8=1 9=0
root@igkboard:~# gpioset 4 8=0 9=1
root@igkboard:~# gpioset 4 8=1 9=0 
  • gpioget获取GPIO05_IO09GPIO05_IO8的电平值。使用杜邦线短接VCC3.3或GND进行实际电平控制。

root@igkboard:~# gpioget gpiochip4 8 9
0 1
root@igkboard:~# gpioget 4 8 9        
0 1

2.3.2.2 libgpiod编程相关结构体

  • gpiod_chip

代表支持的gpio芯片的相关信息

struct gpiod_chip {
    struct gpiod_line **lines; //每个 gpio芯片的gpiod_line 数组地址,每一个gpio口对应一个line
    unsigned int num_lines;  //该gpio芯片下的gpio线路数量

    int fd;    //设备描述符,即库中底层使用ioctl打开的gpio芯片设备节点的描述符

    char name[32];   //芯片的名称
    char label[32];    //芯片的标签
};
  • gpiod_line

每个gpio芯片下的gpio口的信息

struct gpiod_line {
    unsigned int offset;  //gpio 的偏移量,如GPIO05_IO09 偏移 9
    int direction;    //gpio的方向
    bool active_low;  //是否是低电平有效,前面介绍过此属性
    int output_value;   //最后写入 GPIO 的逻辑值
    __u32 info_flags;
    __u32 req_flags;
    int state;		//和事件相关的一个状态值
    struct gpiod_chip *chip;//所属芯片的地址
    struct line_fd_handle *fd_handle;
    char name[32];		//名字,编程时候可以给使用的gpio赋予名字
    char consumer[32];	//使用者名字
};

2.3.2.3 libgpiod常用函数解析

  • 获取需要的gpio芯片

struct gpiod_chip *gpiod_chip_open(const char *path);

功能描述:根据gpiochip路径打开需要的chip 参数解析:path:要打开的 gpiochip 的路径 返回值:成功返回GPIO 芯片句柄,失败则返回 NULL 示例代码:

struct gpiod_chip *chip;
chip = gpiod_chip_open("/dev/gpiochip4");
  • 获取需要的gpio口

struct gpiod_line* gpiod_chip_get_line(struct gpiod_chip* chip,uint offset);

功能描述:获取给定偏移量值 GPIO 句柄 参数解析:chip:GPIO 芯片句柄 offset:GPIO 偏移量 返回值:成功返回GPIO 句柄,失败则返回 NULL 示例代码:

struct gpiod_line *line;
uint gpio_off_num = 9;
line = gpiod_chip_get_line(chip, gpio_off_num);
  • 设置gpio为输出方向并且初始化逻辑值

int gpiod_line_request_output(struct gpiod_line* line,const(char)* consumer,int default_val);

功能描述:设置输出方向并且初始化逻辑值 参数解析: line:GPIO 句柄 consumer:使用者的名称 default_val:初始值 返回值:成功返回0,失败则返回-1 示例代码:设置初始值为1,高电平

int ret;
ret = gpiod_line_request_output(line, "led_out", 1);
  • 设置gpio的逻辑值

int gpiod_line_set_value(struct gpiod_line* line,int value);

功能描述:设置单个 GPIO 值 参数解析:line:GPIO 句柄 value:设定的值 返回值:成功返回0,失败则返回-1 示例代码:设置gpio逻辑值 为1或0

int ret;
ret = gpiod_line_set_value(line, 1);
ret = gpiod_line_set_value(line, 0);
  • 设置gpio为输入方向

int gpiod_line_request_input(struct gpiod_line *line, const char *consumer);

功能描述:设置gpio为输入方向 参数解析: line:GPIO 句柄 consumer:使用者的名称 返回值:成功返回0,失败则返回-1

  • 读取gpio的逻辑值

int gpiod_line_get_value(struct gpiod_line *line);

功能描述:读取单个 GPIO 逻辑 参数解析:line:GPIO 句柄 返回值:成功返回0或1(即逻辑值),失败则返回-1

  • 关闭gpio芯片句柄

void gpiod_chip_close(struct gpiod_chip*  chip);

功能描述:关闭 GPIO 芯片句柄并释放所有分配的资源 参数解析:chip: GPIO 芯片句柄

  • 释放gpio口

void gpiod_line_release(struct gpiod_line*  line);

功能描述:释放gpio口 参数解析:line:GPIO 句柄

2.3.3 程序设计

2.3.3.1 函数封装设计

  • 查看开发板dev下的gpiochip节点,有五组GPIO,分别对应GPIO1_XX~GPIO5_XX

root@igkboard:~# ls /dev/gpiochip*
/dev/gpiochip0  /dev/gpiochip1  /dev/gpiochip2  /dev/gpiochip3  /dev/gpiochip4

示例:现需要对GPIO05_IO09 进行电平控制 易知需选择的chip是gpiochip4 所需要的偏移是9

  • 对gpio的初始化进行如下函数封装

typedef struct gpiod_led_s
{
    struct          gpiod_chip *chip;/* gpio 芯片*/
    struct          gpiod_line *line;/* gpio控制口*/
}gpiod_led_t;

/* led 初始化函数*/
int led_init(gpiod_led_t *gpiod_led, unsigned char gpio_chip_num, unsigned char gpio_off_num)
{
    char dev_name[16];

    if(gpio_chip_num == 0 || gpio_chip_num > 5 || gpio_off_num == 0 || gpio_off_num > 32 || !gpiod_led )
    {
        printf("[INFO] %s argument error.\n", __FUNCTION__);
        return -1;
    }

    memset(dev_name, 0, sizeof(dev_name));
    snprintf(dev_name, sizeof(dev_name), "/dev/gpiochip%d", gpio_chip_num-1);

    if(!(gpiod_led->chip = gpiod_chip_open(dev_name)))
    {
        printf("fail to open chip0\n");
        return -2;
    }

    if(!(gpiod_led->line = gpiod_chip_get_line(gpiod_led->chip, gpio_off_num)))
    {
        printf("fail to get line_led\n");
        return -3;
    }
    /* 设置初始值为灭 */
    if(gpiod_line_request_output(gpiod_led->line, "led_out", OFF) < 0)
    {
        printf("fail to request line_led for output mode\n");
        return -4;
    }

    return 0;
}
  • 首先进行参数判断,确保传参正确

  • 对参数进行字符串加工,然后开启对应的chip号

  • 根据偏移获取对应的gpio_line , 并初始化为输出管脚,默认电平为低电平

led控制与释放函数的封装

/*led控制函数*/
int led_control(gpiod_led_t *gpiod_led, int status)
{
    int level;
    if( !gpiod_led )
    {
        printf("[INFO] %s argument error.\n", __FUNCTION__);
        return -1;
    }

    level = (status==ON) ? ON: OFF; //纠正status参数
    if(gpiod_line_set_value(gpiod_led->line, level) < 0)
    {
        printf("fail to set line_led value\n");
        return -2;
    }

    return 0;
}
  • 根据参数 ,利用gpiod_line_set_value函数将 gpiod_led->line的gpio进行电平的高低设置

2.3.3.2 完整代码

完整代码如下

guowenxue@ubuntu20:~/igkboard/apps$ vim gpiod_led_test.c
/*********************************************************************************
 *      Copyright:  (C) 2022 Guo Wenxue<Email:guowenxue@gmail.com QQ:281143292>
 *                  All rights reserved.
 *
 *       Filename:  gpiod_led_test.c
 *    Description:  This file used to test tricolour light blink
 *                 
 *        Version:  1.0.0(10/17/2022~)
 *         Author:  Guo Wenxue <guowenxue@gmail.com>
 *      ChangeLog:  1, Release initial version on "10/17/2022 17:46:18 PM"
 *                 
 ********************************************************************************/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <gpiod.h> //gpiod 头文件支持,链接时候加上 -lgpio

#define OFF         0
#define ON          1

typedef struct gpiod_led_s
{
    struct          gpiod_chip *chip;/* gpio 芯片*/
    struct          gpiod_line *line;/* gpio控制口*/
}gpiod_led_t;

enum LED_INDEX
{
    LED_RED = 0,
    LED_GREEN,
    LED_BLUE,
    LED_MAX,
};

int led_init(gpiod_led_t *gpiod_led, unsigned char gpio_chip_num, unsigned char gpio_off_num);
int led_control(gpiod_led_t *gpiod_led, int status);
static inline void msleep(unsigned long ms);
void blink_led(gpiod_led_t *gpiod_led, unsigned int interval);
int led_release(gpiod_led_t *gpiod_led);

int main(int argc, char *argv[])
{
    gpiod_led_t led[LED_MAX];
    if(led_init(&led[LED_BLUE], 5, 9) < 0)
    {
        printf("LED_BLUE init error.\n");
        return -1;
    }
    if(led_init(&led[LED_RED], 1, 11) < 0)
    {
        printf("LED_RED init error.\n");
        return -1;
    }
    if(led_init(&led[LED_GREEN], 1, 10) < 0)
    {
        printf("LED_GREEN init error.\n");
        return -1;
    }

    while(1)
    {
        blink_led(&led[LED_BLUE], 500);
        blink_led(&led[LED_RED], 500);
        blink_led(&led[LED_GREEN], 500);
    }
    led_release(&led[LED_BLUE]);
    led_release(&led[LED_RED]);
    led_release(&led[LED_GREEN]);

    return 0;
}

/* led 初始化函数*/
int led_init(gpiod_led_t *gpiod_led, unsigned char gpio_chip_num, unsigned char gpio_off_num)
{
    char dev_name[16];

    if(gpio_chip_num == 0 || gpio_chip_num > 5 || gpio_off_num == 0 || gpio_off_num > 32 || !gpiod_led )
    {
        printf("[INFO] %s argument error.\n", __FUNCTION__);
        return -1;
    }

    memset(dev_name, 0, sizeof(dev_name));
    snprintf(dev_name, sizeof(dev_name), "/dev/gpiochip%d", gpio_chip_num-1);

    if(!(gpiod_led->chip = gpiod_chip_open(dev_name)))
    {
        printf("fail to open chip0\n");
        return -2;
    }

    if(!(gpiod_led->line = gpiod_chip_get_line(gpiod_led->chip, gpio_off_num)))
    {
        printf("fail to get line_led\n");
        return -3;
    }
    /* 设置初始值为灭 */
    if(gpiod_line_request_output(gpiod_led->line, "led_out", OFF) < 0)
    {
        printf("fail to request line_led for output mode\n");
        return -4;
    }

    return 0;
}

/* ms级休眠函数 */
static inline void msleep(unsigned long ms)
{
    struct timespec cSleep;
    unsigned long ulTmp;

    cSleep.tv_sec = ms / 1000;
    if (cSleep.tv_sec == 0)
    {
        ulTmp = ms * 10000;
        cSleep.tv_nsec = ulTmp * 100;
    }
    else
    {
        cSleep.tv_nsec = 0;
    }

    nanosleep(&cSleep, 0);
}

/*led控制函数*/
int led_control(gpiod_led_t *gpiod_led, int status)
{
    int level;
    if( !gpiod_led )
    {
        printf("[INFO] %s argument error.\n", __FUNCTION__);
        return -1;
    }

    level = (status==ON) ? ON: OFF; //纠正status参数
    if(gpiod_line_set_value(gpiod_led->line, level) < 0)
    {
        printf("fail to set line_led value\n");
        return -2;
    }

    return 0;
}

/*led闪烁函数*/
void blink_led(gpiod_led_t *gpiod_led, unsigned int interval)
{
    led_control(gpiod_led, ON);
    msleep(interval);

    led_control(gpiod_led, OFF);
    msleep(interval);    
}

/* led gpiod释放和关闭函数*/
int led_release(gpiod_led_t *gpiod_led)
{
    if(!gpiod_led)
    {
        printf("[INFO] %s argument error.\n", __FUNCTION__);
        return -1;
    }
    gpiod_line_release(gpiod_led->line);
    gpiod_chip_close(gpiod_led->chip);

    return 0;
}

编写makefile如下

guowenxue@ubuntu20:~/igkboard/apps$ vim Makefile
CC=/opt/buildroot/cortexA7/bin/arm-linux-gcc
APP_NAME=gpiod_led_test

all:clean
	@${CC} ${APP_NAME}.c -o ${APP_NAME} -lgpiod

clean:
	@rm -f ${APP_NAME}

编译后文件tree

.
├── gpiod_led_test
├── gpiod_led_test.c
└── Makefile

现在我们在开发板上通过 tftp 命令 或其它方式将编译生成的测试程序下载到开发板上。

root@igkboard:~# tftp -gr gpiod_led_test 192.168.2.2

将可执行文件传输到开发板上后,利用chmod命令增加执行权限

root@igkboard:~# chmod +x gpiod_led_test 
root@igkboard:~# ./gpiod_led_test 

2.3.4 硬件连接

我们使用三色灯与开发板的**#137(GPIO5_IO09)、#10(GPIO1_IO10、#11(GPIO1_IO11)、GND**连接

如下是三色灯模块与IGKBoard的实物硬件连接示意图。

GPIOD_tricolour_IGKBoard_connect

实物连接图如下

GPIOD_tricolour_IGKBoard_real

2.3.5 测试效果

三色灯模块灯珠按照蓝绿红的顺序依次闪烁,闪烁时间500ms。