ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

开源UPS实时监测系统:基于STM32与Modbus的电池健康与断电告警方案

开源UPS实时监测系统:基于STM32与Modbus的电池健康与断电告警方案 “夜间断电了备用的双回路没切过来UPS在配电间嘀嘀响蓄电池组却在一分钟内就掉到了警戒线以下。”这类问题不是少数医疗场景里UPS被当成“买了就安全”的硬件可它内部的电池健康状况、实时负载率、回路温度大多数医院根本不知道。隔半年做一次巡检巡检完了数据就锁在表里了。我去年带头做了一个开源的能源监测系统核心就是盯着UPS的电量和电池健康配合各类传感器做成实时监测看板一旦断电、电池劣化、负载过高微信和手机短信能马上收到告警。这套系统的源码已经整理完用的是最普通的STM32和Modbus协议后端加一个轻量级网关纯前端做可视化。动手之前我先说清楚市面上很多商用监控系统是黑盒你还得按节点买license而开源方案一个月就能跑通成本一支万用表的钱都不到极其适合社区医院、乡镇卫生院、口腔诊所这类预算有限但供电安全性要求很高的地方。1. 医疗场景下的UPS为什么总在“关键时刻掉链子”1.1 断电事故里的三个盲区先聊现状。很多医疗机构的配电结构已经把市电做了双回路但双回路切换再快中间也存在明显的断电窗口。UPS的价值就是在市电消失、发电机启动、双回路切换这三者之间把手术室、ICU、检验科的关键设备兜住。可UPS兜不兜得住主要看三样东西电池实际可用容量。铅酸电池标称可以用五年实际上在医疗环境里的浮充电压、温度、放电深度共同作用下两年半到三年就开始明显劣化。更麻烦的是劣化是逐渐的今天能撑二十分钟明天可能只有五分钟。实时负载率。接入UPS的设备功率一直在变新增的CT、超声、除颤仪可能让负载从百分之四十跳升到百分之九十容量余量被偷走需要大屏直观展示。环境温度。电池间温度每升高十度铅酸浮充寿命大约减半机房空调故障或者夏季高温电池组提前报废是很正常的但多数巡检只看电压不测温。这三个盲区在平时不爆炸一旦断电就全部暴露。我做这个系统的初衷就是把这些“看不见”变成“看得见”而且要实时、连续地看见。1.2 UPS自带报警为什么不够用现在的UPS主机普遍有报警功能声光报警、RS232串口、继电器干接点都有。问题在于这些方式都是“单机/本地”式的UPS放在地下室配电间报警了楼上办公室没人听到干接点虽然可以接到楼宇自控系统但楼宇自控系统很多是半淘汰状态点位没规划好UPS根本没有接入RS232串口只能接到附近一台电脑没人盯着屏幕也白搭。更尴尬的是UPS的蜂鸣器响起来往往已经是电池电压非常低的时候这时候能留给你处置的时间可能只有几分钟某些小功率UPS甚至会在电池枯竭前给负载断电你连开机文件都没来得及保存。所以监测系统的价值不在“它响了”在于“它在UPS彻底没电之前提前告诉你有风险并且把趋势数据留给你决策”。1.3 开源方案到底解决了什么我设计的这套开源能源监测系统定位是“统一监测、趋势分析、分级告警”。它要解决的核心问题有三个数据集中在网页上无论UPS在哪个角落用手机浏览器随时看电池电量、负载率、温湿度、市电状态。趋势化而非瞬时化电池电压缓慢下降的趋势比瞬间读数更重要系统每秒采样每分钟入库画出一周、一个月的曲线劣化情况一目了然。告警主动推送断电、电池低电量、负载突增、温度超标秒级触发微信和短信告警值班人员不用盯屏幕。这套系统全部基于开源组件主控端用的是STM32F103和ESP32通信协议走Modbus RTU服务端用Python写数据采集和告警引擎前端用ECharts做仪表盘。许可证选了MIT商用、二次开发都很方便。2. 从电池端子到浏览器页面整套系统的数据链路与选型2.1 系统分成哪四层每层干什么别一上来就想着写程序先理清数据搬家的路径。我按功能把系统拆成四层每一层的职责和接口都很明确这样后面排查问题才不用翻着代码找半天。层级核心模块接口方式数据内容采集层UPS主机、电池采样板、温湿度传感器RS485/Modbus、IO采样、I2C输入输出电压、电池电压、负载率、温度、市电状态控制层STM32/ESP32主控串口/UART/GPIOModbus轮询、信号解析、状态打包网关层树莓派或二手迷你主机MQTT/HTTP数据清洗、格式化、入库、告警判断展示层Web服务EChartsWebSocket/HTTP实时仪表盘、趋势曲线、告警记录选型的时候我反复权衡过采集层和控制层必须用专门的MCU不能用软路由上的软件直接去读RS485因为Modbus RTU对时序要求严格软件栈太杂容易抖动。控制层选STM32F103C8T6还是ESP32要看现场有没有网线。有网线用ESP32它自带Wi-Fi和BLE可以直接把数据推到局域网内的网关没网线、配电间环境差用STM32F103加RS485或者LoRa模块更稳。我在第一个试点现场就遇到配电间在负一楼、完全没有Wi-Fi信号的情况最后用STM32RS485接到了三楼的工控机上跑了大半年都没断过。2.2 UPS侧通信选型为什么优先走Modbus RTU市面上主流UPS厂商都有自己的智能通信卡通信协议底层绝大多数是Modbus RTU只是寄存器地址表各有差异。项目里我拿到的这台UPS提供标准的RS485智能卡槽出厂手册里直接给了寄存器表例如保持寄存器40001是输入电压40002是输出电压40003是负载率百分比40004是电池电压40005是剩余容量估算再往后还有报警字、事件记录。用带隔离的RS485转USB接到控制板轮询速度一次不超过30毫秒。这个方案的最大优点是不需要碰UPS的高压回路完全是旁路读取不影响UPS的安全性和认证状态。如果现场UPS不带智能化接口那就退回到纯IO采样方案用市电检测模块和一个接触器辅助触点判断市电是否消失电池电压用分压电阻采样负载率没有数据就不展示。宁可少一个参数也不要乱接高压。2.3 电池侧传感器的关键选型UPS主机本身报的电池电压通常是一整组的总电压这对判断单节电池健康意义不大。一套48V的电池组是由四节12V电池串联的某一节坏了总电压可能只看出微弱的下降单体电压却已经掉到了10.5V以下。所以我额外加了一块电池单体电压采集板用光耦继电器做通道切换配合分压电阻和STM32的ADC轮流测每一节电池的端电压。温度和电流也在采集范围内电池柜里放一个SHT30温湿度传感器I2C接口精度正负0.3摄氏度电流用ACS712霍尔传感器二十安培量程的版本测起来方便不串入回路对原系统零侵入。如果是三相UPS电流那部分可能需要三个霍尔传感器成本会高一些但原则上不变。2.4 网关服务端如何选不折腾才是王道网关层我试过两条路线。第一条是树莓派直接装Python脚本从串口读数据定期写入SQLite再跑一个Flask服务提供API。第二条是开发板直接走MQTT把数据推到公网或内网的消息代理再由后端消费。我最终选择的是本地SQLite加轻量Flask的方案现场环境网络不稳定MQTT断链的恢复逻辑反而比简单轮询复杂而且医疗机构对数据出域很敏感存放在本地更稳妥。后端只做三件事解析Modbus数据帧、清理异常值、写时间和记录。前端单独跑一个静态页面通过WebSocket和服务端通信刷新频率一秒一次ECharts曲线无压力。数据库我连MySQL都没用SQLite够跑了一台设备一天最多新增一万条记录SQLite在几万条量级上毫无压力。3. 采集层实战把UPS的“沉默语言”翻译成可读数据3.1 Modbus RTU读取UPS状态的报文解析先看最硬核的部分怎么从UPS的RS485口读出数据。Modbus RTU报文格式是地址码、功能码、寄存器起始地址、寄存器数量、CRC校验。假设UPS的Modbus从站地址是01要读40001到40004这四个寄存器报文就是01 03 00 00 00 04 44 0901从站地址03功能码读保持寄存器00 00寄存器起始地址的十六进制00 04读四个寄存器44 09CRC16Modbus版返回帧大概是01 03 08 02 26 02 34 00 2C 00 64 23 B3其中08表示数据字节数之后的02 26是第一个寄存器的值十六进制转十进制就是0226H550对应输入电压55.0V不对这个寄存器可能带十进制度实际要根据手册看。我在代码里写一个通用的Modbus客户端函数把寄存器地址、字节序、倍率都做成配置表换另一台UPS只改配置不改逻辑。下面这段是网关层Python读取Modbus的简写逻辑完整版在源码里import serial import minimalmodbus instrument minimalmodbus.Instrument(/dev/ttyUSB0, 1) instrument.serial.baudrate 9600 instrument.serial.timeout 0.5 # 配置表: (寄存器名, 寄存器地址, 倍率, 单位) reg_map [ (input_voltage, 0x0000, 0.1, V), (output_voltage, 0x0001, 0.1, V), (load_percent, 0x0002, 1, %), (battery_voltage,0x0003, 0.1, V), ] for name, addr, scale, unit in reg_map: raw instrument.read_register(addr, 0, signedFalse) value raw * scale print(f{name}: {value} {unit})需要提醒的是寄存器地址请严格按手边这台UPS的说明书来不同厂商差一位就会读到完全离谱的数据。我在调试时就把40004当成40040读出来一个八千多伏的电压差点吓出一身冷汗。3.2 电池单体电压采样电路与校准方法单体电压采样是这套系统技术难度最高的部分。四节电池串起来电压最高可达58V上下绝对不能直接进STM32的ADC我的做法是每一节电池正负极引出一条采样线进入一个模拟开关板板上有分压电阻网络把每节12V级电压降到3.3V以下。用光耦继电器控制模拟开关逐个把某一节电池的采样点接入ADC通道。光耦继电器的好处是通道之间隔离电压高不会在切换瞬间把高压串到MCU。分压比选100kΩ:10kΩ这样12V分压后大约1.09V28V分压后2.55V都在ADC安全范围内。分压电阻的精度直接影响读数建议用1%精度的金属膜电阻。校准方法也很朴素用一块四位半万用表实测每一节电池电压然后在代码里做线性修正每节电池存两个系数k和b校验公式是真实值 原始读数值 * k b。代码示例#define ADC_CH_NUM 4 static float calib_k[ADC_CH_NUM] {1.021, 0.998, 1.043, 0.987}; static float calib_b[ADC_CH_NUM] {0.02, 0.01, -0.03, 0.04}; float get_battery_cell_voltage(uint8_t idx) { uint16_t raw adc_read_channel(idx); float v_adc raw * 3.3f / 4095.0f; float v_batt v_adc * (110.0f / 10.0f); return v_batt * calib_k[idx] calib_b[idx]; }这里有个细节分压电阻的温漂会直接影响读数所以采样板上的电阻要远离发热元件并且别放在电池柜的散热口附近。3.3 电流与温度采样的一些细节电流采样用ACS712它输出的是模拟电压读取方式和电池采样一样但要注意两点一是量程选择20A版本的输出电压灵敏度是100mV/A但实际用下来总线的纹波会让读数抖动比较大建议在软件里做滑动平均比如连续采二十次取中间值二是传感器的零点零电流时输出是VCC的一半如果供电是5V零点就是2.5V采集后要先扣除。温度那个就简单了SHT30通过I2C直接读一年多来我还没见过它失准。但要注意I2C线长度不要超过一米否则在电机干扰强的配电间容易通讯失败加个上拉电阻到3.3V能改善不少。3.4 硬件上必须避开的坑采集层我踩过几个印象很深的坑列出来RS485的A/B线不能接反接反了完全收不到数据而且不同UPS厂家的A/B定义居然可能相反最好用调试助手反复确认。ADC的参考电压必须是独立基准不要用MCU的VDD。STM32F103内部的参考电压可以作为粗略校准但真正稳定要用外部2.5V基准芯片比如REF3025否则市电波动、MCU供电变化时读数全是假的。隔离必须做。电池柜里不光有48V直流UPS旁路还有220V交流采样线和通信线一旦感应出高压轻则烧ADC重则打坏整块控制板。RS485我用了数字隔离芯片电池采样用了光耦继电器温湿度是板上自带的隔离不能用裸板直接往里怼。电池柜里的静电非常大尤其在干燥天气调试时手碰主板就会出现随机复位。后来我在主板上留了TVS管位置机壳也做了接地问题基本消失。4. 服务端与看板实时监测不只为了看一个百分比4.1 嵌入式端数据上报帧格式设计采集层把这些数据汇总后通过串口或者网络发给网关层。上报帧我会自己拟一个简单格式不用JSON是为了省流量和带宽也便于嵌入式端直接拼ASCII$UPS,2025-06-14 23:30:05,220.3,219.8,42,53.6,12.98,13.02,12.95,13.01,26.4,55.2,0,0*7E字段依次是标识符、时间、输入电压、输出电压、负载百分比、总电池电压、四节单体电池电压、电池柜温度、电池柜湿度、报警字、校验字节。其中报警字用16位表示第0位是市电失电第1位是电池低压第2位是高温自定义规则都在源码里能查。网关端的解析函数很简单按逗号分割、核验头部和校验位、然后入库。如果帧校验失败整帧丢弃不粘包解析。4.2 网关/服务端的数据处理逻辑网关端我用Python写采集线程和WebSocket推送线程因为两件事耦合不大。采集线程每两秒读一次串口写入SQLite时顺便算一个一小时的均值表WebSocket线程维护所有连接的浏览器页面每秒钟把最新一帧数据广播出去。服务程序用supervisor守护挂了自动拉起。数据库表结构大概是这样CREATE TABLE ups_metrics ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts DATETIME DEFAULT CURRENT_TIMESTAMP, input_v REAL, output_v REAL, load_pct REAL, battery_sum_v REAL, cell1_v REAL, cell2_v REAL, cell3_v REAL, cell4_v REAL, temp_c REAL, humidity REAL, alarm_word INTEGER );注意到没有时间字段直接取了数据库机器的本地时间没搞UTC换算。这个决定省了太多事现场系统就在一个机房内前端展示的就是本地时间告警记录对运维人员来说也直观。4.3 前端可视化ECharts仪表盘与实时曲线前端页面的确占了整份源码比较大的篇幅但界面设计其实很简单一个总览看板左边是模拟表盘显示电池电量和负载率中间是最近一小时的输入/输出电压和电流的曲线在下边是单体电池电压的柱状图右上角是最新的告警信息。ECharts部分的核心就是把WebSocket推来的数据丢进series动画关闭只在数据变化时更新。const socket new WebSocket(ws://192.168.1.10:8080/ws); socket.onmessage function(event) { const data JSON.parse(event.data); updateGauge(battery_gauge, data.battery_sum_v); updateGauge(load_gauge, data.load_pct); startSeriesInputV.push({ time: data.ts, value: data.input_v }); batteryBarData [ data.cell1_v, data.cell2_v, data.cell3_v, data.cell4_v ]; chartInputV.setOption({ series: [{ data: startSeriesInputV }] }); chartBatteryBar.setOption({ series: [{ data: batteryBarData }] }); };为了让页面看起来更专业我还加了状态色逻辑输入电压在198V到242V之间显示正常绿色超出显示黄色电池电压低于46V或单体低于11.5V直接红色高亮。值班人员不需要专业知识看到颜色就知道该不该处理。4.4 没有网关怎么办用ESP32直连网页也能跑如果你的场景就是一间小诊所不想再搬一台电脑当网关可以用ESP32的同时扮演采集端和服务端。ESP32跑一个小型HTTP服务器前端页面直接放在Flash文件系统里通过Wi-Fi访问。少了网关层服务逻辑全部写在ESP32上虽然数据只能保留最近几百条但看实时状态完全够用。在我实际项目里两套方案并存规范的正式系统用“STM32网关”临时给朋友诊所搭的就是ESP32单板方案一只开发板加温湿度传感器总共不到八十块钱放到电池柜旁边就能看数据。5. 告警机制与断电抢报最怕的不是断电而是没人知道5.1 告警分级低电量、劣化趋势、高负载、高温告警不是一刀切我把告警设计成四个等级级别条件告警方式处理建议提示电池柜温度超过30℃、负载率超过70%只在看板变黄关注通风散热评估是否要扩容警告单体电池电压低于11.8V、总压低于47V微信推送联系电池厂家做放电检测严重市电失电、电池电压低于46V微信短信、每5分钟重复推送启动预案切换备用回路准备发电紧急电池电压低于42V、预计剩余容量小于10%电话语音告警立即处理否则负载必然停电这个分级很关键。把“可以第二天处理”和“必须立刻处理”分开运维人员才不会被告警疲劳淹没。我见过把所有异常都设置成紧急推送的客户一天几十条短信三天后无人在意真正断电时反而没人当回事。5.2 断电抢报必须在UPS静默之前发出第一条告警断电抢报是整个系统的灵魂。市电一断UPS是能继续供电但它自己的蜂鸣器会越来越急促如果系统在市电断掉的10秒内就把告警逼出去运维人员可能还有半小时甚至一小时去处置如果等到电池快没电才发留给人的反应时间可能只有几分钟。我的方案是分别触发IO开关量检测。用一个220V交流检测模块直接接在UPS输入侧的断路器下端市电有电时模块输出高电平市电一断立刻变低。STM32的GPIO外部中断捕捉这个沿马上把“市电丢失”事件帧传给网关。UPS自身报警字轮询。Modbus轮询间隔两秒报警字里市电状态位一旦翻转也能兜底检测。IO方案是秒级协议轮询是两秒级两者互相备份。网关心跳机制。网关每十秒向服务端发送一个心跳如果连续三个心跳丢失服务端主动短信通知“采集链路异常”至少让运维知道系统本身出问题了。实测效果市电断开后最快1.2秒就收到了微信推送最慢的情况轮询刚好错过也在5秒以内。这个时间意味着值班人员能在UPS电池耗尽前从容处理。5.3 电池维护周期提醒趋势数据比瞬时报文更有价值最后聊一下维护提醒。这套系统的真正价值体现在长期运行后——电池单体电压不是恒定不变的铅酸电池浮充状态下单体电压会有高低差异。我把每节电池每天的平均电压存下来每周生成一个对比曲线当某一节电池的平均电压比整组平均低0.15V以上系统就标记为“需检测电池”完全不依赖定期巡检的人为判断。有一次现场就是靠这个功能发现第三节电池的电压比其他三节低了0.2V通知电池厂商过来放电检测果然那节电池的容量已经掉到额定值的40%。如果那天晚上刚好断电这节电池就会拖垮整组手术室的麻醉机可能撑不到发电机启动。6. 部署验证与踩坑实录真实跑起来之后才会懂的事6.1 现场部署步骤总结再好的系统部署顺序错了都会出乱子。我的完整流程是先摸底再动手。找到UPS主机铭牌、电池组数量、通信卡型号确认接线端子位置画一张草图再离场。断电前先接线不断电调试。所有采样线和通信线都在安全区域布好先用万用表验证各点电压确认无误后再连主控板。单点验证。先用调试助手读一次Modbus数据确认能读到后再启动整个服务不要一下子把所有模块都通电出了问题不好定位。跑24小时基线。观察满充状态下的电池浮充电压、环境温度波动、负载率变化记录正常区间作为后续告警阈值的依据。做一次模拟断电测试。关掉市电输入确认系统秒级告警记录UPS带载时间结束测试后恢复市电。这套顺序看起来繁琐但每一步都有一个明确目的不把风险带进现场。6.2 校准与验证用万用表和放电实验说话校准电压我推荐用六位半台式万用表如果没有四位半的手持表也够用关键是校准时采样点和万用表的测量点必须完全一致不能你量端子A系统量端子B中间隔了一个断路器压降都不一样。放电实验是检验整个系统最好的方式。把UPS负载调整到60%以上断开市电记录系统显示的电池电压从满电到放电终止的完整曲线然后再把这条曲线跟电池厂家给的放电曲线对比。偏差超过5%就要查采样或者倍率配置。我在一次放电实验中发现系统显示的总电压比实测高了0.8V排查完发现是分压电阻有一脚虚焊接触电阻导致读数偏高。这种问题如果不是做放电实验正常巡检根本发现不了。6.3 踩坑实录:那些文档里不会写的事整个项目从研发到落地踩的坑比想象中多捡几个有代表性的说RS485线缆过长导致数据错误。第一次部署距离不到30米但线缆走了桥架和动力电缆并排了一段结果Modbus偶发错包。后来换了屏蔽双绞线并把屏蔽层单端接地问题立即解决。注意不是两端接地两端接地反而可能形成地环路。光耦继电器的寿命问题。早期型号用的是簧片继电器频繁切换后触点氧化出现某节电池通道读数漂移。换用光耦MOS继电器后就没再复发过。Wi-Fi信道干扰。ESP32方案部署在一间CT室附近2.4GHz频段被各类设备占满数据延迟忽高忽低。最后把ESP32设为5GHz热点但距离稍微远了稳定性又下降。所以新方案里优先用有线RS485无线只作为备份。网关NTP时钟漂移。用树莓派当网关时系统时钟不准历史曲线的时间戳全乱。加了NTP同步后又遇到NTP服务偶尔被防火墙挡住的问题所以我在网关代码里做了一层“时间戳由云端校时”的逻辑具体做法是每次开机时向服务端请求一次当前时间偏差大于30秒就自动校正。告警风暴。第一次实测定点告警测试时告警重复推送给值班手机发了四十多条消息。后来在告警引擎里加了防抖逻辑同一告警级别在10分钟内只推一次除非状态位发生了反转再恢复。6.4 系统实际跑了一个月后的真实数据长什么样项目上线一个月后的数据很有说服力。电池组浮充电压平均53.8V偶尔波动到54.2V四节单体电压在13.2V到13.45V之间电池柜温度稳定在26到28度之间夏季空调间歇运行时负载率最高出现在上午十点达到63%和检验科批量开机的规律完全吻合。这些基线数据后来被直接用来和电池厂家讨论更换策略现场再也不用凭经验拍脑袋。如果未来要扩展这套系统的架构也预留了位置Modbus协议可以继续接入空调、配电柜的多回路电表实现整个配电间的联动监测采集板增加一个继电器输出口可以作为“放电测试”的自动执行开关看板加一个PDF导出功能巡检报告就能一键生成。这些等后续有需求再慢慢补。这套系统让我感触最深的一点是做电源监测不是做个漂亮的大屏给领导看而是真正在断电发生前就能预测风险、通知人员、留好证据。我不指望所有人都能理解电池电压趋势图的含义但是当那台老UPS在深夜突然报警时手机能提前收到那条“市电失电、电池带载中”的消息这套系统就值回所有成本了。
返回列表