简介:面向嵌入式Linux开发者与驱动初学者的GPIO应用编程讲义,定位于补齐通用输入输出接口在用户层的使用知识,适合板级调试、课程实验和驱动入门阶段查阅。内容以sysfs接口为主线,梳理export与unexport的导出与清除机制,说明gpiochipN目录下base、label、ngpio等属性文件的职责,并逐一解释gpioN目录中direction、value、active_low、edge的文件语义,帮助读者建立从GPIO编号、方向配置到电平读取与输出控制的整体认识。讲义同时对照Shell命令与C语言系统调用两种访问方式,给出open、read、write、close的操作范式,并收录M28x GPIO应用编程、EPC-28x GPIO资源汇总等实例,便于结合具体硬件平台理解软件控制路径及资源占用提示。内容由概念、接口到实例递进,适合按章节检索。全包仅1个PDF文件,约419KB,轻量易查阅,已有338人学习,可作为嵌入式Linux外设调试与自学GPIO的参考材料。
1. 先弄明白一件事:GPIO 到底是个什么东西
很多人第一次听到 GPIO 应用 Linux 编程这几个字,脑子里先浮现的是密密麻麻的电路图和寄存器手册,还没动手就先怯了。其实把名字拆开看没那么吓人:GPIO 就是芯片上引出来的一排能被程序指挥的小开关,它能干的事只有两件——往外送一个电平,或者从外面读一个电平。往外送,就能点亮一个 LED、吸合一个继电器、让蜂鸣器响起来;往里读,就能知道按键有没有被按下、外面的传感器吐出来的是高还是低。
所以它不是玄学,也不是只有硬件工程师才配碰的东西。这份资料讲的就是把这件事讲透:在 Linux 系统上,用命令行和 C 程序,让一个引脚按你的意思变高变低、读回外面的状态。入门的话建议先记住一句话——GPIO 是程序和真实世界之间最便宜的一根线,会用它,你写的东西才第一次能对外有所影响。
2. 先认识一个操作台:为什么改个文件就能控制硬件
2.1 一个被低估的好处:不用碰内核就能控制引脚
刚开始学的时候,最容易产生的误解是「控制硬件一定要写驱动」。很多老资料确实这么教:改引脚复用、配寄存器、编译内核模块、重新烧固件,一趟下来半天过去,中间任何一步翻车都得从头再来。对零基础的人来说,这条路的挫败感太强,我第一次照着做的时候,光是环境没对上就卡了一整天。
Linux 后来提供了一条完全不同的路:把 GPIO 抽象成文件。系统里有个目录,里面每个引脚对应一个子目录,子目录里放着几个普通文本文件。你想要引脚输出高电平,就往 value 文件里写一个 1;你想知道外面是高还是低,就把 value 读出来。读写文件这件事,shell 会、C 程序也会、Python 也会,于是硬件控制的门槛一下就降到了「会读写文件」的水平。
这套机制的正式名字叫 sysfs 接口。它的价值不在于先进,而在于透明。引脚现在是什么方向、电平是高是低,全都摆在那几个文件里,你随时能 cat 出来看。调试的时候不用猜、不用抓包、不用对着看不见的黑匣子瞎试——状态就写在文件里。对新手来说,这种看得见的反馈比任何原理讲解都管用,因为你每敲一条命令,都能马上验证自己有没有做对。
提示:后面的例子都用 root 权限操作,普通用户直接写这些文件会被拒绝。这不是你写错了,是权限不够。
2.2 走进 /sys/class/gpio 目录:export、unexport 和 gpiochipN
先把目录列出来看看长什么样,这一步本身没风险,先看个新鲜:
# 列出 GPIO 操作目录下的内容,先观察再动手 ls /sys/class/gpio/你会看到一长串名字,大致分三类:
export unexport gpiochip0 gpiochip32 gpiochip64 gpiochip128export和unexport是两个普通文件,剩下带 gpiochip 前缀的是目录(有的是符号链接)。这三个东西的分工非常清楚:
export:把某个引脚从系统手里拿过来,交给用户使用。往这里写引脚编号,等于告诉内核——这个脚我要用了,请在目录下给我建一个对应的操作入口。unexport:用完之后还回去。往这里写同一个编号,对应的目录就消失。gpiochipN:这是控制器本身的档案柜,只读为主,用来查这个控制器管着哪些引脚、编号从几开始。
这里有个新手最容易发懵的点:为什么有四个 gpiochip?因为一颗芯片上的引脚不是一个大池子,而是被分成若干个组,每组由一个控制器管着。gpiochip0 管编号 0 到 31,gpiochip32 管 32 到 63,依次往下排。所以你要是想用一个引脚,第一步不是急着 echo,而是先确认它的编号落在哪个芯片的管理范围里。
2.3 第一次看 gpiochipN:base、label、ngpio 是给你查户口的
随便挑一个控制器目录进去看看:
# 查看 0 号控制器管理的引脚范围和数量 ls /sys/class/gpio/gpiochip0/ cat /sys/class/gpio/gpiochip0/base cat /sys/class/gpio/gpiochip0/ngpio里面几个文件各有用途,入门阶段记住三个就够用:
| 文件名 | 作用 | 新手怎么用 |
|---|---|---|
| base | 该控制器管理的起始编号 | 确认这个芯片从几号开始算 |
| ngpio | 该控制器管理的引脚总数 | 用 base + ngpio 算出结束编号 |
| label | 设备信息字符串 | 排查时确认自己看的是哪个控制器 |
| power | 供电相关信息 | 入门阶段不用管 |
| subsystem | 指向父目录的符号链接 | 入门阶段不用管 |
| uevent | 内核和设备管理程序之间的通信接口 | 入门阶段不用管 |
为什么值得花时间看这几个文件?因为新手最常见的翻车原因之一,就是引脚编号算错或者用错控制器。你以为自己在操作 68 号引脚,实际上那个编号根本不在任何一个 gpiochip 的管理范围内,命令敲下去毫无反应,然后就开始怀疑人生——是不是板子坏了、是不是系统不支持、是不是资料过期了。其实只是编号没对上。
算完你会发现一个规律:base 是 32 的整数倍,ngpio 基本是 32,说明每个控制器管 32 个引脚,用 0 到 31、32 到 63 这样一段一段切开。这个规律在下一章算具体引脚编号时还会再用到一次。
还有一点必须提前说清楚:gpiochip 目录里的这些文件,是给你查信息用的,不是给你改状态的。真正让引脚动起来,靠的是导出之后生成的那个gpioN目录。很多人对着 gpiochip0 里的文件一顿写,写完发现什么也没发生,就是因为找错了操作对象。
3. 第一个能跑的例子:用几条 Shell 命令让引脚动起来
3.1 先把引脚序号算对:BANK × 32 + N
大部分 ARM 开发板的引脚命名是「组 + 组内序号」的形式,比如 P2.4 表示第 2 组第 4 个。但 sysfs 认的是全局编号,所以要换算一次。换算公式很简单:
GPIO 全局编号 = BANK × 32 + NBANK 是引脚所在的组号,N 是组内序号。以 P2.4 为例,BANK = 2,N = 4,那么编号就是 2 × 32 + 4 = 68。所以后面所有的例子都用 68 这个编号,你换成自己板子上的引脚时,照着同一个公式算一遍就行。
为什么公式里是乘 32?因为每组正好 32 个引脚,前面的组全部占满了编号才轮到你这一组。这个 32 不是随便定的,跟上一章看到的 ngpio 是一致的。如果你手上的板子分组规则不一样,去看硬件手册里写的编号规则,别硬套。
这里还有一道必须过的坎:不是组里的每个引脚都能当 GPIO 用。很多引脚出厂时被分配给了别的外设,比如串口、网卡、显示屏接口。你把它强行导出成 GPIO,那个外设可能就罢工了。入门阶段最稳的做法是只用手册或板子丝印上明确标了「推荐使用」的那几个脚,其他的一律先别碰。这条经验是用血泪换来的,后面避坑那一章还会再展开。
3.2 导出 GPIO:echo 68 > export 到底做了什么
编号算好了,第一步是导出:
# 进入 GPIO 操作目录 cd /sys/class/gpio # 把 68 号引脚从系统手里接管过来 echo 68 > export # 确认目录是否生成 ls敲完这三条,你应该能看到多出来一个gpio68目录。这一步的本质是向内核登记:我要用 68 号引脚,请给我开一个操作入口。目录出现了,说明登记成功;目录没出现,就是出事了,先别往下走。
如果这时终端回你一句类似Device or resource busy或者提示资源被占用,意思是这个引脚已经被别人占了——可能是某个驱动、可能是设备树里已经配给了别的功能。这种情况的处理方式不是反复重试,而是换一个没用过的引脚,或者去查这个脚到底被谁占着。反复 echo 一百遍不会让它变好,只会浪费时间。
另外还有一个细节:导出这一步只需要做一次。目录已经存在的情况下再 echo 一次,会提示文件已存在或者参数错误,这不代表你搞砸了,只是重复登记而已。判断标准很简单——ls里有没有 gpio68 这个目录。
3.3 设置方向和读电平:in、out 和 0、1
gpio68 目录生成之后,进去看有哪些文件:
# 查看 68 号引脚的操作文件 ls /sys/class/gpio/gpio68/会看到 active_low、direction、edge、value 等几个文件。入门阶段真正要用的只有两个:direction 和 value。剩下几个先放着,等你能熟练控制了再说。
| 文件名 | 作用 | 说明 |
|---|---|---|
| direction | 查看或设置引脚方向 | 写入 out 表示输出,写入 in 表示输入 |
| value | 设置或读取电平 | 输出时写 0 或 1,输入时读出来也是 0 或 1 |
| active_low | 决定 value 是否取反 | 写 0 不翻转,写 1 翻转 |
| edge | 设置中断触发方式 | 入门阶段暂时不用 |
刚导出时,引脚默认处于输入状态。先看一眼确认:
# 查看当前方向,默认应该输出 in cat /sys/class/gpio/gpio68/direction # 设置为输出 echo out > /sys/class/gpio/gpio68/direction # 再确认一次,应该变成 out cat /sys/class/gpio/gpio68/direction方向设成 out 之后,就可以控制电平了:
# 输出低电平 echo 0 > /sys/class/gpio/gpio68/value # 输出高电平 echo 1 > /sys/class/gpio/gpio68/value反过来,如果设成 in,就改读 value:
# 切成输入模式 echo in > /sys/class/gpio/gpio68/direction # 读当前外部输入的电平,返回 0 或 1 cat /sys/class/gpio/gpio68/value读回来的 0 表示低电平,1 表示高电平。这个值不会自己刷新,你读一次就是那一刻的状态,想看变化就再读一次。入门阶段很多人以为这是实时监控,其实是拍快照。
3.4 完整走一遍:从导出到引脚真的变高
把上面的步骤串起来,就是一次完整的输出验证:
# 1. 导出 68 号引脚 cd /sys/class/gpio echo 68 > export # 2. 进入引脚目录 cd gpio68 # 3. 设为输出方向 echo out > direction # 4. 先拉低,再拉高 echo 0 > value echo 1 > value # 5. 确认写进去了 cat value怎么知道它真的动了?最直观的办法是在这个引脚和地之间接一个 LED 串一个限流电阻,写 1 的时候亮,写 0 的时候灭;有万用表就直接量电压。没有硬件条件也没关系,cat value读回来是你刚写的那个值,至少说明写入链路是通的。
用完记得把引脚还回去,这是好习惯:
# 从系统里注销这个引脚,目录会消失 echo 68 > /sys/class/gpio/unexport不 unexport 会怎样?短期内没影响,引脚状态会保持你最后一次设置的样子。但下次开机、或者别的程序想用这个脚的时候,就可能撞上「资源被占用」。养成用完就还的习惯,能省掉后面很多莫名其妙的排查。
4. 从命令到代码:C 程序里操作同一个引脚
4.1 为什么最后还是要写成 C 程序
Shell 命令适合验证,适合临时试一下,但它撑不起真正的应用。你不可能在一个循环里每秒 echo 一百次去翻转电平,也没法在 shell 里优雅地处理「按键按下就做某事,否则继续检测」这种逻辑。真要做成产品或者做成课程设计,就得落到 C 程序里。
好消息是:C 程序走的是完全同一套路径。你不需要学第二套 API,不需要引入什么库,只是把 shell 里的 echo 换成 write、把 cat 换成 read,操作对象还是那几个文件。这也是 sysfs 接口最舒服的地方——命令行里验证通了,代码里就一定能跑通,两者的行为是可对照的。
还有一层好处在调试上。程序跑出问题的时候,你可以同时开一个终端,用 cat 去看 direction 和 value 现在是什么状态。程序说它设了输出,你就去看 direction 是不是真的变成了 out。这种「代码和状态可以对照」的能力,是新手成长最快的方式。
4.2 四个系统调用就够:open、write、read、close
C 语言操作文件有一套标准流程,操作 GPIO 也是照搬这套流程,没有特殊语法:
open:打开某个文件,拿到一个文件描述符。参数里要写清楚是只读、只写还是读写。write:往文件里写内容。写方向、写电平都靠它。read:从文件里读内容。读电平靠它。close:用完关掉。数量多了不关,描述符会泄漏。
有一点必须单独强调,这也是新手最常卡住的地方:value 这个文件读的时候有「读位置」的概念。第一次读完,位置就跑到末尾了,再读一次会读不到东西。所以每次读之前要先把位置拨回开头,用lseek(fd, 0, SEEK_SET)。少了这一行,你的程序可能第一次读正常,第二次读出来是空的,然后你对着代码找半天逻辑错误——其实只是忘了回头。
4.3 完整示例:读一次输入电平
下面这段代码做的事和上一章 shell 里做的一模一样:导出 68 号引脚、设成输入、读一次电平、打印出来。你可以把它当成模板,改编号就能用在自己板子上。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <errno.h> /* 三个路径分别对应:导出入口、方向控制、电平读写 */ #define EXPORT_PATH "/sys/class/gpio/export" #define DIRECT_PATH "/sys/class/gpio/gpio68/direction" #define DEV_PATH "/sys/class/gpio/gpio68/value" #define GPIO_NUM "68" /* 对应 P2.4,换引脚时改这里 */ int main(void) { int fd_export, fd_dir, fd_dev; char value = 0; /* 1. 导出引脚:把编号写进 export 文件 */ fd_export = open(EXPORT_PATH, O_WRONLY); if (fd_export < 0) { perror(EXPORT_PATH); return -1; } write(fd_export, GPIO_NUM, strlen(GPIO_NUM)); close(fd_export); /* 2. 设置方向为输入,注意写入长度不要带结尾的 '\0' */ fd_dir = open(DIRECT_PATH, O_RDWR); if (fd_dir < 0) { perror(DIRECT_PATH); return -1; } write(fd_dir, "in", 2); /* 3. 打开电平文件并读取一次 */ fd_dev = open(DEV_PATH, O_RDONLY); if (fd_dev < 0) { perror(DEV_PATH); close(fd_dir); return -1; } lseek(fd_dev, 0, SEEK_SET); /* 关键:每次读之前把位置拨回开头 */ if (read(fd_dev, &value, 1) < 0) { perror("read"); } printf("当前输入电平: %c\n", value); close(fd_dev); close(fd_dir); return 0; }逐段说一下在干什么,以及哪些地方容易改错:
EXPORT_PATH这一段负责导出。写完立刻 close 是必要的,因为文件描述符不是无限多的。注意strlen(GPIO_NUM)算出来是 2,正好是「68」两个字符,不带结尾的结束符——往 sysfs 文件里写字符串时,多写一个看不见的结束符有时会引发解析错误。
DIRECT_PATH这一段设方向。这里写的是"in", 2,也就是只写 in 两个字符。你可能见过有人写成sizeof("in"),那会写 3 个字节,把结尾符也带进去,某些内核版本上会直接报参数错误。这个坑很隐蔽,改的时候留意一下。
DEV_PATH这一段读电平。lseek那一行不是为了好看,是为了保证每次读都从头开始。如果以后你把它改成一个循环,反复读同一个文件,这一行就是能不能读到的分水岭。
想把输入改成输出,只需要把"in"改成"out",再把读的那段换成写"0"或"1"。逻辑不变,只是方向反了。
4.4 编译和在板子上跑起来
这段代码不能在电脑上直接跑,因为它操作的是开发板上的路径。要在电脑上编译、再把可执行文件放到板子上运行,中间需要一个交叉编译器。用的工具链名字跟你板子的架构有关,常见的样子是这样:
# 交叉编译,工具链前缀按自己板子的实际情况替换 arm-linux-gnueabihf-gcc gpio_test.c -o gpio_test # 确认生成了可执行文件 file gpio_testfile那一条是用来确认架构对不对的。如果输出的不是 ARM 而是 x86,说明你用成了系统自带的 gcc,编出来的东西放到板子上跑不起来,会提示格式错误。
把生成的 gpio_test 拷到板子上,用 root 权限运行:
# 在开发板上运行,需要 root 权限才能写 sysfs 文件 chmod +x gpio_test ./gpio_test运行之前先接线:把要测的引脚用杜邦线接到地(GND)或者电源(VCC),然后再运行,这样读出来的值才有意义。接 GND 应该读到 0,接 VCC 应该读到 1。如果两个都读到同一个值,先别改代码,先检查线有没有插紧、插的是不是你以为的那个脚。
5. 新手避坑指南:五个现象、原因和解决
5.1 导出就报错,说资源被占用
现象:echo 68 > export之后终端返回设备忙、参数无效之类的提示,ls里也没有出现 gpio68 目录。
原因:这个编号对应的引脚已经被系统里的某个功能占用了。最常见的情况是它被分给了串口、网卡、显示接口这些外设,驱动正在用;也可能之前的程序导出过但没还回去。还有一种可能是编号算错了,落在一个根本不存在的区间里。
解决:先换一个明确推荐使用的引脚试,验证自己的操作流程没问题;确认流程通了再回头查这个脚归谁管。如果确实被别的功能占着,通常需要改硬件配置把一个脚的功能切换回 GPIO,这一步对零基础来说先跳过,别在第一天就陷进去。
5.2 参数全对,引脚却毫无反应
现象:导出成功了,direction 也设成 out 了,value 也写了 1,但接的 LED 就是不亮,万用表量出来还是低电平。
原因:两个可能。一是这个引脚在硬件上被复用给了其他功能,软件层面设成 GPIO 并不等于物理上真的切过去了。二是 active_low 被设成了 1,导致你写 1 实际输出的是低电平。
解决:先cat active_low看一眼,是 1 就写 0 回去,再试一次。如果还是不行,回去翻硬件手册确认这个脚到底支不支持 GPIO、需不需要额外的功能切换。别怀疑自己的代码,八成是硬件配置层面的问题。
5.3 程序第二次读 value 就读不到了
现象:循环里反复读 value,第一次能读到 0 或 1,后面几次读出来是空字符串或者乱码。
原因:文件读位置没有复位。读完一次,位置停在末尾,再读自然读不到内容。这不是 GPIO 的特性,是所有文件读写都有的基本行为,只是新手平时不直接操作文件描述符,所以没遇到过。
解决:每次 read 之前加一句lseek(fd, 0, SEEK_SET)。加完之后你会发现循环读就正常了。这个坑不解决,你会以为引脚状态在乱跳,其实是自己的读取方式有问题。
5.4 程序退出后引脚状态变了,下次开机又全没了
现象:程序跑完,引脚保持高电平,但过一阵子自己变回去了;重启之后所有导出过的引脚都消失,得重新导一遍。
原因:sysfs 里的这些状态不是持久化配置。程序退出、系统重启、别的程序重新初始化,都可能把引脚改回去。还有一种情况是程序异常退出,没走到 close,文件描述符泄漏,引脚就一直被占着。
解决:把「用完 unexport」当成固定动作写进流程里,程序里可以用信号处理保证退出时清理。真的要长期保持某个状态,那就不是 sysfs 该管的事了,得在系统启动脚本里重新初始化一次,或者换成更正式的方案。
5.5 随手用了手册外的引脚,把外设搞挂了
现象:为了图省事,随便挑了个看起来空闲的引脚导出,结果板子上的某个功能不能用了,比如串口没输出、屏幕不亮。
原因:那颗引脚本来承担着别的职责,被强行切成 GPIO 之后,对应的外设失去了引脚,自然就罢工了。麻烦的是这种故障表现得很间接——你只是动了引脚,坏掉的却是完全不相干的功能。
解决:只用手册或板子丝印上明确标注可用的引脚,这条没有商量余地。每次动一个新脚之前,先花两分钟查清楚它原本是干什么的。真把外设搞挂了,先 unexport 再重启,多数情况能恢复;恢复不了就说明配置被改动了,需要另想办法。
6. 下一步学什么:从点亮一个脚到做出一个小作品
能导出、能设方向、能读写电平,入门阶段的核心动作其实已经齐了。接下来最怕的是停在原地反复试同一个引脚,试到腻了就放弃。比较稳的走法是把学的东西往一个小目标上凑,让每一步都有验收标准。
| 阶段 | 练什么 | 怎么算过关 |
|---|---|---|
| 第一周 | 熟练 shell 操作,换三四个不同编号的引脚各做一遍输入和输出 | 不看笔记能一次敲对,出错时知道先 cat 哪个文件 |
| 第二周 | 把 C 代码里的导出、设方向、读写分别封装成函数 | 主函数只剩几行调用,改引脚只改一个宏定义 |
| 第三周 | 做一个按键控制 LED 的小循环:按键按下灯亮,松开灯灭 | 程序能连续跑十分钟不崩,不占着引脚不放 |
| 第四周 | 试试 edge 文件,用中断代替死循环轮询 | 能说清轮询和中断在响应速度和 CPU 占用上的差别 |
第三周那个小项目看着简单,但它会逼你把前面所有零碎知识串起来:输入引脚要设 in、输出引脚要设 out、循环里要反复读、读之前要 lseek、程序退出要清理。做通一遍,你对这套接口的理解就从「照着敲」变成「知道为什么这么写」。
再往后有两个方向可以选。一个是往上层走,去了解更现代的 GPIO 字符设备接口,它比 sysfs 更规范,也是新项目的主流做法,但概念上和现在学的这套是一脉相承的,你现在打的基础不会浪费。另一个是往下层走,去了解设备树和驱动,搞清楚一个引脚是怎么被分配给某个功能的——这条路陡一些,但走通了,前面那些「为什么这个脚不能用」的疑问就全都有答案了。
我自己刚学的时候,最常犯的错是「跳过验证直接往下写」。命令还没跑通就开始写 C 程序,程序跑不通又回头怀疑命令,来回折腾。后来定了个规矩:shell 里没验证过的操作,绝不往代码里搬;代码里改一行,就重启一次程序看结果。这个习惯让我少走了很多弯路,也希望帮到你。
本文还有配套的精品资源,点击获取