PLC定时器FB重复执行之谜:一次调用为何触发两次?

发布时间:2026/9/8 22:20:28
PLC定时器FB重复执行之谜:一次调用为何触发两次? 1. 第一现场输出两次动作代码却只有一处定时器调用设备是一台半自动包装机客户打电话过来时语气已经很急了“自动循环的时候下料气缸偶尔会连续动作两次有时候两秒内出两次产品直接被顶飞。”我第一反应是电磁阀有问题让现场先看阀芯是不是卡滞。结果他们把阀换新故障还在。于是又怀疑是接近开关抖动把感应头重新调了问题依然。折腾了一下午最后才把目光转回PLC程序。这台设备的控制用的是ST语言所有定时逻辑统一放在一个自封装的定时器FB里实例名叫Timer_Clamp_Delay。我远程连上程序翻到OB1找到这个FB的调用点一行清清楚楚。按常理OB1每个扫描周期执行一次定时器FB被调用一次输出就应该只出现一次。可现场监测到的结果却是同一个扫描周期里FB的输出出现了两次0→1变化而且时间间隔很规律。这个现象当时让我很困惑。代码里确实只有一处调用为什么执行了两次如果放在几年前我可能会怀疑是PLC本身出了问题或者软件有啥隐藏Bug。但后来我把整个程序结构和任务配置拉出来一看答案其实并不玄学——问题根本不在“调用行数”而在“执行路径”。只不过这种坑藏得比较深如果不了解PLC任务调度模型很难一下子想到。先用表格把当时的怀疑点排了个序这也是排查类似问题的一个基本思路怀疑方向检查内容结果硬件输出电磁阀线圈、输出继电器、接线端子更换后故障依旧输入信号传感器、限位开关、急停回路波形正常无抖动中间变量HMI脚本、上位机二次赋值没有发现写操作程序调用定时器FB在程序块中的调用点交叉引用显示不止一处任务调度循环任务、快速任务、事件任务发现同一实例被两个任务引用把交叉引用打开的那一刻基本就锁定方向了。所谓“代码里只有一处调用”指的是我绩效考核文档里记录的“OB1里写了一次”但程序实际的调用路径有两条。第二个调用点藏在一个后来添加的快速任务里平时很少有人去翻那个任务块而且调用位置在ST源码的最后几行缩进风格和注释都和主程序长得一模一样很容易被认为是重复代码块的一部分。这件事给我的第一个教训就是在PLC程序里“调用一次”是一个语义模糊的说法。你眼睛看到的一行调用和CPU真正执行的调用次数中间可能隔着任务调度、中断优先级、实例背景这些看不见的东西。2. PLC调用模型扫描周期里定时器FB到底执行了几次要搞明白为什么“调用一次”会“执行两次”得先把PLC的程序执行模型说清楚。很多人写ST程序时脑子里仍然是“代码从上往下跑一遍”的PC编程思维这在PLC上是要出事的。IEC 61131-3的ST语言本质上跑在周期性任务里。一个典型的PLC项目任务配置里通常有循环任务、快速任务、事件任务、中断任务等。循环任务按固定周期执行比如10ms、5ms快速任务可能只有1ms事件任务由某个硬件中断或通信事件触发触发后立刻插入执行。关键点在于当一个FB被安排在循环任务里调用时它每个扫描周期执行一次但如果同一个实例也被快速任务或事件任务调用那么在一个扫描周期内该FB就会执行两次。更麻烦的是快速任务和事件任务可以抢占循环任务也就是说定时器FB第一次执行到一半被打断然后第二次调用又开始执行执行完再回到第一次的现场继续跑。这种场景下FB内部的局部变量、静态变量、时间累计状态都会被打乱。那为什么定时器FB对这个坑尤其敏感因为定时器本质上是“时间累计器”。TON、TOF、TP这类IEC定时器每次调用时都会用当前系统时间减去启动时间计算是否达到设定值。如果同一个实例在一个周期内被调用两次它的内部时间基准会被刷新两次输出状态就可能出现提前翻转、重复触发甚至完全乱掉的情况。而普通FB如果只是算个数、传个值两次调用可能结果一致问题反而不明显。我用一个最简单的层次图说明这件事循环任务Task_Main10ms周期 └─ OB1 / 主程序 └─ 调用 FB_TimerTimer_Clamp_Delay 快速任务Task_Fast事件触发 └─ 脉冲捕捉程序 └─ 调用 FB_TimerTimer_Clamp_Delay这两个调用点共同指向同一个实例名Timer_Clamp_Delay也就是同一个背景数据区域。CPU执行时一个周期内会分别从两条路径进入这个FB。对于定时器来说相当于同一块秒表一个有两个人同时在按。这个案例里快速任务本来是为了捕捉一个高速计数信号才加的后来在调试过程中有人把“下料动作延时控制”的逻辑也顺手复制粘贴了进去还用了相同的实例名。这种操作在多人维护的现场项目里非常常见——上一任工程师留下一个任务块下一任工程师不知道里面有什么为了省事直接套用结果埋下隐患。3. 查交叉引用到锁定第二次调用完整排查链路如果你也遇到了类似“明明调用一次却执行两次”的情况别急着怀疑PLC固件或者编译器Bug先把交叉引用表调出来。这应该是排查流程的第一步而不是最后一步。在TIA Portal里选中定时器FB实例名右键点“交叉引用”系统会列出所有引用位置包括调用点、读取点、写入点。在CODESYS平台里对应的是“引用”窗口效果一样。当时我看到的列表是这样的序号所在块行号引用类型所在任务1OB1 / Main第5行FB调用Task_Main循环任务2Fast_Catch第23行FB调用Task_Fast快速任务3Timer_Clamp_Delay背景DB实例访问全局第一行是用户口中“我调用了一次”的那个点第二行就是藏起来的第二次调用。交叉引用一出来问题的轮廓立刻就清晰了。但光看到两个调用点还不够必须验证这两处调用是否真的都会被执行以及它们是否在同一个扫描周期内同时生效。我的做法分三步第一步把快速任务的触发条件找出来。这个任务的触发源是一个硬件中断来自编码器的高速脉冲信号。设备在正常运行状态下编码器脉冲一直在来所以快速任务会频繁触发。也就是说第二个调用点绝对不是“死代码”它是实实在在会被执行的。第二步临时把快速任务里的FB调用注释掉只保留OB1里的那一次然后让设备连续跑一小时。结果下料气缸的双动作现象完全消失。再把注释恢复把调用加回去故障立刻重现。这种“隔离验证法”虽然原始但比任何仿真都直观现场工程师最容易信服。第三步用在线监视和TRACE功能固化证据。把定时器FB的实例变量、Q输出、ET值全部拖进监视窗口同时打开PLC的TRACE功能记录信号跳变的时间戳。TRACE出来以后可以看到在一个扫描周期内Q输出出现了两次0→1跳变两次跳变之间的间隔和快速任务的触发周期完全吻合。这就从数据层面坐实了“两次执行”。这里有个容易忽略的细节ONLINE监控时如果你只看梯形图或者ST代码的“行状态”是很难发现双重执行的因为两个调用点分别在两个任务块里屏幕当前只能显示一个块。所以用交叉引用做静态排查用TRACE做动态验证两者缺一不可。4. 修复方案与代码改造让同一个实例只有一条执行路径找到根因之后修复本身并不复杂但怎么修得干净、修得以后不复发还是需要一点章法的。我当时给了现场工程师三个方案按推荐程度排序。方案一也是最推荐的明确“一个实例只归一个任务管”。定时器FB既然用于下料气缸的延时控制这个控制逻辑本质上是顺序流程的一部分应该只放在循环任务里跑。快速任务不再直接调用同一个定时器实例而是把编码器的高速信号经过简单处理后写到一个全局中间变量里由循环任务在下一次扫描周期读取。这种做法会有最多一个扫描周期的延迟但对于下料气缸这种毫秒级动作来说完全够用。方案二如果两个任务确实都需要独立计时那就各自声明不同的实例。比如循环任务用Timer_Clamp_Delay快速任务用Timer_Clamp_Delay_Fast两个实例互不干扰。这个方法适合那些对时间精度要求极高、两个任务确实需要并行计时的场景。缺点是要多占用一块背景数据区域而且将来维护时要特别小心别把两个实例的变量搞混。方案三改造定时器FB内部加入“忙碌标志”和“重入保护”。在FB内部增加一个静态变量第一次调用时置位第二次调用如果发现标志位已经置位就直接跳过计时逻辑。这种做法的本质是让FB自己能识别“我正在执行中”。但说实话这只是兜底不能替代前两个方案因为“重入保护”治标不治本而且容易掩盖程序结构上的混乱。我当时实际采用的是方案一改造后的ST代码结构大致是这样的// 快速任务 Task_Fast只负责采集高速信号不负责计时 IF bEncoderPulse THEN bPickRequest : TRUE; // 把脉冲转成一个普通电平请求 END_IF; // 循环任务 Task_Main定时器FB唯一调用点 Timer_Clamp_Delay( IN : bPickRequest AND NOT bPickDone, PT : T#1S200MS, Q bDelayOut); IF bDelayOut THEN bPickDone : TRUE; END_IF;这段代码把“信号获取”和“定时处理”彻底分开了。快速任务只做事件捕捉定时器FB只出现在主循环里。bPickRequest是跨任务传递的中间变量bPickDone用于防止重复触发。改造完成后我让设备连续空跑了四个小时又让现场试产了一整批产品双动作现象再没出现过。顺带提一个细节修改程序在线下载的时候定时器FB的背景数据会被重新初始化所以现场改完程序以后必须让设备回一次原点重新进入自动循环不要直接在半自动状态下测试否则可能因为残留状态误判修复效果。5. 这类“重复执行”坑的共性与防线这个案例虽然是定时器FB引发的但“同一个实例被多个调用路径执行”的问题在工控领域其实相当普遍。我这些年见过的重复执行场景至少有这么几类第一类是跨任务调用就是本文这个案例慢速任务和快速任务同时调用同一个FB实例。第二类是启动块和运行块都有调用比如OB100里调用一次做初始化OB1里又调用一次做运行控制上电那一刻定时器会被连续执行两次。第三类是循环体内的调用有人把FB调用写进了FOR循环里数组有10个元素一个周期就执行10次这种通常肉眼一眼能看见但程序大了以后也不容易发现。第四类是库函数重复展开某些集成库在编译时会自动展开多个调用点调试时发现FB被调用了两次但实际上你并没有在代码里看到第二个显式调用。针对这些坑我慢慢养成了几个比较固定的防御习惯。第一每新建一个定时器实例先给它命名命名里直接带上任务前缀。例如TaskMain_AutoTimer、TaskFast_PulseTimer这样交叉引用列表一拉哪个实例属于哪个任务一眼便知不容易出现“两个任务里用同名实例”的情况。第二编写ST程序时遵守“单写多读”原则。定时器FB这种有状态的块执行会改变内部状态属于“写操作”。一个实例在整个项目中只允许有一个写调用点其他任务如果需要它的输出结果只能读输出变量不能再次去调用FB本身。这跟高级编程语言里的并发控制是一个道理不过PLC工程师往往没这个概念。第三开发阶段就要养成查交叉引用的习惯。程序写完后不要急着下载调试先对每个关键FB做一次交叉引用检查确认调用点数量符合预期。这个动作成本很低但能拦下大量类似问题。我认识的一些资深工程师甚至把“交叉引用检查”写进了项目交付清单雷打不动。第四用隔离验证法处理模糊问题。任何时候当程序行为无法用直觉解释时别急着改代码先把怀疑对象的执行路径断掉看故障是否消失再逐步恢复。这种二分定位法虽然土但永远是排查这类问题最高效的方法。说到底PLC里的“执行一次”不是一个自然属性而是程序结构和任务调度共同作用的结果。代码写一行不等于CPU只跑一趟。搞清楚你的程序从任务根部到每个FB之间有几条路比盯着那一行调用代码可靠得多。这也是我这个案例里最想分享的一点。