第四章:TBOX核心硬件接口编程(GPIO)

好,咱们进入正题。这一章讲GPIO,说白了就是TBOX上那些管脚的输入输出控制。你想想看,一个TBOX装到车上,总得干点实事吧?比如控制个指示灯、检测个点火信号、驱动个继电器。这些活儿,全得靠GPIO来完成。

我个人习惯把GPIO比作TBOX的「手脚」。没有它,你的程序就是个只会算数的呆子,跟外界完全没法互动。所以这一章,咱们把Linux下的GPIO玩法彻底搞明白。

4.1 Linux GPIO子系统介绍

Linux内核里有个专门的GPIO子系统,负责管理所有的GPIO管脚。它提供了一套统一的接口,让上层应用不用关心底层硬件差异。

这套子系统分三层:

  • 硬件层:芯片厂商的驱动,直接操作寄存器
  • 核心层:内核的gpiolib,提供通用框架
  • 用户层:我们写应用时接触到的接口

我在项目中遇到过一个问题:同一套代码,换了个平台GPIO就不工作了。后来发现是不同芯片的GPIO编号映射不一样。嗯,这里要注意,GPIO编号不是固定的,得看你的设备树或者芯片手册。

核心概念:Linux把GPIO抽象成了整数编号。比如GPIO 88,对应芯片上的某个物理管脚。但这个编号怎么来的?要么写在设备树里,要么在内核代码里硬编码。

4.2 sysfs接口操作GPIO

这是最传统的方式,也是入门必学的。sysfs把GPIO暴露成了文件系统中的文件。你读写文件,就等于操作GPIO。

操作步骤很简单,就三步:

  1. 导出GPIO:向/sys/class/gpio/export写入编号
  2. 设置方向:写in或out到direction文件
  3. 读写电平:读或写value文件

来,看个实际例子。控制TBOX上的一个LED灯:

# 导出GPIO 88
echo 88 > /sys/class/gpio/export

# 设置为输出
echo out > /sys/class/gpio/gpio88/direction

# 点亮LED(高电平)
echo 1 > /sys/class/gpio/gpio88/value

# 熄灭LED(低电平)
echo 0 > /sys/class/gpio/gpio88/value

# 用完记得取消导出
echo 88 > /sys/class/gpio/unexport

是不是很简单?但这里有个坑。我曾经在项目里直接用shell脚本操作sysfs,结果发现频繁读写value文件时,偶尔会卡住。为什么?因为sysfs的读写是同步的,内核要处理中断和调度。你想想看,如果GPIO被其他进程占用了,你的操作就会阻塞。

避坑指南:我曾经在生产代码里用sysfs控制GPIO,结果发现高并发场景下性能跟不上。sysfs适合调试和简单控制,生产环境建议用libgpiod。

4.3 libgpiod库的使用

libgpiod是GPIO子系统的新一代用户空间接口。它比sysfs更高效,也更安全。说白了,就是内核开发者觉得sysfs太粗糙了,重新设计了一套API。

安装libgpiod很简单:

# 在Ubuntu/Debian上
sudo apt-get install libgpiod-dev gpiod

# 在Buildroot中
make libgpiod

常用的命令行工具:

  • gpiodetect:列出所有GPIO控制器
  • gpioinfo:查看GPIO详细信息
  • gpioget:读取GPIO值
  • gpioset:设置GPIO值

用C语言操作GPIO的示例:

#include <gpiod.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    struct gpiod_chip *chip;
    struct gpiod_line *line;
    int ret;

    // 打开GPIO芯片,通常是/dev/gpiochip0
    chip = gpiod_chip_open_by_name("gpiochip0");
    if (!chip) {
        perror("打开芯片失败");
        return -1;
    }

    // 获取GPIO 88号线
    line = gpiod_chip_get_line(chip, 88);
    if (!line) {
        perror("获取线失败");
        gpiod_chip_close(chip);
        return -1;
    }

    // 设置为输出,初始值低电平
    ret = gpiod_line_request_output(line, "led_control", 0);
    if (ret < 0) {
        perror("请求输出失败");
        gpiod_chip_close(chip);
        return -1;
    }

    // 点亮LED
    gpiod_line_set_value(line, 1);
    sleep(1);

    // 熄灭LED
    gpiod_line_set_value(line, 0);

    // 清理
    gpiod_line_release(line);
    gpiod_chip_close(chip);

    return 0;
}

编译命令:

gcc -o led_control led_control.c -lgpiod

个人经验:我建议你在新项目里优先用libgpiod。它支持批量操作、事件监听、行消费(line consumer)管理。特别是行消费,能告诉你哪个进程在用这个GPIO,调试时特别有用。

4.4 实战:控制TBOX上的LED指示灯

好,理论讲完了,咱们动手做个实战。假设你的TBOX上有三个LED:电源指示灯(绿色)、网络状态灯(蓝色)、故障报警灯(红色)。

硬件连接(假设):

LED 颜色 GPIO编号 有效电平
电源指示 绿色 88 高电平点亮
网络状态 蓝色 89 高电平点亮
故障报警 红色 90 低电平点亮

注意看,故障报警灯是低电平有效。这个细节很容易忽略。我曾经在项目里没看原理图,直接按高电平点亮写代码,结果灯死活不亮。查了半天才发现是电平反了。

完整代码示例:

#include <gpiod.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

static struct gpiod_chip *chip;
static struct gpiod_line *led_power;
static struct gpiod_line *led_network;
static struct gpiod_line *led_alarm;

void cleanup(int sig) {
    printf("\n清理GPIO资源...\n");
    if (led_power) gpiod_line_release(led_power);
    if (led_network) gpiod_line_release(led_network);
    if (led_alarm) gpiod_line_release(led_alarm);
    if (chip) gpiod_chip_close(chip);
    exit(0);
}

int main() {
    // 注册信号处理,确保退出时释放GPIO
    signal(SIGINT, cleanup);
    signal(SIGTERM, cleanup);

    // 打开芯片
    chip = gpiod_chip_open_by_name("gpiochip0");
    if (!chip) {
        perror("打开芯片失败");
        return -1;
    }

    // 获取三个LED的GPIO线
    led_power = gpiod_chip_get_line(chip, 88);
    led_network = gpiod_chip_get_line(chip, 89);
    led_alarm = gpiod_chip_get_line(chip, 90);

    if (!led_power || !led_network || !led_alarm) {
        perror("获取GPIO线失败");
        cleanup(0);
        return -1;
    }

    // 设置为输出
    gpiod_line_request_output(led_power, "power_led", 0);
    gpiod_line_request_output(led_network, "network_led", 0);
    gpiod_line_request_output(led_alarm, "alarm_led", 1); // 故障灯初始熄灭(高电平)

    printf("TBOX LED控制程序启动\n");
    printf("按Ctrl+C退出\n\n");

    // 模拟TBOX启动过程
    printf("1. 系统启动 - 点亮电源灯\n");
    gpiod_line_set_value(led_power, 1);
    sleep(2);

    printf("2. 网络连接中 - 闪烁网络灯\n");
    for (int i = 0; i < 5; i++) {
        gpiod_line_set_value(led_network, 1);
        usleep(200000);
        gpiod_line_set_value(led_network, 0);
        usleep(200000);
    }

    printf("3. 网络连接成功 - 网络灯常亮\n");
    gpiod_line_set_value(led_network, 1);
    sleep(1);

    printf("4. 模拟故障 - 点亮报警灯\n");
    gpiod_line_set_value(led_alarm, 0); // 低电平点亮
    sleep(3);

    printf("5. 故障恢复 - 熄灭报警灯\n");
    gpiod_line_set_value(led_alarm, 1); // 高电平熄灭

    printf("\n演示结束,清理资源\n");
    cleanup(0);

    return 0;
}

关键点总结:

  • 一定要在程序退出时释放GPIO资源,否则下次运行会报错
  • 注意电平的有效极性,别搞反了
  • libgpiod的API是线程安全的,可以在多线程程序里用
  • 用gpiod_line_request_output的最后一个参数设置初始值

编译运行:

gcc -o tbox_led_demo tbox_led_demo.c -lgpiod
sudo ./tbox_led_demo

嗯,这里要注意,操作GPIO需要root权限。如果你不想用root,可以给设备文件加权限,或者用udev规则。

最后说一句,GPIO编程看似简单,但坑不少。电平匹配、上电时序、中断处理、功耗管理,每个点都能让你折腾半天。我建议你拿到TBOX硬件后,先用万用表量一下管脚电平,确认硬件没问题再写代码。别问我怎么知道的...都是泪。

下一章咱们讲更高级的接口:I2C和SPI。到时候你会看到,TBOX跟外部传感器通信,又是另一番天地。