﻿(* ============================================================ *)
(* FB_ModbusRtuSlave - Modbus RTU slave, 1000 holding registers *)
(* ------------------------------------------------------------ *)
(* Platform : Siemens S7-1200 / S7-1500 (TIA Portal SCL)        *)
(* Built on : Modbus_Comm_Load + Modbus_Slave system instruct.  *)
(* Hardware : CM 1241 RS485 / CB 1241 RS485 communication board *)
(* ------------------------------------------------------------ *)
(* Purpose: make this PLC act as a Modbus RTU slave so a master *)
(* (DCS / HMI / another PLC / SCADA) can read with FC03 and     *)
(* write with FC06 / FC16. The 1000-word holding register array *)
(* aHoldReg lives INSIDE the instance DB of this FB - no extra  *)
(* global DB needed. After import, open the instance DB and you *)
(* can monitor and modify the registers directly.               *)
(* ------------------------------------------------------------ *)
(* Address mapping (official MB_SLAVE definition, index from 0) *)
(* 40001  ->  aHoldReg[0]                                       *)
(* 40002  ->  aHoldReg[1]                                       *)
(* ......                                                       *)
(* 41000  ->  aHoldReg[999]                                     *)
(* The master always counts from 40001. FC03 reads 125 words max *)
(* per frame, FC16 writes 123 words max; beyond that the slave  *)
(* replies an exception and this FB counts wExcCnt / bErrHold.  *)
(* Coil access (FC01/05/15) does NOT touch this array - coils   *)
(* map straight onto the CPU process image (from Q0.0).         *)
(* ------------------------------------------------------------ *)
(* [READ FIRST] Port MODE: RS485 two wire half duplex needs     *)
(* uMode485 = 4. MODE is NOT a pin of Modbus_Comm_Load - it is  *)
(* a Static tag inside its instance DB and ships at 0 (RS232    *)
(* full duplex). Left at 0, baud and parity look perfect, bReady *)
(* goes TRUE, yet NOT A SINGLE FRAME gets through, and no       *)
(* "parameter error" is ever reported. This FB handles it: it   *)
(* writes the Static before every REQ. Just keep the default 4  *)
(* and confirm with uModeAct, which must read back 4.           *)
(* 0=RS232 full duplex, 1/2/3=RS422 4-wire, 4=RS485 2w half     *)
(* ------------------------------------------------------------ *)
(* Diagnostics outputs (the point of this block, 26 pins)       *)
(*                                                              *)
(* [Health]                                                     *)
(* bReady    port initialised, communication possible           *)
(* bNdr      master has just written new data (one-scan pulse)  *)
(* bDr       master has just read data (one-scan pulse)         *)
(* bCommBeat heartbeat, toggles on every successful frame       *)
(* bHostLost master lost: no frame within tHostTimeout          *)
(* tIdleTime time elapsed since the last successful frame       *)
(*                                                              *)
(* [Fault code]                                                 *)
(* bError    error present on this scan (live)                  *)
(* bErrLatch fault latched (stays until bErrClr)                *)
(* wStatus   live status word (STATUS of Modbus_Slave)          *)
(* wLastErr  latched last error code (this is what you debug on) *)
(* uErrSrc   source of last error: 1=port init  2=slave instr.  *)
(*                                                              *)
(* [Fault class] (mutually exclusive, tells you where to look)  *)
(* bErrCfg   port config: bad HW id / baud rate / parity        *)
(* bErrHold  holding area: bad pointer, out of range, optimized *)
(* bErrFrame frame format: CRC error, byte count, bad fn code   *)
(* bErrPara  parameter: illegal data address or length          *)
(* bErrBusy  port busy: master polls too fast, or two masters   *)
(* bErrOther any code not covered above                         *)
(* bLineNoise line noise: CRC bad frames above 5 percent        *)
(* bParaFault this FB got an illegal slave address (1..247)     *)
(*                                                              *)
(* [Counters]                                                   *)
(* dwReqCnt / dwReadCnt / dwWriteCnt  served / read / written   *)
(* dwErrCnt  fault count (counts error edges, not error scans)  *)
(* wReqTotal total requests seen by the slave (incl. bad frames) *)
(* wCrcErr   CRC error frame count (slave instance, wraps 65535) *)
(* wExcCnt   exception responses returned to the master         *)
(* wSuccCnt  requests with no protocol error                    *)
(*                                                              *)
(* [Data tracking]                                              *)
(* bDataChanged  a value in the holding area was changed        *)
(* wLastChgIdx   index changed last (0 = 40001)                 *)
(* ------------------------------------------------------------ *)
(* wStatus classes (official Siemens codes, drive the flags)    *)
(* 0000 / 7000 / 7001 / 7002   idle or executing, no alarm      *)
(* 8188 write to read-only area or bad mode   -> bErrPara       *)
(* 8189 illegal data address                 -> bErrPara        *)
(* 818A illegal data length                  -> bErrPara        *)
(* 818B invalid pointer (wrong size)         -> bErrHold        *)
(* 818C pointer to an optimized DB           -> bErrHold        *)
(* 8383 access outside MB_HOLD_REG range     -> bErrHold        *)
(* 8200 port busy                            -> bErrBusy        *)
(* 8380 frame malformed / too few bytes      -> bErrFrame       *)
(* 8381 function code not supported                             *)
(* 8382 length in header != bytes received   -> bErrFrame       *)
(* 8384 illegal data value                                      *)
(* 8385 diagnostic function code unsupported                    *)
(* 80C8 response timeout (a master-side code)                   *)
(* During port init (uErrSrc = 1) these are recognised too:     *)
(* 0001 invalid HW identifier   0002 baud rate unsupported      *)
(* 0003 invalid parity value    0004 parameter assign failed    *)
(* 8180 invalid MB_DB parameter (MB_DB not wired correctly)     *)
(* All of them raise bErrCfg.                                   *)
(* ------------------------------------------------------------ *)
(* Rules you must follow (six, breaking any one breaks it)      *)
(* 1. This FB MUST be non-optimized. The attribute              *)
(* { S7_Optimized_Access := 'FALSE' } is written in this source, *)
(* but do check the FB property "Optimized block access" is     *)
(* cleared after import. MB_HOLD_REG only accepts an area with  *)
(* absolute addresses; optimized access gives you 16#818C.      *)
(* 2. Call it unconditionally once per scan, from OB1 or a      *)
(* cyclic interrupt OB - never inside a conditional branch.     *)
(* Manual: within the master response timeout, Modbus_Slave must *)
(* execute at least twice. With RESP_TO = 1000 ms the call cycle *)
(* must not exceed 500 ms.                                      *)
(* 3. One instance of this FB per RS485 port. Need more data?   *)
(* Enlarge the array. Do not drop a second instance on the port. *)
(* 4. uPortHwId takes the module HARDWARE IDENTIFIER (device    *)
(* config -> module properties -> hardware identifier), often   *)
(* 269 or 271 - not the slot number. Wrong value gives 16#0001. *)
(* 5. MB_DB is wired as #instSlave.MB_DB (multi-instance static).*)
(* If your TIA version refuses to resolve it, switch to the     *)
(* single-instance variant: drop Modbus_Slave outside the FB so *)
(* it gets its own DB (e.g. DB_Slave), then change MB_DB in     *)
(* section 4 to "DB_Slave".MB_DB, the statics in section 8 to   *)
(* "DB_Slave".Bad_CRC_Count and so on, and point MB_HOLD_REG at *)
(* an array inside that same DB. See the guide document.        *)
(* 6. Block names must match your library: new libraries use    *)
(* Modbus_Comm_Load / Modbus_Slave, old ones MB_COMM_LOAD /     *)
(* MB_SLAVE. Rename the two VAR declarations for old libraries - *)
(* never mix versions (the manual forbids it).                  *)
(* ------------------------------------------------------------ *)
(* Version: v1   2026-09-30                                     *)
(* Import: project tree -> External source files -> Add new     *)
(* external file -> Generate blocks from source. Do not create  *)
(* a block of the same name by hand first. This file holds 1 FB. *)
(* ============================================================ *)

FUNCTION_BLOCK "FB_ModbusRtuSlave"
{ S7_Optimized_Access := 'FALSE' }

(* Constant section note: the local-constant section keyword in *)
(* TIA source files varies between versions / docs (VAR_CONSTANT *)
(* vs "VAR CONSTANT"), and a wrong spelling makes the whole     *)
(* interface import fail. To guarantee a clean one-shot import, *)
(* the two "constants" live in the VAR section with fixed start *)
(* values; references stay unchanged (#HOLD_MAX / #CHUNK).      *)
(* Do NOT modify these two at runtime.                          *)
VAR_INPUT
    bEnable      : Bool  := TRUE;       // master enable: FALSE stops responding
    uPortHwId    : UInt  := 269;        // module hardware identifier
    uBaud        : UDInt := 9600;       // baud rate (300..115200, listed values only)
    uParity      : UInt  := 0;          // parity: 0 none / 1 odd / 2 even
    uMode485     : USInt := 4;          // port mode: 4 = half duplex RS485 two wire
    uSlaveAddr   : UInt  := 1;          // our Modbus address 1..247 (0 is broadcast)
    uFlowCtrl    : UInt  := 0;          // flow control: 0 = none (RS485 half duplex)
    uRtsOnDly    : UInt  := 0;          // RTS on delay [ms], normally 0
    uRtsOffDly   : UInt  := 0;          // RTS off delay [ms], normally 0
    uRespTo      : UInt  := 1000;       // response timeout [ms], keep default
    tHostTimeout : Time  := T#10s;      // master lost after this long without a frame
    tRetry       : Time  := T#5s;       // auto retry interval after port init failure
    bTrackChange : Bool  := TRUE;       // TRUE = track which register was changed
    bErrClr      : Bool  := FALSE;      // rising edge: clear fault latch and counters
    bReload      : Bool  := FALSE;      // rising edge: run port init again
END_VAR

VAR_OUTPUT
    (* -------------------------- health -------------------------- *)
    bReady       : Bool;                // port ready
    bNdr         : Bool;                // master wrote new data (pulse)
    bDr          : Bool;                // master read data (pulse)
    bCommBeat    : Bool;                // heartbeat, toggles per good frame
    bHostLost    : Bool;                // master lost
    tIdleTime    : Time;                // time since last successful frame
    (* ------------------------ fault code ------------------------ *)
    bError       : Bool;                // error on this scan
    bErrLatch    : Bool;                // fault latched (clear with bErrClr)
    wStatus      : Word;                // live status word
    wLastErr     : Word;                // latched last error code
    uErrSrc      : USInt;               // 0 none / 1 port init / 2 slave instr.
    (* ----------------------- fault class ------------------------ *)
    bErrCfg      : Bool;                // port configuration
    bErrHold     : Bool;                // holding area pointer / range
    bErrFrame    : Bool;                // frame format / protocol
    bErrPara     : Bool;                // address / length parameter
    bErrBusy     : Bool;                // port busy
    bErrOther    : Bool;                // uncovered error code
    bLineNoise   : Bool;                // line noise (CRC bad frames > 5 percent)
    bParaFault   : Bool;                // slave address outside 1..247
    uModeAct     : USInt;               // MODE actually in force (read back)
    bModeFault   : Bool;                // TRUE = uMode485 out of 0..4, clamped to 4
    (* ------------------------- counters ------------------------- *)
    dwReqCnt     : DWord;               // requests served
    dwReadCnt    : DWord;               // reads served
    dwWriteCnt   : DWord;               // writes served
    dwErrCnt     : DWord;               // fault count (error edges)
    wReqTotal    : Word;                // total requests seen (incl. bad frames)
    wCrcErr      : Word;                // CRC error frame count
    wExcCnt      : Word;                // exception responses returned
    wSuccCnt     : Word;                // requests without protocol error
    (* ---------------------- data tracking ----------------------- *)
    bDataChanged : Bool;                // holding area value changed (pulse)
    wLastChgIdx  : Word;                // index changed last (0 = 40001)
END_VAR

VAR
    (* ---- block constants (in VAR for import compatibility) ----- *)
    HOLD_MAX     : Int := 1000;         // holding register count (40001..41000)
    CHUNK        : Int := 100;          // words scanned per cycle for tracking

    (* -------- holding registers: inside the instance DB --------- *)
    aHoldReg     : ARRAY[0..999] OF Word;
    aShadow      : ARRAY[0..999] OF Word;   // shadow copy: who changed this cycle

    (* ----------- library instruction multi-instances ------------ *)
    instLoad     : "Modbus_Comm_Load";
    instSlave    : "Modbus_Slave";

    iStep        : Int   := 0;          // 0 start init / 5 wait init / 10 run / 100 retry
    bLoadReq     : Bool;                // REQ for Modbus_Comm_Load (needs an edge)
    bReloadLast  : Bool;
    bReloadRise  : Bool;
    bErrClrLast  : Bool;
    bErrClrRise  : Bool;
    bErrLast     : Bool;                // previous ERROR, used to get the edge
    uAddrEff     : UInt  := 1;          // clamped slave address
    uModeEff     : USInt := 4;          // clamped port MODE (written to Comm_Load Static)
    bShadowInit  : Bool;                // shadow copy synchronised once
    iScanPos     : Int   := 0;          // chunked scan cursor
    i            : Int   := 0;          // loop variable
    iPos         : Int   := 0;          // index currently scanned
    wCrcLast     : Word  := 0;          // previous CRC counter (to take the delta)
    dwCrcTotal   : DWord := 0;          // CRC errors accumulated past the wrap
    tonRetry     : TON;
    tonIdle      : TON;
    bNdrNow      : Bool;
    bDrNow       : Bool;
    bErrNow      : Bool;
    wStatNow     : Word;
END_VAR


BEGIN

(* ============================================================ *)
(* 1. Clear the pulse outputs every cycle                       *)
(* ============================================================ *)
#bNdr         := FALSE;
#bDr          := FALSE;
#bDataChanged := FALSE;

(* ============================================================ *)
(* 2. Edge detection and parameter clamping                     *)
(* ============================================================ *)
#bReloadRise := #bReload AND NOT #bReloadLast;
#bReloadLast := #bReload;
#bErrClrRise := #bErrClr  AND NOT #bErrClrLast;
#bErrClrLast := #bErrClr;

(* An illegal address is clamped to 1 and flagged: 0 is the     *)
(* broadcast address and standard Modbus stops at 247. VAR_INPUT *)
(* cannot be written back, so the result goes into uAddrEff.    *)
#bParaFault := (#uSlaveAddr < 1) OR (#uSlaveAddr > 247);
IF #bParaFault THEN
    #uAddrEff := 1;
ELSE
    #uAddrEff := #uSlaveAddr;
END_IF;

(* Port MODE: anything outside 0..4 is clamped back to 4 (RS485 *)
(* half duplex). Again no write-back into VAR_INPUT - with an   *)
(* actual parameter wired, it would be overwritten every scan and   *)
(* the clamp would be pointless. Result goes into uModeEff.     *)
#bModeFault := (#uMode485 > 4);
IF #bModeFault THEN
    #uModeEff := 4;
ELSE
    #uModeEff := #uMode485;
END_IF;

(* ============================================================ *)
(* 3. Port init retry timer (called every cycle, or it sticks)  *)
(* ============================================================ *)
#tonRetry(IN := (#iStep = 100), PT := #tRetry);

(* ============================================================ *)
(* 4. State machine                                             *)
(* ============================================================ *)
CASE #iStep OF

    0:  (* ---- start port init: give REQ a rising edge ---- *)
        #bLoadReq := TRUE;
        #bReady   := FALSE;
        #iStep    := 5;

    5:  (* ---- wait for init. EN must stay TRUE until done ---- *)
        (* Modbus_Comm_Load works through RDREC/WRREC, which are        *)
        (* asynchronous; the manual requires EN to stay TRUE            *)
        (* until completion, otherwise the config is lost.              *)

        (* ------------------------------------------------------------
           MODE is NOT a pin! It is a Static tag inside the
           Modbus_Comm_Load instance DB. Siemens doc (Entry ID
           47756141): default 0 = full duplex RS232; set it "via the
           start value" to 4 = half duplex RS485 two wire. Left at 0,
           baud and parity look fine but not a single frame gets
           through (STATUS sits at 818B).
           It must be written BEFORE the REQ rising edge - the
           instruction only pushes the config into the module on REQ.
           Written every scan, so whatever an external agent breaks
           is written back on the next scan.
           ------------------------------------------------------------ *)
        #instLoad.MODE := #uModeEff;

        #instLoad(EN          := #bEnable,
                  REQ         := #bLoadReq,
                  PORT        := #uPortHwId,
                  BAUD        := #uBaud,
                  PARITY      := #uParity,
                  FLOW_CTRL   := #uFlowCtrl,
                  RTS_ON_DLY  := #uRtsOnDly,
                  RTS_OFF_DLY := #uRtsOffDly,
                  RESP_TO     := #uRespTo,
                  MB_DB       := #instSlave.MB_DB);
        (* DONE / ERROR / STATUS are read straight off the instance     *)
        #bLoadReq := FALSE;                 (* one scan high only *)
        #uModeAct  := #instLoad.MODE;       (* read back: confirm it is 4 *)

        IF #instLoad.ERROR THEN
            (* port did not come up: record it and wait for retry,          *)
            (* never pretend everything is fine                             *)
            #wStatus   := #instLoad.STATUS;
            #wLastErr  := #instLoad.STATUS;
            #uErrSrc   := 1;
            #bErrCfg   := TRUE;
            #bErrLatch := TRUE;
            #bError    := TRUE;
            #iStep     := 100;
        ELSIF #instLoad.DONE THEN
            #bReady  := TRUE;
            #wStatus := #instLoad.STATUS;
            #iStep   := 10;
        END_IF;

    10: (* ---- running: Modbus_Slave must run every cycle ---- *)
        IF #bReloadRise THEN
            #bShadowInit := FALSE;          (* resync the shadow copy *)
            #iStep := 0;
        ELSE
            #instSlave(EN          := #bEnable,
                       MB_ADDR     := #uAddrEff,
                       MB_HOLD_REG := #aHoldReg,
                       NDR         => #bNdrNow,
                       DR          => #bDrNow,
                       ERROR       => #bErrNow,
                       STATUS      => #wStatNow);

            #wStatus := #wStatNow;
            #bError  := #bErrNow;

            (* -------- 4.1 latch the fault on the error edge only -------- *)
            (* A pointer fault such as 818C repeats every scan;             *)
            (* counting scans makes dwErrCnt climb by thousands a           *)
            (* second, which looks like a explosion but is one bug.         *)
            IF #bErrNow AND NOT #bErrLast THEN
                #wLastErr  := #wStatNow;
                #uErrSrc   := 2;
                #bErrLatch := TRUE;
                #dwErrCnt  := #dwErrCnt + 1;
            END_IF;
            #bErrLast := #bErrNow;

            (* ----------- 4.2 communication activity counters ------------ *)
            IF #bNdrNow THEN
                #bNdr       := TRUE;
                #dwWriteCnt := #dwWriteCnt + 1;
                #dwReqCnt   := #dwReqCnt + 1;
                #bCommBeat  := NOT #bCommBeat;
            END_IF;
            IF #bDrNow THEN
                #bDr        := TRUE;
                #dwReadCnt  := #dwReadCnt + 1;
                #dwReqCnt   := #dwReqCnt + 1;
                #bCommBeat  := NOT #bCommBeat;
            END_IF;
        END_IF;

    100: (* ---- port init failed, wait and retry ---- *)
        IF #tonRetry.Q OR #bReloadRise THEN
            #iStep := 0;                    (* go and configure again *)
        END_IF;

    ELSE
        #iStep := 0;
END_CASE;

(* ============================================================ *)
(* 5. Fault class: turn the status code into "where to look"    *)
(* ============================================================ *)
(* Recomputed every cycle, so the class flags drop by themselves *)
(* once the fault is gone. wLastErr and bErrLatch are latched   *)
(* and do not drop with them.                                   *)
IF #uErrSrc = 1 THEN
    (* port init stage: 0001/0002/0003/0004/8180 are all config     *)
    #bErrCfg   := (#wStatus <> 16#0000);
    #bErrHold  := FALSE;
    #bErrFrame := FALSE;
    #bErrPara  := FALSE;
    #bErrBusy  := FALSE;
    #bErrOther := FALSE;
ELSE
    CASE #wStatus OF
        16#0000, 16#7000, 16#7001, 16#7002:
            (* idle or executing, all fine                                  *)
            #bErrCfg   := FALSE;
            #bErrHold  := FALSE;
            #bErrFrame := FALSE;
            #bErrPara  := FALSE;
            #bErrBusy  := FALSE;
            #bErrOther := FALSE;

        16#8188, 16#8189, 16#818A:
            #bErrPara := TRUE;              (* address / length / mode illegal *)

        16#818B, 16#818C, 16#8383:
            #bErrHold := TRUE;              (* holding area pointer or range *)

        16#8200:
            #bErrBusy := TRUE;              (* master is pushing too hard *)

        16#8380, 16#8381, 16#8382, 16#8384, 16#8385, 16#80C8:
            #bErrFrame := TRUE;             (* bad frame: noise or wrong baud *)

    ELSE
        (* unlisted code: non zero and not a 70xx execution code        *)
        IF (#wStatus <> 16#0000) AND ((#wStatus AND 16#FF00) <> 16#7000) THEN
            #bErrOther := TRUE;
        ELSE
            #bErrOther := FALSE;
        END_IF;
        #bErrCfg   := FALSE;
        #bErrHold  := FALSE;
        #bErrFrame := FALSE;
        #bErrPara  := FALSE;
        #bErrBusy  := FALSE;
    END_CASE;
END_IF;

(* ============================================================ *)
(* 6. Master lost detection                                     *)
(* ============================================================ *)
(* NDR or DR means a frame was just served, so reset the timer. *)
(* A CRC-bad frame never raises NDR/DR - that case is "there is *)
(* traffic but nothing gets through", exactly what wCrcErr and  *)
(* bLineNoise are there to show.                                *)
#tonIdle(IN := #bEnable AND #bReady AND NOT (#bNdrNow OR #bDrNow),
         PT := #tHostTimeout);
#bHostLost := #bEnable AND #bReady AND #tonIdle.Q;
#tIdleTime := #tonIdle.ET;

(* ============================================================ *)
(* 7. Data tracking: which register did the master change       *)
(* ============================================================ *)
(* Scanning all 1000 words every cycle costs too much CPU, so it *)
(* is chunked: 100 words per cycle, 10 cycles for a full pass.  *)
(* A changed word is still caught, at most 10 cycles late.      *)
(* Note: when the PLC itself writes aHoldReg it also triggers   *)
(* this - that is your own doing, not the master's. Set         *)
(* bTrackChange to FALSE if you do not need the feature.        *)
IF #bTrackChange AND #bReady THEN
    IF NOT #bShadowInit THEN
        FOR #i := 0 TO #HOLD_MAX - 1 DO
            #aShadow[#i] := #aHoldReg[#i];
        END_FOR;
        #bShadowInit := TRUE;
    ELSE
        FOR #i := 0 TO #CHUNK - 1 DO
            #iPos := #iScanPos + #i;
            IF #iPos >= #HOLD_MAX THEN
                EXIT;                       (* hit the end, restart at 0 *)
            END_IF;
            IF #aHoldReg[#iPos] <> #aShadow[#iPos] THEN
                #aShadow[#iPos] := #aHoldReg[#iPos];
                #wLastChgIdx  := INT_TO_WORD(#iPos);
                #bDataChanged := TRUE;
            END_IF;
        END_FOR;

        #iScanPos := #iScanPos + #CHUNK;
        IF #iScanPos >= #HOLD_MAX THEN
            #iScanPos := 0;
        END_IF;
    END_IF;
END_IF;

(* ============================================================ *)
(* 8. Official diagnostic counters in the slave instance        *)
(* ============================================================ *)
(* These are the public static tags listed in the S7-1200 system *)
(* manual, Table 13-85. Read them directly instead of parsing   *)
(* frames yourself. If your TIA refuses the access, replace     *)
(* #instSlave. with the single instance DB name.                *)
#wReqTotal := #instSlave.Request_Count;
#wCrcErr   := #instSlave.Bad_CRC_Count;
#wExcCnt   := #instSlave.Exception_Count;
#wSuccCnt  := #instSlave.Success_Count;

(* The CRC counter is a Word and wraps at 65535, so keep an     *)
(* independent DWord total here.                                *)
IF #wCrcErr <> #wCrcLast THEN
    #wCrcLast   := #wCrcErr;
    #dwCrcTotal := #dwCrcTotal + 1;
END_IF;

(* Bad frames above 5 percent and more than five of them: the   *)
(* usual causes are a missing terminator resistor, a shield     *)
(* grounded at both ends, or baud rate / parity mismatch.       *)
#bLineNoise := (#dwCrcTotal > 5) AND (#dwReqCnt > 0)
               AND ((#dwCrcTotal * 20) > #dwReqCnt);

(* ============================================================ *)
(* 9. Fault clear                                               *)
(* ============================================================ *)
IF #bErrClrRise THEN
    #bErrLatch  := FALSE;
    #wLastErr   := 16#0000;
    #uErrSrc    := 0;
    #dwErrCnt   := 0;
    #dwCrcTotal := 0;
END_IF;

END_FUNCTION_BLOCK
