☰
西门子PLC白班夜班流量累计:结构化编程与精度设计
2026/11/4 10:47:28 网站建设 项目流程

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_BLOCK

UnitFactor就是最小计数单位对应的数量,取 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 划清楚、把保持性配好、把测试步骤走完,后面能省下几十个被电话叫醒的夜晚。这笔账,做过夜班维护的人都懂。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询