(* ============================================================ *) (* DiagOB_Types —— 诊断 OB 错误捕获模块:数据类型 + 共享 DB *) (* ------------------------------------------------------------ *) (* 适用平台:西门子 S7-1200(固件 V4 起)/ S7-1500 TIA Portal *) (* V13 SP1 及以上,SCL *) (* *) (* 本文件是本模块的数据层,含 2 个 PLC 数据类型 + 1 个 DB: *) (* UDT_DiagEvent 一条诊断事件(谁出错、什么时候、什么错) *) (* UDT_DiagState 汇总状态 + 环形历史(全工程唯一) *) (* DB_DiagOB 上面那个数据类型的实体,OB/FC/FB 三方共用 *) (* *) (* 配套两个程序文件,在同目录: *) (* FC_DiagOBReport 在 OB80/82/83/86/121/122 里各调用一次 *) (* FB_DiagOB 在 OB1 里调用,做判定、汇总、确认复位 *) (* ------------------------------------------------------------ *) (* 为什么数据层要单独一个文件(三个原因,第一个是硬约束): *) (* 1. FC 与 FB 的接口都要引用这两个数据类型。各自在自己文件里 *) (* 声明一份的话,导入第二个文件时会报"块已存在"。先导本文件生成 *) (* 类型与 DB,两边共用,顺序不能反。 *) (* 2. 诊断 OB 是**异步打断 OB1** 的。两边共享的数据必须地处同 *) (* 一个全局 DB,不能放进 FB 的背景 DB —— 否则 OB82 打断 OB1 *) (* 时,FC 改的是你自己这份,OB1 里 FB 读的是另一份。 *) (* 3. HMI 要直接翻历史队列。放全局 DB 里,触摸屏照着下标读就行 *) (* ------------------------------------------------------------ *) (* 三条硬规矩(都踩过): *) (* 1. 事件由诊断 OB 侧(FC)写入,OB1 侧(FB)只读。反过来写 *) (* 必然出竞态:OB82 在你算到一半时插进来,前后两半对不上。 *) (* 2. 流水号必须**最后**更新。写完事件内容再动 SerialGen, *) (* OB1 侧只要看见序号变了,这条就一定已经写完整了。 *) (* 3. 导入顺序:先导本文件(生成 2 个类型 + 1 个 DB), *) (* 再导 FC_DiagOBReport,最后导 FB_DiagOB。 *) (* ------------------------------------------------------------ *) (* 版本:v1 2026-10-07 *) (* 导入:项目树 -> 外部源文件 -> 添加新的外部源文件 -> 从源生成 *) (* 块。不要先手动新建同名对象。中文导入请用 _GB2312 版最稳妥。 *) (* ============================================================ *) TYPE "UDT_DiagEvent" VERSION : 0.1 STRUCT Serial : DWord; // 流水号,1 起单调递增;0 = 这条是空槽位 OBNr : Word; // 事件来自哪个 OB:80/82/83/86/121/122 // 16#FFFF = 本地错误通道上报的(不是 OB 报的) EvClass : Byte; // 事件类别 Event_Class(OB83/OB86 有) FaultId : Byte; // 故障标识符 Fault_ID(OB83/OB86 有) IOstate : Word; // IO 状态字(OB82 有):位4=有错、位5=组态错、位7=访问错 Laddr : Word; // 出错对象的硬件标识符 LADDR Channel : Word; // 通道号(OB82 有) Leaving : Bool; // FALSE = 到达事件(故障发生);TRUE = 离去事件(故障消了) Stamp : DTL; // 发生时刻(本地时间)。CPU 时钟没设过的话这是垃圾值, // 可信与否看 FB_DiagOB 的 bClockValid 输出 END_STRUCT; END_TYPE TYPE "UDT_DiagState" VERSION : 0.1 STRUCT (* -------------------- 版本与流水号发生器 -------------------- *) VER : Byte; // 版本指纹:在线监视 16#01 即本版 SerialGen : DWord; // 流水号发生器。只有 FC_DiagOBReport 写它, // 并且必须最后写;FB 侧从不写它 (* ------------------------- 累计计数 ------------------------- *) EvtTotal : DWord; // 事件总数(到达 + 离去都算) RepeatTotal : DWord; // 同一个来源在去重窗口内反复触发的次数 CntOB80 : DWord; // OB80 循环时间超限 CntOB82 : DWord; // OB82 模块诊断中断(断线/短路/超量程…) CntOB83 : DWord; // OB83 分布式模块拔插 CntOB86 : DWord; // OB86 PROFINET IO 设备 / DP 从站故障 CntOB121 : DWord; // OB121 编程错误(仅 S7-1500) CntOB122 : DWord; // OB122 I/O 访问错误(仅 S7-1500) CntLocalErr : DWord; // 本地错误通道上报次数(GET_ERR_ID) CntUnknown : DWord; // OB 号不是上面六个:调用方填错了, // 单独记账,好让你一眼看出接口接错 (* ------------------------- 事件快照 ------------------------- *) Last : "UDT_DiagEvent"; // 最近一条事件(不管哪一类) LastHw : "UDT_DiagEvent"; // 最近一条**硬件类**错误(82/83/86 到达) LastProg : "UDT_DiagEvent"; // 最近一条**程序类**错误(121/122 + 本地通道) FirstSinceRst: "UDT_DiagEvent"; // 复位后第一条事件。现场最值钱的就是它: // 后面那一大串往往都是它的跟班症状 (* ------------------------- 环形历史 ------------------------- *) Hist : Array[0..15] of "UDT_DiagEvent"; // 16 条滚动历史,越界自动覆盖最老的 HistWptr : UInt; // 下一个要写的槽位 HistCount : UInt; // 已存条数,满 16 就不再涨 (* --------------------- 硬件故障配平计数 --------------------- *) uHwOpen : UInt; // 还没等到配对离去事件的硬件类"到达"条数。 // HwFault 就是"这个数 > 0",由 FB 每周期重算。 // 为什么不用一个简单的 BOOL:一个工程里少则 // 三五个、多则几十个分布式设备,用 BOOL 的话 // 任何一个恢复就把全局清了,剩下还在报错的那 // 些就凭空消失 —— 现场最怕这种假"已恢复" (* ------------------------ 锁存故障位 ------------------------ *) HwFault : Bool; // 硬件类故障未确认 ProgFault : Bool; // 程序类故障未确认 CycFault : Bool; // 循环时间类故障未确认 LocalFault : Bool; // 本地错误通道上报过未确认 END_STRUCT; END_TYPE DATA_BLOCK "DB_DiagOB" { S7_Optimized_Access := 'TRUE' } VERSION : 0.1 STRUCT st : "UDT_DiagState"; // 整个模块就这一份数据。HMI、OB、OB1 全部对准它 END_STRUCT; BEGIN (* 只要在 BEGIN 里给值,重新下载时会被刷回初值;要改请用监视表 *) st.VER := 16#01; st.SerialGen := 0; st.HistWptr := 0; st.HistCount := 0; st.FirstSinceRst.Serial := 0; END_DATA_BLOCK (* ============================================================ *) (* 字段说明速查(这块写在这儿是因为它比任何一个块都重要) *) (* *) (* 1. Leaving 为什么不用 Event_Class 自己算? *) (* 因为 **OB83 和 OB86 的 38/39 语义是反的**: *) (* OB83:39 = 模块拔掉(故障);38 = 模块插上(含"插了但 *) (* 不匹配/参数错/有毛病"这几档,也得算故障) *) (* OB86:39 = IO 设备故障(到达);38 = 故障恢复(离去), *) (* 但 C5~C8 / F9 这几档是"部分恢复、仍有故障",归 38 却不能当成 *) (* 彻底好了。所以让块去猜,迟早猜错。本模块的办法是:**判定权交 *) (* 给调用方** —— 在 OB 模板里按上表把 bLeaving 算好传进来,块只 *) (* 负责照抄记账。改了一个 OB 的语义也不用动块。 *) (* *) (* 2. 为什么 FirstSinceRst 单独留一条,不用 Hist[0]? *) (* 历史是环形的,"复位后第一条"那个槽位迟早被覆盖掉。而它就是第 *) (* 一现场,覆盖不起 —— 比如第一次报的下标,后面三千条都是它引起 *) (* 的,第一现场一丢,现场就只能瞎猜。 *) (* *) (* 3. RepeatTotal 是干什么的? *) (* 专治"每个扫描周期都报同一个错"。典型就是一个跑飞的数组下标或 *) (* 者一段根本没配模块的 I/O 访问:它不会只报一次,而是一直报到 *) (* 天荒地老。如果只做计数,一小时能涨到几十万,现场看着这个数完 *) (* 全判断不出"到底发生了几件事"。所以 FB 里另有一套窗口去重(同 *) (* 类来源在 tDedup 之内再报,只往 RepeatTotal 里加,并点亮 *) (* bRepeatBurst 稳态位),告诉现场"现在正在被同一个错误刷屏"。 *) (* ============================================================ *)