LV09-01-pinctrl和gpio子系统-04-gpio控制与调试

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

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
kernel/git/stable/linux.git - Linux kernel stable tree linux kernel源码(官网,tag 4.19.71)
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、准备工作

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

1. LED灯

LED灯的硬件原理图如下:

image-20250331194840703

搜一下原理图,就可以知道这个LED灯所连的GPIO为GPIO1_IO03,相关的寄存器如下:

1
2
3
4
5
6
7
8
9
10
11
0X020E0068 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 
0X020E02F4 IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03

0x0209C000 GPIO data register (GPIO1_DR)
0x0209C004 GPIO direction register (GPIO1_GDIR)
0X0209C008 GPIO pad status register (GPIO1_PSR)
0X0209C00C GPIO interrupt configuration register1 (GPIO1_ICR1)
0X0209C010 GPIO interrupt configuration register2 (GPIO1_ICR2)
0X0209C014 GPIO interrupt mask register (GPIO1_IMR)
0X0209C018 GPIO interrupt status register (GPIO1_ISR)
0X0209C01C GPIO edge select register (GPIO1_EDGE_SEL)

2. KEY按键

接下来是按键:

image-20250331195332131

这个按键key0是使用的GPIO为UART1_CTS,我们可以用它来作为输入或者中断,它可以复用为GPIO1_IO18,相关的寄存器如下:

1
2
3
4
5
6
7
8
9
10
11
0x020E008C IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B
0x020E0318 IOMUXC_SW_PAD_CTL_PAD_UART1_CTS_B

0x0209C000 GPIO data register (GPIO1_DR)
0x0209C004 GPIO direction register (GPIO1_GDIR)
0X0209C008 GPIO pad status register (GPIO1_PSR)
0X0209C00C GPIO interrupt configuration register1 (GPIO1_ICR1)
0X0209C010 GPIO interrupt configuration register2 (GPIO1_ICR2)
0X0209C014 GPIO interrupt mask register (GPIO1_IMR)
0X0209C018 GPIO interrupt status register (GPIO1_ISR)
0X0209C01C GPIO edge select register (GPIO1_EDGE_SEL)

3. I2C1——ap3216c

我们还有一个I2C1,上面接了ap3216c:

image-20250331195813023

I2C1其实是可以用由不同的GPIO复用而来的:

image-20250329200416047

但是在正点原子alpha开发板上,其中 I2C1_SCL 使用的 UART4_TXD 这个IO、 I2C1_SDA 使用的是 UART4_RXD 这个 IO。 所以我们也就要看UART4_TX_DATA和UART4_RX_DATA这两个引脚。前面学习pinctrl子系统的时候有分析过这个UART4_TX_DATA相关寄存器的取值情况:

1
2
3
0x020E00b4 IOMUXC_SW_MUX_CTL_PAD_UART4_TX_DATA (0x2)
0x020E0340 IOMUXC_SW_PAD_CTL_PAD_UART4_TX_DATA (0x4001b8b0)
0x020E05A4 IOMUXC_I2C1_SCL_SELECT_INPUT (0x1)

4. 参考文档

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

二、GPIO调试

先来看怎么调试把,后面控制的时候也会有一些问题,可能需要查看引脚情况。

1. debugfs文件系统

1.1 debugfs 简介

debugfs 是 Linux 内核提供的一个调试文件系统, 可以用于查看和调试内核中的各种信息,包括 GPIO 的使用情况。 通过挂载 debugfs 文件系统, 并查看/sys/kernel/debug/目录下的相关文件, 可以获取 GPIO 的状态, 配置和其他调试信息。如下图:

image-20250330201742032

1.2 debug目录下没有文件?

我们打开这个目录,发现什么都没有:

image-20250330200240035

这个时候怎么办?

1.2.1 内核配置

我们先看一下linux系统是否支持debugfs文件系统:

1
cat /proc/filesystems
image-20250330200343468

发现是支持的,若是不支持的话,我们需要在内核打开对应的配置项,我们执行:

1
2
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v6_v7_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

按照以下路径,找到对应的菜单项:

1
2
3
Kernel hacking  --->
Compile-time checks and compiler options --->
-*- Debug Filesystem
image-20250330200751064

勾选后,我们把它编译进内核,然后重新编译内核并更新。

1.2.2 挂载debugfs

我们已经支持了debugfs文件系统,但是debug目录还是空的,我们可以执行mount命令看一下是不是没挂载:

1
mount
image-20250330200929794

发现确实没有挂载,然后我们可以手动挂载:

1
mount -t debugfs none /sys/kernel/debug/
image-20250330201107727

然后就有对应的目录了,要是想开机自动挂载的话,可以修改/etc/fstab文件自动挂载到该目录:

1
debugfs /sys/kernel/debug debugfs defaults 0 0
image-20250330201532652

1.3 /sys/kernel/debug/gpio

1.3.1 这个文件是什么?

在Linux系统中,/sys/kernel/debug/gpio 文件提供了关于系统GPIO(通用输入输出)状态的调试信息。通过解析该文件,可以查看GPIO引脚的使用情况、配置和当前状态。

1.3.2 gpio文件结构

文件内容通常分为两部分:GPIO控制器信息具体GPIO引脚状态,例如imx6ull:

image-20250330202215788

GPIO控制器信息

  • gpiochipx:看图中,x的值为0、1、2、3、4,这就是imx6ull的5组GPIO。
  • GPIOs x-y:该控制器管理的GPIO编号范围(全局编号)。
  • parent:控制器所属的硬件设备(如芯片或外设地址)。
  • platform/209c000.gpio :是 GPIO 控制器的名称或地址。

GPIO引脚状态

  • gpio-3gpio-9gpio-19表示这几个gpio已经被内核空间或者用户空间占用。

(1)in 表示引脚是输入模式,out 表示输出模式。

(2)hi 表示引脚当前为高电平,lo 表示低电平。

(3)有时候还会有IRQ ,表示该引脚被配置为中断引脚。

1.3.2 总结一下

/sys/kernel/debug/gpio 是一个虚拟文件系统(debugfs)中的文件,用于显示当前系统中 GPIO(通用输入输出)引脚的状态和配置信息。它通常包含以下信息:

  • GPIO 控制器信息‌:系统中每个 GPIO 控制器的名称和状态、控制器的基地址或标识符。
  • GPIO 使用情况‌:哪些 GPIO 引脚已被占用(被内核或用户空间程序使用)、使用该引脚的驱动或模块的名称。
  • GPIO 引脚状态‌:每个 GPIO 引脚的编号(如 gpiochipX 中的引脚)、引脚的方向(输入或输出)、引脚的当前值(高电平或低电平)、引脚是否被配置为中断引脚。
  • GPIO 中断信息‌:如果 GPIO 引脚被配置为中断引脚,可能会显示中断触发类型(如上升沿、下降沿等)。

1.4 /sys/kernel/debug/pinctrl

当进入/sys/kernel/debug/pinctrl 目录时, 我们可以获取有关 GPIO 控制器的调试信息。在该目录下, 通常会有以下文件和目录:

  • (1)/sys/kernel/debug/pinctrl/*/pinmux-pins: 这些文件列出了每个 GPIO 引脚的引脚复用配置。

可以查看每个引脚的功能模式、 引脚复用选择以及其他相关的配置信息。 我们进入下面的目录:

1
cd /sys/kernel/debug/pinctrl/20e0000.iomuxc/
image-20250330203444260
  • (2)/sys/kernel/debug/pinctrl/*/pins:这些文件列出了 GPIO 的引脚编号, 可以查看 GPIO 编号。

我们进入下面的目录看一下:

1
2
ls /sys/kernel/debug/pinctrl/20e0000.iomuxc
cat /sys/kernel/debug/pinctrl/20e0000.iomuxc/pins
image-20250330203718614
  • (3)/sys/kernel/debug/pinctrl/*/gpio-ranges:这些文件列出了每个 GPIO 控制器支持的 GPIO 范围。
1
2
cd /sys/kernel/debug/pinctrl/20e0000.iomuxc
cat cd /sys/kernel/debug/pinctrl/20e0000.iomuxc/gpio-ranges
image-20250330203948308
  • (4)/sys/kernel/debug/pinctrl/*/pinmux-functions:这些文件列出了每个功能模式的名称以及与之关联的 GPIO 引脚。
1
2
ls /sys/kernel/debug/pinctrl/20e0000.iomuxc/
cat /sys/kernel/debug/pinctrl/20e0000.iomuxc/pinmux-functions
image-20250330204232094
  • (5)/sys/kernel/debug/pinctrl/*/pingroups:该路径提供有关用于配置和控制系统上的 GPIO 引脚的引脚组的信息。
  • (6)/sys/kernel/debug/pinctrl/*/pinconf-pins:这些文件包含了 GPIO 引脚的配置信息, 如输入/输出模式、 上拉/下拉设置等。可以查看和修改 GPIO 的电气属性, 以便进行 GPIO 的调试和配置。

2. /dev/mem设备

2.1 /dev/mem简介

/dev/mem 是 Linux 系统中的一个虚拟设备, 通常与 mmap 结合使用, 可以将设备的物理内存映射到用户态, 以实现用户空间对内核态的直接访问。 无论是标准 Linux 系统还是嵌入式Linux 系统, 都支持使用/dev/mem 设备。

/dev/mem 设备是内核所有物理地址空间的全映像,这些地址包括:

  • 物理内存(RAM)空间
  • 物理存储(ROM)空间
  • cpu总线地址
  • cpu寄存器地址
  • 外设寄存器地址,GPIO、定时器、ADC

因为涉及访问内核空间,因此只有root用户才有访问“/dev/mem”设备的权限

2.2 支持/dev/mem

然而, 直接访问内核空间是一项潜在危险的操作, 因此只有 root 用户才能访问/dev/mem设备。 此外有些系统可能需要单独启动/dev/mem 设备的功能。 我们执行:

1
2
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v6_v7_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

按照以下路径,找到对应的菜单项:

1
2
3
Device Drivers  --->
Character devices --->
[*] /dev/mem virtual device support
image-20250330205939376

打开之后,编译更新内核,当出现 /dev/mem 设备的时候说明开启成功:

1
ls /dev/mem
image-20250330205852190

2.3 /dev/mem 设备的使用

2.3.1 使用步骤

使用/dev/mem 设备需要具有 root 权限, 并且谨慎操作, 因为直接访问内核空间是一项潜在的危险操作。 以下是使用/dev/mem 设备的基本步骤:

  • (1)使用 open 函数打开”/dev/mem”文件描述符, 并指定访问权限和阻塞方式。 访问权限可以是只读(O_RDONLY) 、 只写(O_WRONLY) 或读写(O_RDWR) 阻塞方式或非阻塞(O_NDELAY)。
1
2
int fd = 0;
fd = open("/dev/mem", O_RDWR | O_NDELAY); /* 读写权限, 非阻塞 */

请注意, 这里使用 O_RDWR 表示读写权限, 并使用 O_NDELAY 表示非阻塞方式。 可以根据实际需求选择适当的访问权限和阻塞方式。

  • (2)使用 mmap 函数将需要访问的物理地址与”/dev/mem”文件描述符建立映射。mmap 函数将返回一个指向映射内存区域的指针。
1
2
char *mmap_addr = NULL;
mmap_addr = (char *)mmap(NULL, MMAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, MMAP_ADDR);

在这里, 使用 mmap 函数将物理内存地址映射到 mmap_addr 指针所指向的内存区域。MMAP_SIZE 表示映射的大小, PROT_READ | PROT_WRITE 表示访问权限为读写, MAP_SHARED表示共享映射, fd 是之前打开的 /dev/mem 文件描述符, MMAP_ADDR 是要映射的物理地址。

  • (3)对映射的地址进行访问, 即对寄存器进行读写操作。
1
2
3
int a = 0;
*(int *)mmap_addr = 0xff; // 写地址
a = *(int *)mmap_addr; // 读地址

在这里, 使用指针操作对 mmap_addr 指向的地址进行读写操作。 *(int *)mmap_addr 表示将 mmap_addr 解释为 int 类型的指针, 对于写操作, 将 0xff 写入该地址; 对于读操作, 将地址的值读取到变量 a 中。

2.3.2 mmap()

mmap 函数原型如下:

1
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);

函数参数:

  • start: 指定文件应被映射到进程空间的起始地址, 一般被指定为一个空指针, 选择起始地址的任务留给内核来完成。 映射成功之后, 函数返回值为最后文件映射到进程空间的地址, 进程可直接操作起始地址为该值的有效地址。
  • length: 是映射到调用进程地址空间的字节数。
  • prot: 参数指定共享内存的访问权限。 可取如下几个值的或。 PROT_READ(映射区域可读)、PROT_EXEC(映射区域可执行)、PROT_WRITE(映射区域可写)、 PROT_NONE(映射区域不可访问)。
  • flags: 由以下几个常值指定, MAP_SHARED, MAP_PRIVATE, MAP_FIXED, 其中 MAP_SHARED,MAP_PRIVATE 必选其一, MAP_FIXED 不推荐使用。
  • fd: 有效的文件描述符。 一般是由 open()函数返回。
  • offset: 文件映射的偏移量, offset 的大小必须是页的整数倍, 如果设备为 0 代表从文件最前方开始映射。

函数返回值: 成功执行时, mmap()返回被映射区的指针, 失败时, mmap()返回-1.

2.4 自己写一个memdev

2.4.1 源码编写

我们会在有些地方看到memdev工具,它可以直接读写内存,方便我们调试,这个其实就是通过访问/dev/mem实现的,网上也会有源码,例如sources.buildroot.net/devmem2.c。这个其实就是个应用程序,我们可以自己实现的。

2.4.2 开发板测试

我们用这个带有ap3216c的demo,加载相关驱动后,可以读一下i2c相关的寄存器:

1
2
./app_demo.out 0x020e0340
./app_demo.out 0x020e00b4
image-20250330212959779

2.5 根文件系统支持memdev

2.5.1 busybox配置

其实busybox中是支持memdev命令的,只是,在1.36.1版本的busybox中叫devmem,我之前用的是buildroot构建的根文件系统,它其实用到了busybox,我们可以在buildroot源码顶层目录下执行以下命令配置busybox:

1
sudo make busybox-menuconfig

然后就会打开busybox的配置界面:

image-20250330224153850

我们按以下路径找到对应的配置项:

1
2
Miscellaneous Utilities  --->
[*] devmem (2.5 kb)
image-20250330224509577

退出的时候保存,注意这个配置修改后,会修改busybox源码目录中的.config文件,要是需要保存的话,这里需要注意一下。然后重新编译buildroot:

1
sudo make

编译完成后看一下 output/images 目录下 rootfs.tar 的创建时间是否为刚刚编译的,如果不是的话就删除掉 rootfs.tar,然后重新执行“sudo make”重新编译一下即可。我们可以把这个目录解压看一下:

1
2
tar xvf rootfs.tar
find ./ -name "devmem"
image-20250330224917005

会发现有这个命令,我们进入开发板,使用以下命令:

1
which devmem
image-20250330225009867

可以看到确实有这个命令。

2.5.2 开发板测试

我们还是读这两个寄存器:

1
2
devmem 0x020e0340 32
devmem 0x020e00b4 32
image-20250330225107272

会发现和前面读到的结果一致,其实都一样,都是通过mmap来映射内存,只是根文件系统这个直接做成命令了。

三、GPIO的控制

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

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

1.1 开始之前要知道的

1.1.1 内核配置

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

1
2
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v6_v7_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

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

1
2
3
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编号可以通过以下公式计算:

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

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

1.2 GPIO控制器相关

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

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

1
ls -alh /sys/bus/gpio/devices
image-20250330175127447

1.2.2 每个GPIO控制器的详细信息

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

1
ls -alh /sys/class/gpio/gpiochipX
image-20250330175521101
1
2
3
4
5
6
7
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被内核或者用户程序占用,那么就会导出不成功), 导出成功如下图所示:

1
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, 删除成功如下图所示:

1
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

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

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

1
2
3
4
5
6
7
8
&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

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

1
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 输入输出模式, 如下图所示:

1
cat direction
image-20250331230605378

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

1
2
echo out > direction
cat direction
image-20250331230629595

1.4.2 active_low

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

1
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 引脚的中断触发模式之前,需将其设置为输入模式, 四种触发模式的设置如下所示:

1
2
3
4
echo "none" > edge    # 非中断引脚
echo "rising" > edge # 上升沿触发
echo "falling" > edge # 下降沿触发
echo "both" > edge # 边沿触发

1.4.4 value

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

1
2
3
4
# 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函数就可以实现:

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
/**
* @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引脚

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
/**
* @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属性

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
/**
* @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数据

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
/**
* @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 监听中断

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
/**
* @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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 表示程序正常退出
}