嵌入式Linux C应用编程——GPIO
概述我们可以在 Linux 应用层通过 sysfs 文件系统操控 GPIO 引脚的方法。sysfs 挂载于 /sys 目录内核将 GPIO 控制器及每个引脚以文件和目录的形式导出应用程序可以通过读写这些文件来配置引脚方向、读取电平、触发中断等。这种方式是嵌入式 Linux 中最常用的 GPIO 用户态操作方法适用于 LED 控制、按键检测、传感器读取等场景。操控GPIO的方法GPIO 在 sysfs 中的组织进入 /sys/class/gpio 目录可以看到两类内容gpiochipX 目录每个 gpiochipX 对应 SoC 的一个 GPIO 控制器如 I.MX6UL 的 GPIO1~GPIO5分别对应 gpiochip0、gpiochip32、gpiochip64、gpiochip96、gpiochip128。每个目录下有三个只读属性文件base该控制器管理的 GPIO 编号最小值。label控制器名称。ngpio该控制器管理的引脚数量。通过这些文件可以计算出任意引脚对应的编号例如 GPIO4_IO16 对应编号 96 16 112。96表示的是这个组里最小的标号值。export 和 unexport 文件均为只写文件。向 export 写入一个编号可以导出对应的 GPIO 引脚成功后会在当前目录下生成一个名为 gpioX 的目录X 为编号。向 unexport 写入该编号可以删除该目录释放引脚。img注意并非所有 GPIO 都能成功导出。如果该引脚已被内核中的其他驱动占用例如作为 LCD 背光、SD 卡检测等则导出会失败echo 操作会提示 Device or resource busy。导出后生成的 gpioX 目录该目录下包含多个属性文件用于控制引脚direction可读可写。写入 “in” 或 “out” 配置为输入或输出读取可查看当前模式。value可读可写。输出模式写入 “1” 输出高电平“0” 输出低电平。输入模式读取获取当前电平“0” 或 “1”。active_low可读可写默认 “0”。当设置为 “1” 时电平极性反转即 value 写入 1 实际输出低电平写入 0 输出高电平输入同理。edge可读可写用于配置中断触发模式。可选值“none”无中断“rising”上升沿触发“falling”下降沿触发“both”双边沿触发配置中断前需先将 direction 设为 “in”。img常用操作示例导出 GPIO 编号 0即 GPIO1_IO0echo 0 /sys/class/gpio/export配置为输出并设置高电平echo out /sys/class/gpio/gpio0/directionecho 1 /sys/class/gpio/gpio0/value配置为输入并读取电平echo in /sys/class/gpio/gpio0/directioncat /sys/class/gpio/gpio0/value配置边沿触发中断echo in /sys/class/gpio/gpio0/directionecho both /sys/class/gpio/gpio0/edge删除导出echo 0 /sys/class/gpio/unexport4. /sys/class/gpio 目录的来源该目录下的文件并非由开发板厂商“提供”而是由 Linux 内核在启动时动态创建。其生成过程如下内核中的 GPIO 驱动如 gpiolib在初始化时调用 class_create 和 device_create 接口在 /sys/class/ 下创建 gpio 目录及其子项。设备树.dtb描述了 SoC 中的 GPIO 控制器信息如寄存器基地址、中断号等内核解析设备树后匹配相应驱动进而创建对应的 gpiochipX 目录。export 和 unexport 文件由 gpiolib-sysfs 模块提供当用户写入编号时该模块负责动态生成 gpioX 目录。5. gpiochipX 符号链接的实际指向从上面的图可以看出这些 gpiochip文件实际上是链接文件它指向 /sys/devices/platform/soc/…/gpio/gpiochip0 等路径那里是内核设备模型中该 GPIO 控制器的真实位置。/sys/class/ 下的目录为了方便应用层按功能查找使用符号链接指向物理设备树。如果查看芯片手册就可以知道实际上209c000是gpio1_0的起始地址。imgGPIO输出实现思路操作 GPIO 输出电平的基本步骤确定 GPIO 编号。检查 /sys/class/gpio/gpioX 目录是否存在若不存在则通过 export 导出。向 direction 文件写入 “out” 配置为输出模式。向 value 文件写入 “1” 或 “0” 控制高低电平。2. 示例程序通过命令行参数控制可以指定 GPIO 输出电平的功能。#include stdio.h#include stdlib.h#include sys/types.h#include sys/stat.h#include fcntl.h#include unistd.h#include string.hstatic char gpio_path[100];static int gpio_config(const char *attr, const char *val){char file_path[100];int len;int fd;sprintf(file_path, %s/%s, gpio_path, attr); if (0 (fd open(file_path, O_WRONLY))) { perror(open error); return fd; } len strlen(val); if (len ! write(fd, val, len)) { perror(write error); close(fd); return -1; } close(fd); return 0;}int main(int argc, charargv[]){/校验传参 */if (3 ! argc) {fprintf(stderr, “usage: %s \n”, argv[0]);exit(-1);}/* 判断指定编号的 GPIO 是否导出 */ sprintf(gpio_path, /sys/class/gpio/gpio%s, argv[1]); if (access(gpio_path, F_OK)) { // 目录不存在则需要导出 int fd; int len; if (0 (fd open(/sys/class/gpio/export, O_WRONLY))) { perror(open error); exit(-1); } len strlen(argv[1]); if (len ! write(fd, argv[1], len)) { // 导出 gpio perror(write error); close(fd); exit(-1); } close(fd); } /* 配置为输出模式 */ if (gpio_config(direction, out)) exit(-1); /* 极性设置默认 0 */ if (gpio_config(active_low, 0)) exit(-1); /* 控制 GPIO 输出高低电平 */ if (gpio_config(value, argv[2])) exit(-1); exit(0);}代码解释程序执行时需要传入两个参数argv[1] 为 GPIO 编号argv[2] 为电平值“0” 或 “1”。运行过程如下通过 sprintf 构造 gpio_path格式为 /sys/class/gpio/gpioX。调用 access(gpio_path, F_OK) 判断该目录是否存在。若不存在返回 -1则打开 /sys/class/gpio/export 文件将编号写入以完成导出。调用 gpio_config(“direction”, “out”)该函数构造 gpio_path/direction 文件路径打开并写入 “out”。调用 gpio_config(“active_low”, “0”)设置极性为默认值可选默认即为 0。调用 gpio_config(“value”, argv[2])向 value 文件写入 “1” 或 “0” 控制电平。gpio_config 函数封装了打开属性文件 → 写入字符串 → 关闭的通用操作简化了代码。注意写入 value 的内容必须是字符串形式即 “1” 或 “0”不能是二进制整数 1 或 0。同样direction 也要写入字符串 “out” 而非二进制值。注意事项程序中使用 access(gpio_path, F_OK) 判断目录是否存在这是一种安全的做法。如果直接尝试导出已经导出的引脚向 export 写入会返回错误因为该引脚已被占用。在输出模式下虽然可以读取 value 文件但读到的是最后写入的值并非引脚实际电平例如引脚被外部电路拉低时读到的仍可能是 “1”。要获取实际电平需配置为输入模式。GPIO 输入读取 GPIO 输入电平的流程与输出控制基本一致主要区别在于将 direction 设置为 “in”然后通过读取 value 文件获取当前电平。该操作常用于检测按键状态、传感器数字信号等。示例程序 gpio_in.c 与 gpio_out.c 结构非常相似仅改动了配置和读取部分。下面是 main 函数中关键代码段。/* 配置为输入模式 */if (gpio_config(“direction”, “in”))exit(-1);/* 极性设置 */if (gpio_config(“active_low”, “0”))exit(-1);/* 配置为非中断方式 */if (gpio_config(“edge”, “none”))exit(-1);/* 读取 GPIO 电平状态 */sprintf(file_path, “%s/%s”, gpio_path, “value”);if (0 (fd open(file_path, O_RDONLY))) {perror(“open error”);exit(-1);}if (0 read(fd, val, 1)) {perror(“read error”);close(fd);exit(-1);}printf(“value: %c\n”, val);代码执行过程通过 gpio_config(“direction”, “in”) 将引脚设为输入模式。active_low 保持默认 0若需要极性反转可置 1此时 value 读到的电平与实际物理电平相反。向 edge 写入 “none” 明确关闭中断功能默认即无中断此步可省略但显式写出可避免后续误配置。构造 value 文件的完整路径并打开只读。调用 read 读取 1 个字符存入 val该字符为 ‘0’ 或 ‘1’。打印该字符。读取 value 文件时返回的是 ASCII 字符 ‘0’ 或 ‘1’而非二进制数值 0 或 1。比较时应使用字符常量如 if (val ‘1’)。GPIO 中断当需要检测引脚电平变化比如按键按下、传感器触发时用轮询死循环读 value会白白消耗 CPU 资源实际项目中更常见的做法是使用中断。GPIO 的中断也是通过 sysfs 来配置的往 edge 文件写入 “rising”、“falling” 或 “both” 来设定触发边沿然后用 poll 函数阻塞等待中断事件。示例程序 gpio_intr.c 跟前面输入输出的代码非常相似导出引脚和设置方向的代码几乎一样关键的不同在于配置 edge 以及后续的 poll 等待逻辑。下面只给出 main 函数中与中断相关的核心代码段。/* 配置为输入模式 */if (gpio_config(“direction”, “in”))exit(-1);/* 极性设置 */if (gpio_config(“active_low”, “0”))exit(-1);/* 配置中断触发方式: 上升沿和下降沿 */if (gpio_config(“edge”, “both”))exit(-1);/* 打开 value 属性文件 */sprintf(file_path, “%s/%s”, gpio_path, “value”);if (0 (pfd.fd open(file_path, O_RDONLY))) {perror(“open error”);exit(-1);}/* 调用 poll */pfd.events POLLPRI; // 只关心高优先级数据可读中断read(pfd.fd, val, 1); // 先读取一次清除状态for ( ; ; ) {ret poll(pfd, 1, -1); // 阻塞等待中断if (0 ret) {perror(“poll error”);exit(-1);} else if (0 ret) {fprintf(stderr, “poll timeout.\n”);continue;}/* 校验高优先级数据是否可读 */ if (pfd.revents POLLPRI) { if (0 lseek(pfd.fd, 0, SEEK_SET)) { perror(lseek error); exit(-1); } if (0 read(pfd.fd, val, 1)) { perror(read error); exit(-1); } printf(GPIO 中断触发value%c\n, val); }}代码的执行逻辑可以分为几个步骤将引脚设为输入模式后向 edge 文件写入 “both”表示上升沿和下降沿都会触发中断。你也可以根据需要改成 “rising” 或 “falling”。打开 value 文件获得文件描述符。注意这里只打开一次后续 poll 和读写都复用这个描述符。在进入主循环之前先调用一次 read 把当前状态消费掉避免之前的残留状态干扰后续的中断等待。poll 的 events 字段指定为 POLLPRI高优先级数据可读而不是普通的 POLLIN。GPIO 中断在内核里被归类为高优先级事件用 POLLPRI 才能抓到。poll 返回后检查 revents 是否包含 POLLPRI如果是则说明中断发生了。此时必须先 lseek 把文件偏移移到开头然后再 read。这两个操作配合才能把内核里的中断状态清除掉否则下次 poll 可能会直接返回或者无法正确等待下一次中断。在驱动中读操作会让内核自动清理状态第二次之所以需要用Iseek把读位置移动到头部正是因为第一次的读取使读位置后移了。实际跑起来之后用杜邦线把引脚在 3.3V 和 GND 之间来回碰一下终端就会打印出相应的触发信息。poll 函数在之前有学习过具体参考我之前的笔记https://juejin.cn/post/7660712622824718390#heading-7它本来是用来同时监视多个文件描述符的但在这里只监视一个 GPIO 的 value 文件也能很好地完成阻塞等待中断的工作比轮询节省 CPU 得多。每次 poll 返回后必须先 lseek 再 read这一步是必须做的。其次必须先读取一次清除状态如果没有这一步下一次 poll 可能会因为状态没有被清除而表现异常。