简介GPIO应用Linux编程.pdf 面向嵌入式 Linux 入门者、驱动调试与板级适配人员聚焦通用输入/输出接口在应用层的控制方法帮助读者理清从 sysfs 目录结构到 C 程序读写的完整链路。内容系统梳理了 /sys/class/gpio 下的 export 与 unexport 机制、gpiochipN 中 base、label、ngpio 等属性文件的用途以及 gpioN 目录下 direction、value、active_low、edge 的读写规则并给出输入读取、输出控制与中断检测的 Shell 命令范例及对应的 C 代码实现另附 M28x GPIO 应用编程、EPC-28x 资源汇总等实例可作为板级调试时随查随用的对照手册。压缩包内为 1 个 PDF 文件整体约 419KB体积轻量、便于检索与离线阅读。目前已有 338 人学习适合希望快速上手 GPIO 应用编程、补齐 sysfs 操作细节并积累排错思路的中初级开发者。1. 一块板子上的 68 号引脚为什么要从 sysfs 入手 GPIO上周有人拿着一块跑 Linux 的工控板来问串口能登录网口能通现在想把一路继电器拉高、再读一个光电开关但不想动内核也不想改设备树。我让他在串口里敲了三条命令继电器就响了。这套 GPIO 应用编程走的是 Linux 的 sysfs 通用接口路径固定在/sys/class/gpio下不用写驱动、不用碰芯片寄存器写文件就是拉电平读文件就是取电平。它适合设备联调、产线夹具、低频 IO 扩展这类活电平变化在毫秒级文件读写完全够用。反过来微秒级时序、PWM 调光、精确边沿捕获就别指望它那几个场景该换字符设备接口或者专用库硬用 sysfs 只会把自己坑进去。下面从一个具体的引脚编号开始把目录结构、Shell 操作、C 代码和实际踩过的坑一次讲清楚照着做完你手上那块板子的任意一路可用 IO 都能被程序控制。2. 目录即接口/sys/class/gpio 的布局与导出机制sysfs 是内核把设备模型映射成文件和目录的一层虚拟文件系统读写这些文件相当于调用内核里注册好的回调没有真实磁盘 IO。GPIO 子系统gpiolib把每一组 GPIO 控制器注册成一个 gpiochip用户空间看到的就是/sys/class/gpio下的一堆目录。先看一眼原始状态ls /sys/class/gpio/ # export gpiochip0 gpiochip32 gpiochip64 gpiochip96 gpiochip128 unexportexport和unexport是普通文件其余五个是符号链接指向/sys/devices/...下真正的控制器目录。名字里的数字不是随便起的它是这组 GPIO 的起始全局编号这一点在 2.3 节会解释清楚。2.1 gpiochipN 里的四个关键文件进到任意一个 gpiochip 目录里看到的东西基本一样ls /sys/class/gpio/gpiochip0/ # base label ngpio power subsystem uevent文件名作用base本控制器管理的 GPIO 起始全局编号label控制器名称用于区分不同 GPIO 控制器ngpio本控制器管理的 GPIO 总数power电源管理相关信息日常操作基本用不到subsystem符号链接指回父目录uevent内核与 udev 之间的通信接口热插拔事件走这里这四个值组合起来就能算出这组 GPIO 的编号范围[base, base ngpio - 1]。看到 gpiochip0/32/64/96/128 排成一串说明芯片内部把引脚按 32 个一组切成了多个 BANK每个 gpiochip 管一个 BANK。这个编号规律不是巧合而是 SoC 内部寄存器分组的直接反映。我一般拿到一块新板子第一件事是跑一遍下面这段把结果记到硬件的备注文档里后面写代码时不用反复翻手册cd /sys/class/gpio for c in gpiochip*; do printf %s base%-4s ngpio%-3s label%s\n \ $c $(cat $c/base) $(cat $c/ngpio) $(cat $c/label) donebase和ngpio是只读的别想着往里写label通常也是只读它来自设备树或平台代码里的注册信息。有些板子会把控制器名字写成 SoC 内部的 GPIO 模块名有些会写成gpio...形式的地址看懂它的意义不大能对上号就行。2.2 export/unexport 与 gpioN 目录的七个属性文件export是唯一的入口。往它里面写一个十进制编号内核就动态创建/sys/class/gpio/gpioN/目录这个动作叫「导出」。写unexport则相反目录被回收之前打开的 fd 全部失效。echo 68 /sys/class/gpio/export # 导出 68 号 ls /sys/class/gpio/gpio68/ # active_low direction edge power subsystem uevent value echo 68 /sys/class/gpio/unexport # 用完清掉几个必须知道的细节。写入的必须是十进制字符串写0x44会直接失败重复导出同一个引脚内核返回Device or resource busy如果这个引脚已经被别的驱动申请了比如板载按键、SD 卡检测脚同样会报资源占用。导出成功后生成的目录里还有一层属性文件文件名读写作用direction读写查看或设置方向写入in或outvalue读写输出时写 0/1 设置电平输入时读 0/1 取电平active_low读写写 1 后 value 的语义翻转低电平有效时用edge读写设置中断触发方式none/rising/falling/bothpower只读电源管理信息subsystem只读符号链接指向父目录uevent只读内核与 udev 的通信接口刚导出的引脚默认是输入方向这一点很多人第一次用会忽略没写direction就直接往value写 1有的内核版本会静默丢弃有的返回EIO表现就是「代码没报错但电平不动」很容易误判成硬件坏了。2.3 引脚编号怎么算BANK × 32 N硬件手册和底板丝印上标的通常是P2.4这种形式前一位是 BANK后一位是 BANK 内的引脚序号。转到 sysfs 用的全局编号要过一道换算bank2 n4 echo $((bank * 32 n)) # 输出 68公式是全局编号 BANK × 32 N。之所以是 32因为每个 BANK 固定 32 个引脚这是 SoC 内部寄存器位宽决定的分组粒度不是软件约定。算出来的编号必须落在某个[base, base ngpio - 1]区间里否则导出一定失败——写 200 进去而板子上只到 160内核会直接返回EINVAL这个报错信息很不友好第一次遇到容易怀疑人生。还有一条血泪经验不是所有物理引脚都能当 GPIO 用。SoC 的引脚大多是复用的同一个脚可能同时是 UART 的 TX、I2C 的 SCL 和 GPIO。如果你导出了一个当前被配置成 I2C 的引脚并把方向设成 outI2C 总线就废了而且现象很玄学——设备时好时坏。所以只使用硬件手册或底板丝印明确标注为 GPIO 的引脚丝印上PN.N这种格式的一般就是推荐使用的。真要动复用引脚得先改设备树里的 pinmux 配置这已经超出 sysfs 的能力范围了。3. Shell 命令跑通一次输出与输入从 export 到 value真正上手之前先用 Shell 把一整条链路走通比直接写 C 代码快得多。Shell 出问题时错误信息是内核直接吐出来的看得清楚等确认硬件和编号都没问题再翻译成 C能省掉一大半调试时间。3.1 输出最小闭环五条命令点亮一路以 68 号引脚为例完整流程如下cd /sys/class/gpio echo 68 export # 1. 导出生成 gpio68 目录 cd gpio68 echo out direction # 2. 设为输出不设的话写 value 无效 echo 0 value # 3. 输出低电平 echo 1 value # 4. 输出高电平 cat direction # 5. 回读确认应输出 out第二条是整套操作里最容易被跳过的一步。导出后的默认方向是in此时value是只读语义往里写不会有任何效果。判断有没有生效别靠猜直接cat direction看返回值out就是成功了in就是没写进去。如果第 1 步就报write error: Device or resource busy说明这个脚已经被占用。内核带 debugfs 的话可以查占用者mount -t debugfs none /sys/kernel/debug # 已挂载可跳过 cat /sys/kernel/debug/gpio输出里会列出每个已被申请的 GPIO 编号和申请者名字一眼就能看出是哪个驱动占的。没有 debugfs 就只能换脚试或者去翻设备树。3.2 输入模式与电平回读输入方向的流程短一些但接线不能省cd /sys/class/gpio echo 68 export echo in gpio68/direction cat gpio68/value用杜邦线把目标引脚分别接到 GND 和 VCCcat的结果应该在 0 和 1 之间切换。如果接了 GND 还是读到 1先检查线有没有插紧再确认接的是不是你以为的那个物理脚——底板丝印和排针顺序对不上是常有的事。引脚悬空时读到的值是不确定的可能反复跳变也可能一直停在某个值。这不是代码 bug是 CMOS 输入高阻态的正常表现。常见做法是外部加一个 10k 的上拉或下拉电阻把电平固定住或者直接用 SoC 内部的可配置上下拉需要提前在 pinmux 里配好。拿悬空引脚的读数去判断程序对不对是最容易翻车的一种调试方式。3.3 active_low 与 edge两个容易被忽略的文件active_low解决的是语义问题。有些硬件是低电平有效的比如继电器模块给 0 才吸合。此时把active_low写成 1value的语义整体翻转物理低电平读出来是 1写 1 输出的是物理低电平。这样上层业务代码就不用在每个地方都写!echo 1 active_low cat value # 物理低电平时这里显示 1edge是 sysfs 里做中断检测的唯一手段。默认值是none改成rising、falling或both之后value文件就支持poll()的POLLPRI事件了echo falling edge # 之后在 C 程序里用 poll() 监听 value 文件即可不用忙等这条路子能把 CPU 占用从 100% 降到接近 0但可靠性不如专用驱动。机械按键的抖动会连续触发多次事件软件侧必须做二次确认通常的做法是收到事件后延时 10~20ms 再读一次电平两次一致才认。光电开关、霍尔传感器这类无抖动的信号源可以直接用。4. C 语言接管open/write/read 操作 sysfs 的完整链路Shell 验证通过之后翻译成 C 就是把每条echo换成openwrite每条cat换成openread。听起来简单实际上有三个地方特别容易写错字符串长度、文件偏移、错误码判断。4.1 导出与方向设置路径就是字符串先写两个基础函数一个负责导出一个负责设方向#include stdio.h #include string.h #include unistd.h #include fcntl.h #include errno.h #define EXPORT_PATH /sys/class/gpio/export #define GPIO_NUM 68 /* 全局编号BANK*32N */ #define DIR_PATH /sys/class/gpio/gpio68/direction static int gpio_export(const char *num) { int fd open(EXPORT_PATH, O_WRONLY); if (fd 0) { perror(EXPORT_PATH); return -1; } /* 用 strlen 而不是 sizeof避免把结尾的 \0 也写进内核 */ if (write(fd, num, strlen(num)) 0) { if (errno EBUSY) { /* 已导出过视为成功 */ close(fd); return 0; } perror(write export); close(fd); return -1; } close(fd); return 0; } static int gpio_set_dir(const char *path, const char *dir) { int fd open(path, O_WRONLY); if (fd 0) { perror(path); return -1; } if (write(fd, dir, strlen(dir)) 0) { perror(write direction); close(fd); return -1; } close(fd); return 0; }strlen和sizeof的区别是这段代码里最容易埋雷的地方。sizeof(68)等于 3会把结尾的\0一起写进export。有的内核版本能容忍这个多余字节有的直接返回EINVAL同一份代码在不同板子上表现不一样很难查。direction同理sizeof(in)是 3写进去的是in\0正规做法还是strlen。EBUSY的处理也有讲究。实际项目里程序可能被反复启动第二次导出同一个引脚必然失败把它当成错误处理会导致程序直接退出。当成「已经导出过」放行逻辑上更合理。4.2 读写 valuelseek 与 sizeof 的坑读写电平的函数比想象中短但两个细节不能少static int gpio_read_value(const char *path) { char buf[8] {0}; int fd open(path, O_RDONLY); if (fd 0) { perror(path); return -1; } lseek(fd, 0, SEEK_SET); /* 每次读之前把偏移拉回文件头 */ int n read(fd, buf, sizeof(buf) - 1); close(fd); if (n 0) return -1; return (buf[0] 1) ? 1 : 0; } static int gpio_write_value(const char *path, int v) { int fd open(path, O_WRONLY); if (fd 0) { perror(path); return -1; } char c v ? 1 : 0; int ret write(fd, c, 1); /* 只写一个字节不要带 \0 */ close(fd); return (ret 1) ? 0 : -1; }lseek那一行是后悔药。如果 fd 被复用比如在循环里反复读同一个引脚却只 open 一次第一次read之后文件偏移停在末尾第二次read直接返回 0 字节表现出来就是「读到空值」或者「值一直是上一次的」。每次读之前lseek(fd, 0, SEEK_SET)是成本最低的规避方式干脆每次重新open也行只是多几次系统调用。写value时只写一个字符1或0不要写字符串1。写字符串会带上结束符虽然多数内核能容错但没必要给自己找不确定性。4.3 交叉编译与板端验证写完之后在 PC 上交叉编译工具链前缀按你板子实际的来arm-linux-gnueabihf-gcc gpio_test.c -o gpio_test # 如果板子根文件系统里缺对应的 libc加 -static 静态链接 arm-linux-gnueabihf-gcc -static gpio_test.c -o gpio_test动态链接版拷到板子上跑起来报No such file or directory九成是 libc 版本对不上-static一把梭能解决大部分这类问题代价是体积大几百 KB对这种小工具完全无所谓。下发方式看板子有什么能 ssh 就scp只有串口就用rz/sz或者挂 U 盘、用 tftp。拷过去之后先chmod x gpio_test然后接线杜邦线把 GND 或 VCC 接到目标引脚上运行./gpio_test串口应该打印出当前电平。测输出的话拿万用表量或者接一个 LED 串 1k 电阻拉高亮拉低灭比看代码直观。5. 常见问题排查五条踩过的坑5.1 写 export 报 Device or resource busy现象echo 68 export返回write error: Device or resource busy目录建不出来。原因有两种。一是这个引脚已经被内核里的某个驱动申请了比如板载 LED、按键、SD 卡热插拔检测脚驱动持有它用户空间就抢不到。二是之前导出过但没unexport重复导出会撞上同一个错误码。解决先跑cat /sys/kernel/debug/gpio看申请者是谁是系统驱动就换个脚别硬抢。是自己上次留下的先echo 68 unexport再重新导出。程序里则按 4.1 节的做法把EBUSY当成「已导出」处理。5.2 direction 写成功但电平不变化现象echo out direction没报错echo 1 value也没报错万用表量出来始终是 0V。原因最常见的是引脚被 pinmux 复用成了别的外设GPIO 控制器根本没接管这个脚方向寄存器写了也不生效。其次是输出类型是开漏没有外部上拉高电平根本推不上去。还有一种是自己量错了物理引脚。解决翻硬件手册确认该脚的默认功能和复用配置量之前先用万用表通断档确认丝印和排针的对应关系开漏输出必须外接上拉电阻。5.3 read value 读到空或者一直读到旧值现象C 程序里read返回 0或者读到的值永远停在第一次的结果。原因文件描述符复用时偏移没有复位。sysfs 的value文件是有状态的读完一次偏移就在末尾不lseek回去第二次读不到东西。另一种情况是把read的返回值当成了电平值本身read返回的是字节数真正的电平在缓冲区里。解决每次读之前lseek(fd, 0, SEEK_SET)检查代码里判断的是buf[0]而不是n。5.4 板子上运行报 Permission denied现象./gpio_test提示open /sys/class/gpio/export: Permission denied。原因/sys/class/gpio下的文件默认属主是 root、权限 644 或 664普通用户没有写权限。很多板子的默认登录用户不是 root。解决用 root 跑或者写 udev 规则把这个目录的属主改掉再简单点程序启动时检查geteuid()非 root 直接打印一句明确提示比让用户对着Permission denied猜半天强。5.5 机械开关读到的值乱跳现象按下按键程序连续读到0 1 0 1 0一串变化一次按下被识别成好几次。原因机械触点在闭合瞬间会抖动几毫秒到十几毫秒sysfs 读到的是瞬时电平没有任何滤波。解决软件侧做二次确认读到变化后延时 10~20ms 再读一次两次一致才认硬件侧加 RC 低通滤波用edge触发的话事件本身也会重复上报消抖逻辑同样不能省。6. 二次开发把散落的 sysfs 操作收成一个能复用的小模块一份能跑通的示例代码和一个能进项目的模块中间差的是可配置性。最直接的做法是把引脚号、方向、业务名抽成一张表初始化函数遍历这张表批量导出业务代码里只按名字操作不再出现魔法数字 68。/* gpio_cfg.h */ typedef struct { int num; /* 全局编号 BANK*32 N */ const char *name; /* 业务名日志里好认 */ int dir_out; /* 1 输出0 输入 */ int active_low; /* 1 表示低电平有效 */ } gpio_cfg_t; static const gpio_cfg_t g_gpios[] { { 68, relay1, 1, 0 }, { 69, sensor_in, 0, 1 }, };gpio_init_all()里遍历数组逐个拼出/sys/class/gpio/gpio%d/direction路径调 4.1 节的函数导出并设方向active_low为 1 的顺手把active_low文件写成 1。之后业务层只需要gpio_write(relay1, 1)和gpio_read(sensor_in)引脚换了只改表不动逻辑。这套封装我一般还会加一个gpio_dump()把表里每个引脚的当前方向和电平打成一行日志现场排查时省掉一大半 ssh 来回。验证方法很朴素写一个自测程序遍历所有输出脚依次拉高拉低万用表或 LED 确认输入脚分别接 GND 和 VCC 各读一次两次结果必须相反。跑完这一圈硬件接线和软件映射就都对上了。再往深一层改就是换接口。sysfs 在较新的内核里已经被标记为过时官方推荐用/dev/gpiochipN的字符设备接口配GPIO_GET_LINEHANDLE_IOCTL或者直接用 libgpiod好处是一次 open 拿到整组线的句柄读写不用反复走文件系统时序能好一个数量级。但工业板子上跑的往往是几年前的内核sysfs 还在短期内不会消失。我的做法是业务层接口保持gpio_read/gpio_write不变底层实现做成可切换的哪天要换字符设备只动一个 .c 文件。从那以后我每次拿到一块新板子第一件事都是把 gpiochip 的 base 和 ngpio 全打一遍抄在纸上贴在工位第二件事是拿一个脚接 LED 跑通导出和拉高。这两步做完后面写多少行代码心里都有底。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?