﻿(* ============================================================ *)
(* FC_DiagOBReport - report one diagnostic OB event             *)
(* ------------------------------------------------------------ *)
(* Target: SIMATIC S7-1200 (FW V4 or later) / S7-1500 TIA       *)
(* Portal V13 SP1 or later, SCL                                 *)
(*                                                              *)
(* Writes one diagnostic OB event into the shared DB            *)
(* (DB_DiagOB). It is an FC rather than a snippet inside every  *)
(* OB because it has to be called from six different OBs: an FC *)
(* keeps no instance data, so those calls cannot disturb each   *)
(* other, while an FB would need six instance DBs that cannot   *)
(* see one another.                                             *)
(*                                                              *)
(* Division of labour - the most important design decision      *)
(* here:                                                        *)
(* diagnostic OB (this FC)  one job only: record what the       *)
(* event carried, count it, store it. Assignments and           *)
(* increments, nothing else.                                    *)
(* OB1 (FB_DiagOB)          classify, latch, dedup, reset,      *)
(* present everything to the HMI.                               *)
(*                                                              *)
(* Why the split matters - the reasons are hard, not stylistic: *)
(* 1. OB80 runs when the cycle already exceeded its limit.      *)
(* Doing real work here adds time to a cycle that is already    *)
(* too long, digging the hole deeper.                           *)
(* 2. OB121 runs because the program already faulted. Anything  *)
(* remotely complex can raise the error OB again, and nesting   *)
(* on the same priority class has a limit - exceed it and the   *)
(* CPU stops. That is why this FC contains no data handling, no *)
(* communication calls and no ANY pointer unpacking.  3.        *)
(* OB121/OB122 inherit the priority of the interrupted OB and   *)
(* can preempt OB1 at any point. Every extra instruction        *)
(* lengthens the interruption.                                  *)
(* ------------------------------------------------------------ *)
(* Rules:                                                       *)
(* 1. Wire st to "DB_DiagOB".st, identically in all six OBs.    *)
(* 2. wOBNr accepts 80 / 82 / 83 / 86 / 121 / 122 only.         *)
(* Anything else is not clamped (no white list here) but lands  *)
(* in CntUnknown - deliberately visible wiring aid.             *)
(* 3. Call it exactly once per OB; two calls make two events.   *)
(* 4. bLeaving is decided by the caller. This FC does not guess *)
(* Event_Class semantics; see the notes at the end of           *)
(* DiagOB_Types for the per-OB table.                           *)
(* 5. Formal types must match the OB interface types: SCL :=    *)
(* does no implicit type check. IO_State is Word, LADDR is      *)
(* HW_ANY whose base type is Uint, Channel is Uint. Hence       *)
(* wIOstate : Word, wLaddr : Uint, wChannel : Uint. Wiring      *)
(* LADDR into a Word is the classic trap.                       *)
(* ------------------------------------------------------------ *)
(* Version: v1   2026-10-07                                     *)
(* Import: Project tree -> External source files -> Add new     *)
(* external file -> Generate blocks from source Requires        *)
(* DiagOB_Types first. Do NOT create a block with the same name *)
(* beforehand.                                                  *)
(* ============================================================ *)

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

   VAR_INPUT
      wOBNr     : Word;      (* OB number: 80 / 82 / 83 / 86 / 121 / 122 *)
      byEvClass : Byte;      (* Event_Class (OB83/OB86; some OB82 images
                                have it too), else 0 *)
      byFaultId : Byte;      (* Fault_ID - OB80/83/86/121/122 all carry it,
                                always wire #Fault_ID; OB82 has none, use 0 *)
      wIOstate  : Word;      (* IO state word (OB82), else 0 *)
      wLaddr    : Uint;      (* hardware id of the faulty object. The OB
                                delivers HW_ANY, whose base type is Uint,
                                so Uint is the only type that compiles
                                here - a Word fails the SCL type check *)
      wChannel  : Uint;      (* channel number (OB82), else 0 *)
      bLeaving  : Bool;      (* FALSE = arriving event, TRUE = leaving event *)
   END_VAR

   VAR_IN_OUT
      st : "UDT_DiagState";  (* shared state, must be "DB_DiagOB".st *)
   END_VAR

   VAR_TEMP
      ev : "UDT_DiagEvent";  (* assembled locally first, then stored in one
                                shot, so no half-written record can ever be
                                left in the DB by a higher priority OB *)
      iTimeRc : INT;         (* RD_LOC_T return value - must be captured,
                                see the note in section 1 below *)
   END_VAR


BEGIN

(* ======== 1. Assemble the record (assignments only) ========= *)
(* Building it in the local stack and moving it in one piece keeps
   content and serial number in the same write batch: even if a
   higher priority OB interrupts in between, the worst case is an
   incomplete record, never a new serial with stale content. *)
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;

(* Local time via RD_LOC_T, no instance DB needed. If the clock was
   never set the year comes back as 1990 or similar; this FC does not
   fix it, FB_DiagOB.bClockValid tells the HMI the times are void.

   The return value MUST be captured. RD_LOC_T is declared with
   RET_VAL : INT (a Return parameter) plus OUT : DTL. Called as a bare
   statement in SCL the compiler rejects it with "the function
   returned a value" - hit for real on 2026-10-09. Do not drop
   iTimeRc, and do not "tidy it up" as an unused variable.
   RD_SYS_T and WR_LOC_T are the same family and behave alike. *)
iTimeRc := RD_LOC_T(OUT => ev.Stamp);

(* ============== 2. Store into the ring history ============== *)
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. Count per OB number ================== *)
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
    (* Outside the six accepted numbers. Usually a wrong wOBNr in an
       OB template, or this FC called from OB100 / OB40. A visible
       counter beats folding it into EvtTotal: when the total adds up
       but none of the six buckets does, the mistake is obvious. *)
    st.CntUnknown := st.CntUnknown + 1;
END_CASE;

(* ================= 4. Keep the first sample ================= *)
(* Stored once while nothing has been recorded since the last reset,
   never overwritten afterwards. Serial = 0 is the test, avoiding an
   extra flag that could drift out of sync. *)
IF st.FirstSinceRst.Serial = 0 THEN
    st.FirstSinceRst := ev;
END_IF;

(* ==== 5. Finish: newest event first, serial number LAST ===== *)
(* Deliberate order. FB_DiagOB detects new events by watching
   SerialGen, so the number has to be the final byte of the record.
   Whenever OB82 interrupts OB1, a changed number guarantees that
   steps 1 to 4 already landed completely; no half record is read. *)
st.Last      := ev;
st.SerialGen := ev.Serial;

END_FUNCTION
(* ============================================================ *)
(* Call templates for the six OBs                               *)
(*                                                              *)
(* IMPORTANT: BOTH the names and the types of the start         *)
(* information differ a lot between TIA versions. This block is *)
(* a reference only. Open the OB in your project, read its      *)
(* actual Input table, and wire by those names and types.       *)
(*                                                              *)
(* Measured on TIA V17+ / S7-1500 (OB82): the interface reads   *)
(* IO_State / LADDR / Channel / MultiError, while older         *)
(* S7-1200 material uses IOstate / laddr / channel /            *)
(* multierror. A wrong name means "undefined variable".         *)
(* LADDR is HW_ANY and cannot be assigned to a Word in SCL -    *)
(* use Uint, which is what wLaddr expects.                      *)
(*                                                              *)
(* ---------------- OB80   cycle time exceeded ---------------- *)
(* Supported on S7-1200 and S7-1500. Careful: exceeding TWICE   *)
(* the configured cycle monitoring time stops the CPU even with *)
(* OB80 loaded. OB80 saves the occasional overrun, not a        *)
(* runaway cycle.                                               *)
(*                                                              *)
(* The S7-1200/1500 OB80 start information carries only three   *)
(* values: fault_id, csg_OBnr, csg_prio - no Event_Class and no *)
(* LADDR. So fault_id is the only sub-class source:             *)
(* 16#01 cycle time exceeded   16#02 requested OB cannot start  *)
(* 16#07 / 16#09 interrupt queue overflow                       *)
(*                                                              *)
(* "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 has no matching leaving event, therefore its fault flag *)
(* requires an operator acknowledge. Deliberate.                *)
(*                                                              *)
(* ------------ OB82   module diagnostic interrupt ------------ *)
(* Supported on S7-1200 and S7-1500. Requires "Enable           *)
(* diagnostic interrupt" in the module properties.              *)
(*                                                              *)
(* Names below use the TIA V17+ / S7-1500 spelling; older       *)
(* projects use IOstate / laddr / channel / multierror:         *)
(* IO_State  bit0 = configured correctly; bit4 = error present  *)
(* bit5 = wrong configuration; bit7 = IO access error           *)
(* LADDR     hardware id of the faulty device / unit (HW_ANY,   *)
(* feed it into a Uint)                                         *)
(* Channel   channel number (Uint)                              *)
(* MultiError  more than one error present (Bool, optional)     *)
(*                                                              *)
(* "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 was meant to carry the low byte of the channel for *)
(* a quick HMI read, but Channel is Uint and may exceed 255, so *)
(* it would silently truncate. Default is 16#00; the channel    *)
(* travels in wChannel anyway.                                  *)
(*                                                              *)
(* Bits 4, 5 and 7 all zero means the event reports a cleared   *)
(* fault. Mask W#16#00B0 covers exactly those three bits.       *)
(*                                                              *)
(* ---------------- OB83   module pull / plug ----------------- *)
(* S7-1200 from FW V4 / S7-1500. Typical start information:     *)
(* LADDR / Event_Class / Fault_ID NOTE: the meaning of 38/39 is *)
(* OPPOSITE to OB86 here:                                       *)
(* 39 + 51/54     = module pulled        -> arriving (fault)    *)
(* 38 + 54        = inserted, matching   -> leaving (ok)        *)
(* 38 + 55/56/57  = inserted but mismatch / wrong parameters /  *)
(* faulty               -> still a fault                        *)
(* 38 + 58        = access error cleared -> leaving             *)
(*                                                              *)
(* "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 device / DP slave failure ------------ *)
(* S7-1200 from FW V4 / S7-1500. Mandatory with distributed IO. *)
(* Typical start information: LADDR / Event_Class / Fault_ID    *)
(* 39 + CA/CB  = PN IO system / device failure -> arriving      *)
(* 38 + CB     = device recovered              -> leaving       *)
(* 38 + C5..C8 / F9 = partly recovered, still faulty -> fault   *)
(* 32 / 33     = activate / deactivate IO device -> no fault    *)
(*                                                              *)
(* "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  programming error (S7-1500 only) ---------- *)
(* Triggered by missing DB, area length error, BCD conversion   *)
(* error, array index out of range and similar. Synchronous, NO *)
(* leaving event, so the flag needs an operator acknowledge.    *)
(*                                                              *)
(* Start information has the same shape as OB122 on S7-1500:    *)
(* there is NO Event_Class and no LADDR / IO_State / Channel,   *)
(* so Fault_ID is the ONLY source of the error sub-class. Wire  *)
(* it, or the low byte of wLastCode stays 00 forever.           *)
(*                                                              *)
(* Fields available:                                            *)
(* BlockNr   UInt  number of the causing block (see optional)   *)
(* Fault_ID  Byte  fault identifier, value depends on the error *)
(* BlockType USInt 1=OB 2=FC 3=FB 4=SFC 5=SFB 6=DB              *)
(* Area      USInt area where the error occurred                *)
(* Width     USInt 0=bit 1=byte 2=word 3=dword 4=lword          *)
(*                                                              *)
(* "FC_DiagOBReport"(wOBNr     := 121,                          *)
(* byEvClass := 16#00,                                          *)
(* byFaultId := #Fault_ID,                                      *)
(* wIOstate  := 16#0000,                                        *)
(* wLaddr    := 16#0000,                                        *)
(* wChannel  := 0,                                              *)
(* bLeaving  := FALSE,                                          *)
(* st        := "DB_DiagOB".st);                                *)
(*                                                              *)
(* Optional: to show WHICH block faulted on the HMI, feed       *)
(* BlockNr into wChannel (OB121 has no channel number, so the   *)
(* meaning does not clash). Say so in the HMI comment - do not  *)
(* let anyone read that column as a channel.                    *)
(*                                                              *)
(* ---------- OB122  IO access error (S7-1500 only) ----------- *)
(* Triggered by direct access to a peripheral address that does *)
(* not exist or is powered down. No leaving event either.       *)
(*                                                              *)
(* Same start information shape as OB121. Fault_ID is the only  *)
(* sub-class source, and it has exactly two values here:        *)
(* 16#42 = error while READING an I/O address                   *)
(* 16#43 = error while WRITING an I/O address                   *)
(* The S7-1500 interface carries no 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);                                *)
(* ============================================================ *)
