(* ============================================================ *)
(* FB_ModbusRtuSlave —— 485 从站：保持寄存器 1000 个            *)
(* S7-1200/1500 TIA Portal SCL（导入用，GB2312/ANSI 编码）      *)
(* 导入：项目树 -> 外部源文件 -> 添加 -> 从源生成块             *)
(* 本文件含 1 个 FB。不要先手动新建同名对象。                   *)
(* 依赖 Modbus_Comm_Load + Modbus_Slave（CM/CB 1241）。         *)
(* 背景 DB 必须非优化访问，否则 MB_HOLD_REG 报 16#818C。        *)
(* 保持寄存器就在本块背景 DB 的 aHoldReg 里，下标 0 = 40001。   *)
(* 每扫描周期无条件调用一次；一个 485 口只能有一个本块实例。    *)
(* 诊断引脚含义、状态码分类表、排错表都在 UTF-8 版块头里。      *)
(* 版本：v1 2026-09-30                                          *)
(* ============================================================ *)

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

(* 常量段说明：TIA 源文件的局部常量段写法在不同版本里差异大     *)
(* （VAR_CONSTANT 还是 VAR CONSTANT），写错会让整个接口导入失败。 *)
(* 为了保证一次导入成功，这里把两个"常量"放进 VAR 段用初始值固定， *)
(* 引用方式不变（#HOLD_MAX / #CHUNK）。**运行时别改这两个值**。 *)
VAR_INPUT
    bEnable      : Bool  := TRUE;       // 总使能：FALSE = 停止响应，端口配置保持
    uPortHwId    : UInt  := 269;        // 通信模块硬件标识符（设备组态里查）
    uBaud        : UDInt := 9600;       // 波特率（只认 300/600/1200/2400/4800/9600/19200/38400/57600/76800/115200）
    uParity      : UInt  := 0;          // 校验：0 无 / 1 奇 / 2 偶
    uMode485     : USInt := 4;          // 端口工作模式：4 = 半双工 RS485 二线制（485 就用 4）
    uSlaveAddr   : UInt  := 1;          // 本站 Modbus 站号 1..247（0 是广播地址，不能用）
    uFlowCtrl    : UInt  := 0;          // 流控制：0 = 无（RS485 半双工就用 0）
    uRtsOnDly    : UInt  := 0;          // RTS 接通延时 [ms]，一般 0
    uRtsOffDly   : UInt  := 0;          // RTS 关断延时 [ms]，一般 0
    uRespTo      : UInt  := 1000;       // 响应超时 [ms]，对从站影响不大，保持默认
    tHostTimeout : Time  := T#10s;      // 上位机掉线判定：这么久没来过一帧就算掉线
    tRetry       : Time  := T#5s;       // 端口初始化失败后的自动重试间隔
    bTrackChange : Bool  := TRUE;       // TRUE = 追踪哪个寄存器被上位机改写
    bErrClr      : Bool  := FALSE;      // 上升沿：清除故障锁存与错误计数
    bReload      : Bool  := FALSE;      // 上升沿：重新执行一次端口初始化（改波特率后用它）
END_VAR

VAR_OUTPUT
    (* -------------------------- 健康度 -------------------------- *)
    bReady       : Bool;                // 端口就绪，可以通信
    bNdr         : Bool;                // 上位机刚写入新数据（脉冲）
    bDr          : Bool;                // 上位机刚读取数据（脉冲）
    bCommBeat    : Bool;                // 通信心跳，每成功一帧翻转一次
    bHostLost    : Bool;                // 上位机掉线
    tIdleTime    : Time;                // 距上次成功通信的时间
    (* -------------------------- 故障码 -------------------------- *)
    bError       : Bool;                // 当前拍有错误
    bErrLatch    : Bool;                // 故障锁存（bErrClr 清除）
    wStatus      : Word;                // 实时状态字
    wLastErr     : Word;                // 锁存的最后错误码
    uErrSrc      : USInt;               // 最后错误来源：0 无 / 1 端口初始化 / 2 从站指令
    (* ------------------------- 故障分类 ------------------------- *)
    bErrCfg      : Bool;                // 端口组态类
    bErrHold     : Bool;                // 保持区指针 / 越界类
    bErrFrame    : Bool;                // 帧格式 / 协议类
    bErrPara     : Bool;                // 地址 / 长度参数类
    bErrBusy     : Bool;                // 端口忙
    bErrOther    : Bool;                // 未分类的错误码
    bLineNoise   : Bool;                // 线路干扰（CRC 错帧占比 > 5%）
    bParaFault   : Bool;                // 本站站号不在 1..247
    uModeAct     : USInt;               // 端口实际生效的 MODE（回读，应等于 uMode485）
    bModeFault   : Bool;                // TRUE = uMode485 不在 0..4，已钳成 4
    (* -------------------------- 计数器 -------------------------- *)
    dwReqCnt     : DWord;               // 成功处理的请求总数
    dwReadCnt    : DWord;               // 被读次数
    dwWriteCnt   : DWord;               // 被写次数
    dwErrCnt     : DWord;               // 故障次数（按错误沿计）
    wReqTotal    : Word;                // 从站收到的请求总数（含坏帧与广播）
    wCrcErr      : Word;                // CRC 错误帧计数
    wExcCnt      : Word;                // 回给主站的异常响应计数
    wSuccCnt     : Word;                // 无协议错误的请求数
    (* ------------------------- 数据追踪 ------------------------- *)
    bDataChanged : Bool;                // 保持区有值被改写（脉冲）
    wLastChgIdx  : Word;                // 最近被改写的下标（0 = 40001）
END_VAR

VAR
    (* -- 块内常量（放 VAR 段以保证导入兼容，运行时别改） --        *)
    HOLD_MAX     : Int := 1000;         // 保持寄存器个数（Modbus 40001..41000）
    CHUNK        : Int := 100;          // 每周期扫描多少个字做改写检测

    (* -- 保持寄存器：就在背景 DB 里，导入后可直接在背景 DB 改值 -- *)
    aHoldReg     : ARRAY[0..999] OF Word;
    aShadow      : ARRAY[0..999] OF Word;   // 影子区：比对本周期谁被改了

    (* ---------------------- 库指令多重实例 ---------------------- *)
    instLoad     : "Modbus_Comm_Load";
    instSlave    : "Modbus_Slave";

    iStep        : Int   := 0;          // 0 发起初始化 / 5 等初始化 / 10 运行 / 100 等重试
    bLoadReq     : Bool;                // 给 Modbus_Comm_Load 的 REQ（要上升沿）
    bReloadLast  : Bool;
    bReloadRise  : Bool;
    bErrClrLast  : Bool;
    bErrClrRise  : Bool;
    bErrLast     : Bool;                // 上一拍的 ERROR，用来取错误沿
    uAddrEff     : UInt  := 1;          // 钳位后的有效站号
    uModeEff     : USInt := 4;          // 钳位后的有效端口 MODE（写进 Comm_Load 的 Static）
    bShadowInit  : Bool;                // 影子区是否已完成首次全量同步
    iScanPos     : Int   := 0;          // 改写检测的分块扫描游标
    i            : Int   := 0;          // 循环变量
    iPos         : Int   := 0;          // 当前扫描到的下标
    wCrcLast     : Word  := 0;          // 上一拍的 CRC 计数值（取增量用）
    dwCrcTotal   : DWord := 0;          // CRC 错误累计（跨 65535 回绕也不断）
    tonRetry     : TON;
    tonIdle      : TON;
    bNdrNow      : Bool;
    bDrNow       : Bool;
    bErrNow      : Bool;
    wStatNow     : Word;
END_VAR


BEGIN

(* ============================================================ *)
(* 1. 脉冲量每周期清零                                          *)
(* ============================================================ *)
#bNdr         := FALSE;
#bDr          := FALSE;
#bDataChanged := FALSE;

(* ============================================================ *)
(* 2. 边沿检测与参数钳位                                        *)
(* ============================================================ *)
#bReloadRise := #bReload AND NOT #bReloadLast;
#bReloadLast := #bReload;
#bErrClrRise := #bErrClr  AND NOT #bErrClrLast;
#bErrClrLast := #bErrClr;

(* 站号非法时钳回 1 并报警——0 是广播地址，247 以上标准          *)
(* Modbus 不认。不能写回 VAR_INPUT，所以存进 uAddrEff。         *)
#bParaFault := (#uSlaveAddr < 1) OR (#uSlaveAddr > 247);
IF #bParaFault THEN
    #uAddrEff := 1;
ELSE
    #uAddrEff := #uSlaveAddr;
END_IF;

(* 端口 MODE：0..4 之外一律钳回 4（RS485 半双工）。             *)
(* 同样不写回 VAR_INPUT——接了实参的话每拍都会被实参盖掉，       *)
(* 钳位等于白做。存进 uModeEff，STEP 5 时写进指令的 Static。    *)
#bModeFault := (#uMode485 > 4);
IF #bModeFault THEN
    #uModeEff := 4;
ELSE
    #uModeEff := #uMode485;
END_IF;

(* ============================================================ *)
(* 3. 端口初始化重试定时器（每周期都调用，状态才不会卡住）      *)
(* ============================================================ *)
#tonRetry(IN := (#iStep = 100), PT := #tRetry);

(* ============================================================ *)
(* 4. 状态机                                                    *)
(* ============================================================ *)
CASE #iStep OF

    0:  (* ---- 发起端口初始化：给一个 REQ 上升沿 ---- *)
        #bLoadReq := TRUE;
        #bReady   := FALSE;
        #iStep    := 5;

    5:  (* ---- 等初始化完成。EN 必须一直挂到 DONE/ERROR ---- *)
        (* Modbus_Comm_Load 内部走 RDREC/WRREC，是异步的，              *)
        (* 手册明确要求 EN 保持 TRUE 直到完成，中途掉了就白配。         *)

        (* ------------------------------------------------------------
           MODE：不是引脚，是 Modbus_Comm_Load 背景 DB 里的 Static 变量。
           西门子官方文档（Entry ID 47756141）原文：MODE 默认 0 = 全双工
           RS232，需"通过起始值"改成 4 = 半双工 RS485 二线制。
           不改的后果：波特率校验全对，但一帧都不通（STATUS 挂 818B）。
           必须在 REQ 上升沿【之前】写：指令只在 REQ 时把配置刷进模块。
           每拍都写，外部谁改坏了下一拍也会被写回来。
           ------------------------------------------------------------ *)
        #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 直接读实例，不再另接引脚               *)
        #bLoadReq := FALSE;                 (* 只留一拍高电平 *)
        #uModeAct  := #instLoad.MODE;       (* 回读：HMI 上确认真的是 4 *)

        IF #instLoad.ERROR THEN
            (* 端口没配起来：记下来，进等重试，不假装没事                   *)
            #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: (* ---- 正常运行：Modbus_Slave 必须每周期调用 ---- *)
        IF #bReloadRise THEN
            #bShadowInit := FALSE;          (* 重新同步影子区 *)
            #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 故障锁存：只在错误沿上记一次 ------------- *)
            (* 指针类错误（818C）是每拍都报的，按拍累加会让                 *)
            (* dwErrCnt 一秒涨几千，看着像炸了其实就一个毛病。              *)
            IF #bErrNow AND NOT #bErrLast THEN
                #wLastErr  := #wStatNow;
                #uErrSrc   := 2;
                #bErrLatch := TRUE;
                #dwErrCnt  := #dwErrCnt + 1;
            END_IF;
            #bErrLast := #bErrNow;

            (* --------------------- 4.2 通信活动统计 --------------------- *)
            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: (* ---- 端口初始化失败，定时重试 ---- *)
        IF #tonRetry.Q OR #bReloadRise THEN
            #iStep := 0;                    (* 回去重配一次 *)
        END_IF;

    ELSE
        #iStep := 0;
END_CASE;

(* ============================================================ *)
(* 5. 故障分类：把状态码翻译成"该去查什么"                      *)
(* ============================================================ *)
(* 这里每周期重算，所以故障恢复后分类位会自动落下；             *)
(* wLastErr / bErrLatch 是锁存的，不会跟着落。                  *)
IF #uErrSrc = 1 THEN
    (* 端口初始化阶段：0001/0002/0003/0004/8180 全是组态问题        *)
    #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:
            (* 空闲或执行中，一切正常                                       *)
            #bErrCfg   := FALSE;
            #bErrHold  := FALSE;
            #bErrFrame := FALSE;
            #bErrPara  := FALSE;
            #bErrBusy  := FALSE;
            #bErrOther := FALSE;

        16#8188, 16#8189, 16#818A:
            #bErrPara := TRUE;              (* 地址 / 长度 / 模式非法 *)

        16#818B, 16#818C, 16#8383:
            #bErrHold := TRUE;              (* 保持区指针或越界 *)

        16#8200:
            #bErrBusy := TRUE;              (* 上位机催得太急 *)

        16#8380, 16#8381, 16#8382, 16#8384, 16#8385, 16#80C8:
            #bErrFrame := TRUE;             (* 帧坏了，多半是干扰或波特率不对 *)

    ELSE
        (* 没列出的码：非 0 且不是 70xx 执行码，就算"其他"              *)
        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. 上位机掉线检测                                            *)
(* ============================================================ *)
(* 有 NDR 或 DR 就说明刚成功处理了一帧，定时器复位。            *)
(* CRC 坏帧不会让 NDR/DR 抬起——那种情况是"线上有动静但          *)
(* 一帧都没成"，正好由 wCrcErr 和 bLineNoise 去反映。           *)
#tonIdle(IN := #bEnable AND #bReady AND NOT (#bNdrNow OR #bDrNow),
         PT := #tHostTimeout);
#bHostLost := #bEnable AND #bReady AND #tonIdle.Q;
#tIdleTime := #tonIdle.ET;

(* ============================================================ *)
(* 7. 数据追踪：哪个寄存器被上位机改写了                        *)
(* ============================================================ *)
(* 1000 个字每周期全扫一遍太占 CPU，所以分块扫：每周期 100 个， *)
(* 10 个周期覆盖一轮。改一个字照样能抓到，只是最多晚 10 个周期。*)
(* 注意：PLC 自己往 aHoldReg 里写也会触发——那是自己干的，       *)
(* 不是主站干的；不需要这个功能就把 bTrackChange 置 FALSE。     *)
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;                       (* 扫到尾部就退出，下周期从 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. 从站实例里的官方诊断计数器                                *)
(* ============================================================ *)
(* 这些是西门子系统手册 Table 13-85 明列的"公开静态变量"，      *)
(* 程序里可以直接读，不用自己解析报文。                         *)
(* 若你的 TIA 提示无法访问，把 #instSlave. 换成单实例 DB 名。   *)
#wReqTotal := #instSlave.Request_Count;
#wCrcErr   := #instSlave.Bad_CRC_Count;
#wExcCnt   := #instSlave.Exception_Count;
#wSuccCnt  := #instSlave.Success_Count;

(* CRC 计数是 Word，65535 会回绕，所以自己攒一个 DWord 总量。   *)
IF #wCrcErr <> #wCrcLast THEN
    #wCrcLast   := #wCrcErr;
    #dwCrcTotal := #dwCrcTotal + 1;
END_IF;

(* 坏帧占比超过 5% 且不止 5 次 —— 典型的是终端电阻没接、        *)
(* 屏蔽层单端接地没做、或者波特率/校验和上位机不一致。          *)
#bLineNoise := (#dwCrcTotal > 5) AND (#dwReqCnt > 0)
               AND ((#dwCrcTotal * 20) > #dwReqCnt);

(* ============================================================ *)
(* 9. 故障清除                                                  *)
(* ============================================================ *)
IF #bErrClrRise THEN
    #bErrLatch  := FALSE;
    #wLastErr   := 16#0000;
    #uErrSrc    := 0;
    #dwErrCnt   := 0;
    #dwCrcTotal := 0;
END_IF;

END_FUNCTION_BLOCK
