1. 先把需求拆开:一句话背后的五件事
“白班夜班流量累计”这六个字,听起来像是个入门级练习——不就是个加法吗?我最初接这类需求的时候也这么想,结果第一次上线就被班组长堵在配电室门口问:“昨天夜班交接的时候,怎么少了 3 方水?”那次之后我才明白,流量累计在 PLC 里从来不是“加法”问题,而是时间基准、数值精度、状态保持和边界判定四件事叠在一起的工程问题,而“白班夜班”只是压在顶上的那层皮。
用西门子 PLC 做结构化编程,真正要解决的是:一台设备跑三班倒,瞬时流量从 4-20mA 模拟量或者脉冲口进来,我需要在任意时刻准确回答三个问题——这个班到现在累计了多少、上个班最终是多少、今天(生产日)累计了多少。“能显示”和“敢拿去结算”是两码事,中间隔着累加算法、班次边界、断电保持和精度设计这四道关卡。
这篇文章适合谁看?如果你手上有 S7-1200 或 S7-1500,需要给水、蒸汽、压缩空气、导热油这类介质做分班计量,或者你正在把一堆“复制粘贴出来的梯形图”重构成结构化程序,那接下来的内容基本可以照着抄。我不打算讲 FB、DB 是什么这种教科书内容,默认你至少写过几个功能块、知道什么是背景数据块。如果你连 SCL 都没碰过,也不用怕,核心逻辑只有二十来行,我会逐行拆开讲。
另外说一句我的立场:凡是涉及“结算”两个字的累计量,我坚决不用浮点数存总量。后面会给出具体原因和替代方案,这条原则贯穿全文。
2. 三种技术路线怎么选:别一上来就写积分
在动手写代码之前,先搞清楚“累计”这个动作到底发生在哪一层。我见过太多项目,问题根本不出在 PLC 程序,而是路线一开始就选错了。
2.1 路线一:仪表自己累计,PLC 只做差值和分班
电磁流量计、涡街、质量流量计,绝大多数自带内部累积器,通过 4-20mA、脉冲或者通信把累积值送出来。这种情况下 PLC 最省事的做法是:把仪表累积值读进来,每个扫描周期算本次值 - 上次值得到增量,再分别加到总累计和班累计上。
优点很明显:积分运算交给仪表做,PLC 只承担分班逻辑,扫描周期抖动、CPU 负载对这些数字没影响,精度取决于仪表本身。缺点也很实在:一旦仪表累积器溢出翻转、被现场人员手动清零、或者通信闪断导致读数跳变,PLC 侧的差值就会算出一个巨大的负数或者正数。所以这条路线的关键不在累加,而在差值合法性判断——我一般会加一条规则:单次差值超过“量程上限 × 采样周期 × 3”就判定为异常帧,丢弃并按 0 处理,同时置一个故障位给 HMI 报警。
还有一点容易被忽略:仪表的累积值寄存器位宽。很多表是 32 位无符号数,最大值 4294967295,如果脉冲当量是 0.01 m³,满打满算 4294 万方就翻转。选路线一之前,务必翻一下仪表手册确认累积器位宽和翻转策略。
2.2 路线二:PLC 侧做数值积分(模拟量 4-20mA)
仪表只给瞬时量(m³/h),累计完全由 PLC 算。这是改造项目里最常见的情况,也是坑最多的。核心公式就一条:
累计增量 = 瞬时流量(m³/h) × 采样间隔(s) ÷ 3600看起来人畜无害,但我见过至少三种翻车方式:采样间隔写死成 0.1 秒,实际循环中断被通信任务拖到 0.15 秒,累计值系统性偏小三分之一;瞬时值没做滤波,模拟量抖动让累计值一夜之间涨了 5%;还有直接拿 OB1 的扫描周期当采样间隔的,负载一变精度就跟着变。这条路线的正确姿势,后面第 4 章会详细讲——核心是用真实测得的间隔做积分步长,而不是用标称值。
2.3 路线三:脉冲当量累加(高速计数器)
脉冲输出的流量计(涡轮、椭圆齿轮、部分电磁)最干脆:来一个脉冲代表固定的体积,直接数脉冲就行,不涉及任何浮点积分。S7-1200/1500 用 HSC 高速计数器读脉冲数,每个周期取一次当前值,和上次值做差,差值乘以脉冲当量就是这段时间的体积。
这条路线精度最高、最抗扫描周期波动,因为脉冲是硬件计数的,哪怕 CPU 忙了一秒钟,脉冲一个都不会丢。代价是 HSC 资源有限(一块 CPU 就那么几个通道),而且 32 位计数器同样存在翻转问题,需要在软件里做回绕处理。
2.4 三条路线的取舍对照
| 维度 | 仪表累计差值 | PLC 数值积分 | 脉冲当量累加 |
|---|---|---|---|
| 依赖条件 | 仪表有累积寄存器 | 只要有瞬时量 | 仪表有脉冲输出 |
| 精度决定因素 | 仪表精度 | 采样间隔 + 数值精度 | 脉冲当量 + 硬件计数 |
| 扫描周期影响 | 无 | 大,必须实测步长 | 无 |
| 主要风险 | 溢出翻转、手动清零 | 积分漂移、浮点精度 | HSC 资源、回绕处理 |
| 我的推荐场景 | 通信/模拟量改造,表已投运 | 无法改造、只取 4-20mA | 新建项目、精度要求高 |
实际项目里经常是混着用的:主计量用脉冲,辅助监测用模拟量积分。但无论走哪条路线,分班逻辑这一层是完全共用的,所以下面我把这部分单独抽出来做成结构化模块,这也是标题里“结构化编程”四个字真正的价值所在。
3. 结构化编程的骨架:UDT + FB + 多实例
一个车间十几台流量计,如果每台都单独写一段梯形图,改一次班次定义要改十几处,这种代码我称为“一次性代码”——只能跑,不能维护。结构化的目标很朴素:分班规则只写一次,流量计只配参数。
3.1 为什么坚持“一块表一个 FB 实例”
我的做法是把整套累计逻辑封装成一个功能块FB_FlowTotalizer,现场有几块表就在一个全局 DB 或者 OB 的静态变量里建几个多重背景实例,用数组或者独立的实例名管理。
这样做的好处不是“看起来高级”,而是三件很具体的事:第一,班次定义(几点换班)只存在于一个地方,工艺改了上夜班时间,改一处全部生效;第二,所有表的数据结构完全一致,HMI 上可以用一个 faceplate 循环显示,不用为每块表单独画画面;第三,出了问题只查一个块的代码,不用在十几段网络里翻。
代价也有:首次调试时必须确认每个实例的 InOut 参数绑定正确,绑定错了不会报错,只会在几个月后对账时发现数字不对。所以我的习惯是,在 FB 里加一个InstName : String的输入参数,把表位号传进去,HMI 上报警时能直接显示“FT-103 累计异常”,而不是“某个实例”。
3.2 数据类型与命名规范
命名这件事在 PLC 里比在高级语言里更重要,因为梯形图监控时你只能看到变量名。我沿用的规则是加前缀:i_输入、o_输出、io_输入输出、stat_静态、tmp_临时。看起来啰嗦,但好处是在交叉引用列表里一眼能看出方向。
类型选择上有几条硬性经验:
- 瞬时流量用
Real,别用Int。模拟量经过 NORM_X/SCALE_X 出来天然是浮点; - 累计总量用
LInt(64 位整数),单位取 0.001 m³,绝对不要用Real,原因下面细说; - 班次编号用
Int,不要用Bool表示白班夜班,因为将来大概率会扩成三班甚至四班; - 时间戳用
DTL,别用DInt存秒数,交接班报表上写着“1970 年”很难看。
3.3 接口设计:Input、InOut、Output 怎么分
这是结构化编程里最容易做歪的地方。我的划分原则是:
VAR_INPUT放每周期都会变的“过程量”,比如瞬时流量、班次号、清零按钮。这些是调用方算好传进来的。
VAR_IN_OUT放需要跨周期保存、而且必须保持的数据,比如各类累计值、残差、上次班次号。用 InOut 而不是 Static 的原因是想把数据块和逻辑块分开——逻辑块升级版本时,数据块里的历史累计值不会被覆盖。这一点在改造项目里救命:你给客户更新了 FB 版本,结果人家三年的总量数据全清零,这个事故我见过一次,之后所有累计量我都放独立的数据块里。
VAR_OUTPUT只放给 HMI 或者上位机看的换算结果,比如以 m³ 为单位的实时累计值。这些值每周期重算,不保持。
3.4 UDT 与保持性设置:断电不丢数的关键
数据块我用两个 UDT 来组织:UDT_FlowCfg存配置(量程上下限、脉冲当量、单位系数、班次切换点),UDT_FlowData存运行数据(各类累计、残差、上次班次、上次时间戳)。
关键动作在 TIA Portal 的 DB 编辑器里:把UDT_FlowData里所有累计相关的变量,在“保持性”那一列勾上。不勾的后果是断电重启后全部归零,而这个错误在调试阶段几乎发现不了——因为你调试的时候很少真去断电。
另外提醒一个真实存在的坑:CPU 的保持性存储区是有上限的。有些小型号默认只分配了一小块,你在 DB 里勾了一堆 Retain,编译能过,下载也能跑,但超出部分实际不保持。上线前一定要到 CPU 属性里看一眼实际可用保持性内存,把十几块表的累计量加起来的字节数和这个上限对一下。我一般还会额外留 30% 余量,因为后面总会加东西。
最后是块访问方式。优化块访问(Optimized)下变量名访问、支持保持性设置,但没法用绝对地址;标准块访问能用绝对地址但保持性设置受限。新项目我一律用优化访问,因为结构化编程本来就靠符号寻址,绝对地址反而是负担。
4. 核心算法落地:SCL 代码逐段讲
这一章是全文的重点。下面这段代码是我现在项目里的底稿,去掉了一些项目特有的报警逻辑,保留核心骨架。
4.1 积分步长:别再用固定的 0.1 秒
第一个要解决的问题是采样间隔。哪怕你把 FB 放在 100 ms 的循环中断 OB 里调用,实际间隔也不可能是精确的 100 ms——通信任务、中断嵌套、块执行时间都会让这个周期抖动。如果按标称值 0.1 秒积分,流量越大、运行时间越长,累计偏差越大。
正确做法是在块内部实测间隔。S7-1200/1500 提供RUNTIME指令,它返回两次调用之间实际经过的时间:
// 实测两次调用之间的间隔,单位转换为秒 #tDelta := #instRun(IN := TRUE, MEM := #statLastTick); #rDelta := TIME_TO_DINT(#tDelta) / 1000.0; // 首次调用或异常跳变时用标称值兜底 IF #rDelta <= 0.0 OR #rDelta > 5.0 THEN #rDelta := #Cfg.NominalCycle; #Fault := TRUE; ELSE #Fault := FALSE; END_IF;MEM参数必须是静态变量或者 DB 变量,第一次调用返回 0,所以要有兜底分支。如果你手上的固件版本里RUNTIME用起来别扭(不同版本对它的封装形式略有差异),用TIME_TCK取毫秒计数做差是完全等效的替代方案,精度差一点但足够。
那个> 5.0的判断是干嘛的?如果 CPU 停机再启动,两次调用之间可能隔了几个小时,这时候用一个巨大的DeltaT去积分,会瞬间算出一个离谱的增量。碰到这种情况我宁愿丢掉这段时间的流量,也不要往累计值里灌一个假数。
4.2 整数化累计:把 REAL 精度问题挡在门外
这是我认为整篇文章最值钱的一段。
Real是 32 位浮点,尾数只有 24 位,大约 7 位有效十进制数字。假设某台表的总累计已经到了 1000000.0 m³,这个数量级下Real能分辨的最小增量大约是 0.0625。如果你的采样周期是 100 ms、瞬时流量是 10 m³/h,那么每一拍的增量只有 0.00028 m³——这个数加到一个百万级的 Real 上,直接被舍入吃掉,一点都不剩。表现出来就是:总量到某个数值之后就不涨了,或者涨得明显比实际慢。
解决办法是把累计量换成整数,单位取足够小:
本次增量(0.001 m³ 为单位) = 瞬时流量 × 采样间隔 ÷ 3600 × 1000然后用“残差法”处理不到一个最小单位的那部分:
// 本次增量,换算成 0.001 m³ 的整数单位 #rInc := #FlowPV * #rDelta / 3600.0 * 1000.0; // 残差累加,保证不足一个单位的零头不会丢 #Data.Residual := #Data.Residual + #rInc; #diWhole := REAL_TO_DINT(#Data.Residual); IF #diWhole <> 0 THEN #Data.AccAbs := #Data.AccAbs + #diWhole; #Data.AccShift := #Data.AccShift + #diWhole; #Data.AccDay := #Data.AccDay + #diWhole; #Data.Residual := #Data.Residual - DINT_TO_REAL(#diWhole); END_IF;Residual始终保持在 0 到 1 之间(对正向流量而言),这样截断产生的误差永远不累积,最多只是当前这一拍还没进位。很多人会担心“截断是不是每次都丢一点”,用残差法就不存在这个问题,因为零头被存下来了,下一拍接着攒。
为什么用LInt而不是DInt?算一下就知道:DInt上限约 21.47 亿,以 0.001 m³ 为单位就是 214.7 万 m³。一条 100 m³/h 的管线跑满,大约两年半就溢出。LInt是 64 位,上限 9.22×10¹⁸,换算下来是 9.22×10¹⁵ m³,基本上可以认为永不溢出。多占 4 个字节换十年安心,这笔账怎么算都值。
注意:如果现场存在双向流(比如某些回水、倒灌工况),
REAL_TO_DINT是向零截断,残差法在负值区间会失效。这种情况必须改成正向、反向两个独立的计数器,各自用残差法累加,显示时再相减。
4.3 班次切换与跨零点的“生产日”
班次判定看着简单,其实最容易埋雷。我的原则是:班次判断必须放在累加之前。
如果先累加再判断班次,交接班那一刻产生的增量会被记到新班头上,虽然只有 0.1 秒的量,但账就是不平的。现场对账时,两个班的数加起来和总量差个零点几方,解释起来非常麻烦。顺序调换一下,问题就没了:
// 1) 班次切换:先把上一班结果搬走,再清零本班 IF #ShiftID <> #Data.LastShift THEN #Data.AccPrev := #Data.AccShift; #Data.AccShift := 0; #Data.LastShift := #ShiftID; END_IF; // 2) 生产日切换:跨零点的夜班算同一个生产日 IF #ProdDay <> #Data.LastDay THEN #Data.AccDay := 0; #Data.LastDay := #ProdDay; END_IF;这里引出一个非常实际的问题:夜班跨零点,怎么算天。如果按自然日切,一个 20:00 到次日 08:00 的夜班会被劈成两半,报表上难看,统计也难受。所以我引入了“生产日”这个概念——从当天早上 8 点(白班开始)到次日早上 8 点算一个生产日,跨零点的夜班整体归属前一个生产日。
实现上不要让 PLC 去猜,而是由调用方算好ProdDay传进来:取当前本地时间的日期,如果小时数小于白班起始小时,就减一天。这个计算放在一个独立的 FC 里,所有流量计实例共用:
// FC_GetShiftInfo:输出班次号与生产日 #dtNow := RD_LOC_T(); // 以白班起始时刻为界计算生产日 IF #dtNow.HOUR < #Cfg.DayShiftStartHour THEN #ProdDay := DINT_TO_INT(#dtNow.DAY) - 1; ELSE #ProdDay := DINT_TO_INT(#dtNow.DAY); END_IF; // 班次号:白班起始点至关班时刻为 1,其余为 2 IF (#dtNow.HOUR >= #Cfg.DayShiftStartHour) AND (#dtNow.HOUR < #Cfg.NightShiftStartHour) THEN #ShiftID := 1; ELSE #ShiftID := 2; END_IF;月份边界(1 号的“前一天”)需要额外处理,我通常直接算儒略日序号,避免日期相减带来的麻烦。这种方式依赖 CPU 实时时钟,所以时钟必须准确。如果 CPU 掉电后时钟只能靠超级电容维持几十小时,长期停机再上电,时间就是错的,班次也跟着错。我的做法是让上位机定期校时,或者干脆加一个开机时钟校验,发现年份小于某个合理值就报警提示人工校时,而不是默默用错误时间跑。
4.4 完整 FB 代码
把上面几段拼起来,去掉项目特有的报警分支,就是下面这个骨架:
FUNCTION_BLOCK "FB_FlowTotalizer" { S7_Optimized_Access := 'TRUE' } VERSION : 1.2 VAR_INPUT FlowPV : Real; // 瞬时流量,工程单位 m³/h ShiftID : Int; // 班次编号 1=白班 2=夜班 ProdDay : Int; // 生产日序号 Reset : Bool; // 手动清本班 END_VAR VAR_IN_OUT Cfg : "UDT_FlowCfg"; // 配置参数 Data : "UDT_FlowData"; // 运行数据(全部保持) END_VAR VAR_OUTPUT TotalAbs : Real; // 总累计 m³ TotalShift : Real; // 本班累计 m³ TotalDay : Real; // 本生产日累计 m³ TotalPrev : Real; // 上一班累计 m³ Fault : Bool; // 采样异常标志 END_VAR VAR instRun : "RUNTIME"; tDelta : Time; rDelta : Real; rInc : Real; diWhole : DInt; rtReset : R_TRIG; statTick : Time; END_VAR BEGIN // ---- 1. 实测采样间隔 ---- #tDelta := #instRun(IN := TRUE, MEM := #statTick); #rDelta := TIME_TO_DINT(#tDelta) / 1000.0; IF (#rDelta <= 0.0) OR (#rDelta > 5.0) THEN #rDelta := #Cfg.NominalCycle; #Fault := TRUE; ELSE #Fault := FALSE; END_IF; // ---- 2. 班次与生产日切换(务必在累加之前)---- IF #ShiftID <> #Data.LastShift THEN #Data.AccPrev := #Data.AccShift; #Data.AccShift := 0; #Data.LastShift := #ShiftID; END_IF; IF #ProdDay <> #Data.LastDay THEN #Data.AccDay := 0; #Data.LastDay := #ProdDay; END_IF; // ---- 3. 残差法整数化累加 ---- #rInc := #FlowPV * #rDelta / 3600.0 * #Cfg.UnitFactor; #Data.Residual := #Data.Residual + #rInc; #diWhole := REAL_TO_DINT(#Data.Residual); IF #diWhole <> 0 THEN #Data.AccAbs := #Data.AccAbs + #diWhole; #Data.AccShift := #Data.AccShift + #diWhole; #Data.AccDay := #Data.AccDay + #diWhole; #Data.Residual := #Data.Residual - DINT_TO_REAL(#diWhole); END_IF; // ---- 4. 手动清本班 ---- #rtReset(CLK := #Reset); IF #rtReset.Q THEN #Data.AccShift := 0; #Data.Residual := 0.0; END_IF; // ---- 5. 换算输出 ---- #TotalAbs := LINT_TO_REAL(#Data.AccAbs) / #Cfg.UnitFactor; #TotalShift := LINT_TO_REAL(#Data.AccShift) / #Cfg.UnitFactor; #TotalDay := LINT_TO_REAL(#Data.AccDay) / #Cfg.UnitFactor; #TotalPrev := LINT_TO_REAL(#Data.AccPrev) / #Cfg.UnitFactor; END_FUNCTION_BLOCKUnitFactor就是最小计数单位对应的数量,取 1000 时单位是 0.001 m³。如果某台表流量特别小(比如实验室用的微量计量),可以改成 100000,单位变成 0.00001 m³,代价是溢出时间缩短。这个值要根据实际流量范围算一下,原则是:最小单位至少比典型瞬时增量小两个数量级,同时保证十年内不溢出。
4.5 梯形图维护版怎么写
结构化不等于所有东西都用 SCL。我的实际做法是核心算法用 SCL 写,但给现场维护人员留一个“看得懂”的接口层。
具体来说,在梯形图里调用这个 FB 时,输入端只接三样东西:模拟量换算后的瞬时流量、来自公共 FC 的班次号和生产日。输出端接 HMI 变量和几个报警比较指令。真正的累加过程被封装在块里,维护人员需要做的只是“看块引脚上的数对不对”,而不需要理解残差法。
我还会在 FB 的属性里写好注释和版本号,把关键的几个参数(标称周期、单位系数、白班起始时刻)做成可在 HMI 上查看的只读显示,现场工艺想确认“夜班到底从几点开始”,直接看画面就行,不用找我要源程序。这件事做一次,后面能省掉无数次“你帮我查一下”的电话。
5. 断电、重启与异常工况的兜底
程序跑通只是开始,真正考验设计的是各种非正常状态。这一章讲的都是我在现场被教过的内容。
5.1 首次扫描与断电补偿
CPU 从 STOP 切到 RUN 的那一瞬间,RUNTIME返回的是一个巨大的值,我在 4.1 里已经用> 5.0的判断挡住了。但还有另一种情况:正常运行中有人按了急停,设备停了十分钟,流量为零,那么这段时间的“流量”本来就是 0,不累加是对的。真正麻烦的是停机期间有流量流过但没被计量——比如旁路阀开着,管线还在流动。
这种情况没办法靠软件补偿,只能在方案设计阶段跟工艺说清楚:要么加装本地机械累积器,要么在 HMI 上做一个“停机补录”功能,由操作工手工输入补录量,程序把它加到一个独立的AccManual变量里,报表上单独列一栏。我不建议把手工补录量直接混进自动累计,一旦混进去,以后所有的对账都说不清楚。
首次扫描还有一件事要做:初始化LastShift和LastDay。如果这两个变量保持性生效,首次启动时它们已经有值,不会误触发切换;如果没生效(新下载、或者存储区被清),它们都是 0,第一拍就会触发一次班次切换,把AccPrev覆盖成 0,AccShift清 0。这个副作用本身不大,但会让人误以为“程序在乱清零”。我的做法是在启动 OB 里判断LastShift = 0时,直接把当前班次号赋进去,跳过切换逻辑。
5.2 班组交接班报表的数据组织
只有当前班的数还不够,班组长要的是“上一班干了多少”。所以除了AccPrev,我还会在数据块里维护一个环形缓冲,把每班的最终结果连同时间戳存下来。
结构体大概是这样的:班次号、生产日、班开始时间、班结束时间、该班累计量、该班平均瞬时流量、异常标志。环形缓冲区长度取 90 或者 180,够存一两个月的班次记录。写入时机就是班次切换那一拍,把上一班的数据打包写进环形数组的下一个位置,索引自增、到头回零。
这里有个细节值得说:写环形缓冲和清本班计数必须在同一个周期里完成,中间不能有跳转。我见过一个程序,把写记录放在班次切换后的第二个周期,结果遇到一次意外断电正好卡在中间,那一班的数据就永久丢了。逻辑上把这几步写在一起,是成本最低的保险。
报表数据往后怎么出去?三条常见路径:HMI 上做历史数据记录直接存曲线;通过 CPU 的 Web 服务器导出 CSV;或者用通信把数组周期性传给定上位机落库。小项目我一般就用 HMI 的历史数据,简单可靠;有 MES 对接需求的,走通信传结构体,比让 HMI 存数据库省心。
5.3 数据自检:总量守恒校验
这是我很喜欢加的一个功能,成本极低,收益极大。逻辑是:在一个完整的生产日结束时,检查
昨日白班累计 + 昨日夜班累计 ≈ 昨日生产日累计误差如果在几个最小单位以内(考虑取整),判定为正常;如果差得离谱,说明有班次漏切、有实例没被调用、或者有断电丢数,直接置一个诊断位并记录时间。这个校验不需要额外硬件,纯粹是逻辑上的自我验证,但能提前几周发现那些“要等到月底对账才暴露”的问题。
同样的思路可以横向扩展:所有流量计的总量之和,和总管上的流量计总量做对比,偏差超过 2% 就报警,这基本上就是免费的泄漏检测。
6. 现场调试与排坑实录
调试这一章我写得比较散,因为很多问题不是按顺序出现的,是随机蹦出来的。我把印象最深的几条整理出来。
6.1 上线前的五步验证
我的固定流程是这样的,缺一步我都不敢签字移交。
第一步,静态注入测试。不接现场信号,直接在 HMI 或者强制表里给FlowPV写一个固定值,比如 36 m³/h,跑十分钟,看累计是不是 6 m³。这一步验证单位和系数,绝大多数“差 3.6 倍”“差 60 倍”的问题在这一步就能抓到。
第二步,采样周期扰动测试。故意在程序里给循环中断 OB 加负载(在别的 OB 里做一段延时循环),观察累计值是否依然准确。如果这一步偏差明显,说明积分步长没用实测值。
第三步,班次切换测试。把 CPU 时钟临时改到交接班前一分钟,观察切换是否在预期时刻发生、AccPrev是否正确搬运、本班是否归零。这一步最好录屏,因为手动改时间容易错过那一瞬间。
第四步,断电保持测试。这是最容易被跳过的一步。累计跑出一定数值后,直接断电,等两分钟再上电,看数值是否原样恢复。如果归零了,回去检查 DB 的保持性勾选和 CPU 的保持性存储区容量。
第五步,长周期对比。有条件的项目,让 PLC 累计跑 24 小时,同时用一台仪表或者人工抄表做对比。偏差在 0.5% 以内我认,超过就得查原因,不能靠“大概差不多”糊过去。
6.2 常见问题速查表
下面这张表是我这些年攒下来的,基本上涵盖了九成现场反馈。
| 现象 | 最可能的原因 | 排查方向与处理 |
|---|---|---|
| 总量到某个数后不再增长 | 用Real存总量,精度被吃光 | 改成LInt存最小单位,残差法累加 |
| 累计值比实际偏小固定比例 | 积分步长用了标称周期,实际更长 | 改用RUNTIME或TIME_TCK实测间隔 |
| 断电重启后全部归零 | DB 变量未设保持性,或超出保持区容量 | 检查 DB 属性与 CPU 保持性内存上限 |
| 交接班时两班之和与总量不等 | 先累加后切班,那一拍被记到新班 | 把班次判断提到累加之前 |
| 夜班数据被拆成两天 | 按自然日切换而非生产日 | 引入“生产日”概念,跨零点归前一班 |
| 累计值突然跳变一个巨大数 | 仪表累积值翻转,或通信断帧导致跳变 | 加单次差值合法性判断,超限丢弃 |
| HMI 上的数比 PLC 里少一点 | 换算时用了不同单位系数 | 统一在 FB 输出端换算,HMI 不再二次计算 |
| 单台表正常,多台一起跑就漂 | 循环中断周期被拖长,实例过多 | 精简块内逻辑,控制在 100 ms 内跑完 |
| 时钟不准导致班次错乱 | CPU 掉电后时钟丢失未校时 | 上位机定期校时,加时钟合理性校验 |
6.3 几条花钱买来的经验
第一条,不要相信“标称周期”这四个字。我曾经在一个项目里,循环中断设的是 100 ms,但因为同一个 OB 里还塞了十台设备的通信轮询,实际周期跑到 180 ms 左右,累计值偏小将近一半。客户第一周没发现,因为产量本来就波动;第二周对账才炸。从那以后我所有的累计块都用实测间隔。
第二条,每块表的实例名要能对得上现场位号。有一次夜里被叫去处理“累计不准”,查了两个小时,最后发现是两块表的模拟量通道在硬件组态里接反了。如果程序里有位号字符串显示,五分钟就能定位。这个习惯看着可有可无,真出事的时候能救命。
第三条,给清零操作加权限和留痕。手动清零按钮如果谁都能按,迟早会有人按错。我的做法是:清零需要 HMI 上的一个权限等级,且每次清零都把操作时间、清零前的数值写进一个小的操作记录数组。后来真出过一次纠纷,靠这条记录说清楚了是谁在几点清的零。
第四条,班次定义要能在画面上改,但改完必须重启生效。工艺调整班次时间这种事,一年也就一两次,做成随时可改反而容易出事——改到一半,正好卡在切换点,整个班的归属就乱了。我一般做成“参数可改,标注需重启 PLC 生效”,简单粗暴但可靠。
第五条,别在累计块里做通信。有些方案会把累计值直接发给上位机,写在同一个 FB 里。一旦通信超时阻塞,整个累计逻辑跟着卡顿。累计块就老老实实做累计,通信放到别的 OB 里,用数据块做隔离,各干各的。
7. 一点延伸:这套骨架还能怎么用
写完这段之后我发现,FB_FlowTotalizer这个结构其实是个通用模板:“带班次边界的工程量累计”。把内部的累加公式换掉,它就变成了电量累计(kWh)、蒸汽质量累计(t)、产量计数(件)、运行时长累计(h)。
比如设备运行时长统计,把FlowPV换成“运行信号为 1 则输入 1.0,否则 0.0”,单位系数改成 3600(秒转小时),得到的就是一台设备的分班运行小时数。我上个月就用这个改法,半天时间给一个车间的十几台电机加了运行时间统计,代码一行没重写,只换了参数绑定的对象。
再往前一步,如果把这些累计值按时间戳存成数组,配合一次简单的趋势计算,就能算出班内平均负荷、峰值出现时刻这类工艺指标。这些东西在 HMI 上做一个柱状图就能看,成本几乎为零,但对生产管理的价值比单纯的累计值大得多——毕竟班组长真正关心的不只是“用了多少”,而是“什么时候用得最多、能不能挪一挪”。
我个人在实际项目里的体会是:结构化编程最大的回报不在写代码的那一刻,而在半年后你要改需求、加设备、查故障的时候。当初多花两天把 UDT 划清楚、把保持性配好、把测试步骤走完,后面能省下几十个被电话叫醒的夜晚。这笔账,做过夜班维护的人都懂。