深入解析 Linux GPIO 采集与控制程序(DI/DO 篇)

在嵌入式 Linux 系统中,通过 GPIO 采集数字输入(DI)和控制数字输出(DO)是常见的需求。本文以一份真实的 BMS(电池管理系统)采集程序 Collect.cpp 为例,详细梳理其中关于 GPIO 操作的实现细节,包括数据结构设计、多芯片支持、输入滤波、输出控制以及线程周期管理,适合开发者参考或撰写博客。

1. 程序功能概述

Collect.cpp 是一个周期性运行的后台进程,主要任务:

  • 读取多个 DI 引脚状态(如接触器状态、急停开关、烟感信号等),经过 5 次连续确认滤波 后更新到共享内存。

  • 从共享内存中读取控制命令(如继电器开合指令),实时写入对应的 DO 引脚。

  • 以 10ms 为固定周期循环执行,保证实时性。

程序使用 libgpiod 操作 GPIO,支持多个 GPIO 芯片(gpiochip0gpiochip1gpiochip2gpiochip3),并通过共享内存与其它进程通信。

2. GPIO 映射表设计

2.1 DI 映射结构体

cpp

复制代码
struct DiMapping {
    unsigned int chipNum;      // GPIO chip 编号(0,1,2,3...)
    unsigned int line_offset;  // 引脚偏移量(0~31)
    int point_id;              // 共享内存中的点位ID
    const char* description;
    unsigned int useState;     // 1: 使用共享内存,0: 不使用
    unsigned int negation;     // 1: 读取后取反,0: 不取反
};
  • chipNumline_offset 唯一确定一个 GPIO 引脚。

  • point_id 对应共享内存中该信号的索引,便于上层逻辑通过统一 ID 访问。

  • useState 允许在不删除定义的情况下临时禁用某个引脚(例如硬件未连接)。

  • negation 支持硬件电平与逻辑电平相反的场合(如低电平有效)。

2.2 DI 映射表实例

cpp

复制代码
DiMapping diMap[] = {
    {2, 26, 611, "GPIO2_D2", 1, 0}, // negRelay_flag
    {3, 8,  612, "GPIO3_B0", 1, 0}, // posRelay_flag
    {0, 17, 613, "GPIO0_C1", 1, 0}, // preRelay_flag
    {0, 23, 614, "GPIO0_C7", 1, 0}, // circuitBreaker_flag
    {2, 24, 615, "GPIO2_D0", 1, 0}, // fuse_flag
    {2, 25, 616, "GPIO2_D1", 1, 0}, // stop_flag
    {2, 27, 617, "GPIO2_D3", 1, 0}, // smoke_flag
    {2, 28, 617, "GPIO2_D4", 0, 0}, // 未使用
    {3, 12, 617, "GPIO3_B4", 0, 0}  // 未使用
};

2.3 DO 映射结构体及映射表

cpp

复制代码
struct DoMapping {
    int point_id;
    unsigned int chipNum;
    unsigned int line_offset;
    const char* description;
    unsigned int useState;
    unsigned int negation;
};

DoMapping doMap[] = {
    {100, 0, 20, "GPIO0_C4", 1, 0}, // posRelayCmd
    {101, 0,  8, "GPIO0_B0", 1, 0}, // negRelayCmd
    {102, 0,  6, "GPIO0_A6", 1, 0}, // preRelayCmd
    // ... 共 14 个输出
    {119, 3,  7, "GPIO3_A7", 1, 0}  // K7 dryContactCmd7
};

3. 多芯片 GPIO 初始化

由于引脚分布在多个 gpiochip 上,程序为每个 useState==1 的引脚独立打开对应的 gpiod_chip 并申请 line,同时保存句柄用于后续操作和释放。

cpp

复制代码
static gpiod_line *g_di_lines[diCount] = {nullptr};
static gpiod_chip *g_di_chips[diCount] = {nullptr};
// DO 同理

bool init_gpio() {
    for (int i = 0; i < diCount; ++i) {
        if (diMap[i].useState == 0) continue;
        gpiod_chip *chip = gpiod_chip_open_by_number(diMap[i].chipNum);
        if (!chip) { /* 错误处理 */ }
        gpiod_line *line = gpiod_chip_get_line(chip, diMap[i].line_offset);
        gpiod_line_request_input(line, "collect_in");
        g_di_lines[i] = line;
        g_di_chips[i] = chip;
    }
    // 类似初始化 DO(request_output)
}

这种设计避免了全局打开一个 chip 然后反复 get_line 的混乱,每个引脚独立管理,便于调试和释放。

4. DI 读取与 5 次连续确认滤波

为什么需要滤波?

机械触点或外部干扰可能导致电平瞬间跳变。直接更新共享内存会引起上层逻辑误判。采用"连续 5 次相同才确认"的软件滤波,有效消除抖动。

4.1 滤波实现

cpp

复制代码
void read_di() {
    static uint8_t di_last_value[diCount] = {0};
    static uint8_t di_stable_count[diCount] = {0};

    LOCK(&g_shared->mutex);
    for (int i = 0; i < diCount; ++i) {
        if (diMap[i].useState == 0) continue;
        int val = gpiod_line_get_value(g_di_lines[i]);
        if (diMap[i].negation) val = !val;
        uint8_t new_val = val;

        if (di_stable_count[i] == 0) {
            // 首次读取直接确认并设置计数为5(避免后续重复更新)
            di_last_value[i] = new_val;
            di_stable_count[i] = 5;
            // 更新共享内存
            update_shared_memory(diMap[i].point_id, new_val);
        } else {
            if (new_val == di_last_value[i]) {
                if (di_stable_count[i] < 5) di_stable_count[i]++;
                if (di_stable_count[i] == 5) {
                    update_shared_memory(diMap[i].point_id, new_val);
                }
            } else {
                di_last_value[i] = new_val;
                di_stable_count[i] = 1;
            }
        }
    }
    UNLOCK(&g_shared->mutex);
}

4.2 逻辑说明

  • 首次启动:直接确认并计数 5,保证初始状态立即生效。

  • 状态稳定:连续相同值累计计数,达到 5 次才更新共享内存。

  • 状态变化:一旦新值与上次不同,重置计数为 1,不更新内存,直到再次连续 5 次相同。

  • 当计数达到 5 后,后续相同读数不再重复写内存,减少互斥锁竞争。

5. DO 更新逻辑

DO 更新相对简单:从共享内存的 sys_state_ctrl 数组中读取对应 point_id 的命令值,然后一次性设置所有 DO。

cpp

复制代码
void update_do() {
    uint8_t cmdValues[doCount] = {0};
    // 加锁读取共享内存中的命令
    LOCK(&g_shared->mutex);
    for (int i = 0; i < doCount; ++i) {
        if (doMap[i].useState == 0) continue;
        for (int j = 0; j < MAX_POINTS; ++j) {
            if (g_shared->sys_state_ctrl.points[j].point_id == doMap[i].point_id) {
                cmdValues[i] = g_shared->sys_state_ctrl.points[j].value.u8;
                break;
            }
        }
    }
    UNLOCK(&g_shared->mutex);

    // 无锁写入硬件
    for (int i = 0; i < doCount; ++i) {
        if (doMap[i].useState == 0) continue;
        uint8_t val = cmdValues[i];
        if (doMap[i].negation) val = !val;
        gpiod_line_set_value(g_do_lines[i], val ? 1 : 0);
    }
}

分离锁 的设计:先加锁读取所有命令值到本地数组,释放锁后再逐个写入 GPIO。这样做缩短了锁持有时间,避免在慢速 I/O 期间阻塞其他进程访问共享内存。

6. 采集线程与周期控制

cpp

复制代码
#define COLLECT_CYCLE_MS 10

void collect_thread() {
    while (g_collect_running.load()) {
        auto start = std::chrono::steady_clock::now();
        read_di();
        update_do();
        auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
                           std::chrono::steady_clock::now() - start).count();
        if (elapsed < COLLECT_CYCLE_MS)
            std::this_thread::sleep_for(
                std::chrono::milliseconds(COLLECT_CYCLE_MS - elapsed));
    }
}
  • 使用 std::chrono::steady_clock 测量实际执行时间,动态补偿睡眠,保证稳定的 10ms 周期。

  • g_collect_runningstd::atomic<bool>,在 SIGINT/SIGTERM 信号处理函数中置为 false,实现安全退出。

7. 资源清理

程序退出时(收到信号后主循环结束),调用 cleanup_gpio() 释放所有 GPIO 资源:

cpp

复制代码
void cleanup_gpio() {
    for (int i = 0; i < diCount; ++i) {
        if (g_di_lines[i]) gpiod_line_release(g_di_lines[i]);
        if (g_di_chips[i]) gpiod_chip_close(g_di_chips[i]);
    }
    // DO 同理
}

确保每个 gpiod_linegpiod_chip 都被正确释放,避免资源泄漏。

8. 技术亮点总结

特性 实现方式 优点
多芯片支持 每个引脚独立打开 chip,保存句柄 灵活适应不同 GPIO 控制器
软件滤波 连续 5 次相同值确认 消除触点抖动,提升可靠性
取反支持 negation 字段 兼容低电平有效的硬件设计
动态周期控制 steady_clock + 动态睡眠 周期稳定,不受执行时间波动影响
短锁设计 分离读取命令和硬件写入 减少锁竞争,提高并发性
优雅退出 std::atomic<bool> + 信号处理 安全释放资源,避免数据损坏

9. 完整代码结构

text

复制代码
main()
├── signal() 注册 SIGINT/SIGTERM 处理
├── attach_shared_memory()     // 映射共享内存
├── init_gpio()                // 初始化 DI/DO
├── std::thread(collect_thread).detach()
├── while (g_collect_running) sleep(1)
└── cleanup_gpio()

10. 适用场景与扩展

  • 该模式适用于任何需要周期性采集数字输入和控制数字输出的嵌入式 Linux 项目。

  • 可以轻松扩展到更多 GPIO,只需修改映射表。

  • 滤波次数可根据硬件抖动情况调整(例如改为 3 次或 10 次)。

  • 若需要更高精度,可将 COLLECT_CYCLE_MS 改为 1ms 并使用 nanosleeptimerfd

结语

本文详细剖析了 Collect.cpp 中 GPIO 相关代码的设计与实现。从数据结构、初始化、滤波算法到线程周期控制,每一步都体现了嵌入式实时系统的典型实践。希望这篇博客能为你的 Linux GPIO 编程提供有价值的参考。

相关推荐
霸道流氓气质1 小时前
导入历史跟踪机制实战指南
java·linux·服务器
肖爱Kun1 小时前
GB28181启动传参的设计
linux·服务器·数据库
剑神一笑1 小时前
Linux systemctl 服务管理命令:从 systemd 架构到实战技巧
linux·服务器·架构
艾莉丝努力练剑1 小时前
【Linux网络】传输层协议TCP(六)补充 - 面试题:HTTP 获取网页的完整过程
linux·运维·网络·tcp/ip·计算机网络·http·udp
norsd1 小时前
CentOS Rocky Linux 设置 ip
linux·tcp/ip·centos
minji...1 小时前
Linux高级IO(六)基于ET模式、单reactor反应堆的epoll版本的TCP计算服务器
linux·服务器·网络·c++·epoll·socket套接字·reactor反应堆模式
jcbut2 小时前
在Linux上安装Kingbase 9
linux·kingbase·人大金仓·电科金仓
小此方3 小时前
Re:Linux系统篇(二十六)进程篇·十一:从底层原理到 exec* 家族:彻底搞懂 Linux 进程程序替换
linux·运维·服务器
赵民勇11 小时前
fuse-overlayfs命令详解
linux·容器