1. 报警为什么总是"闪一下就没了"
1.1 一个几乎每个PLC工程师都遇到过的场景
设备正在运行,触摸屏上突然跳出一条报警,操作工还没来得及看清内容,报警就消失了。等你去查历史记录,发现什么都没留下。操作工说"刚才明明报警了",你查程序发现报警位确实触发过,但就是没锁住。
这种情况在iQ-R系列PLC上尤其常见,原因也很直接:大多数报警信号是瞬时信号。比如一个热继电器动作,可能只持续几百毫秒就恢复了;一个伺服驱动器的报警输出,在故障排除后自动复位。如果你的程序只是把报警位直接映射到HMI显示,那它闪一下消失就是必然的。
我在一个包装线项目上就吃过这个亏。客户反映"偶尔会停机,但查不到原因"。到现场蹲了两天,终于抓到一次:伺服驱动器报了一个瞬时过载,PLC程序里报警位只ON了大概200ms,HMI的刷新周期是500ms,根本没来得及显示。操作工只看到设备停了,不知道发生了什么。
这个问题的本质不是"报警信号太短",而是程序没有做锁存处理。报警信号来了又走,你的程序必须把它"抓住",直到操作工确认之后才放行。这就是FB锁存要解决的核心问题。
1.2 锁存和自保持有什么区别
很多人会把锁存和自保持混为一谈,觉得都是"把信号保持住"。但实际使用中,两者有本质区别。
自保持(SET/RST方式)是最简单的做法:报警信号ON时SET一个位,确认按钮按下时RST这个位。这种做法能用,但有几个明显的坑:
- 如果同一个报警位在确认后再次触发,你需要额外的逻辑来处理
- 多个报警同时触发时,确认逻辑容易混乱
- 没有记录报警的发生时间、持续时间等关键信息
- 程序里到处散落着SET/RST指令,维护起来很痛苦
FB锁存则是把报警处理封装成一个功能块,每个报警实例独立管理自己的状态。它不仅能锁存报警,还能记录报警的发生时刻、确认时刻、持续时间,甚至能区分"首次报警"和"重复报警"。更重要的是,FB的封装特性让程序结构清晰,100个报警和10个报警的代码复杂度几乎一样。
我现在的习惯是:任何需要追溯的报警,一律用FB锁存,不用SET/RST。多花十分钟写FB,后面省下的是几十个小时的排查时间。
1.3 iQ-R平台上做锁存的天然优势
iQ-R系列PLC在报警处理上有几个别的平台比不了的优势,这也是我为什么推荐在iQ-R上认真做FB锁存的原因。
第一,iQ-R的标签编程(Label Programming)让FB的接口定义非常清晰。你可以给每个报警定义一个有意义的标签名,比如Alarm_ConveyorOverload,而不是用M1001这种毫无意义的地址。调试的时候一眼就能看出是哪个报警。
第二,iQ-R的结构化文本(ST)支持非常完善。用ST写FB的逻辑,比梯形图清爽得多,尤其是涉及时间戳记录、状态机切换这些操作时,ST的可读性远超梯形图。
第三,iQ-R的SD卡数据记录功能可以直接把FB里记录的报警历史写到CSV文件。这意味着你不需要额外的SCADA系统,就能实现报警历史的持久化存储。对于中小型设备来说,这个功能太实用了。
第四,iQ-R的模块化设计让FB可以跨项目复用。我现在的做法是维护一个标准的报警FB库,新项目直接导入,改改参数就能用。这套东西用了三年多,从包装线到装配线到检测设备,基本没怎么大改过。
2. 报警FB的接口设计:哪些引脚必须有
2.1 输入引脚的设计逻辑
一个报警FB的输入引脚,看起来简单,但设计不好后面会很麻烦。我经过多个项目的迭代,现在固定用这几个输入:
| 引脚名 | 数据类型 | 说明 | 是否必须 |
|---|---|---|---|
| i_xTrig | BOOL | 报警触发信号 | 必须 |
| i_xAck | BOOL | 确认按钮 | 必须 |
| i_xReset | BOOL | 复位/清除 | 必须 |
| i_sName | STRING | 报警名称 | 建议 |
| i_nPriority | INT | 报警优先级 | 可选 |
| i_xEnable | BOOL | 使能 | 可选 |
i_xTrig是报警源,直接接你的报警条件。这里有个细节要注意:不要在这个引脚前面加延时或滤波。很多人喜欢在报警信号上加一个TON延时,防止抖动误报。我的建议是把这个延时做在FB内部,用参数控制,而不是在外面加。原因很简单:如果延时在外面,FB就不知道报警信号是什么时候真正开始的,记录的时间戳就不准了。
i_xAck是确认按钮。这里的关键是确认和复位要分开。确认是操作工看到了报警,复位是报警条件消失了。这两个动作在时间上可能间隔很久。如果只有一个按钮,操作工确认之后报警就消失了,但实际上报警条件可能还在,这就造成了"假复位"。
i_xReset是复位。只有在报警条件消失且已确认的情况下,复位才有效。这个逻辑必须在FB内部实现,不能靠外部逻辑保证。
i_sName是报警名称,用STRING类型。iQ-R对STRING的支持很好,可以直接在HMI上显示。我一般会把报警名称和报警代码都写进去,比如"E1023 输送带过载"。
2.2 输出引脚的设计逻辑
输出引脚的设计直接决定了HMI上能显示什么信息。我常用的输出引脚:
| 引脚名 | 数据类型 | 说明 |
|---|---|---|
| o_xActive | BOOL | 报警激活中(锁存) |
| o_xUnAck | BOOL | 未确认报警 |
| o_xNewAlarm | BOOL | 新报警(单扫描周期) |
| o_tTrigTime | TIME | 触发时间戳 |
| o_tAckTime | TIME | 确认时间戳 |
| o_dDuration | DINT | 持续时间(秒) |
| o_nCount | INT | 触发次数 |
o_xActive是锁存后的报警位,直接接HMI的报警显示。这个位在报警触发时ON,在复位时OFF。注意:确认不会让这个位OFF,只有复位才会。
o_xUnAck是未确认报警位。这个位在报警触发时ON,在确认时OFF。HMI上可以用这个位来做闪烁效果——未确认的报警闪烁,已确认的报警常亮。
o_xNewAlarm是新报警位,只在报警首次触发的那个扫描周期ON。这个位用来触发报警记录、声音、弹窗等一次性动作。
o_tTrigTime和o_tAckTime是时间戳。iQ-R可以用TIME()函数获取当前时间,或者用RTC指令读取实时时钟。我一般用RTC,因为TIME()是系统运行时间,断电就归零了,而RTC是实时时钟,断电靠电池保持。
o_dDuration是持续时间,单位秒。这个值在报警复位时计算并保持,用来做报警统计。
o_nCount是触发次数。同一个报警在确认后再次触发,计数加一。这个值对于分析"反复报警"的问题非常有用。
2.3 为什么不用iQ-R自带的报警功能
iQ-R的GX Works3里有一个"报警"功能,可以设置报警条件、报警消息、报警历史。很多人会问:既然PLC自带了,为什么还要自己写FB?
我试过用自带的报警功能,结论是:简单场景够用,复杂场景不够灵活。
自带报警功能的问题在于:
- 报警消息是静态的,不能动态拼接变量值。比如你想显示"当前温度85度,超过上限80度",自带功能做不到
- 报警历史存储在PLC的内部缓冲区,容量有限,而且导出不方便
- 报警的确认和复位逻辑是固定的,不能自定义
- 不能做报警分级、报警抑制、报警延时等高级功能
自己写FB虽然前期投入大一点,但后面想怎么改就怎么改。而且FB一旦写好,复用成本几乎为零。
3. 用ST写报警FB的核心逻辑
3.1 状态机的设计
报警FB的核心是一个状态机。我一般用四个状态:
- IDLE:无报警
- ACTIVE:报警触发,未确认
- ACKED:已确认,但报警条件还在
- WAIT_RESET:报警条件消失,等待复位
状态转移逻辑:
IDLE -> ACTIVE: i_xTrig = TRUE ACTIVE -> ACKED: i_xAck = TRUE ACKED -> IDLE: i_xTrig = FALSE AND i_xReset = TRUE ACTIVE -> WAIT_RESET: i_xTrig = FALSE WAIT_RESET -> IDLE: i_xReset = TRUE这个状态机保证了几个关键行为:
- 报警触发后必须确认才能复位
- 报警条件消失后,如果没确认,报警仍然锁存
- 确认后如果报警条件还在,报警保持激活状态
- 只有确认且条件消失后,复位才有效
3.2 ST代码实现
下面是我实际项目中用的报警FB的ST代码框架。这个代码在iQ-R上跑了三年多,稳定性没问题。
(* 报警锁存FB - 状态机部分 *) CASE i_nState OF 0: (* IDLE *) o_xActive := FALSE; o_xUnAck := FALSE; o_xNewAlarm := FALSE; IF i_xTrig THEN i_nState := 1; o_xActive := TRUE; o_xUnAck := TRUE; o_xNewAlarm := TRUE; o_tTrigTime := GetRTC(); o_nCount := o_nCount + 1; END_IF; 1: (* ACTIVE - 未确认 *) o_xActive := TRUE; o_xUnAck := TRUE; o_xNewAlarm := FALSE; IF i_xAck THEN i_nState := 2; o_xUnAck := FALSE; o_tAckTime := GetRTC(); ELSIF NOT i_xTrig THEN i_nState := 3; END_IF; 2: (* ACKED - 已确认 *) o_xActive := TRUE; o_xUnAck := FALSE; IF NOT i_xTrig THEN i_nState := 3; END_IF; 3: (* WAIT_RESET - 等待复位 *) o_xActive := TRUE; o_xUnAck := FALSE; IF i_xReset THEN i_nState := 0; o_xActive := FALSE; o_dDuration := CalcDuration(o_tTrigTime, GetRTC()); END_IF; END_CASE;这段代码有几个关键点:
第一,o_xNewAlarm只在状态0到状态1的转移中ON一个扫描周期。这个位用来触发报警记录和声音,不能持续ON,否则会反复触发。
第二,时间戳用GetRTC()函数获取。这个函数是我自己封装的,内部调用iQ-R的RTC读取指令,返回一个TIME类型的数据。用RTC而不是系统运行时间,是为了保证断电后时间戳仍然有意义。
第三,o_dDuration在复位时才计算。这样即使报警持续了很长时间,持续时间也能准确记录。
第四,o_nCount在每次触发时加一。这个计数在复位时不清零,用来统计同一个报警的触发次数。如果需要清零,可以加一个单独的复位引脚。
3.3 时间戳的处理细节
时间戳看起来简单,但在PLC里处理起来有几个坑。
坑一:RTC读取的格式。iQ-R的RTC指令返回的是BCD码格式的年月日时分秒,需要转换成TIME类型。我封装了一个GetRTC()函数,内部做BCD到二进制的转换,然后计算从某个基准时间(比如2000年1月1日0点)到当前的秒数,返回DINT类型。
坑二:时间戳的存储。TIME类型在iQ-R里是32位有符号整数,单位是毫秒。如果从2000年开始算,大概49天就会溢出。所以我的做法是用DINT存储秒数,需要显示的时候再转换成日期时间格式。
坑三:断电保持。如果PLC断电,RTC靠电池保持,但FB内部的状态变量(比如o_tTrigTime)如果不设置断电保持,就会丢失。我的做法是把所有报警FB的状态变量都放在断电保持区域,这样即使断电,报警历史也不会丢。
坑四:时间同步。如果设备联网,最好用SNTP做时间同步。iQ-R支持SNTP客户端功能,可以定期从时间服务器同步RTC。这样多台设备之间的报警时间戳才能对齐,方便集中监控。
4. 报警历史的存储与导出
4.1 用SD卡做报警记录
iQ-R的CPU模块自带SD卡插槽,这是做报警历史存储最方便的方案。我的做法是:每个报警FB在触发时,把报警信息写入一个数据记录文件。
具体实现方式:
- 在SD卡上创建一个CSV文件,比如
AlarmLog.csv - 每次报警触发时,用
SP.DATWR指令把一行数据追加到文件末尾 - 数据格式:时间戳、报警名称、报警代码、优先级、触发次数
这个方案的优点是简单、可靠、不依赖外部系统。缺点是SD卡的写入寿命有限,不能太频繁地写。我的经验是:每分钟写入不超过10次,SD卡用个五六年没问题。
如果报警触发太频繁,可以做一个缓冲:先把报警记录写到PLC的内部缓冲区,每5分钟批量写入SD卡一次。这样既保证了实时性,又减少了SD卡的写入次数。
4.2 在HMI上显示报警历史
报警历史存在SD卡上,但操作工不可能去看CSV文件。所以需要在HMI上做一个报警历史画面。
我的做法是用iQ-R的文件读取指令,把CSV文件的内容读到一个字符串数组里,然后在HMI上显示。具体步骤:
- 在HMI上创建一个报警历史画面,用一个列表控件显示
- PLC里做一个FB,定期读取CSV文件的最后N行
- 把读取到的数据转换成HMI能显示的格式
- HMI通过周期通信获取这些数据
这个方案的好处是不需要额外的SCADA软件,用普通的GOT触摸屏就能实现。缺点是HMI的显示能力有限,一般只能显示最近100条左右的记录。如果需要更长的历史,还是得上SCADA或者数据库。
4.3 报警数据的分析价值
报警历史不只是用来"查原因"的,它还有很大的分析价值。
我有个客户是做食品包装的,他们用报警历史数据做了一件很有意思的事:分析设备的OEE(整体设备效率)。具体做法是:
- 把报警历史按时间段统计,找出报警高发时段
- 把报警按类型统计,找出最主要的停机原因
- 把报警按设备统计,找出最需要维护的设备
这些分析结果直接指导了他们的预防性维护计划。比如发现某个伺服驱动器在连续运行4小时后容易报过载,他们就把维护周期从8小时调整为4小时,停机时间减少了30%。
这个案例说明:报警锁存不只是为了"看到报警",更是为了"用好报警数据"。如果你的报警只是闪一下就消失,这些分析根本无从谈起。
5. 实际项目中踩过的坑
5.1 报警抖动导致的误报
这是最常见的问题。一个机械触点式的限位开关,在设备振动时可能产生几十毫秒的抖动,PLC扫描周期是10ms,就会捕捉到多次触发。
我的解决方案是在FB内部加一个可调的滤波时间。具体做法:
(* 报警滤波 *) TON_1(IN := i_xTrig, PT := i_tFilterTime); xTrigFiltered := TON_1.Q;i_tFilterTime默认设200ms,可以根据实际情况调整。注意:滤波时间不能太长,否则真正的报警会被延迟响应。我的经验值是100-500ms,具体看报警的紧急程度。
还有一个更隐蔽的坑:滤波时间设了,但报警记录的时间戳用的是滤波后的时间。这会导致记录的时间比实际发生时间晚。如果对时间精度要求高,需要在滤波前记录时间戳,滤波后触发报警。
5.2 多个报警同时触发时的处理
当设备发生重大故障时,往往会有多个报警同时触发。比如电源故障会导致所有伺服同时报警。这时候如果每个报警都弹窗、都响声音,操作工根本处理不过来。
我的做法是引入报警优先级和报警抑制机制:
- 每个报警FB有一个
i_nPriority输入,1-5级,5级最高 - 当高优先级报警激活时,低优先级报警只记录不显示
- 报警确认时,按优先级从高到低依次确认
这个机制在FB内部实现,不需要外部逻辑。具体做法是在FB里加一个全局的"最高优先级"变量,每个FB实例在触发时检查自己的优先级是否高于当前最高优先级。
5.3 确认按钮的防抖和互锁
确认按钮是操作工最常按的按钮,也是最容易出问题的按钮。
问题一:按钮抖动。操作工按一下,PLC可能检测到多次ON/OFF。如果FB的确认逻辑是边沿触发,就会多次确认。解决方案是在确认信号上加一个100ms的滤波。
问题二:误确认。操作工可能不小心碰到确认按钮,把没看到的报警确认掉了。解决方案是加一个"确认使能"条件,比如只有在报警画面打开时确认才有效。
问题三:批量确认。有时候操作工想一次确认所有报警。我的做法是加一个"全部确认"按钮,这个按钮触发一个全局的确认信号,所有FB实例都响应。但要注意:高优先级的报警不应该被批量确认,必须单独确认。
5.4 FB实例的命名和注释
FB写好了,但如果实例命名乱七八糟,后面维护还是很痛苦。
我的命名规范是:fbAlarm_设备名_报警名。比如fbAlarm_Conveyor1_Overload、fbAlarm_Robot1_ServoError。这样在交叉引用列表里一眼就能看出是哪个报警。
注释也很重要。我一般会在FB实例的注释里写清楚:
- 报警的触发条件
- 报警的后果(停机/降速/仅提示)
- 报警的处理方法
- 报警的优先级
这些注释在调试和交接时非常有用。我见过太多项目,报警FB写得很好,但注释一片空白,接手的人根本不知道每个报警是干什么的。
6. 从单机到产线:报警FB的规模化应用
6.1 报警FB的标准化
当一个项目从单机扩展到整条产线时,报警FB的数量会从几十个增加到几百个。这时候如果没有标准化,维护成本会指数级上升。
我的标准化做法是:
- 统一的FB接口:所有报警FB用同一个接口定义,不允许自定义引脚
- 统一的命名规范:报警名称、报警代码、变量名都有固定的格式
- 统一的优先级定义:1-5级,每级的含义在项目文档里写清楚
- 统一的确认逻辑:所有报警的确认和复位逻辑完全一致
这套标准一旦定下来,新报警的添加就变成了"填表格":填上报警名称、触发条件、优先级,剩下的FB自动处理。
6.2 报警FB的集中管理
几百个报警FB实例,如果分散在程序各处,查找和修改都很麻烦。我的做法是集中管理:
- 把所有报警FB实例放在一个专门的程序块里,比如
AlarmManagement - 每个FB实例的输入信号从其他程序块引用过来
- 报警的输出信号统一映射到一个报警数据块,供HMI和SCADA读取
这样做的优点是:报警逻辑集中,修改方便;报警数据集中,HMI配置简单;报警统计集中,分析方便。
6.3 报警FB的性能考量
几百个报警FB实例同时运行,对PLC的扫描周期会有影响。我的实测数据是:每个报警FB实例大约消耗0.5-1微秒的扫描时间。500个实例大约消耗0.25-0.5毫秒,对于iQ-R来说完全可以接受。
但如果报警FB里做了复杂的操作,比如字符串处理、文件读写,扫描时间会显著增加。我的建议是:
- 字符串处理(比如拼接报警消息)只在报警触发时做一次,不要每个扫描周期都做
- 文件读写用异步方式,不要阻塞主扫描
- 报警历史的统计和分析放在低速任务里做,不要放在主任务里
iQ-R支持多任务,可以把报警FB分成两个任务:高速任务处理报警的触发和锁存,低速任务处理报警的记录和统计。这样既保证了实时性,又不会拖慢主程序。
7. 几个值得注意的细节
7.1 报警FB的初始化
FB实例在第一次运行时,状态变量可能是随机值。如果不做初始化,可能会出现"上电就报警"的怪现象。
我的做法是在FB里加一个初始化标志:
IF NOT xInit THEN i_nState := 0; o_xActive := FALSE; o_xUnAck := FALSE; o_nCount := 0; xInit := TRUE; END_IF;这个初始化只在FB第一次运行时执行一次。注意:初始化标志也要放在断电保持区域,否则每次断电重启都会初始化,报警历史就丢了。
7.2 报警FB的在线修改
在调试阶段,经常需要修改报警FB的逻辑。iQ-R支持在线修改,但有几个注意事项:
- 修改FB定义时,所有实例都会受影响。如果只想改一个实例,需要单独修改
- 在线修改可能导致FB的状态变量被重置。如果报警正在激活状态,修改后可能会丢失
- 修改后要重新下载FB定义和所有实例,下载过程中PLC会停止扫描
我的建议是:调试阶段尽量在离线状态下修改,修改完统一下载。如果必须在线修改,先确认没有正在激活的报警。
7.3 报警FB的版本管理
FB库用久了,会有多个版本。如果没有版本管理,很容易出现"这个项目用的是哪个版本的FB"的问题。
我的做法是:
- 每个FB都有一个版本号,写在FB的注释里
- 每次修改FB,版本号加一,并在注释里写清楚修改内容
- 项目文档里记录每个项目使用的FB版本
- FB库用Git管理,每次修改都提交
这套做法看起来麻烦,但在多项目并行的时候,能省下大量的排查时间。
7.4 报警FB的测试
FB写好了,怎么测试?我的做法是:
- 单元测试:单独测试一个FB实例,模拟各种输入组合,验证输出是否正确
- 集成测试:把所有FB实例连起来,模拟真实报警场景,验证报警显示、记录、确认是否正常
- 压力测试:同时触发大量报警,验证PLC的扫描周期是否在可接受范围内
- 异常测试:模拟断电、通信中断等异常情况,验证报警历史是否丢失
这些测试在项目初期花的时间,会在后期调试和运维中加倍省回来。我见过太多项目,FB写得很好,但没测试,上线后各种奇怪问题。
8. 写在最后
报警锁存这件事,看起来简单,但要做好并不容易。它涉及状态机设计、时间戳处理、数据存储、HMI交互、性能优化等多个方面。一个设计良好的报警FB,不仅能解决"报警闪一下就消失"的问题,还能为设备运维提供宝贵的数据支持。
我在多个项目上迭代出来的这套报警FB方案,核心思想就是:把报警当作数据来管理,而不是当作信号来处理。信号是瞬时的,数据是持久的。只有把报警数据持久化,才能做分析、做优化、做预防性维护。
如果你现在还在用SET/RST做报警锁存,建议花点时间改成FB。前期投入可能是一两天,但后面省下的排查时间和维护成本,绝对值得。