
1. 报警闪一下就消失问题到底出在哪做过产线设备的人大概率都遇到过这种场景HMI 上一条报警记录刚跳出来操作工还没来得及看清是哪个工位报的画面就恢复正常了。等设备停下来去查历史报警发现记录里干干净净什么也没留下。操作工说刚才明明报了警工程师翻遍日志找不到证据最后只能归咎于可能是干扰或者看错了吧。这种报警闪一下就消失的现象在三菱 iQ-R 系列 PLC 配合 GX Works3 开发的产线程序里特别常见。核心原因其实不复杂报警信号本身是一个瞬态脉冲而程序里只做了实时判断没有做状态锁存。当触发条件满足的那一个扫描周期报警位被置 ON下一个扫描周期条件消失报警位又变回 OFF。如果 HMI 的刷新周期比这个脉冲宽或者操作工的眼睛没跟上这条报警就等于没发生过。标题里说的历史欠账指的就是这个——该留下的记录没留下该追溯的问题追溯不到。设备停机时间、故障频次、责任判定全都成了糊涂账。而解决这个问题的关键工具就是 iQ-R 里的FB功能块配合锁存逻辑用ST 语言写一个可复用的报警锁存功能块再借助R_TRIG这类边沿检测指令把瞬态信号抓住。这篇文章面向的是已经上手 iQ-R、写过一些梯形图但还没系统用过 FB 和 ST 的工程师也适合那些被报警留不住折磨过、想找一个稳定可复用方案的人。我会从设计思路讲到具体代码再到实际调试中踩过的坑尽量把每一步的为什么说清楚让你看完能直接抄作业也能理解背后的逻辑遇到变种需求自己能改。2. 报警锁存这件事为什么值得单独做个 FB2.1 瞬态信号为什么必须锁存先把这个物理事实讲透。PLC 是循环扫描的一个扫描周期可能只有几毫秒甚至更短。假设某个报警条件是气缸到位传感器 200ms 内没信号这个条件在某个瞬间成立PLC 检测到之后置位报警。但如果这个条件只持续了一个扫描周期就恢复了那么如果程序里写的是OUT或者直接赋值报警位下一个周期就灭了HMI 的轮询周期通常是 100ms 到 500ms很可能根本没读到这个 ON历史报警记录如果依赖 HMI 侧采集那这条记录直接丢失。这就是闪一下就消失的本质。锁存Latch的作用就是把这个瞬态 ON 状态保持住直到有人为确认Acknowledge或者复位Reset才清除。这跟单片机里的锁存器概念是一回事——把某一时刻的状态记住而不是让它随输入变化。2.2 为什么用 FB 而不是直接写梯形图很多人的第一反应是我在梯形图里加一个自保持回路不就行了报警条件 OR 报警位再串一个NOT 复位输出到报警位。这个做法没错能解决单条报警。但一条产线动辄几十上百条报警如果每条都手写自保持会带来几个问题代码量爆炸每条报警三四个触点一百条就是几百行维护起来眼花。复位逻辑分散有的报警要手动复位有的要自动复位有的要延时复位散落在各处改一个忘一个。无法统一管理想做全部报警确认、报警计数、首出报警First Out这些高级功能时没有统一的数据结构很难下手。FB 的价值就在于把一条报警抽象成一个可复用的模块。你定义好输入报警条件、复位信号、确认信号和输出锁存位、报警状态、时间戳然后每条报警只需要调用一次这个 FB传入不同的参数即可。代码量下来了逻辑统一了后续加功能也只在 FB 内部改一处。2.3 ST 语言在这个场景下的优势iQ-R 支持梯形图、ST、FBD 等多种语言。做报警锁存这种带条件判断、边沿检测、状态机的逻辑ST 语言比梯形图更清晰。原因很直接条件判断用IF...THEN...ELSIF...ELSE一目了然不用堆一堆并联串联触点边沿检测、计数器、定时器可以直接调用函数块代码紧凑状态机逻辑用CASE语句写比梯形图的步进继电器清爽得多。当然不是说梯形图不能做而是同样的逻辑ST 写出来更短、更易读、更容易复用。对于习惯了梯形图的老工程师ST 的门槛其实不高基本语法半天就能上手后面我会把用到的语法都解释清楚。3. 核心细节拆解锁存逻辑到底怎么设计3.1 一条报警的完整生命周期在动手写代码之前先把一条报警从产生到消失的完整过程理清楚。这样设计出来的 FB 才不会漏状态。触发报警条件成立比如传感器超时、温度超限产生一个瞬态信号。锁存FB 检测到这个信号把报警位置 ON 并保持。呈现HMI 读取报警位显示报警条记录历史。确认操作工点击确认报警从未确认变为已确认但报警位可能仍然 ON因为故障还在。复位故障排除后操作工或程序发出复位信号报警位清 OFF。归档报警记录写入历史包含发生时间、确认时间、复位时间。一个设计良好的报警 FB应该把这六个阶段都覆盖到。很多简易方案只做了第 2 步结果就是报警能留住但没法区分已确认和未确认也没法统计持续时间。3.2 R_TRIG 边沿检测抓住那一瞬间标题热词里出现了R_TRIG这是三菱 PLC 里的上升沿检测指令。它的作用是当输入从 OFF 变 ON 的那一个扫描周期输出一个周期的 ON 脉冲。为什么报警锁存需要它考虑一个场景报警条件是一个持续信号比如温度一直超限。如果你直接用这个信号去置位报警那没问题。但如果报警条件本身是脉冲式的比如通信超时这种瞬时事件你就需要 R_TRIG 来捕捉它的上升沿确保即使信号只 ON 一个周期也能被可靠地锁存。在 ST 里R_TRIG 的用法是这样的VAR trig_Alarm : R_TRIG; END_VAR trig_Alarm(CLK : xAlarmCondition); IF trig_Alarm.Q THEN xAlarmLatch : TRUE; END_IF;这里trig_Alarm是一个 R_TRIG 实例CLK是它的输入Q是输出。每个扫描周期调用一次当xAlarmCondition上升沿到来时Q为 TRUE 一个周期我们就在这个周期里把锁存位置位。注意R_TRIG 实例必须声明为 FB 的静态变量VAR 而不是 VAR_TEMP否则每次调用状态会丢失边沿检测就失效了。这是新手最容易踩的坑之一。3.3 锁存、确认、复位三个状态的关系很多人把确认和复位混为一谈其实它们是两件事确认Acknowledge操作工知道这条报警了但故障可能还没排除。报警位仍然 ON只是标记为已确认。复位Reset故障排除了报警位清 OFF报警消失。为什么要分开因为在很多产线上操作工确认报警后需要去现场处理处理期间报警应该继续显示提醒还没解决但颜色或图标可以变化表示已知晓。如果确认就直接复位那报警一确认就消失操作工转头就忘了还没处理。所以 FB 内部至少要有两个状态位xAlarmActive报警是否激活和xAcknowledged是否已确认。复位信号到来时两个位一起清。3.4 首出报警First Out的考虑在多条报警同时触发的场景下首出报警非常重要——它告诉你第一个报的是谁往往就是根因。比如一条产线上电机过载导致传送带停传送带停又导致下游缺料报警。如果你只看报警列表可能以为是缺料实际根因是电机过载。首出报警的实现依赖锁存每条报警锁存的同时记录一个全局的首出标志和首出编号。第一个锁存的报警占据首出位置后续报警不覆盖它直到全部复位。这个功能在 FB 里可以通过一个全局结构体来实现后面代码部分会讲。4. 实操过程从零写一个报警锁存 FB4.1 环境准备与工程结构先确认你的环境硬件三菱 iQ-R 系列 PLC如 R08CPU、R16CPU 等软件GX Works3版本建议 1.050 以上对 ST 和 FB 支持更完善工程语言新建工程时选择 iQ-R 系列语言可以选梯形图为主ST 作为 FB 内部语言。工程结构建议这样组织FB 定义放在FB 定义文件夹下命名如FB_AlarmLatch全局标签定义报警相关的全局变量如报警数组、首出标志主程序在扫描程序里调用 FB每条报警一次调用HMI 接口报警位、确认位、复位位映射到 HMI 地址。这样分层的好处是FB 是纯逻辑不依赖具体地址全局标签是数据层主程序是调用层。换项目时FB 直接复用只改标签和调用。4.2 FB 的接口定义先定义 FB 的输入输出。我习惯用x前缀表示 BOOLi表示 INTt表示 TIMEs表示 STRING。FUNCTION_BLOCK FB_AlarmLatch VAR_INPUT xCondition : BOOL; // 报警条件瞬态或持续 xAck : BOOL; // 确认信号HMI 按钮 xReset : BOOL; // 复位信号故障排除后 sAlarmName : STRING[32]; // 报警名称用于记录 END_VAR VAR_OUTPUT xActive : BOOL; // 报警激活锁存后 xAcked : BOOL; // 已确认 xNewAlarm : BOOL; // 新报警脉冲用于触发记录 tActiveTime : TIME; // 报警持续时间 END_VAR VAR trig_Cond : R_TRIG; // 条件上升沿 trig_Ack : R_TRIG; // 确认上升沿 trig_Reset : R_TRIG; // 复位上升沿 ton_Duration : TON; // 持续时间计时 END_VAR这里几个设计点解释一下xCondition既支持瞬态也支持持续信号因为内部用了 R_TRIG 抓上升沿持续信号只在上升沿触发一次锁存后续保持。xAck和xReset都用 R_TRIG避免 HMI 按钮按住不放导致重复触发。xNewAlarm是一个单周期脉冲用来触发历史记录写入避免每个扫描周期都写。tActiveTime用 TON 累计报警持续时间方便统计。4.3 核心逻辑实现下面是 FB 的主体逻辑用 ST 写// 边沿检测 trig_Cond(CLK : xCondition); trig_Ack(CLK : xAck); trig_Reset(CLK : xReset); // 报警锁存 IF trig_Cond.Q AND NOT xActive THEN xActive : TRUE; xAcked : FALSE; xNewAlarm : TRUE; ELSE xNewAlarm : FALSE; END_IF; // 确认处理 IF trig_Ack.Q AND xActive THEN xAcked : TRUE; END_IF; // 复位处理 IF trig_Reset.Q THEN xActive : FALSE; xAcked : FALSE; END_IF; // 持续时间计时 ton_Duration(IN : xActive, PT : T#24H); tActiveTime : ton_Duration.ET;逐段解释边沿检测段三个 R_TRIG 实例分别检测条件、确认、复位的上升沿。注意这里trig_Cond检测的是xCondition如果报警条件是持续的那只在第一次 ON 时触发如果条件断开再闭合会再次触发这符合再次报警的语义。锁存段当条件上升沿到来且当前未激活时置xActive清xAcked并输出一个xNewAlarm脉冲。ELSE分支把xNewAlarm清掉保证它只 ON 一个周期。确认段确认信号上升沿且报警激活时置xAcked。注意这里不改变xActive报警仍然显示。复位段复位信号上升沿时清xActive和xAcked。这里没有判断报警条件是否还在因为复位是人为动作表示我知道怎么处理了清掉吧。如果你希望条件还在时不允许复位可以加一个AND NOT xCondition的判断。计时段TON 在xActive为 TRUE 时计时ET是已计时时间。PT 设为 24 小时超过就饱和避免溢出。4.4 在主程序里调用FB 定义好之后在主程序里为每条报警创建一个实例。假设你有 16 条报警可以定义一个 FB 实例数组VAR_GLOBAL fbAlarms : ARRAY[1..16] OF FB_AlarmLatch; xAlarmCond : ARRAY[1..16] OF BOOL; xAlarmAck : ARRAY[1..16] OF BOOL; xAlarmReset : ARRAY[1..16] OF BOOL; xAlarmActive : ARRAY[1..16] OF BOOL; END_VAR然后在扫描程序里循环调用FOR i : 1 TO 16 DO fbAlarms[i]( xCondition : xAlarmCond[i], xAck : xAlarmAck[i], xReset : xAlarmReset[i] ); xAlarmActive[i] : fbAlarms[i].xActive; END_FOR;这样 16 条报警共用一套逻辑新增报警只需要扩展数组和条件映射FB 本身不用动。实操心得FB 实例数组在 iQ-R 里是静态分配的16 个实例占用的内存很小不用担心。但要注意 FB 内部的 R_TRIG 和 TON 实例也是每个 FB 实例独立的所以数组调用时每个元素的状态是隔离的不会互相干扰。4.5 首出报警的实现首出报警需要一个全局结构VAR_GLOBAL iFirstOutIndex : INT : 0; // 首出报警编号0 表示无 xFirstOutValid : BOOL : FALSE; END_VAR在 FB 里增加一个输出xFirstOut或者在主程序里判断FOR i : 1 TO 16 DO IF fbAlarms[i].xNewAlarm AND NOT xFirstOutValid THEN iFirstOutIndex : i; xFirstOutValid : TRUE; END_IF; END_FOR; // 全部复位后清除首出 IF NOT xAlarmActive[1] AND NOT xAlarmActive[2] ... THEN xFirstOutValid : FALSE; iFirstOutIndex : 0; END_IF;更优雅的做法是在 FB 里加一个xFirstOutClaim输出主程序用一个统一的仲裁逻辑处理。这样 FB 保持纯粹仲裁逻辑集中在一处。5. 常见问题与排查技巧实录5.1 报警锁不住还是闪一下就消失这是最常见的问题。排查顺序检查 R_TRIG 实例是否声明为静态变量。如果声明在VAR_TEMP里每次调用状态清零边沿检测永远不触发。这是头号坑。检查 FB 是否每个扫描周期都被调用。如果 FB 放在某个条件分支里条件不满足时不调用那 R_TRIG 的状态就断了。FB 必须无条件每周期调用。检查报警条件是否真的到达了 FB 输入。有时候条件在梯形图里被其他逻辑屏蔽了到 FB 时已经是 FALSE。检查 HMI 读取的地址是否正确。FB 的输出要映射到 HMI 能读的地址如果映射错了逻辑对了但 HMI 看不到。5.2 报警复位后立刻又报这种情况通常是报警条件还在。比如温度超限报警操作工复位了但温度还没降下来下一个扫描周期条件仍然成立R_TRIG 检测到上升沿因为之前复位时条件可能刚好断开又闭合又触发一次。解决办法有两个复位时判断条件IF trig_Reset.Q AND NOT xCondition THEN才复位条件还在就不让复位。加延时复位后加一个短延时比如 2 秒再允许重新触发避免抖动。我一般用第一种逻辑清晰操作工也能理解故障没排除不能复位。5.3 多条报警同时触发首出判断不准如果多条报警在同一个扫描周期触发FOR循环里先判断到的会占据首出位置。这时候首出取决于数组顺序不一定反映真实根因。改进方法给每条报警加一个优先级首出仲裁时优先选优先级高的。或者用时间戳记录每条报警的触发时刻精度到毫秒首出取时间最早的。iQ-R 可以用TIME()或者系统时钟来打时间戳。5.4 报警持续时间统计不准TON 的ET在IN为 FALSE 时会清零。如果你希望报警复位后还能看到上次持续时间需要在复位时把ET保存到一个单独的变量里比如tLastDuration。IF trig_Reset.Q THEN tLastDuration : ton_Duration.ET; xActive : FALSE; END_IF;这样 HMI 可以显示本次报警持续了 XX 分钟即使报警已经复位。5.5 常见问题速查表现象可能原因排查方法解决报警闪一下就消失R_TRIG 实例非静态检查 VAR 声明改为 VAR 静态变量报警锁不住FB 未每周期调用检查调用位置移到无条件扫描段复位后立刻重报条件仍成立监控条件位复位加条件判断首出不准同周期多触发检查触发时序加优先级或时间戳持续时间清零TON 特性检查 ET 读取时机复位时保存 ETHMI 看不到报警地址映射错对比标签与 HMI修正映射避坑技巧调试报警逻辑时善用 GX Works3 的监视功能把 FB 内部的xActive、xAcked、trig_Cond.Q都加到监视窗口。很多时候问题一眼就能看出来——比如trig_Cond.Q从来没 ON 过那就说明边沿检测没工作直接查 R_TRIG 实例声明。6. 几个让方案更稳的进阶技巧6.1 报警分组与批量确认产线上报警多了之后操作工不可能一条条确认。可以按工位或按类型分组每组一个批量确认按钮。实现上在 FB 里加一个xGroupAck输入主程序把组确认信号广播给组内所有 FB 实例。FOR i : 1 TO 16 DO fbAlarms[i].xAck : xAlarmAck[i] OR xGroupAck[GetGroup(i)]; END_FOR;GetGroup是一个映射函数返回报警所属组号。这样组确认和单条确认可以并存。6.2 报警抑制Suppress设备调试或维护时某些报警会频繁触发干扰正常操作。可以加一个抑制功能维护模式下指定报警不锁存、不显示。在 FB 里加xSuppress输入逻辑上IF trig_Cond.Q AND NOT xSuppress THEN xActive : TRUE; ... END_IF;注意抑制只影响新报警已经锁存的报警不受影响。抑制解除后如果条件还在下次上升沿会重新触发。6.3 报警历史记录的数据结构如果 PLC 侧要存历史记录不依赖 HMI可以定义一个环形缓冲区TYPE ST_AlarmRecord : STRUCT iIndex : INT; sName : STRING[32]; tOccur : TIME; tAck : TIME; tReset : TIME; xAcked : BOOL; END_STRUCT END_TYPE VAR_GLOBAL aAlarmHistory : ARRAY[1..100] OF ST_AlarmRecord; iHistoryHead : INT : 1; END_VAR每条新报警写入一条记录iHistoryHead循环递增覆盖最旧的记录。100 条记录占用的内存很小iQ-R 完全扛得住。HMI 可以按需读取这个数组显示历史报警列表。6.4 与 HMI 的对接要点HMI 侧需要读取的地址每条报警的xActive、xAcked首出报警编号iFirstOutIndex报警总数、未确认数可以在 PLC 侧算好HMI 直接读。HMI 写入的地址每条报警的xAck、xReset组确认信号抑制信号。对接时注意HMI 的按钮信号是按住为 ON而 FB 内部用 R_TRIG 检测上升沿所以按钮按一下就能触发一次不用做自复位。但如果 HMI 刷新慢可能出现按钮信号持续多个 PLC 周期R_TRIG 也只触发一次没问题。实操心得HMI 和 PLC 的地址映射建议用标签Label而不是直接地址GX Works3 支持全局标签HMI 侧导入标签表即可避免地址对不上。改地址时只改标签定义两边自动同步。7. 写在最后的一点个人体会这套 FB 锁存方案我在几个产线项目里用过从最初的单条报警自保持到后来做成 16 路、32 路的 FB 数组再到加首出、加分组确认、加历史记录基本覆盖了中小型产线的报警需求。最大的感受是报警逻辑一定要在项目初期就设计好不要等到调试阶段再补。后期补锁存往往要动很多已有代码还容易引入新问题。另外ST 语言写 FB 这件事一开始可能会觉得不如梯形图直观但写过两三个 FB 之后就会发现复杂逻辑用 ST 表达确实清爽。尤其是条件判断和状态机ST 的代码量可能只有梯形图的三分之一。建议还在犹豫的同行找个简单的功能块练练手比如做个延时报警或者计数器 FB熟悉之后再做报警锁存这种稍复杂的。最后分享一个小技巧FB 写好后先在仿真环境里跑一遍用强制输入模拟报警条件的瞬态脉冲观察锁存、确认、复位三个状态的变化。仿真通过再下载到实机能省下不少现场调试时间。iQ-R 的仿真功能挺好用别浪费了。