当前位置: 首页 > news >正文

深入解析 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 编程提供有价值的参考。

http://www.jsqmd.com/news/942651/

相关文章:

  • OnmyojiAutoScript:阴阳师自动化脚本的技术实现与应用指南
  • 基于Arduino的脚踏PTT按钮制作:解放双手的硬件DIY指南
  • 基于Arduino的物理勿扰开关:从数字IO到环境设计的嵌入式实践
  • 2026北京法式定制家具公司盘点 附真实评价 - 资讯纵览
  • 热式气体质量流量计厂家十大品牌盘点|助你快速选型 - 流量计品牌
  • 从零搭建交互式3D投影桌:硬件选型、软件集成与调试全攻略
  • 广州黄金回收避坑:全维度实测+白名单 - 合扬奢侈品交易中心
  • 基于TP4056的DIY应急充电手电筒:从锂电池管理到LED驱动全解析
  • 别再手动画圆了!用Arcpy脚本工具批量生成矢量圆(附完整Python代码与ArcGIS工具箱配置)
  • 2026年6月不锈钢方棒品牌推荐,不锈钢六角棒/不锈钢方棒/不锈钢光圆/不锈钢黑棒/锻棒,不锈钢方棒品牌怎么选择 - 品牌推荐师
  • PyTorch老项目救星:手把手教你用Conda精准锁定并安装1.13.0等历史版本(附版本对照表)
  • STM32F103新手必看:Keil5 MDK-ARM界面详解与高效开发设置(附快捷键清单)
  • 乐平管道频繁堵塞怎么办?2026年这5家靠谱疏通服务推荐 - 本地品牌推荐
  • 2026宁波奢侈品回收全品类:合扬持证鉴定一站式盘活闲置资产 - 合扬奢侈品交易中心
  • 告别Labelme!用EISeg+飞桨PaddlePaddle,5分钟搞定AI标注(附避坑指南)
  • 告别编译报错!手把手教你用VS2022编译64位libmodbus动态库(附完整依赖项配置)
  • 忆阻器神经形态计算优化:TiO2器件与算法协同设计实战
  • 京东e卡回收省心技巧,回收合规操作全攻略 - 京回收小程序
  • 免焊接3D打印手电筒:弹性开关设计与DIY制作全攻略
  • LeagueAkari:提升英雄联盟游戏体验的5大智能功能解析
  • 基于搜索数据的宏观经济研究:NLP与空间可视化在劳动力市场分析中的应用
  • 基于555定时器的单稳态延时开关电路设计与实践指南
  • DIY音频驱动LED节奏灯:用晶体管实现音乐可视化
  • 工业远程雾炮机生产厂家排行 适配多场景粉尘治理 - 奔跑123
  • VLC不止是播放器:手把手教你用Lua脚本实现智能视频播放自动化
  • 基于Arduino的DIY水质监测:从电导率探头到公民科学实践
  • 【珠海+余生黄金回收+全城上门变现】2026年珠海黄金回收靠谱机构测评 - 润富黄金回收
  • PPTist:完全开源的网页版演示文稿编辑工具终极指南
  • 如何快速掌握AriaNg:告别命令行下载的终极可视化解决方案
  • 2026年PC端移动应用跨端运行方案选型指南