﻿(* ============================================================ *)
(* FC_DiagOBReport —— 诊断 OB 事件上报（在错误 OB 里调用）      *)
(* ------------------------------------------------------------ *)
(* 适用平台：西门子 S7-1200（固件 V4 起）/ S7-1500 TIA Portal   *)
(* V13 SP1 及以上，SCL                                          *)
(*                                                              *)
(* 作用：把一条诊断 OB 事件写进共享 DB（DB_DiagOB）。一个文件拎 *)
(* 出来做成 FC 而不是塞进每个 OB，是因为它要在六个不同的 OB 里  *)
(* 各调用一次 —— FC 没有实例数据，多处调用互不干扰；换成 FB 就  *)
(* 得开六个背景 DB 还互相看不到对方。                           *)
(*                                                              *)
(* 谁负责干什么（这个分工是本模块最要紧的设计）：               *)
(* 诊断 OB（本 FC）    只做一件事：把现场的第一手信息原样       *)
(* 记下来，记账、落历史。全程几十条赋值。                       *)
(* OB1（FB_DiagOB）    做分类、判定、去重、复位、输出给 HMI。   *)
(*                                                              *)
(* 为什么要这么分：**错误 OB 里的代码必须极简**。理由是硬的：   *)
(* 1. OB80 是循环时间超限时进来的。你在这儿跑一大坨逻辑，就是在 *)
(* 已经超时的周期上继续加时间，等于给自己挖第二个坑。           *)
(* 2. OB121 是程序已经出错时进来的。这时候任何"稍微复杂一点"的  *)
(* 访问都可能二次触发错误 OB —— 而同一个优先级上的嵌套是有上限  *)
(* 的，超了就直接 STOP。所以本 FC 里没有一个数据处理段、没有调  *)
(* 用任何通信块、没有查 ANY 指针，全是赋值和自增。              *)
(* 3. OB121/OB122 继承出错 OB 的优先级，随时可能打断 OB1。越多  *)
(* 指令，OB1 被打断的时间越长。                                 *)
(* ------------------------------------------------------------ *)
(* 必须遵守的约定：                                             *)
(* 1. st 形参必须接 "DB_DiagOB".st，六个 OB 全部接同一个。      *)
(* 2. wOBNr 只允许填 80 / 82 / 83 / 86 / 121 / 122。填别的不会  *)
(* 报错（本 FC 不做科目范围内的白名单钳位），但会落到           *)
(* CntUnknown 里 —— 那是刻意留出来的"你这儿接错了"的提示。      *)
(* 3. 每个 OB 只调用一次本 FC。同一个 OB 里调两次会产两条事件。 *)
(* 4. bLeaving 由调用方判定，本 FC 不猜 Event_Class 的语义。    *)
(* 理由和各 OB 的对照表见 DiagOB_Types 文件末尾的说明。         *)
(* 5. 形参类型必须和 OB 接口变量对得上，SCL 的 := 不做隐式      *)
(* 检查：IO_State 是 Word，LADDR 是 HW_ANY（基本类型是          *)
(* Uint），Channel 是 Uint。本 FC 里 wIOstate 用 Word、wLaddr   *)
(* 和 wChannel 用 Uint。最容易栽的，是把 LADDR 接成 Word。      *)
(* ------------------------------------------------------------ *)
(* 版本：v1   2026-10-07                                        *)
(* 导入：项目树 -> 外部源文件 -> 添加新的外部源文件 -> 从源生成 *)
(* 块。导入前须先有 DiagOB_Types 里的两个类型与 DB。不要先手动  *)
(* 新建同名 FC。中文导入请用 _GB2312 版最稳妥。                 *)
(* ============================================================ *)

FUNCTION "FC_DiagOBReport" : Void
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1

   VAR_INPUT
      wOBNr     : Word;      // OB 号：80 / 82 / 83 / 86 / 121 / 122
      byEvClass : Byte;      // 事件类别 Event_Class（OB83/OB86 有；OB82 有的版本
                             //   也有；其余填 0）
      byFaultId : Byte;      // 故障标识符 Fault_ID。OB80/83/86/121/122 的接口里
                             //   都有，务必接 #Fault_ID；OB82 没有，填 0
      wIOstate  : Word;      // IO 状态字（OB82 有；其余填 0）
      wLaddr    : Uint;      // 硬件标识符 LADDR。OB 侧类型是 HW_ANY，其基本
                             //   类型是 Uint，所以这儿只能是 Uint。写 Word
                             //   编译会报“数据类型不一致”（2026-10-09 实测）
      wChannel  : Uint;      // 通道号（OB82 有；其余填 0）。OB 侧就是 Uint
      bLeaving  : Bool;      // FALSE = 到达（故障发生）；TRUE = 离去（故障消了）
   END_VAR

   VAR_IN_OUT
      st : "UDT_DiagState";  // 共享状态。必须接 "DB_DiagOB".st
   END_VAR

   VAR_TEMP
      ev : "UDT_DiagEvent";  // 先在本周期临时区里拼好了，最后一次性落进历史，
                             //   免得写到一半被别的 OB 打断，DB 里躺着个半截事件
      iTimeRc : INT;         // RD_LOC_T 的返回值必须接住，见下面第 1 段的说明
   END_VAR


BEGIN

(* ========= 1. 拼一条事件（全是赋值，不做任何运算） ========== *)
(* 本地安排一个临时事件再整体落进去，是为了保证"内容和序号"是同 *)
(* 一批写进去的：即使中间被更高优先级的 OB 打断，最多是这条没写 *)
(* 完，绝不会留下 Serial 对得上而内容是旧的那半截。             *)
ev.Serial  := st.SerialGen + 1;
ev.OBNr    := wOBNr;
ev.EvClass := byEvClass;
ev.FaultId := byFaultId;
ev.IOstate := wIOstate;
ev.Laddr   := wLaddr;
ev.Channel := wChannel;
ev.Leaving := bLeaving;

(* 时刻戳取本地时间。RD_LOC_T 无需背景 DB，直接用。CPU 时钟没设 *)
(* 过时会给回 1990 之类的年份，本 FC 不替你修，由 FB_DiagOB 的  *)
(* bClockValid 统一告诉 HMI"这些时间不可信"。                   *)
(*                                                              *)
(* RD_LOC_T 带 RET_VAL : INT 返回值，SCL 里当独立语句调用会报   *)
(* "该函数返回了一个值"、编译不过（2026-10-09 实测踩过）。所以  *)
(* 这个 iTimeRc 必须保留，别当无用变量优化掉。RD_SYS_T 与       *)
(* WR_LOC_T 同属一家人，用法一样。                              *)
iTimeRc := RD_LOC_T(OUT => ev.Stamp);

(* ===================== 2. 落进环形历史 ====================== *)
st.Hist[st.HistWptr] := ev;
st.HistWptr := (st.HistWptr + 1) MOD 16;
IF st.HistCount < 16 THEN
    st.HistCount := st.HistCount + 1;
END_IF;

(* ================== 3. 计数：按 OB 号分桶 =================== *)
st.EvtTotal := st.EvtTotal + 1;
CASE wOBNr OF
    80:  st.CntOB80  := st.CntOB80  + 1;
    82:  st.CntOB82  := st.CntOB82  + 1;
    83:  st.CntOB83  := st.CntOB83  + 1;
    86:  st.CntOB86  := st.CntOB86  + 1;
    121: st.CntOB121 := st.CntOB121 + 1;
    122: st.CntOB122 := st.CntOB122 + 1;
ELSE
    (* 不是这六个号。多数是 OB 模板里把 wOBNr 写成了别的值，或者是  *)
    (* 你在 OB100/OB40 里顺手调了本 FC。留一条明账，比沉默记到总数  *)
    (* 里强 —— 总数对得上、六个桶都对不上时，一眼就看得出错在哪儿。 *)
    st.CntUnknown := st.CntUnknown + 1;
END_CASE;

(* ======================= 4. 首件留样 ======================== *)
(* 只在"复位后还没有第一条"的时候记一次，之后永不覆盖。判据用   *)
(* Serial = 0，不用额外的标志位：少一个状态就少一处不同步。     *)
IF st.FirstSinceRst.Serial = 0 THEN
    st.FirstSinceRst := ev;
END_IF;

(* =========== 5. 收尾：先最新事件，最后才动流水号 ============ *)
(* 顺序是刻意排的。OB1 侧的 FB 就是盯着 SerialGen 有没有变来判  *)
(* 断"有没有新事件"，所以流水号必须是这条事件的最后一个字节。这 *)
(* 样无论 OB82 什么时候打断 OB1，FB 只要看见序号变了，前三步的  *)
(* 内容就一定已经全部落地，不会读到半截。                       *)
st.Last      := ev;
st.SerialGen := ev.Serial;

END_FUNCTION

(* ============================================================ *)
(* 六个 OB 的调用模板                                           *)
(*                                                              *)
(* 【重要·必读】各 OB 的启动信息，变量名**和数据类型**都随 TIA  *)
(* 版本变，下面这组只是对照。抄之前先打开你工程里那个 OB，看它  *)
(* Input 区实际列了哪些名字、什么类型，然后照那个接。           *)
(*                                                              *)
(* 实测踩过的坑（TIA V17+ / S7-1500，OB82）：                   *)
(* 接口变量叫 IO_State / LADDR / Channel / MultiError，         *)
(* 而老教程里是 IOstate / laddr / channel / multierror。        *)
(* 名字对不上 → 未定义的变量，编译不过。                        *)
(* LADDR 是 HW_ANY，SCL 里不能赋给 Word，必须赋给 Uint          *)
(* （本 FC 的 wLaddr 已按 Uint 声明）。                         *)
(*                                                              *)
(* ------------------- OB80   循环时间超限 -------------------- *)
(* S7-1200 / S7-1500 都支持。注意：一个周期内超时**两倍**循环监 *)
(* 控时间时，即使有 OB80 也照样会 STOP —— OB80 救不了已经跑飞的 *)
(* 周期，它救的是偶尔超一次。                                   *)
(*                                                              *)
(* S7-1200/1500 的 OB80 启动信息只有三个：fault_id、csg_OBnr、  *)
(* csg_prio —— 没有 Event_Class，也没有 LADDR。所以 fault_id    *)
(* 是唯一的错误小类来源：                                       *)
(* 16#01 超出最大循环时间    16#02 请求的 OB 无法启动           *)
(* 16#07 / 16#09 中断队列溢出                                   *)
(*                                                              *)
(* "FC_DiagOBReport"(wOBNr     := 80,                           *)
(* byEvClass := 16#00,                                          *)
(* byFaultId := #Fault_ID,                                      *)
(* wIOstate  := 16#0000,                                        *)
(* wLaddr    := 16#0000,                                        *)
(* wChannel  := 0,                                              *)
(* bLeaving  := FALSE,                                          *)
(* st        := "DB_DiagOB".st);                                *)
(*                                                              *)
(* OB80 没有"离去"配对事件，所以它的故障位只能人工复位，这是刻  *)
(* 意的                                                         *)
(*                                                              *)
(* ------------------- OB82   模块诊断中断 -------------------- *)
(* S7-1200 / S7-1500 都支持。触发条件：模块必须先勾了诊断中断。 *)
(*                                                              *)
(* 变量名按你工程里的实际名字填。下面这组是 TIA V17+ / S7-1500  *)
(* 的写法（老版本是 IOstate / laddr / channel / multierror）：  *)
(* IO_State  位0 = 组态正确；位4 = 有错误（断线、短路…）        *)
(* 位5 = 组态不正确；位7 = 发生了 I/O 访问错误                  *)
(* LADDR     出错设备/功能单元的硬件标识符（HW_ANY，接 Uint）   *)
(* Channel   通道号（Uint）                                     *)
(* MultiError 有不止一个错误（Bool，可选）                      *)
(*                                                              *)
(* "FC_DiagOBReport"(wOBNr     := 82,                           *)
(* byEvClass := 16#00,                                          *)
(* byFaultId := 16#00,                                          *)
(* wIOstate  := #IO_State,                                      *)
(* wLaddr    := #LADDR,                                         *)
(* wChannel  := #Channel,                                       *)
(* bLeaving  := (#IO_State AND W#16#00B0)                       *)
(* = W#16#0000,                                                 *)
(* st        := "DB_DiagOB".st);                                *)
(*                                                              *)
(* byFaultId 本来想填通道号低字节方便 HMI 一眼看全，但通道号是  *)
(* Uint、可能超过 255，硬塞会截断，所以默认 16#00；通道号本身   *)
(* 已经单独放在 wChannel 里了。                                 *)
(*                                                              *)
(* 判据说明：位4/5/7 全为 0 才算"这条事件是故障消除"。掩蔽字    *)
(* W#16#00B0 = 位4 + 位5 + 位7。                                *)
(*                                                              *)
(* ------------------ OB83   分布式模块拔插 ------------------- *)
(* S7-1200 固件 V4 起 / S7-1500 支持。常见启动信息：            *)
(* LADDR/Event_Class / Fault_ID**注意 38/39 的含义和 OB86 是反  *)
(* 的**：                                                       *)
(* 39 + 51/54  = 模块拔掉     -> 到达（故障）                   *)
(* 38 + 54     = 插上且匹配   -> 离去（好了）                   *)
(* 38 + 55/56/57 = 插上但不匹配/参数错/有毛病 -> 仍是故障       *)
(* 38 + 58     = 已更正访问错误 -> 离去                         *)
(*                                                              *)
(* "FC_DiagOBReport"(wOBNr     := 83,                           *)
(* byEvClass := #Event_Class,                                   *)
(* byFaultId := #Fault_ID,                                      *)
(* wIOstate  := 16#0000,                                        *)
(* wLaddr    := #LADDR,                                         *)
(* wChannel  := 0,                                              *)
(* bLeaving  := (#Event_Class = 16#38)                          *)
(* AND ((#Fault_ID = 16#54)OR (#Fault_ID = 16#58)),             *)
(* st        := "DB_DiagOB".st);                                *)
(*                                                              *)
(* ---------------- OB86   IO 设备/DP 从站故障 ---------------- *)
(* S7-1200 固件 V4 起 / S7-1500 支持。分布式 IO 掉站必留。常见  *)
(* 启动信息：LADDR / Event_Class / Fault_ID 39 + CA/CB = PN IO  *)
(* 系统/设备故障 -> 到达（故障）                                *)
(* 38 + CB    = 设备故障恢复        -> 离去（好了）             *)
(* 38 + C5..C8 / F9 = 部分恢复但仍有问题 -> 仍算故障            *)
(* 32 / 33    = 激活 / 禁用 IO 设备 -> 不算故障                 *)
(*                                                              *)
(* "FC_DiagOBReport"(wOBNr     := 86,                           *)
(* byEvClass := #Event_Class,                                   *)
(* byFaultId := #Fault_ID,                                      *)
(* wIOstate  := 16#0000,                                        *)
(* wLaddr    := #LADDR,                                         *)
(* wChannel  := 0,                                              *)
(* bLeaving  := (#Event_Class = 16#38)                          *)
(* AND NOT (((#Fault_ID >= 16#C5) AND (#Fault_ID <= 16#C8)) OR  *)
(* (#Fault_ID = 16#F9)),                                        *)
(* st        := "DB_DiagOB".st);                                *)
(*                                                              *)
(* ---------------- OB121  编程错误（仅 S7-1500）-------------- *)
(* 触发：访问不存在的 DB、区域长度错误、BCD 转换错误、数组越界… *)
(* 同步错误，**没有离去事件**，故障位只能人工复位。             *)
(*                                                              *)
(* 本 OB 启动信息与 OB122 同构（S7-1500 优化接口），里面没有    *)
(* Event_Class，也没有 LADDR / IO_State / Channel —— 所以       *)
(* Fault_ID 是唯一的错误小类来源。不接它，wLastCode 低字节就    *)
(* 永远是 00，"低字节=故障小类"这一栏形同虚设。                 *)
(*                                                              *)
(* 可用字段：                                                   *)
(* BlockNr   UInt  出错块的编号（信息量最大，见下面"可选"）     *)
(* Fault_ID  Byte  故障标识，值随错误种类变，查 TIA 在线帮助    *)
(* BlockType USInt 出错块类型 1=OB 2=FC 3=FB 4=SFC 5=SFB 6=DB   *)
(* Area      USInt 出错区域（DB / DI / 位存储…）                *)
(* Width     USInt 访问宽度 0=位 1=字节 2=字 3=双字 4=长字      *)
(*                                                              *)
(* "FC_DiagOBReport"(wOBNr     := 121,                          *)
(* byEvClass := 16#00,                                          *)
(* byFaultId := #Fault_ID,                                      *)
(* wIOstate  := 16#0000,                                        *)
(* wLaddr    := 16#0000,                                        *)
(* wChannel  := 0,                                              *)
(* bLeaving  := FALSE,                                          *)
(* st        := "DB_DiagOB".st);                                *)
(*                                                              *)
(* 可选：想把"到底哪个块出的错"也带上画面，就把 BlockNr 填进    *)
(* wChannel（OB121 本来没有通道号，语义不冲突）。画面注释里要   *)
(* 写清"OB121/122 时这一栏是块号"，别让人误读成通道。           *)
(*                                                              *)
(* ---------------- OB122  I/O 访问错误（仅 S7-1500）---------- *)
(* 触发：直接访问一个不存在 / 已停机的外设地址。同样没有离去    *)
(* 事件。                                                       *)
(*                                                              *)
(* 启动信息与 OB121 同构。Fault_ID 是唯一的错误小类来源，而且   *)
(* 在这里只有两个值：                                           *)
(* 16#42 = 读 I/O 地址时出错                                    *)
(* 16#43 = 写 I/O 地址时出错                                    *)
(* S7-1500 的接口里同样没有 LADDR / IO_State / Channel。        *)
(*                                                              *)
(* "FC_DiagOBReport"(wOBNr     := 122,                          *)
(* byEvClass := 16#00,                                          *)
(* byFaultId := #Fault_ID,                                      *)
(* wIOstate  := 16#0000,                                        *)
(* wLaddr    := 16#0000,                                        *)
(* wChannel  := 0,                                              *)
(* bLeaving  := FALSE,                                          *)
(* st        := "DB_DiagOB".st);                                *)
(* ============================================================ *)
