02-GPIO与LED应用编程.md 32 KB


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

pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十五章 点亮LED、第十六章 GPIO应用编程"

GPIO与LED应用编程

💡 关联知识:[[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 控制。


1. 应用层操控硬件的两种方式

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

2. sysfs 文件系统与 /sys 目录

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 电源选项,可控制系统电源状态

读属性文件 = 读设备属性;写属性文件 = 设置属性、控制设备。


3. LED 子系统

3.1 sys-led 设备目录

进入 /sys/class/leds,可以看到 sys-led 文件夹,这就是底板上的用户 LED。进入其中,主要关注三个属性文件:

属性文件 读写 含义
brightness 可读可写 设置/获取亮度等级。0 表示灭,正整数表示亮,值越大越亮(PWM 控制的 LED 有亮度等级;GPIO 控制的只有亮/灭两态)
max_brightness 只读 最大亮度等级
trigger 可读可写 触发模式,读为当前模式,写为设置模式

注意:对本文这块 GPIO 控制的 LED,只有亮和灭两种状态,不存在连续亮度等级。

3.2 trigger 触发模式

cat trigger 会列出所有支持的触发模式,方括号 [...] 括起来的是当前模式:

模式 说明
none 无触发,LED 由 brightness 手动控制
mmc0 对 mmc0 设备读写时 LED 闪烁
timer 由定时器控制,有规律地一明一灭
heartbeat 心跳呼吸模式,模仿心跳亮灭变化

系统启动后通常把某颗 LED 设为 heartbeat 作为系统运行指示灯。

3.3 echo 命令控制

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 自动心跳闪烁]

4. 完整例程:LED 应用程序(led.c)

例程路径:开发板光盘 → 11、Linux C 应用编程例程源码15_ledled.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);
}

逐段解释:

  • 两个宏分别指向 triggerbrightness 属性文件;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 可执行文件

5. GPIO sysfs 接口

5.1 /sys/class/gpio 目录概览

进入 /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 文件夹消失

5.2 gpiochipX 与引脚编号

每个 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 = 112GPIO3_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]

5.3 gpioX 属性文件

导出成功后生成的 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() 监听引脚电平变化。


6. 完整例程:GPIO 输出(gpio_out.c)

例程路径:11、Linux C 应用编程例程源码16_gpiogpio_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 导出。
  • 然后依次配置为输出模式、极性为 0,最后写 value 输出目标电平。
  • 用法示例:./testApp 1 1 让 GPIO1_IO01 输出高,./testApp 1 0 输出低。

7. 完整例程:GPIO 输入(gpio_in.c)

例程路径:16_gpiogpio_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 当前电平。

8. 完整例程:GPIO 中断(gpio_intr.c)

例程路径:16_gpiogpio_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/OO_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中断触发"

9. 设备节点 /dev/led 与 ioctl 控制

⚠️ 来源说明:本节不属于《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


10. 实验步骤与调试方法

10.1 交叉编译

# 方式一:直接使用交叉编译器
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

10.2 拷贝到开发板

scp testApp_led testApp_out testApp_in testApp_intr root@<开发板IP>:/home/root/

进入开发板 Linux 系统,在 ~/ 下执行。

10.3 运行与验证

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 退出。

10.4 常见问题排查

现象 原因/处理
echo N > export 报错 该 GPIO 已被内核/驱动占用;换未被占用的引脚
输出模式下 cat value 无意义 输出模式不能用 cat value 读电平,需外接 LED 或万用表
brightness 后 LED 仍闪烁 没有先把 trigger 设为 none,触发模式覆盖了亮度控制
中断只触发一次 lseek 回文件头或未先 read 清状态
open /dev/led 失败 内核未注册对应 misc 驱动,没有创建设备节点

11. 跨平台对比(IMX6ULL vs STM32 vs RK3568)

维度 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/*/brightnesstrigger 普通 GPIO,无 LED 子系统(自行实现) LED 框架,同 IMX6ULL
中断 配置 edgepoll(POLLPRI) 或异步 I/O EXTI 外部中断 + 中断服务函数 同 IMX6ULL,内核中断子系统
性能 低配 Linux,适合工业 HMI 实时性强、成本低,适合实时控制节点 高性能 Linux,适合边缘计算/视频
开发门槛 需懂 sysfs/驱动模型 需懂寄存器/外设 需懂 sysfs/设备树/pinctrl

结论:STM32 是"直接操作硬件",Cortex-A 平台是"通过内核子系统和文件接口间接操作硬件"。同一份 sysfs GPIO 代码在 IMX6ULL 与 RK3568 上基本通用,差别只在编号映射与设备树配置。


12. 深度面试题

Q1:应用层操控硬件,/dev 设备节点和 /sys 属性文件有什么区别?什么时候用哪种?

/dev 下的设备节点由驱动注册,提供 open/read/write/ioctl 等接口,接口往往由驱动自定义,能力丰富(如 LCD、摄像头这种复杂设备需要大量 ioctl 命令和缓冲区);/sys 是 sysfs 虚拟文件系统,内核把设备对象导出成目录、把设备属性导出成文件,应用层用普通文件 I/O 读写属性即可。

选择取决于设备和驱动的实现:结构简单的设备(LED、GPIO、部分 PWM)常用 sysfs,驱动把属性直接导出,接口标准化;复杂设备(LCD、触摸屏、摄像头、音频)通常用 /dev 节点加 ioctl。标准设备驱动框架(如 LED 框架)会为应用层提供统一接口,降低使用和移植成本;非框架的杂项设备接口就是定制的。

Q2:GPIO4_IO16 在 sysfs 中的编号是多少?为什么有时 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 导出。解决办法是确认引脚复用情况、换一个空闲引脚,或修改设备树释放该引脚。

Q3:gpio_intr.c 里为什么用 POLLPRI 而不是 POLLIN?为什么在 poll 前要先 read 一次?

POLLIN 表示普通优先级数据可读,POLLPRI 表示有高优先级数据可读。GPIO 中断在 sysfs 中被建模成"高优先级事件",触发时 value 文件上产生的是 POLLPRI 就绪,因此要监视 POLLPRI;用 POLLIN 无法正确捕捉。

poll 前先 read(pfd.fd, &val, 1) 是为了清除上一次残留的就绪状态。文件描述符可能带着历史状态,若不先读掉,第一次 poll 可能立即返回一个"假中断"。读一次后,后续 poll 返回才代表真正发生了新的边沿跳变。

Q4:LED 的 triggerbrightness 是什么关系?为什么控制亮灭前常要先把 trigger 写成 none

brightness 直接控制 LED 的亮/灭(或亮度等级),但只在该 LED 处于手动模式时有效;trigger 是自动触发模式,启用后由内核根据事件(定时器、mmc 读写、心跳)自动改写 LED 状态,会覆盖应用层对 brightness 的设置。

所以想让 write(brightness) 稳定生效,必须先把 trigger 写成 none 关闭自动触发,再写 brightness。否则 LED 会继续按 heartbeat/timer 的节奏闪烁,看起来"命令没生效"。反过来,若想要呼吸灯或系统状态指示,就直接设置 triggerheartbeat

Q5:应用层直接操作 sysfs GPIO 和在内核驱动里操作 GPIO,各有什么优劣?

应用层 sysfs:无需写/编译内核驱动,开发和调试快,改动灵活(脚本即可控制);但每次读写都要经过文件系统层、用户态/内核态切换,性能与实时性差,不适合高频翻转(软件 PWM、高速时序);且引脚被占用或未导出时无法操作,权限/并发控制也弱。

内核驱动:直接操作 pinctrl/gpio 子系统,能配置复用功能、上下拉、驱动能力、中断,速度快、时序可控,可提供稳定标准接口并做资源管理;但需要懂驱动模型、设备树,开发与调试成本高,改动要重编内核/模块。

工程取舍:调试、低频控制用 sysfs 快速验证;产品化、高频或需要精确时序(PWM、编码器、高速中断)时写成内核驱动或在驱动里实现。


内容来源:《I.MX6U嵌入式Linux C应用编程指南》第十五章 点亮LED、第十六章 GPIO应用编程 + 例程路径:开发板光盘 11、Linux C 应用编程例程源码\15_led16_gpio