前阵子调试一台设备碰到一个特别诡异的毛病设备上一个用ST写的定时器功能块程序里明明只调用了一次可在监控画面里看输出它硬是执行了两回。第一遍动作结束之后隔了几百毫秒又来了一遍。现场操作工问我是不是程序改错了我说不可能这段逻辑我翻来覆去看了好几遍调用点就一个FB的实例也就用了那一次。但事实摆在眼前定时器就是多输出了一次。后来我花了大半天时间排查从交叉引用一路查到OB配置最后才搞清楚问题出在哪儿。回头想想这坑踩得其实挺典型的尤其是用ST写定时器FB的朋友大概率都会遇到。所以我把整个排查过程和根因分析整理成文分享出来希望能帮各位少走点弯路。这个内容比较适合做设备程序开发的工程师、调试工程师以及那些习惯用ST语言封装功能块的同行参考。整个排查思路不限于某一款PLC凡是支持FB、支持ST编程的环境逻辑都是通的。1. 故障现象一台设备的幽灵输出1.1 现场描述多出来的一次动作设备上有一段顶料机构的控制逻辑核心是一个用ST写的定时器FB功能是检测到顶料到位信号之后延迟800ms给下一个气缸发动作指令防止两个气缸同时伸出产生机械干涉。程序结构很简单OB1周期扫描里调用这个FBFB内部用的是TON定时器做延时。但调试的时候发现气缸每完成一次动作流程最后总会莫名其妙地再抖一下。监控定时器FB的Q端输出波形很清楚第一次Q置位正常延时结束输出动作复位之后大约过了不到1秒Q又置位了一次。从逻辑上看整个动作循环的触发条件只有一个而且一条扫描周期里FB只被调用了一次可输出就是不对。1.2 第一反应先查交叉引用遇到这种事程序员的直觉永远是先怀疑调用点。我当时打开交叉引用表把那个FB实例搜了个遍确认在OB1里只调用了一次OB100、OB10这些组织块里也没有涉及。查完调用点又把定时器的输入变量查了一遍确认触发信号没有在程序其他位置被重复置位。这一轮排查下来程序逻辑层面确实没问题。但问题恰恰就出在这里——我以为程序逻辑没问题实际上有个层面我压根没注意到。2. 排查过程从代码正常到系统异常的三次转折2.1 交叉引用与调用点检查代码确实只有一处调用先说结论交叉引用检查本身没有白做。它可以排除最直接的错误重复调用同一个FB实例。在博途或者TIA Portal里交叉引用能看到一个FB的实例被哪些位置调用双击就能跳到对应的网络。我当时把调用列表拉出来里面只有一条记录就是OB1里的那个网络。顺带一提如果你的程序里用的是多重背景Multi-instance交叉引用查看时要注意展开ARRAY或者结构体里的实例路径有些隐藏引用不会直接在顶层显示。我当时查的时候差点漏掉一个通过多重背景间接调用的引用后来展开看了虚惊一场。2.2 扫描周期的时间窗口分析定时器值在跳动既然调用没问题那我开始怀疑是不是定时器本身出了问题。我用监控表把TON的当前值ET和输入条件IN一起盯住发现一个有意思的现象ET走到800ms置位Q之后IN信号确实变FALSE了ET也跟着归零。但过了一段时间IN又莫名其妙地变成了TRUEET重新开始计时。这个现象说明什么说明驱动FB的输入条件在FB外部又被动过了。我查了一圈没有任何一个程序段给这个输入变量赋值。那只有一个可能——这个输入连接的是一个物理输入点或者另一个设备输出的映射而那个映射值变了。结果一查发现这个到位信号是从一个通信模块映射过来的通信模块那端的PLC程序里把这个信号同时输出给了两个不同的地址。2.3 多任务与中断OB配置中藏着的问题通信模块那边为什么会出现两次输出追根究底是那个PLC的OB配置里同一个程序段被放进了两个不同的OB一个OB1还有一个时间中断OB。这两个OB都调用了封装通信数据的功能块而这个功能块里的逻辑会把同一个信号映射到两个地址上。这一下就豁然开朗了。FB本身确实只调用了一次但FB所依赖的外部输入在同一个系统里被另一套逻辑又驱动了一次。从调用次数上看定时器FB的程序确实只执行了一次但从功能上看它的输入条件被重复触发导致定时器完成了两次计时周期。这个小节的关键在于排查问题不能只盯住目标FB本身而是要从它的输入条件往前倒推检查整个信号链路。尤其是当信号来自通信模块、第三方设备或者上位机映射时更要警惕存在双重驱动的隐患。3. 根因剖析FB、实例和定时器三者之间的关系3.1 FB与FC的本质区别存储区的记忆功能要理解这个坑得先把FB和FC的区别弄明白。FB功能块和FC函数在IEC 61131-3里有本质区别FB有背景数据块Instance DB可以存储内部状态FC没有所有变量都是临时的调用结束就释放。定时器之所以要用FB来实现就是因为它需要记忆。TON定时器的工作过程本质上是一个累加器输入IN为TRUE时ET值在每次扫描周期累加当ET达到预设时间PT时Q置位。如果这个累加值不能保存那定时器每次扫描都会从零开始永远也到不了设定时间。所以用FC封装定时器是不现实的除非你用全局变量存值但那是另一套玩法。FB的存储能力决定了它可以被记住上一次执行时内部状态。这既是FB的优势也是出问题的根源——如果两个调用点共享同一个实例或者外部逻辑反复触发输入条件FB的记忆就会产生你预期之外的行为。3.2 背景数据块与多重实例的映射关系说到实例就不得不提多重背景。多重背景是西门子PLC里很常用的功能一个FB内部可以声明另一个FB作为静态变量然后在程序里直接调用这个静态变量对应的方法。这样做的好处是可以少建很多背景数据块程序结构也更紧凑。但多重背景用多了实例之间的关系就变得盘根错节。如果两个不同的FB里都实例化了同一个底层FB比如同一个定时器FB而这两个FB又被同一个OB调用那么底层定时器的状态就会在两个FB之间互相干扰。具体表现就是你在一个地方调用它看起来正常换个地方再调用数据就串了。我在排查时把通信模块那边的程序打开发现它把信号映射功能做成了一个FB而这个FB在两个OB中各实例化了一次。两个实例虽然物理上是两个独立的数据区但功能逻辑上都在给同一个通信映射地址赋值等于双重驱动。3.3 定时器FB在ST中的工作流程拆解这里把定时器FB在ST里的常见写法拆开看一下。一个标准的TON定时器FB核心逻辑大致如下FUNCTION_BLOCK MyTimer VAR_INPUT IN : BOOL; PT : TIME; END_VAR VAR_OUTPUT Q : BOOL; ET : TIME; END_VAR VAR startTime : TIME; running : BOOL; END_VAR IF IN THEN IF NOT running THEN startTime : CURRENT_TIME; // 记录开始时刻 running : TRUE; END_IF ET : CURRENT_TIME - startTime; Q : ET PT; ELSE running : FALSE; ET : T#0MS; Q : FALSE; END_IF END_FUNCTION_BLOCK这段代码逻辑本身没有问题IN为TRUE时累加计时达到PT后输出QIN为FALSE时复位。但请注意这个FB的状态完全依赖IN信号。如果IN在外部被另一只手重新置成TRUE那么FB就会像新的一样再走一遍完整的计时流程而且这一切在程序调用逻辑上完全看不出来因为调用次数确实只有一次。换句话说我这次遇到的FB调用1次却执行2次本质是FB输入信号被外部重复驱动但如果你只看调用关系永远找不到问题。4. 解决方案针对重复执行问题的四种处理方式4.1 独立实例化不给串数据留机会排查清楚根因之后接下来的修改就很明确了。首先是程序架构层面通信模块那边的信号映射FB不能再让它在两个OB里各实例化一次。正确的做法是只保留一个实例把这个实例放在OB1里调用然后把映射结果存到一个全局数据块中另一个OB里的逻辑只负责读取全局数据块不再直接调用功能块。这样改造之后即使两个OB仍然都在执行但它们一个写一个读不会出现重复驱动输入信号的问题。独立实例化的核心原则是一个FB实例只在一个地方被调用多个地方需要共享数据用全局DB中转。4.2 多重背景的命名规范与核查方法在多重背景下这个问题更容易被忽略。因为多重背景的实例是嵌在宿主FB里的有时候你看程序表面只有一个调用点但实际上宿主FB被调用了几次底层实例就会跟着执行几次。我建议在使用多重背景时务必建立一套命名规范。比如每个底层FB实例的前缀要能看出宿主归属例如Conveyor_Timer_Left、Conveyor_Timer_Right尽量不要用泛化的名字像Timer1、Timer2在交叉引用中勾选显示所有实例选项将多重背景的实例路径全部展开确认每个底层实例的调用位置每次新增多重背景调用时养成全局搜索宿主FB调用次数的习惯确保底层实例不会因为宿主FB被多处调用而产生重复。这次排查中我花了不少时间在区分底层实例被重复调用和宿主实例被重复调用上。说实话这两种情况的表象一模一样只有把交叉引用彻底展开才能看清。4.3 从任务与中断层面锁死执行次数OB配置是另一个容易被忽视的盲区。很多工程师包括我自己在写程序时习惯把所有逻辑都丢进OB1但实际项目中时间中断OB、延时中断OB经常会承担一些周期性任务。如果一个功能块同时被OB1和OB10调用那它在一次主循环里就被执行了两次这属于程序逻辑上只写了一处但执行层面执行了两处。预防的方法是建立OB使用清单在程序架构设计阶段明确每个OB的职责。OB1只跑主循环逻辑定时中断OB只跑周期采样或通讯处理两者之间通过全局DB交互数据尽量避免同一段程序被多个OB调用。如果确实需要在多个OB中使用同一个功能块那么必须为每个OB创建独立的实例并明确它们的数据交互方式。最安全的办法是一个OB内只调用某个功能块的一个实例不同OB用不同实例实例之间通过全局DB同步。4.4 程序逻辑互锁最后一道防线架构层面的修改能解决问题但为了彻底防止类似情况再次发生我还在定时器FB本身加了互锁逻辑。具体来说就是增加一个触发完成的标志位只有当标志位处于未完成状态时IN信号才有效一旦定时完成标志位置位除非收到外部复位信号否则即使IN再变TRUEFB也不会重新计时。这等于在FB内部加了一道保险丝程序架构哪怕再有疏漏FB本身也不会反复动作。这个思路类似硬件电路里的互锁简单粗暴但很有效。需要注意的是加互锁逻辑会增加一点FB的复杂度。我的经验是对于设备保护类、安全联锁类的定时器逻辑务必加上对于普通动作控制视情况而定。5. 这个坑的教训与工程实践建议5.1 排查重复执行问题的通用步骤这次排查下来我总结了一套排查功能块感觉执行了两次问题的通用步骤在这里分享给各位第一步交叉引用检查。确认目标FB到底被调用了几次同时注意展开多重背景检查隐藏实例。第二步输入信号链路追溯。从FB的输入条件开始往前倒顺着数据流找源头看有没有第二处代码给输入信号赋值或者信号是否来自通信映射/上位机。第三步OB与任务配置检查。列出所有OB的调用关系确认有没有多个OB调用同一段逻辑或者某个OB被多种事件触发。第四步定时器输出波形监控。用监控表或者数据记录功能把定时器的ET、Q、IN三个值放在一起长时间观察看是否存在IN先变FALSE再变TRUE的异常波形。第五步如果还是找不到那就把程序二分砍半。把可疑的外部输入暂时写到固定值看问题是否消失逐步缩小可疑范围。这五步走完99%的幽灵执行问题都能定位到根因。这次我基本是走了一遍这套流程最后卡在了第二步和第三步的交叉点上。5.2 功能块封装时的最小化接口原则乱插一句这次的问题也让我反思了功能块封装的问题。通信模块那边的信号映射FB之所以会在两个OB里重复调用很大程度是因为设计时没想清楚这个FB到底由谁负责。一个功能块的接口越少越不容易被误用。如果那个信号映射FB只暴露一个执行输入其他数据都在内部管理外面的人就不会动起多次调用的心思。所以我现在的习惯是功能块接口尽量少暴露控制性输入能写死的参数就写死不能写死的放在全局配置里统一管理。5.3 调试阶段必做的验证动作最后说一个调试阶段的习惯。以前我做程序联调喜欢看到动作对了就往下走很少做严格的状态验证。这次踩坑之后我养成了一个习惯所有用ST写的定时器FB验收阶段都要做一次输入抖动测试。具体操作是给定时器输入一个不稳定的信号手动反复切换观察输出是否出现预期之外的重复动作。同时在监控画面里把定时器的ET值和扫描周期时间也加上确认ET的增长是平滑的、每周期递增的而不是跳变或者重复归零。这个测试动作看起来简单但真的能暴露很多隐蔽问题。我后来在另一个项目里就是因为这个测试发现了一个通信信号周期性跳变的问题提前处理了没让它炸到现场。个人观点工控程序里最坑的问题往往不在算法多复杂而在状态管理不清。定时器FB本身就是状态机它所依赖的外部信号又是另一个状态机两个状态机之间一旦出现你动一下我动一下的耦合就会呈现出这种看起来不可能的故障。希望这篇内容能帮各位少踩一次坑。