(* ============================================================ *)
(* FB_DiagOB —— 硬件/编程错误判定与汇总（在 OB1 里调用）        *)
(* S7-1200/1500 TIA Portal SCL（导入用，GB2312/ANSI 编码）      *)
(* 导入：项目树->外部源文件->添加新外部源文件->从源生成块       *)
(* 须先导 DiagOB_Types 和 FC_DiagOBReport。                     *)
(* 不要先手动新建同名 FB。完整说明见 UTF-8 主版本。             *)
(* 先把六个诊断 OB 建出来（哪怕是空的），否则 CPU 直接 STOP。   *)
(* S7-1200 没有 OB121/122，编程错误只能走本地通道 bLocalErr。   *)
(* 硬件故障能被离去事件自动清；程序类只能人工 bReset。          *)
(* 在线监视 byVer 应为 16#01。                                  *)
(* 版本：v1 2026-10-07                                          *)
(* ============================================================ *)

FUNCTION_BLOCK "FB_DiagOB"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1

VAR_INPUT
    bEnable     : BOOL := TRUE;      // 总使能。关掉后本地错误通道不记账，
                                     //   OB 报上来的事件照旧记（那部分不能关）
    bReset      : BOOL := FALSE;     // 人工确认 / 复位（清掉所有锁存故障与历史）
    tDedup      : TIME := T#2s;      // 同源事件的去重窗口：窗口内重复出现即算"刷屏"
    bAutoClearHw: BOOL := TRUE;      // 硬件故障是否随"离去事件"自动消失。见第 6 段注释：
                                     //   分布式设备超过三五个的场所，建议填 FALSE，
                                     //   改成"到了就亮、人来看过才灭"
    (* ----- 本地错误通道（S7-1200 没有 OB121 时的替代入口） ------ *)
    bLocalErr   : BOOL := FALSE;     // 有新本地错误（上升沿有效）
    wLocalErrId : WORD := 16#0000;   // 错误码，填 GET_ERR_ID 的返回值
END_VAR

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

VAR_OUTPUT
    (* ----- 分类故障位（锁存，不自动消失，见头注释第 1 条） ------ *)
    bAnyFault    : BOOL;             // 三类里只要有一个亮，就是总故障
    bHwFault     : BOOL;             // 硬件类：模块诊断 / 拔插 / IO 设备掉站
    bProgFault   : BOOL;             // 程序类：OB121 / OB122 / 本地错误通道
    bCycFault    : BOOL;             // 循环类：OB80 循环时间超限
    bLocalFault  : BOOL;             // 本地错误通道上报过（含在 bProgFault 里）
    (* ------------------------- 事件提示 ------------------------- *)
    bNewEvent    : BOOL;             // 本扫描有新事件进来了（单周期脉冲）
    bNewProgErr  : BOOL;             // 本扫描新来的是程序类错误（单周期脉冲）
    bRepeatBurst : BOOL;             // 稳态：正被同一个来源的错误反复刷屏
    (* --------------------------- 体检 --------------------------- *)
    bParaFault   : BOOL;             // 参数非法
    bClockValid  : BOOL;             // CPU 时钟可信。FALSE 时所有时间戳不可信
    byVer        : BYTE;             // 版本指纹：在线监视 16#01 即本版
    (* --------------------------- 数值 --------------------------- *)
    wLastCode    : WORD;             // 组合故障码：高字节 = OB 号，低字节 = 故障小类
    wLastLocalErr: WORD;             // 最近一次本地错误的原始 16 位错误码
    dwEvtTotal   : DWORD;            // 事件总数
    dwRepeatTotal: DWORD;            // 被判定为"重复刷屏"的次数（不含首条）
    uHistCount   : UINT;             // 历史队列里已存条数
    uHistNewest  : UINT;             // 最新一条历史所在的下标，给 HMI 翻队列用
END_VAR

VAR
    byVerReg     : BYTE := 16#01;    // 版本指纹寄存器
    dwSerialSnap : DWORD := 0;       // 上次处理到的流水号
    dwLastKey    : DWORD := 16#FFFFFFFF;  // 上一条事件的去重 key，初值取个不可能的值
    bLocalLast   : BOOL := FALSE;    // 本地错误通道上一拍
    bWinPrev     : BOOL := FALSE;    // 去重窗口上一拍的状态
    bBurstLatch  : BOOL := FALSE;    // 刷屏锁存（窗口内第二次起置起，窗口过去自动退）
    tWinTmr      : TOF_TIME;         // 去重窗口计时（关断延时）
END_VAR

VAR_TEMP
    bLocEdge   : BOOL;               // 本地错误上升沿
    dwGap      : DWORD;              // 这次要补处理几条事件
    uBack      : UINT;               // 回扫条数（最多 16）
    uPos       : UINT;               // 回扫游标
    wFault     : WORD;               // 本条事件算出来的组合码
    dwKey      : DWORD;              // 本条事件的去重 key
    bSameSrc   : BOOL;               // 本拍的事件与上一次是同一个来源吗
    ev         : "UDT_DiagEvent";    // 当前正在处理的那条事件
    tNow       : DTL;                // 本地时间
    iTimeRc    : INT;                // RD_LOC_T 的返回值，必须接住（见第 3 段）
    bIsHw      : BOOL;
    bIsProg    : BOOL;
    bIsCyc     : BOOL;
END_VAR


BEGIN

byVerReg := 16#01;

(* ================= 1. 段首清零：脉冲型输出 ================== *)
(* 清零必须放在所有会置起它的代码之前。挪到段尾的话脉冲会被自己 *)
(* 抹掉，表现是"计数、历史、锁存全都对，唯独 HMI 上那个提示灯从 *)
(* 来不亮" —— 这类 bug 最难查，因为它看起来一切都正常。         *)
bNewEvent   := FALSE;
bNewProgErr := FALSE;

(* ===== 2. 参数体检（每周期重算，不是某个状态下置一次） ====== *)
(* 只在某个状态里置起的标志，生命周期就一个扫描周期，等你打开监 *)
(* 视表去看，它早就把自己灭了，等于白做。                       *)
bParaFault := FALSE;
IF (tDedup <= T#0s) OR (tDedup > T#24d) THEN
    bParaFault := TRUE;
END_IF;

(* ===================== 3. CPU 时钟体检 ====================== *)
(* RD_LOC_T 取本地时间，无需背景 DB。年份不在合理区间就说明这台 *)
(* CPU 从没设过时间（没设过的典型年份是 1990 / 2012），这时候历 *)
(* 史里所有的 Stamp 都不能当证据用。宁可明着说不可信，也别让人  *)
(* 拿着 1990 年的时间戳去查"昨晚三点发生了什么"。               *)
(* 返回值必须接住：RD_LOC_T 带 RET_VAL : INT 返回值，当独立语句 *)
(* 调用会被判"函数返回了一个值"、编译不过（2026-10-09 实测）。  *)
(* iTimeRc 不是无用变量，别删。                                 *)
iTimeRc := RD_LOC_T(OUT => tNow);
bClockValid := (tNow.YEAR >= 2020) AND (tNow.YEAR <= 2099);

(* ===================== 4. 人工确认复位 ====================== *)
IF bReset THEN
    st.HwFault    := FALSE;
    st.ProgFault  := FALSE;
    st.CycFault   := FALSE;
    st.LocalFault := FALSE;
    st.FirstSinceRst.Serial := 0;
    st.HistWptr    := 0;
    st.HistCount   := 0;
    st.RepeatTotal := 0;
    st.uHwOpen     := 0;             (* 硬件类计数也要一起清，否则复位后 *)
                                     (* HwFault 还会亮着、永远退不掉。SerialGen 刻意不清零：它是流水 *)
                                     (* 号发生器，清了新旧事件会重号。顺带它也成了"这条记录是复位前  *)
                                     (* 还是复位后"的判断依据。                                      *)
END_IF;

(* ===================== 5. 本地错误通道 ====================== *)
(* S7-1200 上没有 OB121/OB122 可用，程序出错直接 STOP。想在     *)
(* 1200 上留下"程序类错误"的痕迹，只能在你自己那个有风险的块里  *)
(* 把 ENO 或者 GET_ERR_ID 接住，再送到这里。归类给程序类，与    *)
(* OB121 同等对待 —— 都是为了逼着人去查，而不是让它在诊断缓冲区 *)
(* 里无声无息。                                                 *)
bLocEdge := FALSE;
IF bEnable THEN
    bLocEdge := bLocalErr AND (NOT bLocalLast);
END_IF;
bLocalLast := bLocalErr;

IF bLocEdge THEN
    st.LocalFault  := TRUE;
    st.ProgFault   := TRUE;
    st.CntLocalErr := st.CntLocalErr + 1;
    bNewProgErr    := TRUE;
    bNewEvent      := TRUE;
    wLastLocalErr  := wLocalErrId;
    (* 合成一条"伪事件"塞进 LastProg，让下游（HMI/记录）不用区分它  *)
    (* 到底是从 OB 来的还是本地通道来的。OBNr 用 16#FFFF 留标记。   *)
    st.LastProg.Serial  := st.SerialGen;
    st.LastProg.OBNr    := 16#FFFF;
    st.LastProg.FaultId := WORD_TO_BYTE(wLocalErrId);
    st.LastProg.EvClass := WORD_TO_BYTE(SHR(IN := wLocalErrId, N := 8));
    st.LastProg.IOstate := wLocalErrId;   (* 原始 16 位错误码原样留一份 *)
    st.LastProg.Laddr   := 0;
    st.LastProg.Channel := 0;
    st.LastProg.Leaving := FALSE;
    st.LastProg.Stamp   := tNow;
    IF st.FirstSinceRst.Serial = 0 THEN
        st.FirstSinceRst := st.LastProg;
    END_IF;
END_IF;

(* =================== 6. 新事件检测与分类 ==================== *)
(* 盯 SerialGen 的变化，而不是盯某个"有新事件"标志。好处是无论  *)
(* OB 什么时候打断 OB1，只要漏过一个周期，下一周期照样能补进    *)
(* 来。哨兵值 16#FFFFFFFF = "本拍没有来自 OB 的事件"。因为本地  *)
(* 错误通道也会置 bNewEvent，那条路产不出 key，必须能区分开，否 *)
(* 则本地错误会被误判成"和上一件重复"。                         *)
dwKey    := 16#FFFFFFFF;
bSameSrc := FALSE;

IF st.SerialGen <> dwSerialSnap THEN

    dwGap        := st.SerialGen - dwSerialSnap;
    dwSerialSnap := st.SerialGen;
    bNewEvent   := TRUE;

    (* 回扫历史：一个扫描里可能挤进来好几条。OB82/OB86 是异步的，打 *)
    (* 断 OB1 一次就写一条，一个周期进来两三条很正常。只处理最后一  *)
    (* 条的话，中间那几条就凭空消失了，分类会漏。                   *)
    IF dwGap > 16 THEN
        uBack := 16;                 (* 环形队列才 16 深，再多就是被覆盖了 *)
    ELSE
        uBack := DWORD_TO_UINT(dwGap);
    END_IF;

    uPos := 0;
    WHILE uPos < uBack DO

        (* 从**最早**那条往最新的方向推进。反过来（由新到旧）的话，     *)
        (* LastHw / LastProg 最后留下的是本批里最老那条 —— 现场打开监视 *)
        (* 表看到的是刚过去那件，而不是真正刚发生的那件。这个顺序是仿真 *)
        (* 跑出来的，不是凭感觉排的。                                   *)
        ev := st.Hist[(st.HistWptr + 16 - uBack + uPos) MOD 16];

        (* --------------------------- 归类 --------------------------- *)
        bIsHw   := (ev.OBNr = 82) OR (ev.OBNr = 83) OR (ev.OBNr = 86);
        bIsProg := (ev.OBNr = 121) OR (ev.OBNr = 122);
        bIsCyc  := (ev.OBNr = 80);

        IF ev.Leaving THEN
            (* 离去 = 故障消了。**只有异步诊断 OB（82/83/86）才有配对的离去 *)
            (* 事件**。同步错误（121/122）和 OB80 天生没有，所以那两类的故  *)
            (* 障位只能靠人工复位才会退，这是刻意的。                       *)
            IF bIsHw THEN
                (* 计数式配平：离去一条减一，减到 0 才算没有硬件故障。用计数而  *)
                (* 不是一个 BOOL，是因为一个工程里少则三五个、多则几十个分布式  *)
                (* 设备，其中随便哪一个恢复就把全局标志清掉，剩下还在报错的那些 *)
                (* 就凭空消失了 —— 现场最怕这种假"已恢复"。这个缺陷是仿真逼出来 *)
                (* 的，看书看不出来。但计数配平也不是万能：它认不出"这条离去配  *)
                (* 的是哪一路"，同一时刻挂着的来源多到一定程度就可能对不上账。  *)
                (* 所以留了 bAutoClearHw：设备多的时候就把它填 FALSE，改成"到了 *)
                (* 就亮、人来看过才灭"，用不方便换一个不骗人。                  *)
                IF bAutoClearHw AND (st.uHwOpen > 0) THEN
                    st.uHwOpen := st.uHwOpen - 1;
                END_IF;
            END_IF;
        ELSE
            IF bIsHw THEN
                IF st.uHwOpen < 65535 THEN
                    st.uHwOpen := st.uHwOpen + 1;
                END_IF;
                st.LastHw  := ev;
            END_IF;
            IF bIsProg THEN
                st.ProgFault := TRUE;
                st.LastProg  := ev;
                bNewProgErr  := TRUE;
            END_IF;
            IF bIsCyc THEN
                st.CycFault := TRUE;
            END_IF;
        END_IF;

        (* --------- 组合故障码：高字节 OB 号，低字节故障小类 --------- *)
        (* OB83/86 有真正的 Fault_ID；OB82 没有，退到 Event_Class。为什 *)
        (* 么不用 EvClass 优先：OB86 的 32/33 是"激活/禁用 IO 设备"，那 *)
        (* 不是错误，让 Fault_ID 优先才能把它们区分开。                 *)
        IF ev.FaultId <> 0 THEN
            wFault := SHL(IN := ev.OBNr, N := 8) OR BYTE_TO_WORD(ev.FaultId);
        ELSE
            wFault := SHL(IN := ev.OBNr, N := 8) OR BYTE_TO_WORD(ev.EvClass);
        END_IF;
        IF NOT ev.Leaving THEN
            wLastCode := wFault;      (* 离去事件不覆盖上一次的故障码 *)
        END_IF;

        (* --- 去重 key 只按本批最新那条算（最后处理到的就是最新） ---- *)
        (* OB 号 + 故障小类 + 硬件标识，三样凑齐才算同一个来源。Channel *)
        (* 刻意不进 key：多通道模块上同一类错会挨个通道报一轮，那本来就 *)
        (* 该算多次；把通道也拢进来，反而会漏掉其他通道的真错。         *)
        IF uPos = uBack - 1 THEN
            dwKey := WORD_TO_DWORD(ev.OBNr) * 16#10000
                   + WORD_TO_DWORD(BYTE_TO_WORD(ev.FaultId)) * 16#100
                   + WORD_TO_DWORD(ev.Laddr);
        END_IF;

        uPos := uPos + 1;
    END_WHILE;

    (* 同源判定要在 dwLastKey 更新**之前**做，用的才是上一条的 key  *)
    IF dwKey <> 16#FFFFFFFF THEN
        bSameSrc   := (dwKey = dwLastKey);
        dwLastKey  := dwKey;
    END_IF;

END_IF;

(* ================= 7. 重复刷屏（burst）判定 ================= *)
(* 判据不是"计数涨得快"，而是"同一个来源在 tDedup 窗口里第二次  *)
(* 以上报出来"。典型就是每个扫描周期都在报的那个错 —— 一个跑飞  *)
(* 的下标、一段访问了没组模块的 I/O。这类东西一小时能灌进来几十 *)
(* 万条，只看总数是完全判断不出"到底发生了几件事"的。TOF 做窗   *)
(* 口：脉冲一来 Q 立起来，脉冲过去开始计时，计满才落。所以 Q 为 *)
(* 真恰好 == "距上一次事件还没到 tDedup"。TOF 的输入用          *)
(* bNewEvent 而不是 bSameSrc：换了个来源也要重新起算窗口，靠    *)
(* dwLastKey 的比对就能保证"换来源后的第一次"不算重复。         *)
IF bSameSrc AND bWinPrev THEN
    st.RepeatTotal := st.RepeatTotal + 1;
    bBurstLatch    := TRUE;
END_IF;

tWinTmr(IN := bNewEvent, PT := tDedup);

IF NOT tWinTmr.Q THEN
    bBurstLatch := FALSE;            (* 窗口过去没再报，自己退掉 *)
END_IF;
bWinPrev := tWinTmr.Q;

(* ======================= 8. 输出汇总 ======================== *)
(* HwFault 每周期由还没配平的到达条数重算，而不是靠"某个事件把  *)
(* 它抓住的那一瞬间 set/reset"。稳态量必须每周期重算 —— 只在事  *)
(* 件那一周期动它的话，现场打开监视表时它早就被下一批逻辑改回去 *)
(* 了，等于白做。                                               *)
st.HwFault    := (st.uHwOpen > 0);
bHwFault      := st.HwFault;
bProgFault    := st.ProgFault;
bCycFault     := st.CycFault;
bLocalFault   := st.LocalFault;
bAnyFault     := st.HwFault OR st.ProgFault OR bCycFault;
bRepeatBurst  := bBurstLatch;
byVer         := byVerReg;
dwEvtTotal    := st.EvtTotal;
dwRepeatTotal := st.RepeatTotal;
uHistCount    := st.HistCount;
IF st.HistWptr = 0 THEN
    uHistNewest := 15;
ELSE
    uHistNewest := st.HistWptr - 1;
END_IF;

END_FUNCTION_BLOCK
(* ============================================================ *)
(* 调用说明（TIA Portal）                                       *)
(*                                                              *)
(* 1. 三步接线，顺序不能乱：                                    *)
(* ① 先在 CPU 里把要用到的 OB 建出来（Add new                   *)
(* block->Organization block），哪怕暂时是空的也行 —— 只要它    *)
(* 在，CPU 就不会因为这一类错误进 STOP。这一步是最要紧的。② 每  *)
(* 个 OB 里调一次 FC_DiagOBReport（模板见那个文件的末尾）③ OB1  *)
(* 里调一次本 FB，st 接 "DB_DiagOB".st                          *)
(*                                                              *)
(* "DiagOB_1"(bEnable := TRUE,                                  *)
(* bReset  := "HMI_故障确认",                                   *)
(* tDedup  := T#2s,                                             *)
(* bLocalErr   := "本地错误_新",                                *)
(* wLocalErrId := "本地错误_码",                                *)
(* st      := "DB_DiagOB".st);                                  *)
(*                                                              *)
(* 2. OB 里的代码尽量短。OB80 是循环已经超时才进来的，你在里面  *)
(* 逻辑就是在已经超时的周期上继续加时间；OB121 是程序已经出错才 *)
(* 进来的，再多一次越界访问 CPU 就直接 STOP。FC_DiagOBReport 本 *)
(* 身就是按这个原则写的（全是赋值和自增），别在 OB 里再加料。   *)
(*                                                              *)
(* 3. tDedup 怎么选：                                           *)
(* 2~5 s    默认。能抓住"每周期都报"的那类，又不会把正常的      *)
(* 偶发重复（拔网线一刹那来两三条）误判成刷屏                   *)
(* 200 ms   想尽早发现持续刷屏时用                              *)
(* >30 s    不建议。窗口太长，真正出事时反而看不出来            *)
(*                                                              *)
(* 4. 组合故障码 wLastCode 怎么读（高字节 OB 号，低字节小类）： *)
(* 16#5200..52FF   OB82  模块诊断（低字节多为 Event_Class）     *)
(* 16#53xx         OB83  拔插，xx = Fault_ID（51/54/55…）       *)
(* 16#56xx         OB86  掉站，xx = Fault_ID（CA/CB/C5…）       *)
(* 16#79xx         OB121 编程错误，xx = Fault_ID                *)
(* 16#7Axx         OB122 I/O 访问错误，xx = 42 读 / 43 写       *)
(* 16#50xx         OB80  循环时间超限                           *)
(* 例：16#56CB = OB86 + C3? 不对 —— C3 是 DP 主站系统故障，     *)
(* CB 才是 PN IO 设备故障。看到 56CB 就是"PN 设备掉线"。        *)
(*                                                              *)
(* 5. 常见坑：                                                  *)
(* · CPU 还是一有错就 STOP：先查那个 OB 到底有没有下载进去      *)
(* （在线 -> 块 -> 看 OB 在不在）。OB 不存在的话，本模块的任何  *)
(* 逻辑都来不及跑。                                             *)
(* · 什么都想抓但 S7-1200：1200 没有 OB121/122，别白找。想在    *)
(* 1200 上看程序类错误，走第 6 步那条本地通道。                 *)
(* · OB82 一次都不来：模块属性里"启用诊断中断"勾了吗？没勾的话  *)
(* 硬件有毛病也不会触发 OB82，它只会静静地在诊断缓冲区里躺平。  *)
(* · 历史里一片同名事件刷满：那是 bRepeatBurst 该亮的时候。赶   *)
(* 紧看 st.FirstSinceRst —— 第一条才是根因。                    *)
(* · 时间戳是 1990 年：CPU 时钟没设。bClockValid 会是 FALSE。   *)
(* · bEnable := FALSE 时 OB 事件照记：这是刻意的。OB 报上来的   *)
(* 东西关不掉，能关的只有本地错误通道。                         *)
(* ============================================================ *)
