title: GPIO与LED应用编程 tags: [
嵌入式Linux,
Linux应用编程,
sysfs,
GPIO,
LED,
LED子系统,
trigger,
brightness,
export,
direction,
edge,
poll,
POLLPRI,
ioctl,
IMX6ULL,
] created: 2026-09-18 updated: 2026-09-18
💡 关联知识:[[03-外设与高级IO编程/01-高级IO]];延伸阅读:[[嵌入式Linux驱动开发实战/03-Linux驱动开发核心/03-pinctrl与gpio子系统]]
LED 是每块开发板都绕不开的第一个外设,而它背后就是 GPIO。本篇讲清应用层如何不写驱动就控制 GPIO/LED:先建立"设备节点 vs sysfs 属性文件"两种操控方式的认知,再分别用 LED 子系统(/sys/class/leds)和 GPIO sysfs(/sys/class/gpio)完成输出、输入、中断三个完整程序。最后补充 /dev/led + ioctl 这类自定义字符设备接口,以及完整的交叉编译与板端测试步骤。
ALPHA/Mini I.MX6U 开发板上共有 3 颗 LED,只有 1 颗能被用户控制(其余为电源指示灯)。用户 LED 由 GPIO 控制,出厂系统用 Linux 标准 LED 驱动框架注册,没有 /dev 设备节点,只能通过 sysfs 控制。
Linux 下"一切皆文件",应用层操控硬件同样通过文件 I/O 实现,但有两条路:
| 方式 | 位置 | 特点 | 典型设备 |
|---|---|---|---|
| 设备节点 | /dev/ 下 |
驱动自定义接口,常配合 ioctl;接口未必标准化 |
LCD、触摸屏、摄像头、串口 |
| sysfs 属性文件 | /sys/ 下 |
内核把设备属性导出成文件,读写文件即读写属性;简单设备常用 | LED、GPIO、PWM |
/dev 下的文件称为设备节点,是硬件设备向应用层提供的接口。而 sysfs 则是另一条途径:把内核对象(硬件设备)导出成目录、对象属性导出成文件、对象关系导出成链接文件。
标准接口与非标准接口:Linux 为常见设备类(LED、输入、FrameBuffer、video、PWM 等)设计了设备驱动框架,框架为应用层提供统一接口,同时降低驱动开发难度。使用了 LED 框架注册的 LED 就是"标准设备";不用框架自己写,接口就是非标准的。杂散/杂项类设备(misc device)无法归入任何标准分类,接口通常非标准,只有驱动工程师知道怎么用——很多嵌入式外设驱动都是定制的。
flowchart TD
A[应用程序] --> B{操控方式}
B -->|设备节点 /dev/xxx| C[驱动自定义接口]
C --> C1[ioctl / read / write]
B -->|sysfs 属性文件 /sys/...| D[内核导出属性]
D --> D1[echo / cat]
D --> D2[应用层 write / read]
C1 --> E[硬件 GPIO/LED]
D2 --> E
sysfs 是基于内存的虚拟文件系统,与 proc 类似,作用是把内核信息以文件形式提供给应用层。它把系统上的设备和总线组织成分级文件,展示设备驱动模型中各组件的层次关系。挂载点是 /sys。
| 内核要素 | sysfs 中的表现 |
|---|---|
| 内核对象(如硬件设备) | 目录 |
| 对象属性(如设备属性) | 文件 |
| 对象关系 | 链接文件 |
/sys 下的主要子目录:
| 子目录 | 说明 |
|---|---|
/sys/devices |
系统中所有设备存放处,是 sysfs 最重要的目录结构 |
/sys/block |
块设备目录(过时接口,多链接到 /sys/devices) |
/sys/bus |
按总线类型分类(如 I2C 设备挂在 I2C 总线下),多为链接 |
/sys/class |
按功能分类(如 /sys/class/leds、/sys/class/input),多为链接 |
/sys/dev |
按主设备号:次设备号命名,链接到 /sys/devices |
/sys/firmware |
内核固件描述 |
/sys/fs |
系统所有文件系统及挂载点 |
/sys/kernel |
内核可调参数 |
/sys/module |
所有模块信息 |
/sys/power |
电源选项,可控制系统电源状态 |
读属性文件 = 读设备属性;写属性文件 = 设置属性、控制设备。
进入 /sys/class/leds,可以看到 sys-led 文件夹,这就是底板上的用户 LED。进入其中,主要关注三个属性文件:
| 属性文件 | 读写 | 含义 |
|---|---|---|
brightness |
可读可写 | 设置/获取亮度等级。0 表示灭,正整数表示亮,值越大越亮(PWM 控制的 LED 有亮度等级;GPIO 控制的只有亮/灭两态) |
max_brightness |
只读 | 最大亮度等级 |
trigger |
可读可写 | 触发模式,读为当前模式,写为设置模式 |
注意:对本文这块 GPIO 控制的 LED,只有亮和灭两种状态,不存在连续亮度等级。
cat trigger 会列出所有支持的触发模式,方括号 [...] 括起来的是当前模式:
| 模式 | 说明 |
|---|---|
none |
无触发,LED 由 brightness 手动控制 |
mmc0 |
对 mmc0 设备读写时 LED 闪烁 |
timer |
由定时器控制,有规律地一明一灭 |
heartbeat |
心跳呼吸模式,模仿心跳亮灭变化 |
系统启动后通常把某颗 LED 设为 heartbeat 作为系统运行指示灯。
echo timer > trigger # 将 LED 触发模式设置为 timer
echo none > trigger # 将 LED 触发模式设置为 none
echo 1 > brightness # 点亮 LED
echo 0 > brightness # 熄灭 LED
Tips:
cat读取和echo写入属性文件的都是字符串。应用层用write()写属性文件时同样要写字符串;read()读到的也是 ASCII 字符串。
flowchart TD
A[写 trigger=none] --> B[关闭自动触发]
B --> C[写 brightness=1]
C --> D[LED 常亮]
B --> E[写 brightness=0]
E --> F[LED 熄灭]
G[写 trigger=heartbeat] --> H[LED 自动心跳闪烁]
例程路径:开发板光盘 → 11、Linux C 应用编程例程源码 → 15_led → led.c。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define LED_TRIGGER "/sys/class/leds/sys-led/trigger"
#define LED_BRIGHTNESS "/sys/class/leds/sys-led/brightness"
#define USAGE() fprintf(stderr, "usage:\n" \
" %s <on|off>\n" \
" %s <trigger> <type>\n", argv[0], argv[0])
int main(int argc, char *argv[])
{
int fd1, fd2;
/* 校验传参 */
if (2 > argc) {
USAGE();
exit(-1);
}
/* 打开文件 */
fd1 = open(LED_TRIGGER, O_RDWR);
if (0 > fd1) {
perror("open error");
exit(-1);
}
fd2 = open(LED_BRIGHTNESS, O_RDWR);
if (0 > fd2) {
perror("open error");
exit(-1);
}
/* 根据传参控制LED */
if (!strcmp(argv[1], "on")) {
write(fd1, "none", 4); //先将触发模式设置为none
write(fd2, "1", 1); //点亮LED
}
else if (!strcmp(argv[1], "off")) {
write(fd1, "none", 4); //先将触发模式设置为none
write(fd2, "0", 1); //LED灭
}
else if (!strcmp(argv[1], "trigger")) {
if (3 != argc) {
USAGE();
exit(-1);
}
if (0 > write(fd1, argv[2], strlen(argv[2])))
perror("write error");
}
else
USAGE();
exit(0);
}
逐段解释:
trigger 和 brightness 属性文件;USAGE() 用于打印用法。open 两个属性文件,拿到两个 fd(O_RDWR 读写)。on:先把 "none" 写入 trigger 关闭自动触发(否则 heartbeat 等模式会覆盖 brightness 的效果),再写 "1" 点亮。off:同样先设 none,再写 "0" 熄灭。trigger:把第二个参数(如 heartbeat)写入 trigger,长度用 strlen(argv[2]),不能写死。交叉编译:
arm-linux-gnueabihf-gcc led.c -o testApp
file testApp # 应为 32 位 ARM 可执行文件
进入 /sys/class/gpio,可以看到两个文件和 5 个 gpiochipX 文件夹:
| 条目 | 说明 |
|---|---|
gpiochip0/32/64/96/128 |
I.MX6UL/I.MX6ULL 的 5 个 GPIO 控制器,分别对应 GPIO1~GPIO5 |
export |
只写。把指定编号的 GPIO 引脚导出,导出的引脚才能使用;写入后在 /sys/class/gpio 下生成 gpioX 文件夹 |
unexport |
只写。删除已导出的 GPIO 引脚,写入后对应 gpioX 文件夹消失 |
每个 gpiochipX 目录下关注三个只读属性文件:
| 属性文件 | 说明 |
|---|---|
base |
该控制器管理的引脚组中最小的编号,与 gpiochipX 中的 X 相同 |
label |
该组 GPIO 的名字 |
ngpio |
该控制器管理的引脚数量(编号范围 base ~ base+ngpio-1) |
I.MX6ULL 的映射关系:
| 控制器 | gpiochip | base |
|---|---|---|
| GPIO1 | gpiochip0 | 0 |
| GPIO2 | gpiochip32 | 32 |
| GPIO3 | gpiochip64 | 64 |
| GPIO4 | gpiochip96 | 96 |
| GPIO5 | gpiochip128 | 128 |
编号计算公式:sysfs 编号 = 该组 base + IO 序号。
例:GPIO4_IO16 → GPIO4 对应 gpiochip96,96 + 16 = 112;GPIO3_IO20 → 64 + 20 = 84。
导出/删除:
echo 0 > export # 导出编号 0 的 GPIO(GPIO1_IO0)
echo 0 > unexport # 删除已导出的编号 0 的 GPIO
注意:并不是所有 GPIO 都能成功导出。如果该引脚已被内核(某个驱动)使用,导出会失败并打印错误信息,需要换一个未被占用的引脚。
只有在输入模式下,才能用
cat value获取引脚输入电平;输出模式下cat value得不到电平,需要用万用表或外接 LED 检测。
flowchart LR
A[GPIO 编号] --> B{查 gpiochip base}
B --> C[base + IO序号 = sysfs编号]
C --> D[写入 /sys/class/gpio/export]
D --> E[生成 /sys/class/gpio/gpioX/]
E --> F[direction / value / active_low / edge]
导出成功后生成的 gpioX 目录下,重点关注四个属性文件:
| 属性文件 | 读写 | 取值 | 说明 |
|---|---|---|---|
direction |
可读可写 | "out" / "in" |
配置为输出或输入模式 |
value |
可读可写 | "0" / "1" |
输出模式:写 0 输出低、写 1 输出高;输入模式:读获取当前电平 |
active_low |
可读可写 | "0" / "1" |
极性控制,默认 0 |
edge |
可读可写 | "none"/"rising"/"falling"/"both" |
中断触发方式(配置前需先设输入模式) |
active_low 的极性作用:
# active_low 等于 0 时
echo "0" > active_low
echo "out" > direction
echo "1" > value # 输出高
echo "0" > value # 输出低
# active_low 等于 1 时
echo "1" > active_low
echo "out" > direction
echo "1" > value # 输出低
echo "0" > value # 输出高
edge 取值 |
触发方式 |
|---|---|
none |
非中断引脚 |
rising |
上升沿触发 |
falling |
下降沿触发 |
both |
边沿触发(上升沿和下降沿) |
配置中断后可用 poll() 监听引脚电平变化。
例程路径:11、Linux C 应用编程例程源码 → 16_gpio → gpio_out.c。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
static 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, char *argv[])
{
/* 校验传参 */
if (3 != argc) {
fprintf(stderr, "usage: %s <gpio> <value>\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);
/* 极性设置 */
if (gpio_config("active_low", "0"))
exit(-1);
/* 控制GPIO输出高低电平 */
if (gpio_config("value", argv[2]))
exit(-1);
/* 退出程序 */
exit(0);
}
逐段解释:
gpio_path 保存当前 GPIO 的目录路径 /sys/class/gpio/gpioX。gpio_config(attr, val) 是通用封装:拼出 gpio_path/attr,以只写打开,把字符串 val 写入,检查写入长度是否等于 strlen(val),最后关闭。这样 direction/active_low/value 都能复用。main 需要两个参数:argv[1] 是 GPIO 编号,argv[2] 是输出电平。access(gpio_path, F_OK) 判断是否已导出,目录不存在才写 export 导出。value 输出目标电平。./testApp 1 1 让 GPIO1_IO01 输出高,./testApp 1 0 输出低。例程路径:16_gpio → gpio_in.c。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
static 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, char *argv[])
{
char file_path[100];
char val;
int fd;
/* 校验传参 */
if (2 != argc) {
fprintf(stderr, "usage: %s <gpio>\n", argv[0]);
exit(-1);
}
/* 判断指定编号的GPIO是否导出 */
sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]);
if (access(gpio_path, F_OK)) {//如果目录不存在 则需要导出
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", "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);
/* 退出程序 */
close(fd);
exit(0);
}
逐段解释:
argv[1] 是 GPIO 编号。export。direction = "in"、active_low = "0"、edge = "none"(非中断)。value 文件(只读),read 一个字节,printf("value: %c\n", val) 打印 '0' 或 '1'——读到的是 ASCII 字符,直接当字符打印。./testApp 1 读取 GPIO1_IO01 当前电平。例程路径:16_gpio → gpio_intr.c。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <poll.h>
static 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");
return -1;
}
close(fd); //关闭文件
return 0;
}
int main(int argc, char *argv[])
{
struct pollfd pfd;
char file_path[100];
int ret;
char val;
/* 校验传参 */
if (2 != argc) {
fprintf(stderr, "usage: %s <gpio>\n", argv[0]);
exit(-1);
}
/* 判断指定编号的GPIO是否导出 */
sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]);
if (access(gpio_path, F_OK)) {//如果目录不存在 则需要导出
int len;
int fd;
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");
exit(-1);
}
close(fd); //关闭文件
}
/* 配置为输入模式 */
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); //调用poll
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);
}
}
/* 退出程序 */
exit(0);
}
逐段解释与关键点:
edge = "both",即上升沿和下降沿都触发中断。poll 而不是直接读? 因为直接 read 只有在中断发生(value 文件高优先级可读)时才有意义,且无法一次性等待多个事件源。poll 可以监视文件描述符上的 I/O 状态变化,POLLPRI 正表示"有高优先级数据可读取",中断就是一种高优先级事件。GPIO 中断场景要用 POLLPRI,不是 POLLIN。read 一次? 打开的 value 文件可能带有上一次残留的状态,先读一次清除,之后 poll 才代表真正的新中断。poll(&pfd, 1, -1) 中 -1 表示一直阻塞等待,直到有中断触发或捕获信号。lseek(pfd.fd, 0, SEEK_SET) 把读位置移回文件头,再 read 取出当前 value,然后打印。Ctrl+C。中断也完全可以用第 3 节的异步 I/O(O_ASYNC + SIGIO/实时信号)来监视,原理相同。
sequenceDiagram
participant App as 应用程序
participant Kernel as 内核/GPIO驱动
App->>Kernel: 配置 edge=both
App->>Kernel: poll(value, POLLPRI, -1) 阻塞
Kernel-->>App: 引脚电平跳变(高低切换)
Kernel->>App: POLLPRI 就绪
App->>Kernel: lseek + read value
Kernel-->>App: 返回 '0' 或 '1'
App->>App: 打印"GPIO中断触发"
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。原书第 15 章明确说明开发板出厂系统的用户 LED 由 LED 驱动框架注册、没有
/dev设备节点,使用 sysfs 方式控制。以下是另一类常见做法——用杂项字符设备提供/dev/led节点,并用ioctl控制,属于自定义非标准接口。
当 LED 驱动不使用 LED 框架、而是注册为杂项字符设备(misc device)时,应用层就通过 /dev/led 设备节点操控,接口形式通常是 open + ioctl(也可用 write)。
内核侧提供的能力(示意):
#include <linux/ioctl.h>
#define LED_IOCTL_MAGIC 'L'
#define LED_ON _IO(LED_IOCTL_MAGIC, 0)
#define LED_OFF _IO(LED_IOCTL_MAGIC, 1)
#define LED_SET_BLINK _IOW(LED_IOCTL_MAGIC, 2, int) //参数为闪烁周期ms
| 宏 | 方向 | 用途 |
|---|---|---|
_IO(type, nr) |
无参数 | 纯命令,如开/关 |
_IOW(type, nr, type) |
用户→内核 | 下发一个整型等参数 |
_IOR(type, nr, type) |
内核→用户 | 读取内核返回的数据 |
_IOWR(type, nr, type) |
双向 | 既下发又回读 |
应用侧完整例程:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#define LED_DEV_PATH "/dev/led"
#define LED_IOCTL_MAGIC 'L'
#define LED_ON _IO(LED_IOCTL_MAGIC, 0)
#define LED_OFF _IO(LED_IOCTL_MAGIC, 1)
#define LED_SET_BLINK _IOW(LED_IOCTL_MAGIC, 2, int)
int main(int argc, char *argv[])
{
int fd;
if (2 != argc) {
fprintf(stderr, "usage: %s <on|off|blink>\n", argv[0]);
exit(-1);
}
/* 打开 LED 设备节点 */
fd = open(LED_DEV_PATH, O_RDWR);
if (0 > fd) {
perror("open error");
exit(-1);
}
if (!strcmp(argv[1], "on")) {
if (0 > ioctl(fd, LED_ON))
perror("ioctl LED_ON error");
}
else if (!strcmp(argv[1], "off")) {
if (0 > ioctl(fd, LED_OFF))
perror("ioctl LED_OFF error");
}
else if (!strcmp(argv[1], "blink")) {
int period = 500; //闪烁周期 500ms
if (0 > ioctl(fd, LED_SET_BLINK, &period))
perror("ioctl LED_SET_BLINK error");
}
else {
fprintf(stderr, "usage: %s <on|off|blink>\n", argv[0]);
}
close(fd);
return 0;
}
ioctl 原型与要点:
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ... /* arg */);
| 项 | 说明 |
|---|---|
fd |
设备节点打开得到的文件描述符 |
request |
命令码,由 _IO/_IOW/_IOR/_IOWR 生成,包含方向、类型、序号、数据大小 |
arg |
可选参数,通常是整型或无类型指针;_IOW 表示的参数按指针传递 |
| 返回值 | 通常成功返回 0,失败返回 -1 并设置 errno |
设备节点出现的前提是内核驱动已注册并创建了该节点(或由 mdev/udev 根据设备号创建)。没有对应驱动时,open("/dev/led") 会报 No such file or directory。
# 方式一:直接使用交叉编译器
arm-linux-gnueabihf-gcc led.c -o testApp_led
arm-linux-gnueabihf-gcc gpio_out.c -o testApp_out
arm-linux-gnueabihf-gcc gpio_in.c -o testApp_in
arm-linux-gnueabihf-gcc gpio_intr.c -o testApp_intr
# 方式二:正点原子光盘工具链(先 source 环境脚本,再使用 $CC)
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
$CC led.c -o testApp_led
# 查看文件类型,确认是 ARM 可执行文件
file testApp_led
scp testApp_led testApp_out testApp_in testApp_intr root@<开发板IP>:/home/root/
进入开发板 Linux 系统,在 ~/ 下执行。
LED(15_led):
./testApp_led on # 点亮 LED
./testApp_led off # 熄灭 LED
./testApp_led trigger heartbeat # 设置为心跳模式
./testApp_led trigger timer # 设置为定时器闪烁
观察板上用户 LED 状态是否与命令一致。
GPIO 输出(16_gpio,以 GPIO1_IO01 为例,sysfs 编号 = 0 + 1 = 1):
./testApp_out 1 1 # 控制 GPIO1_IO01 输出高电平
./testApp_out 1 0 # 控制 GPIO1_IO01 输出低电平
用万用表测量或用 LED 小灯(串限流电阻)连接引脚验证。
GPIO 输入:
# 用杜邦线把 GPIO1_IO01 接 3.3V,读取
./testApp_in 1 # 期望打印 value: 1(高电平)
# 再改接到 GND,读取
./testApp_in 1 # 期望打印 value: 0(低电平)
GPIO 中断:
./testApp_intr 1 # 监测 GPIO1_IO01 中断
运行后用杜邦线在 GND 与 3.3V 之间来回切换,使引脚电平发生由高到低或由低到高的变化,终端会打印 GPIO中断触发<value=x>。测试完毕按 Ctrl+C 退出。
| 现象 | 原因/处理 |
|---|---|
echo N > export 报错 |
该 GPIO 已被内核/驱动占用;换未被占用的引脚 |
输出模式下 cat value 无意义 |
输出模式不能用 cat value 读电平,需外接 LED 或万用表 |
写 brightness 后 LED 仍闪烁 |
没有先把 trigger 设为 none,触发模式覆盖了亮度控制 |
| 中断只触发一次 | 未 lseek 回文件头或未先 read 清状态 |
open /dev/led 失败 |
内核未注册对应 misc 驱动,没有创建设备节点 |
| 维度 | IMX6ULL(Cortex-A7 + Linux) | STM32(Cortex-M) | RK3568(Cortex-A55 + Linux) |
|---|---|---|---|
| GPIO 控制方式 | 应用层 sysfs(/sys/class/gpio、/sys/class/leds)或 /dev 节点 |
直接读写寄存器 / HAL 库(HAL_GPIO_WritePin) |
同 IMX6ULL,也常用 pinctrl/gpio 子系统 |
| 抽象层次 | 内核 pinctrl/gpio 子系统 + 设备驱动框架 | 寄存器/厂商库,无统一框架 | 内核 pinctrl/gpio 子系统 |
| LED 控制 | LED 框架导出 /sys/class/leds/*/brightness、trigger |
普通 GPIO,无 LED 子系统(自行实现) | LED 框架,同 IMX6ULL |
| 中断 | 配置 edge 后 poll(POLLPRI) 或异步 I/O |
EXTI 外部中断 + 中断服务函数 | 同 IMX6ULL,内核中断子系统 |
| 性能 | 低配 Linux,适合工业 HMI | 实时性强、成本低,适合实时控制节点 | 高性能 Linux,适合边缘计算/视频 |
| 开发门槛 | 需懂 sysfs/驱动模型 | 需懂寄存器/外设 | 需懂 sysfs/设备树/pinctrl |
结论:STM32 是"直接操作硬件",Cortex-A 平台是"通过内核子系统和文件接口间接操作硬件"。同一份 sysfs GPIO 代码在 IMX6ULL 与 RK3568 上基本通用,差别只在编号映射与设备树配置。
/dev 设备节点和 /sys 属性文件有什么区别?什么时候用哪种?答:/dev 下的设备节点由驱动注册,提供 open/read/write/ioctl 等接口,接口往往由驱动自定义,能力丰富(如 LCD、摄像头这种复杂设备需要大量 ioctl 命令和缓冲区);/sys 是 sysfs 虚拟文件系统,内核把设备对象导出成目录、把设备属性导出成文件,应用层用普通文件 I/O 读写属性即可。
选择取决于设备和驱动的实现:结构简单的设备(LED、GPIO、部分 PWM)常用 sysfs,驱动把属性直接导出,接口标准化;复杂设备(LCD、触摸屏、摄像头、音频)通常用 /dev 节点加 ioctl。标准设备驱动框架(如 LED 框架)会为应用层提供统一接口,降低使用和移植成本;非框架的杂项设备接口就是定制的。
export 会失败?答:I.MX6ULL 有 5 个 GPIO 控制器,分别对应 gpiochip0/32/64/96/128(base 为 0/32/64/96/128)。GPIO4 对应 base=96,所以编号 = 96 + 16 = 112。
export 失败通常是因为该 GPIO 引脚已经被内核中的某个驱动占用(被其它设备驱动申请了)。一个 GPIO 一旦被内核使用,就不能再通过 sysfs 导出。解决办法是确认引脚复用情况、换一个空闲引脚,或修改设备树释放该引脚。
gpio_intr.c 里为什么用 POLLPRI 而不是 POLLIN?为什么在 poll 前要先 read 一次?答:POLLIN 表示普通优先级数据可读,POLLPRI 表示有高优先级数据可读。GPIO 中断在 sysfs 中被建模成"高优先级事件",触发时 value 文件上产生的是 POLLPRI 就绪,因此要监视 POLLPRI;用 POLLIN 无法正确捕捉。
poll 前先 read(pfd.fd, &val, 1) 是为了清除上一次残留的就绪状态。文件描述符可能带着历史状态,若不先读掉,第一次 poll 可能立即返回一个"假中断"。读一次后,后续 poll 返回才代表真正发生了新的边沿跳变。
trigger 和 brightness 是什么关系?为什么控制亮灭前常要先把 trigger 写成 none?答:brightness 直接控制 LED 的亮/灭(或亮度等级),但只在该 LED 处于手动模式时有效;trigger 是自动触发模式,启用后由内核根据事件(定时器、mmc 读写、心跳)自动改写 LED 状态,会覆盖应用层对 brightness 的设置。
所以想让 write(brightness) 稳定生效,必须先把 trigger 写成 none 关闭自动触发,再写 brightness。否则 LED 会继续按 heartbeat/timer 的节奏闪烁,看起来"命令没生效"。反过来,若想要呼吸灯或系统状态指示,就直接设置 trigger 为 heartbeat。
答:应用层 sysfs:无需写/编译内核驱动,开发和调试快,改动灵活(脚本即可控制);但每次读写都要经过文件系统层、用户态/内核态切换,性能与实时性差,不适合高频翻转(软件 PWM、高速时序);且引脚被占用或未导出时无法操作,权限/并发控制也弱。
内核驱动:直接操作 pinctrl/gpio 子系统,能配置复用功能、上下拉、驱动能力、中断,速度快、时序可控,可提供稳定标准接口并做资源管理;但需要懂驱动模型、设备树,开发与调试成本高,改动要重编内核/模块。
工程取舍:调试、低频控制用 sysfs 快速验证;产品化、高频或需要精确时序(PWM、编码器、高速中断)时写成内核驱动或在驱动里实现。
内容来源:《I.MX6U嵌入式Linux C应用编程指南》第十五章 点亮LED、第十六章 GPIO应用编程 + 例程路径:开发板光盘 11、Linux C 应用编程例程源码\15_led、16_gpio