Skip to content

LV532-gpio控制 ​

在 linux 下,如何对 gpio 进行控制?怎么调试?若笔记中有错误或者不合适的地方,欢迎批评指正 😃。

一、准备工作 ​

这里既然是做 GPIO 的调试,那肯定要先知道几个 GPIO,我这里用的是整点原子 alpha 开发板,我们准备三种 GPIO 用于测试。我们选择连接了外设的 GPIO,例如连接了 I2C 设备的 I2C1,LED 灯以及按键这些,用来测试的话现象比较直观。

1. LED 灯 ​

同 《07-设备驱动子系统/05-pinctrl 与 gpio/LV530-gpio 调试.md》的一、准备工作。

2. KEY 按键 ​

同 《07-设备驱动子系统/05-pinctrl 与 gpio/LV530-gpio 调试.md》的一、准备工作。

3. I2C1——ap3216c ​

同 《07-设备驱动子系统/05-pinctrl 与 gpio/LV530-gpio 调试.md》的一、准备工作。

4. 参考文档 ​

linux 内核源码中有一些参考文档可以看:sysfs.txt - Documentation/gpio/sysfs.txt

二、GPIO 的控制 ​

GPIO 软件编程方式有多种, 可以写驱动程序调用 GPIO 函数操作 GPIO, 也可以直接通过操作寄存器的方式操作 GPIO, 还可以通过 sysfs 方式实现对 GPIO 的控制。 本章节我们来学习使用 sysfs 方式实现对 GPIO 的控制。

1. 使用命令通过 sysfs 文件系统控制 GPIO ​

1.1 开始之前要知道的 ​

1.1.1 内核配置 ​

使用 sysfs 方式控制 gpio, 首先需要底层驱动的支持, 执行以下命令打开图形化配置界面

shell
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v6_v7_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

按以下路径找到配置菜单:

shell
Device Drivers  --->
	-*- GPIO Support  --->
		[*]   /sys/class/gpio/... (sysfs interface)
image-20250330070045481
1.1.2 GPIO 编号计算 ​

i.MX6ULL 的 GPIO 引脚被组织成多个 Bank,每个 Bank 包含 32 个 GPIO 引脚。这个我们可以看数据手册过着参考手册,我这里看的数据手册:

image-20250330070701979

GPIO 引脚的编号通常以 GPIOx_y 的形式表示,其中 x 表示 Bank 编号,y 表示该 Bank 中的引脚编号。例如:

  • GPIO1_0 表示 Bank 1 的第 0 个引脚。
  • GPIO2_15 表示 Bank 2 的第 15 个引脚。

那么,i.MX6ULL 的 GPIO 编号可以通过以下公式计算:

md
GPIO编号 = (Bank编号 - 1) * 32 + 引脚编号

像正点原子 alpha 开发板上的 led 接在 GPIO1_IO03 上,这里 GPIO 编号就是 3.

1.2 GPIO 控制器相关 ​

1.2.1 怎么查看有哪些 GPIO 控制器 ​

/sys/bus/gpio/devices 目录下,列出了所有的 GPIO 控制器,如下表示有 5 个 GPIO 控制器:

shell
ls -alh /sys/bus/gpio/devices
image-20250330175127447
1.2.2 每个 GPIO 控制器的详细信息 ​

GPIO 控制器的详细信息在 /sys/class/gpio/gpiochipXXX 下,有 gpio 这些信息:

shell
ls -alh /sys/class/gpio/gpiochipX
image-20250330175521101
shell
base     # 这个 GPIO 控制器的 GPIO 编号
device
label    # 名字
ngpio    # 引脚个数
power
subsystem
uevent

1.3 gpio 导出与取消 ​

我们可以通过 sysfs 文件系统,将 GPIO 导出到用户空间,这样我们就可以通过 sysfs 直接控制 gpio。sysfs 控制接口为 /sys/class/gpio/export 和/sys/class/gpio/unexport。 如下图所示:

image-20250331230317526

export 和 unexport 这两个文件 都是只写的。 这里的 gpiochipX 代表 GPIO 控制器。

1.3.1 gpio 导出 ​

/sys/class/gpio/export 用于将 GPIO 控制从内核空间导出到用户空间 。

export: 用于将指定编号的 GPIO 引脚导出。 在使用 GPIO 引脚之前, 需要将其导出,导出成功之后才能使用它。 注意 export 文件是只写文件, 不能读取, 将一个指定的编号写入到 export 文件中即可将对应的 GPIO 引脚导出, 以 GPIO0_PB7 为例(pin 计算值为 15) 使用 export 文件进行导出(如果这个 gpio 被内核或者用户程序占用,那么就会导出不成功), 导出成功如下图所示:

shell
echo 3 > /sys/class/gpio/export
image-20250331230349257

会发现在/sys/class/gpio 目录下生成了一个名为 gpio3 的文件夹(gpioX, X 表示对应的编 号) , 该文件夹就是导出来的 GPIO 引脚对应的文件夹, 用于管理、 控制该 GPIO 引脚。导出成功之后进入 gpio3 文件夹如下图所示:

image-20250331230423922

会发现里面生成了很多属性文件,通过在这些文件就可以控制 gpio,这些文件我们后面再来了解。

1.3.2 gpio 取消导出 ​

/sys/class/gpio/unexport 用于取消 GPIO 控制从内核空间到用户空间的导出。

unexport: 将导出的 GPIO 引脚删除。 当使用完 GPIO 引脚之后, 需要将导出的引脚删除, 同样该文件也是只写文件、 不可读, 使用 unexport 文件进行删除 GPIO0_PB7, 删除成功如下图所示:

shell
echo 3 > /sys/class/gpio/unexport
image-20250331230509599

可以看到之前生成的 gpio3 文件夹消失了!

1.3.3 导出失败? ​

需要注意的是, 并不是所有 GPIO 引脚都可以成功导出, 如果对应的 GPIO 已经被导出或者在内核中被使用了, 那便无法成功导出, 例如我用的整点原子 alpha 开发板中,led 灯是接在 GPIO1_IO03,对应的 gpio 编号为 3,这里导出就会失败,导出失败如下图所示:

image-20250330180041908

这个是因为设备树中用到了这个 gpio:我们可以看一下 sys/kernel/debug/gpio:

shell
cat sys/kernel/debug/gpio
image-20250330202215788

如会发现,这里 gpio-3 是被用掉了的,这个时候我们就需要找到设备树中对应的地方,就找相关的关键词,可以找到 imx6ul-14x14-evk.dtsi 这个文件中有:

c
&tsc {
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_tsc>;
	xnur-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
	measure-delay-time = <0xffff>;
	pre-charge-time = <0xfff>;
	status = "okay";
};

这里我们就可以把它屏蔽掉,然后更新设备树:

image-20250331230015999

这个时候重新导出就没问题啦。

shell
echo 3 > export

这个时候就正常了。

1.4 gpio 属性修改 ​

导出 gpio 成功之后进入对应的文件夹,会发现生成了一些属性文件:

image-20250331230227730

可以看到 gpio3 文件夹下分别有 active_low、 device、 direction、 edge、 power、 subsystem、uevent、 value 八个文件, 需要关心的文件是 active_low、 direction、 edge 以及 value 这四个属性文件, 接下来分别介绍这四个属性文件的作用。

1.4.1 direction ​

direction:配置 GPIO 引脚为输入或输出模式。 该文件可读、 可写, 读表示查看 GPIO 当前是输入还是输出模式, 写表示将 GPIO 配置为输入或输出模式; 读取或写入操作可取的值为 "out"(输出模式) 和 "in"(输入模式) 。

在“/sys/class/gpio/gpio15”目录下使用 cat 命令查看 direction 输入输出模式, 如下图所示:

shell
cat direction
image-20250331230605378

默认状态下的输入输出状态为“in”, 由于 direction 为可读可写, 可以使用以下命令将模式配置为输出, 配置完成如下图所示

shell
echo out > direction
cat direction
image-20250331230629595
1.4.2 active_low ​

active_low: 用于控制 极性的属性文件, 可读可写, 默认情况下为 0, 使用 cat 命令进行文件内容的查看, 如下图所示 :

shell
cat active_low
image-20250331230754356

当 active_low 等于 0 时, value 值若为 1 则引脚输出高电平, value 值若为 0 则引脚输出低电平。 当 active_low 等于 1 时 , value 值若为 0 则引脚输出高电平, value 值若为 1 则引脚输出低电平。

1.4.3 edge ​

edge:控制中断的触发模式, 该文件可读可写。 在配置 GPIO 引脚的中断触发模式之前,需将其设置为输入模式, 四种触发模式的设置如下所示:

shell
echo "none" > edge    # 非中断引脚
echo "rising" > edge  # 上升沿触发
echo "falling" > edge # 下降沿触发
echo "both" > edge    # 边沿触发
1.4.4 value ​

value: 设置高低电平, 如果我们要把这个管脚设置成高电平, 我们只需要给 value 设置成 1 ,即可, 反之, 则设置成 0。 使用命令 :

shell
# cat active_low # 通过它看极性

echo 1 > value # 把 GPIO 设置成高电平
echo 0 > value # 把 GPIO 设置成低电平
image-20250331230942937

这个引脚是连接了 LED 灯,根据原理图可知,LED 的一端接 GPIO,另一端接 3.3v,这里输出 1 会熄灭 LED,输出 0 会点亮 LED。

2. 使用 C 程序通过 sysfs 文件系统控制 GPIO ​

上面我们已经实现了用 cat、echo 通过 sysfs 文件系统控制 gpio,其实就会发现,我们做的都是去操作一些属性文件,那么既然如此,我们肯定是可以通过 C 语言程序来操作这些文件的。

2.1 关键函数实现 ​

2.1.1 导出 GPIO 引脚 ​

导出的时候,我们需要访问 /sys/class/gpio/export,通过 open 和 write 函数就可以实现:

c
/**
 * @brief  gpio_export()
 * @note   导出 GPIO 引脚, 相当于 echo X > /sys/class/gpio/export
 *         导出成功会生成 /sys/class/gpio/gpioX 目录
 * @param  [in] pins : gpio 编号,传进来的是一个字符串
 * @param  [out]
 * @retval 
 */
int gpio_export(const char *pins)
{
    int fd = -1;
    size_t len = 0; // size_t  --> 32bit 下通常为 unsigned int
    ssize_t ret = 0;// ssize_t --> 32bit 下通常为 int
    char attr_file_path[32] = {0};

    if(pins == NULL)
    {
        PRTE("pins is NULL\n");
        return -1;
    }
    sprintf(attr_file_path, "%s", "/sys/class/gpio/export"); // 构建文件路径
    fd = open(attr_file_path, O_WRONLY);      // 打开 export 文件
    if (fd < 0)
    {
        PRTE("open %s error!\n", attr_file_path); // 打开文件失败
        return -2;
    }
    //PRT("open %s success!\n", attr_file_path);

    len = strlen(pins);         // 获取参数字符串的长度
    ret = write(fd, pins, len); // 将参数字符串写入文件, 导出 GPIO 引脚
    if (ret < 0)
    {
        PRTE("write %s error!\n", attr_file_path); // 写入文件失败
        return -3;
    }

    close(fd); // 关闭文件

    return 0;
}
2.1.2 取消导出 GPIO 引脚 ​
c
/**
 * @brief  gpio_unexport()
 * @note   取消导出 GPIO 引脚, 相当于 echo X > /sys/class/gpio/unexport
 *         取消导出成功会删除 /sys/class/gpio/gpioX 目录
 * @param  [in] pins : gpio 编号,传进来的是一个字符串
 * @param  [out]
 * @retval 
 */
int gpio_unexport(const char *pins)
{
    int fd = -1;
    size_t len = 0;
    ssize_t ret = 0;
    char attr_file_path[32] = {0};

    if(pins == NULL)
    {
        PRTE("pins is NULL\n");
        return -1;
    }

    sprintf(attr_file_path, "%s", "/sys/class/gpio/unexport"); // 构建文件路径
    fd = open(attr_file_path, O_WRONLY); // 打开 unexport 文件
    if (fd < 0)
    {
        PRTE("open %s error!\n", attr_file_path); // 打开文件失败
        return -2;
    }
    //PRT("open %s success!\n", attr_file_path);

    len = strlen(pins);         // 获取参数字符串的长度
    ret = write(fd, pins, len); // 将参数字符串写入文件, 取消导出 GPIO 引脚
    if (ret < 0)
    {
        PRTE("write %s error!\n", attr_file_path); // 写入文件失败
        return -3;
    }

    close(fd); // 关闭文件

    return 0;
}
2.1.3 控制 GPIO 属性 ​
c
/**
 * @brief  gpio_ctrl()
 * @note   控制 GPIO 引脚的属性
 *         /sys/class/gpio/gpioX/direction  : 配置 GPIO 引脚为输入或输出模式。 
 *         /sys/class/gpio/gpioX/active_low : 配置 GPIO 极性
 *         /sys/class/gpio/gpioX/edge       : 控制中断的触发模式
 *         /sys/class/gpio/gpioX/value      : 设置高低电平/读取引脚高低电平状态
 * @param  [in] p_gpio_path : gpio 路径,/sys/class/gpio/gpioX
 * @param  [in] p_attr_name : 属性文件名称,direction,active_low,edge,value
 * @param  [in] val         : 要写入的值 direction  - in/out ;
 *                                      active_low - 1/0 ;
 *                                      edge       - none/rising/falling/both ;
 *                                      value      - 1/0 ;
 * @param  [out]
 * @retval 
 */
int gpio_ctrl(char *p_gpio_path, const char *p_attr_name, const char *val)
{
    int fd = -1;
    size_t len = 0;
    ssize_t ret = 0;
    char attr_file_path[32] = {0};                 // 文件路径

    if(p_gpio_path == NULL || p_attr_name == NULL || val == NULL)
    {
        PRTE("p_gpio_path, p_attr_name or val is NULL\n");
        return -1;
    }

    sprintf(attr_file_path, "%s/%s", p_gpio_path, p_attr_name); // 构建文件路径, 格式为 p_gpio_path/p_attr_name 
    fd = open(attr_file_path, O_WRONLY);           // 打开文件
    if (fd < 0)
    {
        PRTE("open %s error!\n", attr_file_path); // 打开文件失败
        return -2;
    }
    //PRT("open %s success!\n", attr_file_path);

    len = strlen(val);         // 获取参数字符串的长度

    ret = write(fd, val, len); // 将参数字符串写入文件, 控制 GPIO 引脚的属性
    if (ret < 0)
    {
        PRTE("write %s error!\n", attr_file_path); // 写入文件失败
        return -3;
    }

    close(fd); // 关闭文件

    return 0;
}
2.1.4 读取 GPIO 数据 ​
c
/**
 * @brief  gpio_read_value()
 * @note   获取 GPIO 引脚的电平状态
 *         /sys/class/gpio/gpioX/value      : 设置高低电平/读取引脚高低电平状态
 * @param  [in] p_gpio_path : gpio 路径,/sys/class/gpio/gpioX
 * @param  [in] p_attr_name : 属性文件名称,value
 * @param  [out]
 * @retval 
 */
int gpio_read_value(char *p_gpio_path, const char *p_attr_name)
{
    int fd = -1;
    ssize_t ret = 0;
    char attr_file_path[32] = {0};                 // 文件路径
    char buf[2] = {0}; // 缓冲区
    
    if(p_gpio_path == NULL || p_attr_name == NULL)
    {
        PRTE("p_gpio_path, p_attr_name is NULL\n");
        return -1;
    }

    sprintf(attr_file_path, "%s/%s", p_gpio_path, p_attr_name); // 构建文件路径, 格式为 "p_gpio_path/p_attr_name"
    fd = open(attr_file_path, O_RDONLY);              // 打开文件
    if (fd < 0)
    {
        PRTE("open %s error!\n", attr_file_path); // 打开文件失败
        return -1;
    }
	//PRT("open %s success!\n", attr_file_path);
    ret = read(fd, buf, 1); // 读取文件内容到缓冲区
    if(ret < 0)
    {
        PRTE("read from %s fail!\n", attr_file_path);
    }

    if (!strcmp(buf, "1"))
    {
        PRT("The value is high!\n"); // GPIO 引脚值为高电平
        return 1;
    }
    else if (!strcmp(buf, "0"))
    {
        PRT("The value is low!\n"); // GPIO 引脚值为低电平
        return 0;
    }
    
    close(fd); // 关闭文件

    return -1;
}
2.1.5 监听中断 ​
c
/**
 * @brief  gpio_interrupt()
 * @note   监听 GPIO 引脚的中断事件, 中断发生时读取 gpio 的值
 *         /sys/class/gpio/gpioX/value      : 设置高低电平/读取引脚高低电平状态
 * @param  [in] p_gpio_path : gpio 路径,/sys/class/gpio/gpioX
 * @param  [in] p_attr_name : 属性文件名称,value
 * @param  [out]
 * @retval 
 */
int gpio_interrupt(char *p_gpio_path, const char *p_attr_name)
{
    int fd = -1;
    ssize_t ret = 0;
    char attr_file_path[32] = {0}; // 文件路径
    struct pollfd fds[1];          // poll 结构体数组
    char buf[2] = {0};             // 缓冲区

    sprintf(attr_file_path, "%s/%s", p_gpio_path, p_attr_name); // 构建文件路径
    fd = open(attr_file_path, O_WRONLY);           // 打开文件
    if (fd < 0)
    {
        PRTE("open %s error!\n", attr_file_path); // 打开文件失败
        return -1;
    }
    //PRT("open %s success!\n", attr_file_path);

    memset((void *)fds, 0, sizeof(fds)); // 清空 poll 结构体数组
    fds[0].fd = fd;                      // 设置 poll 结构体的文件描述符
    fds[0].events = POLLPRI;             // 设置 poll 结构体的事件类型为 POLLPRI, 表示有紧急数据可读
    read(fd, buf, 2);                    // 读取文件内容, 清除中断事件
    while (1)
    {
        ret = poll(fds, 1, -1); // 调用 poll 函数等待中断事件发生, 阻塞直到事件发生
        if (ret <= 0)
        {
            PRTE("poll error \n"); // 调用 poll 失败或超时
            return -1;
        }
        PRT("poll return!ret=%d,fds[0].revents=0x%x,fds[0].events=0x%x\n", ret, fds[0].revents, fds[0].events); // 输出中断事件的值
        if (fds[0].revents & POLLPRI)
        {
            lseek(fd, 0, SEEK_SET); // 重新定位文件指针到文件开头
            read(fd, buf, 2);       // 读取文件内容, 获取中断事件的值
            PRT("value from %s : buf[0]=%x buf[1]=%x\n", attr_file_path, buf[0], buf[1]); // 输出中断事件的值
        }
        sleep(1);
    }
    return 0;
}

2.2 控制 GPIO 输出 demo ​

c
int gpio_out_demo(int argc, const char *argv[]) // 主函数
{
    char gpio_path[32] = {0};                 // 文件路径

    sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]); // 构建 GPIO 路径, 格式为“/sys/class/gpio/gpio 引脚号”
    if (access(gpio_path, F_OK))                           // 检查 GPIO 路径是否存在
    {
        gpio_export(argv[1]); // 不存在则导出 GPIO 引脚
    }

    PRT("gpio_path:%s\n", gpio_path);
    gpio_ctrl(gpio_path, "direction", "out"); // 配置 GPIO 为输出模式
    gpio_ctrl(gpio_path, "value", argv[2]);   // 控制 GPIO 输出高低电平
    
    gpio_unexport(argv[1]);        // 最后取消导出 GPIO 引脚

    return 0;                      // 返回 0 表示程序正常退出
}

这个就主要是看 led 灯的亮灭了:

image-20250331232650428

2.3 控制 GPIO 输入 demo ​

c
int gpio_in_demo(int argc, const char *argv[]) // 主函数
{
    int value = -1;
    char gpio_path[32] = {0};                              // 文件路径

    sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]); // 构建 GPIO 路径, 格式为 "/sys/class/gpio/gpio 引脚号"
    if (access(gpio_path, F_OK))                           // 检查 GPIO 路径是否存在
    {
        gpio_export(argv[1]); // 不存在则导出 GPIO 引脚
    }

    PRT("gpio_path:%s\n", gpio_path);
    gpio_ctrl(gpio_path, "direction", "in");       // 配置 GPIO 为输入模式

    value = gpio_read_value(gpio_path, "value");   // 读取 GPIO 引脚的值

    PRT("The value is %d\n", value); // 打印读取的 GPIO 引脚的值
    gpio_unexport(argv[1]);             // 最后取消导出 GPIO 引脚

    return 0;                           // 返回 0 表示程序正常退出
}

我们编译后,拷贝到开发板,抬起按键的时候执行一次,按下按键不松,再执行一次:

image-20250331232419021

2.4 监听 GPIO 中断 demo ​

c
int gpio_interrupt_demo(int argc, const char *argv[]) // 主函数
{
    char gpio_path[32] = {0}; // 文件路径

    sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]); // 构建 GPIO 路径
    if (access(gpio_path, F_OK))                           // 检查 GPIO 路径是否存在
    {
        gpio_export(argv[1]); // 不存在则导出 GPIO 引脚
    }

    PRT("gpio_path:%s\n", gpio_path);
    gpio_ctrl(gpio_path, "direction", "in"); // 设置 GPIO 引脚为输入模式
    gpio_ctrl(gpio_path, "edge", "rising");    // 设置 GPIO 引脚的中断触发方式为上升沿
    gpio_interrupt(gpio_path, "value");      // 监听 GPIO 引脚的中断事件

    gpio_unexport(argv[1]); // 最后取消导出 GPIO 引脚

    return 0;               // 返回 0 表示程序正常退出
}