简介:本资源是一份面向工业自动化领域初学者与组态王开发人员的实用技术文档,聚焦于解决电机运行时间精准记录这一典型设备运维需求。通过组态王报表功能结合脚本二次开发,完整实现了双电机启动时间、停止时间及运行时长(自动换算为天/时/分/秒)的结构化记录与报表填充,适用于设备状态监控、预防性维护与运行数据分析等实际工程场景。资源为1个423KB的Word文档(.docx),内含详细实现思路、变量定义规范、日期格式化处理逻辑、启停事件触发脚本(含HTConvertTime时间转换、字符串截取与拼接、报表行列定位等关键代码段)以及完整的操作步骤说明。目前已有2950人学习下载,读者可直接复用该方案中的内存变量设计、自定义函数框架与报表数据联动逻辑,快速构建同类设备运行日志系统。
1. 组态王电机运行时间记录:不是“加个按钮就能出报表”,而是用内存变量+脚本+报表三线协同的闭环逻辑
你有没有遇到过这种现场:客户指着组态王画面问,“这台电机今天一共开了多久?”——你点开历史曲线,手动拖拽、估算、心算,最后报个“大概7小时23分”,对方眉头一皱:“能导出精确到秒的明细吗?”
别急着翻帮助文档。组态王原生确实不带“设备运行时长自动归档”功能,但它留了一条极其实用的缝:报表控件可编程写入 + 内存变量持久暂存 + 系统时间变量实时可读。这篇笔记讲的,就是怎么把这三条线拧成一股绳,让两台电机的每一次启停,都自动生成带序号、名称、启动时间、停止时间、精确到秒的运行时长(格式为“X天Y时Z分W秒”)的结构化记录。它不依赖OPC服务器、不调用外部数据库、不改组态王安装目录,纯靠组态王6.55及以上版本自带的脚本引擎(KingView Script)和报表控件实现。适合做设备维保台账、能耗初筛、故障频次统计的中小型自动化项目。如果你正被“领导要日报”“审计要原始记录”“运维要追溯依据”三座大山压着,又没预算上MES,那这个方案就是你今晚该抄的第一份作业。
2. 报表结构设计与内存变量规划:为什么必须用3个整形变量,少一个就断链
要让“启动→填行→停止→回填”这个动作闭环,光有报表是空架子。组态王报表本身不记状态,它只负责显示;而设备启停是瞬时事件,脚本执行完就结束。中间缺的“记忆体”,就是内存变量。这里不是随便建几个变量就行,而是有明确分工的三人小组:
2.1 报表10:五列固定结构,拒绝自由发挥
新建报表命名为“报表10”(注意:原文写“报表1”,但后文调用函数时传参是"报表 1",存在笔误;实际工程中建议统一为“报表10”,避免空格引发的ReportSetCellString调用失败)。其列定义必须严格如下:
| 列号 | 列名 | 数据类型 | 说明 |
|---|---|---|---|
| 1 | 序号 | 字符串 | 自增编号,从1开始 |
| 2 | 设备名称 | 字符串 | 如“冰水循环泵2” |
| 3 | 启动时间 | 字符串 | 格式:2024-06-15 14:23:08 |
| 4 | 停止时间 | 字符串 | 同上格式 |
| 5 | 运行时长 | 字符串 | 格式:0天2时15分33秒 |
提示:列宽需手动设为足够容纳最长字符串(如“999天23时59分59秒”共15字符),否则显示截断。右键报表→“列属性”→“宽度”设为120像素起。
2.2 三个内存整形变量:各司其职,一个都不能少
在组态王开发系统中,进入“数据库”→“内存实型/整型变量”,新建以下3个内存整形变量(注意类型必须是“内存整形”,非“IO整形”或“内存实型”):
| 变量名 | 类型 | 初始值 | 作用说明 |
|---|---|---|---|
Motor1Row | 内存整形 | 0 | 记录“设备1”当前运行周期在报表中的行号(启动时写入,停止时读取) |
Motor2Row | 内存整形 | 0 | 记录“设备2”当前运行周期在报表中的行号(同上) |
LastRecordRow | 内存整形 | 1 | 记录报表中下一条新记录应写入的行号(即当前最大行号+1,启动时赋值) |
为什么LastRecordRow初始值设为1?因为报表行号从1开始计数,第一行就是序号“1”。当设备1首次启动,脚本会将LastRecordRow的值(=1)写入第1行,然后LastRecordRow++变为2,下次启动自动写第2行。这个变量是保证记录严格顺序追加的核心,漏建或类型设错,所有后续逻辑都会错位。
2.3 关键约束:报表行号与变量值的同步铁律
组态王报表的报表当前行是一个全局只读变量,表示当前光标所在行。但我们的逻辑不需要光标,需要的是逻辑行号。因此,所有写入操作必须显式指定行号,而非依赖报表当前行。原文脚本中出现报表当前行=报表当前行+1是危险操作——它会改变全局状态,干扰其他报表操作。正确做法是:用LastRecordRow作为写入目标行号,写完立即自增。例如:
// 启动设备时,向报表10第LastRecordRow行写入 ReportSetCellString("报表10", LastRecordRow, 1, StrFromInt(LastRecordRow, 10)); // 序号=行号 ReportSetCellString("报表10", LastRecordRow, 2, name); // 设备名称 ReportSetCellString("报表10", LastRecordRow, 3, str); // 启动时间 // 保存此设备的行号到对应变量 Motor1Row = LastRecordRow; // 设备1启动,记下这一行 // 更新总行号,为下一次记录准备 LastRecordRow = LastRecordRow + 1;这段代码里,Motor1Row存的是“设备1本次启动所在的物理行号”,LastRecordRow存的是“报表下一个空闲行号”。二者解耦,互不干扰。这是整个方案能稳定运行的底层契约。
3. 自定义函数SetReport:把时间拼接、秒级计算、格式转换全封装进一个函数
把原文中零散的脚本揉成一个可复用、易调试的自定义函数,是降低维护成本的关键。我们命名为SetReport,它接收3个参数:motorState(设备当前运行状态,1=启动,0=停止)、motorName(设备名称字符串)、rowVarAddr(指向内存变量的地址,用于读写该设备的行号)。注意:组态王脚本不支持指针传参,所以实际实现中,我们用switch分支区分设备,rowVarAddr改为设备ID(如1代表Motor1Row,2代表Motor2Row)。
3.1 函数完整代码(含关键注释)
// 自定义函数:SetReport // 参数说明: // state: 整型,1=设备启动,0=设备停止 // name: 字符串,设备名称,如"冰水循环泵2" // motorID: 整型,1=设备1,2=设备2 function SetReport(state, name, motorID) { // ===== 步骤1:构建标准时间字符串(YYYY-MM-DD HH:MM:SS)===== string strY = StrFromInt(\\本站点\$年, 10); string strM = StrFromInt(\\本站点\$月, 10); string strD = StrFromInt(\\本站点\$日, 10); string strH = StrFromInt(\\本站点\$时, 10); string strMi = StrFromInt(\\本站点\$分, 10); string strS = StrFromInt(\\本站点\$秒, 10); // 补零逻辑:确保月份、日期、小时、分钟、秒均为2位 if (\\本站点\$月 < 10) strM = "0" + strM; if (\\本站点\$日 < 10) strD = "0" + strD; if (\\本站点\$时 < 10) strH = "0" + strH; if (\\本站点\$分 < 10) strMi = "0" + strMi; if (\\本站点\$秒 < 10) strS = "0" + strS; string timeStr = strY + "-" + strM + "-" + strD + " " + strH + ":" + strMi + ":" + strS; // ===== 步骤2:根据motorID获取/设置对应设备的行号变量 ===== long varRow; switch(motorID) { case 1: varRow = \\本站点\Motor1Row; break; case 2: varRow = \\本站点\Motor2Row; break; default: return; // 无效ID,退出 } // ===== 步骤3:设备启动逻辑(state == 1)===== if (state == 1) { // 写入序号(等于当前LastRecordRow) ReportSetCellString("报表10", \\本站点\LastRecordRow, 1, StrFromInt(\\本站点\LastRecordRow, 10)); // 写入设备名称 ReportSetCellString("报表10", \\本站点\LastRecordRow, 2, name); // 写入启动时间 ReportSetCellString("报表10", \\本站点\LastRecordRow, 3, timeStr); // 更新该设备的行号变量 if (motorID == 1) \\本站点\Motor1Row = \\本站点\LastRecordRow; else \\本站点\Motor2Row = \\本站点\LastRecordRow; // 更新总行号(为下一次启动准备) \\本站点\LastRecordRow = \\本站点\LastRecordRow + 1; } // ===== 步骤4:设备停止逻辑(state == 0)===== else if (state == 0) { // 从报表中读取该设备启动时间(第varRow行,第3列) string startStr = ReportGetCellString("报表10", varRow, 3); if (startStr == "") return; // 防御性检查:若为空,说明未启动过,不处理 // ===== 步骤4.1:解析启动时间字符串,提取年月日时分秒 ===== // 格式固定为"2024-06-15 14:23:08",位置索引从1开始(组态王字符串索引) string midY = StrMid(startStr, 1, 4); // 年:位置1,取4位 string midM = StrMid(startStr, 6, 2); // 月:位置6,取2位(跳过"-") string midD = StrMid(startStr, 9, 2); // 日:位置9,取2位 string midH = StrMid(startStr, 12, 2); // 时:位置12,取2位(跳过" "和":") string midMi = StrMid(startStr, 15, 2); // 分:位置15,取2位 string midS = StrMid(startStr, 18, 2); // 秒:位置18,取2位 long lY = StrToInt(midY); long lM = StrToInt(midM); long lD = StrToInt(midD); long lH = StrToInt(midH); long lMi = StrToInt(midMi); long lS = StrToInt(midS); // ===== 步骤4.2:调用HTConvertTime将启动/停止时间转为秒级时间戳 ===== long startTime = HTConvertTime(lY, lM, lD, lH, lMi, lS); long endTime = HTConvertTime(\\本站点\$年, \\本站点\$月, \\本站点\$日, \\本站点\$时, \\本站点\$分, \\本站点\$秒); long timeLength = endTime - startTime; // 单位:秒 // ===== 步骤4.3:将秒数转换为"X天Y时Z分W秒"格式 ===== string durationStr; long days = timeLength / 86400; long remainingAfterDays = timeLength % 86400; long hours = remainingAfterDays / 3600; long remainingAfterHours = remainingAfterDays % 3600; long minutes = remainingAfterHours / 60; long seconds = remainingAfterHours % 60; // 拼接字符串,避免出现"0天0时0分5秒",只显示有效单位 durationStr = ""; if (days > 0) durationStr = durationStr + StrFromInt(days, 10) + "天"; if (hours > 0 || days > 0) durationStr = durationStr + StrFromInt(hours, 10) + "时"; if (minutes > 0 || hours > 0 || days > 0) durationStr = durationStr + StrFromInt(minutes, 10) + "分"; durationStr = durationStr + StrFromInt(seconds, 10) + "秒"; // ===== 步骤4.4:将停止时间和运行时长写入报表对应行 ===== ReportSetCellString("报表10", varRow, 4, timeStr); // 停止时间写入第4列 ReportSetCellString("报表10", varRow, 5, durationStr); // 运行时长写入第5列 } }参数说明与调用示例:
state:必须是1或0,由设备运行状态位(如PLC的M100.0)触发。不要传-1、2等非法值。name:字符串常量,必须用双引号包裹,如"冰水循环泵2"。motorID:整数1或2,对应前文定义的Motor1Row/Motor2Row。
典型调用场景(写在设备“启动”按钮的“按下”事件中):
SetReport(1, "冰水循环泵1", 1); // 设备1启动典型调用场景(写在设备“停止”按钮的“按下”事件中):
SetReport(0, "冰水循环泵1", 1); // 设备1停止这个函数把所有时间处理、计算、写入逻辑打包,以后新增第三台设备,只需在switch里加case 3,并新建Motor3Row变量,调用时传3即可,无需动核心逻辑。
4. 运行事件配置与启停联动:让PLC信号真正驱动报表记录
函数写好了,但不会自己跑。必须把它挂到真实的设备状态变化上。组态王提供“命令语言”功能,可在IO变量的“值发生变化”事件中触发脚本。这才是工业现场的真实工作流:PLC告诉组态王“电机1启动了”,组态王立刻执行SetReport(1, "电机1", 1)。
4.1 创建IO变量映射PLC地址
假设PLC中:
- 电机1运行状态位地址为
DB1.DBX0.0(西门子S7-300) - 电机2运行状态位地址为
DB1.DBX0.1
在组态王“数据库”中,新建两个IO离散量变量:
- 变量名:
PLC_Motor1_Run,类型:IO离散量,连接设备:已配置的PLC驱动,寄存器:DB1.DBX0.0 - 变量名:
PLC_Motor2_Run,类型:IO离散量,连接设备:同上,寄存器:DB1.DBX0.1
注意:确保PLC驱动已正确配置且通讯正常,
PLC_Motor1_Run的值能实时反映PLC中该位的状态(0或1)。
4.2 配置“值发生变化”事件命令
右键PLC_Motor1_Run变量 → “事件” → “值发生变化” → 勾选“启用”,在下方编辑框中输入:
// 当PLC_Motor1_Run从0变1(上升沿),视为启动 if (\\本站点\PLC_Motor1_Run == 1 && GetLastValue(\\本站点\PLC_Motor1_Run) == 0) { SetReport(1, "冰水循环泵1", 1); } // 当PLC_Motor1_Run从1变0(下降沿),视为停止 if (\\本站点\PLC_Motor1_Run == 0 && GetLastValue(\\本站点\PLC_Motor1_Run) == 1) { SetReport(0, "冰水循环泵1", 1); }同样,为PLC_Motor2_Run配置事件命令:
if (\\本站点\PLC_Motor2_Run == 1 && GetLastValue(\\本站点\PLC_Motor2_Run) == 0) { SetReport(1, "冰水循环泵2", 2); } if (\\本站点\PLC_Motor2_Run == 0 && GetLastValue(\\本站点\PLC_Motor2_Run) == 1) { SetReport(0, "冰水循环泵2", 2); }关键函数说明:
GetLastValue(var):返回变量var在上一个扫描周期的值。这是检测边沿(上升沿/下降沿)的唯一可靠方法。直接用if (old==0 && new==1)是错的,因为old变量不存在。- 为什么不用“按下/弹起”按钮事件?因为现场按钮可能失灵、被短接,而PLC状态是真实设备反馈,更可信。按钮事件仅作调试备用。
4.3 调试技巧:用“变量浏览器”实时监控链条
运行组态王运行系统后,打开“工具”→“变量浏览器”,添加以下变量观察:
PLC_Motor1_Run(看是否随PLC变化)Motor1Row(看启动时是否被赋值为当前LastRecordRow)LastRecordRow(看每次启动后是否+1)报表10(在画面中拖动查看,确认数据实时出现)
如果Motor1Row始终为0,检查:①PLC_Motor1_Run事件是否启用;②SetReport函数中motorID==1分支是否执行(可在函数开头加MessageBox("Debug: Motor1 Start");临时弹窗);③LastRecordRow初始值是否为1(不是0!)。
5. 避坑 / 常见问题 / 排查:血泪经验总结的5个致命细节
这个方案看似简单,但在真实项目中,90%的问题都出在细节。以下是我在某高校实验室、某食品厂DCS改造中踩过的坑,按发生频率排序:
5.1 现象:报表里只显示启动时间,停止时间空白,运行时长为“0天0时0分0秒”
原因:ReportGetCellString("报表10", varRow, 3)读取启动时间失败。常见于:①varRow值为0(设备从未启动过,或Motor1Row变量初始值没设为0);②varRow值超出报表当前行数(如报表只有5行,varRow=10);③ 启动时间列(第3列)被意外清空或格式错误(如写入了空格、乱码)。
解决:在SetReport函数停止逻辑开头加防御性检查:
if (varRow <= 0 || varRow > \\本站点\LastRecordRow - 1) { MessageBox("错误:设备" + StrFromInt(motorID, 10) + "未启动或行号越界!"); return; } string startStr = ReportGetCellString("报表10", varRow, 3); if (startStr == "" || StrLen(startStr) < 19) { // 标准时间字符串长度至少19 MessageBox("错误:启动时间为空或格式错误!行号:" + StrFromInt(varRow, 10)); return; }5.2 现象:运行时长计算为负数,如“-1天23时59分59秒”
原因:HTConvertTime函数对日期合法性校验极严。如果启动时间字符串中月份为13、日期为32、或年份为0000,HTConvertTime会返回0,导致endTime - 0 = endTime,而endTime是当前时间戳,远大于0,但若startTime解析失败返回0,则timeLength为正大数,再经除法后显示为负(因long类型溢出)。更常见的是:启动时间字符串格式不对,如"2024-6-15 14:23:08"(月/日未补零),StrMid取值错位,midM取到"6 "(带空格),StrToInt失败返回0。
解决:严格校验startStr格式。在解析前加:
if (StrLen(startStr) != 19 || StrMid(startStr, 5, 1) != "-" || StrMid(startStr, 8, 1) != "-" || StrMid(startStr, 11, 1) != " " || StrMid(startStr, 14, 1) != ":" || StrMid(startStr, 17, 1) != ":") { MessageBox("错误:启动时间格式非法!应为'YYYY-MM-DD HH:MM:SS',当前:" + startStr); return; }5.3 现象:同一台设备多次启停,报表中只有一行记录,且“停止时间”覆盖了“启动时间”
原因:Motor1Row变量被重复写入。典型场景:设备启动后,PLC状态位因干扰抖动,在100ms内反复0→1→0,导致SetReport(1,...)和SetReport(0,...)在极短时间内连续执行,Motor1Row被覆盖。
解决:增加防抖逻辑。在SetReport启动分支中,加入“如果Motor1Row已非0,且该行第4列(停止时间)为空,则不执行启动写入”:
if (motorID == 1 && \\本站点\Motor1Row > 0) { string stopTime = ReportGetCellString("报表10", \\本站点\Motor1Row, 4); if (stopTime != "") { // 说明上一周期已停止,可安全写新周期 // 执行启动写入... } else { MessageBox("警告:设备1上一周期未停止,忽略本次启动!"); return; } }5.4 现象:报表显示正常,但导出Excel后,时间列变成数字(如44234.592),时长列丢失
原因:组态王报表导出时,对字符串列默认按数值处理。"2024-06-15 14:23:08"被Excel识别为日期,"0天2时15分33秒"被识别为文本但格式错乱。
解决:导出前,在报表设计界面,右键每列 → “列属性” → “格式” → 设置:
- 第3、4列(时间):格式设为“文本”
- 第5列(时长):格式设为“文本”
这样导出的CSV/Excel中,内容会自动加英文双引号,Excel打开时即为纯文本。
5.5 现象:组态王运行一段时间后,报表卡死,点击无响应
原因:ReportSetCellString和ReportGetCellString是重量级操作,频繁调用(如每秒多次)会阻塞UI线程。原文方案在设备启停瞬间调用,本无问题,但如果误将此函数放在“循环脚本”或“定时器”中,就会雪崩。
解决:确认SetReport只在IO变量“值发生变化”事件中调用,绝对禁止放在:① 全局脚本的while(1)循环中;② 画面“定时刷新”事件中(哪怕设为10秒);③ 其他任何周期性触发的命令中。这是性能红线。
6. 进阶技巧:让记录具备断电续写与跨班次统计能力
做到上面五章,你已经能稳定记录电机启停了。但工业现场还有两个刚需:一是组态王意外重启后,不能丢掉正在运行的周期;二是夜班开的电机,白班停的,时长要跨天计算。这两个需求,原文没提,但一线项目天天碰。
6.1 断电续写:用文件存储代替纯内存变量
内存变量Motor1Row、Motor2Row、LastRecordRow在组态王关闭后会清零。如果电机正在运行时断电,重启后Motor1Row=0,再停机就找不到启动时间了。解决方案:用组态王的FileWrite/FileRead函数,将关键变量值存到本地文件。
步骤:
- 在组态王“系统配置”→“启动命令”中,添加一行:
// 启动时读取上次保存的变量值 string data = FileRead("C:\\KingView\\Runtime\\MotorRecord.dat"); if (data != "") { \\本站点\Motor1Row = StrToInt(StrMid(data, 1, 10)); // 前10位存Motor1Row \\本站点\Motor2Row = StrToInt(StrMid(data, 11, 10)); // 中间10位 \\本站点\LastRecordRow = StrToInt(StrMid(data, 21, 10)); // 后10位 } - 在
SetReport函数每次成功写入后(启动/停止逻辑末尾),添加:// 将三个变量值格式化为30位字符串(各10位,不足左补0),写入文件 string saveData = StrFromInt(\\本站点\Motor1Row, 10) + StrFromInt(\\本站点\Motor2Row, 10) + StrFromInt(\\本站点\LastRecordRow, 10); FileWrite("C:\\KingView\\Runtime\\MotorRecord.dat", saveData);
注意:
FileWrite路径必须是组态王运行账户有写入权限的目录(如C:\KingView\Runtime),避免写到Program Files下因UAC被拒。
6.2 跨班次时长统计:用SQL报表替代普通报表
普通报表只能显示,不能聚合。要统计“今日总运行时长”,得手动加。升级方案:将记录存入Access数据库,用SQL报表查询。
实施要点:
- 新建Access数据库
MotorLog.mdb,建表RunLog,字段:ID(AutoNumber),MotorName(Text),StartTime(DateTime),StopTime(DateTime),DurationSec(Long) - 修改
SetReport停止逻辑:不再用ReportSetCellString,改用SQLInsert:string sql = "INSERT INTO RunLog (MotorName, StartTime, StopTime, DurationSec) VALUES ('" + name + "', #" + startStr + "#, #" + timeStr + "#, " + StrFromInt(timeLength, 10) + ")"; SQLExecute("MotorLogDSN", sql); // MotorLogDSN为已配置的ODBC数据源 - 在组态王中新建“SQL报表”,数据源选
MotorLogDSN,SQL语句:
这样就能实时看到每台电机当日累计运行小时数。SELECT MotorName, SUM(DurationSec)/3600.0 AS TotalHours FROM RunLog WHERE StartTime >= #2024-06-15 00:00:00# GROUP BY MotorName
6.3 我的习惯:每次部署必做的三件事
从那以后我每次给客户部署这个功能,都强制走一遍这三步:
- 先关掉所有PLC输出,用组态王“变量仿真器”手动置位
PLC_Motor1_Run,单步执行SetReport,用MessageBox逐行打印varRow、startStr、timeLength,确认数值流无异常; - 在报表设计界面,把第3、4、5列的“格式”全部设为“文本”,并右键列头→“自动调整列宽”,避免导出时错位;
- 在组态王“系统配置”→“退出命令”中,加入
FileWrite保存当前变量值,确保关机前最后一刻也落盘。
这三步多花10分钟,能省去后续80%的现场返工。希望帮到你。
本文还有配套的精品资源,点击获取