文章目录

ST扫描周期超标:复杂循环导致看门狗超时的排查方法

发布于 2026-03-20 04:50:54 · 浏览 5 次 · 评论 0 条

ST扫描周期超标:复杂循环导致看门狗超时的排查方法

在基于IEC 61131-3标准的PLC编程中,结构化文本(Structured Text,ST)因其接近高级语言的表达能力,常被用于实现复杂逻辑、数学运算、状态机和循环控制。但正因灵活性高,不当使用循环结构极易引发扫描周期(Scan Cycle Time)严重超标,最终触发硬件看门狗(Watchdog Timer)复位——表现为PLC无预警停机、输出突变、HMI通信中断,且故障日志仅显示“Watchdog timeout”或“Cycle time exceeded”,不指向具体代码位置。本文聚焦此类典型故障,提供一套无需依赖专用分析工具、纯靠PLC原生功能与系统性推理即可定位并解决的实操排查流程。


一、理解问题本质:为什么ST循环会拖垮扫描周期?

PLC执行遵循“循环扫描”机制:每个扫描周期内,CPU依次完成输入采样 → 程序执行 → 输出刷新。扫描周期时间 = 输入处理时间 + 用户程序执行时间 + 输出处理时间 + 系统开销。其中,用户程序执行时间完全由用户代码决定。

ST语言支持 FORWHILEREPEAT 循环,其执行耗时与循环次数 × 单次迭代耗时成正比。问题常出现在以下场景:

  • 隐式无限循环WHILE TRUE DO ... END_WHILE 未设可靠退出条件,或退出判断依赖未更新的静态变量;
  • 动态大循环:循环上限由外部变量(如 FOR i := 0 TO n DO)控制,而 n 因传感器误读、通信异常或配置错误被赋值为极大数(如 65535);
  • 嵌套循环叠加:外层循环 100 次,内层每次执行 200 次操作,实际迭代达 20,000 次,远超毫秒级扫描窗口;
  • 循环内调用高开销函数:如浮点三角函数 SIN()、字符串分割 FIND()、数组拷贝 MEMCPY(),单次调用耗时达数十微秒,累积效应显著。

看门狗定时器通常设定为 100 ms500 ms(依PLC型号而异),若单次扫描耗时超过该阈值,CPU强制复位。此时,问题根源不在硬件老化或电源波动,而在代码逻辑本身存在不可控的时间复杂度


二、快速确认:验证是否为扫描周期超限

在开始深度排查前,先用PLC内置诊断功能做三步确认:

  1. 启用扫描周期监控

    • 在TIA Portal中,打开 “在线与诊断” → “循环时间” → 勾选 “记录循环时间”;
    • 在Codesys中,调用 GET_CYCLE_TIME() 函数块,将返回值写入 REAL 类型变量 CycleTime_ms
    • 在GX Works3中,进入 “诊断” → “PLC监视” → “扫描时间监视”,点击 “开始监视”。
  2. 观察实时数值

    • 正常工况下,扫描周期应稳定在 5 ms30 ms(视CPU性能与程序规模而定);
    • 若出现 >100 ms 的尖峰,且与某设备动作(如电机启动、配方切换)同步,则高度怀疑该动作触发了问题循环。
  3. 检查看门狗事件日志

    • 在PLC信息界面查找 “Last reset reason” 或 “Reset source”;
    • 明确显示 Watchdog timeoutCycle time exceeded 即可锁定方向。

若以上任一条件不满足,则问题可能源于I/O模块故障、背板总线干扰或电源噪声,需转入硬件排查流程——本文后续步骤不再适用。


三、分层隔离:定位问题代码段的四步法

核心原则:不逐行审阅全部ST代码,而是通过“功能块禁用→周期测量→对比分析”快速缩小范围

步骤1:按功能模块注释全部POU(Program Organization Unit)

  • 打开项目,找到所有 .ST 文件(如 MAIN.ST, MOTOR_CTRL.ST, DATA_PROCESS.ST);
  • 在每个POU开头添加唯一标识注释,例如:
    // === MODULE: MOTOR_CTRL.ST (ID: M1) ===
  • 对每个POU的调用语句临时注释(非删除),例如:
    // MOTOR_CTRL();  // ← 注释此行
    // DATA_PROCESS(); // ← 注释此行
  • 仅保留 MAIN 程序体中最简骨架(如仅 Q0.0 := I0.0;),确保PLC能以 ≤2 ms 周期运行;
  • 下载程序并监视扫描周期,确认基线正常(如 1.8 ms)。

步骤2:逐个解禁POU,捕获周期跳变点

  • 取消注释第一个POU调用(如 MOTOR_CTRL();),下载并运行;
  • 监视扫描周期连续30秒,记录最大值;
  • 若周期仍 <10 ms,继续解禁下一个;
  • 一旦某次解禁后周期跃升至 >80 ms,立即停止——该POU即为嫌疑对象。

✦ 关键技巧:若POU较多,可采用“二分法”加速。例如有8个POU,先解禁前4个,若周期正常,再解禁后4个中的前2个,依此类推。

步骤3:在嫌疑POU内定位问题循环

假设 DATA_PROCESS.ST 被锁定,打开其代码:

  1. 搜索所有循环关键字

    • 在编辑器中按 Ctrl + F,依次搜索 FOR, WHILE, REPEAT
    • 记录每处循环的起始行号及循环变量声明(如 FOR i := 0 TO MaxCount DO)。
  2. 对每个循环添加临时计时戳

    • 在循环前插入:
      StartTime_us := GET_MS_TIMER(); // 使用PLC内置毫秒计时器
    • 在循环后插入:
      LoopTime_us := GET_MS_TIMER() - StartTime_us;
      IF LoopTime_us > 5000 THEN // 超过5ms即告警
          AlarmFlag := TRUE;
          AlarmCode := 101; // 自定义错误码
      END_IF
    • AlarmFlagAlarmCode 连接至HMI报警画面,或写入数据日志区。
  3. 运行并触发工况

    • 当报警激活时,AlarmCode 直接对应循环编号(如 101 表示第一个循环),精准定位。

步骤4:分析循环参数与数据流

定位到具体循环后(例如 FOR i := 0 TO SensorDataLen DO),检查:

  • SensorDataLen 来源:是否来自模拟量模块的原始寄存器?是否未做范围校验?
    • 添加防护:IF SensorDataLen > 1000 THEN SensorDataLen := 1000; END_IF
  • 循环内操作类型
    • 若含 SQRT(), LN(), EXP() 等浮点函数,改用查表法或预计算;
    • 若含 ARRAY[0..1000] OF REAL 遍历,确认是否真正需要全量处理——能否改为增量更新?
  • 是否存在隐式阻塞:如循环内调用 WAIT 指令(虽ST中不常见,但自定义函数可能封装),必须替换为状态机轮询。

四、根治方案:五种安全替代模式

发现问题是起点,重构才是终点。以下为经工业现场验证的安全替代方案,兼顾性能与可维护性:

替代1:大数组处理 → 分片执行(Slice Execution)

❌ 危险写法:

FOR i := 0 TO 5000 DO
    Result[i] := ProcessData(RawData[i]);
END_FOR

✅ 安全写法(每次扫描只处理100项):

VAR
    SliceStart : INT := 0;
    SliceSize  : INT := 100;
    TotalItems : INT := 5000;
END_VAR

// 每次扫描处理一片
FOR i := SliceStart TO MIN(SliceStart + SliceSize - 1, TotalItems - 1) DO
    Result[i] := ProcessData(RawData[i]);
END_FOR

// 更新下一片起始位置
SliceStart := SliceStart + SliceSize;
IF SliceStart >= TotalItems THEN
    SliceStart := 0; // 循环回起始
    FullProcessDone := TRUE;
END_IF

替代2:条件循环 → 状态机驱动

❌ 危险写法:

WHILE NOT Converged DO
    NewValue := CalcNext(NewValue);
    Converged := ABS(NewValue - OldValue) < Tolerance;
    OldValue := NewValue;
END_WHILE

✅ 安全写法(最多执行20次,超限强制退出):

CASE ConvState OF
    0: // 初始化
        IterCount := 0;
        OldValue := InitialValue;
        ConvState := 1;
    1: // 迭代计算
        NewValue := CalcNext(OldValue);
        IterCount := IterCount + 1;
        IF ABS(NewValue - OldValue) < Tolerance THEN
            FinalResult := NewValue;
            ConvState := 3; // 成功
        ELSIF IterCount >= 20 THEN
            FinalResult := NewValue; // 取当前最优值
            ConvState := 2; // 超限警告
        ELSE
            OldValue := NewValue;
        END_IF
    2,3: // 终止态,等待复位
        // 不再执行计算
END_CASE

替代3:字符串解析 → 预分配缓冲区+索引遍历

❌ 危险写法(FIND 在长字符串中多次调用):

Pos1 := FIND(InputStr, ',');
Pos2 := FIND(COPY(InputStr, Pos1+1, LEN(InputStr)), ',');

✅ 安全写法(单次遍历提取所有字段):

VAR
    i, StartPos : INT := 0;
    FieldCount  : INT := 0;
    Fields      : ARRAY[0..9] OF STRING[32];
END_VAR

FOR i := 0 TO LEN(InputStr) - 1 DO
    IF InputStr[i] = ',' OR i = LEN(InputStr) - 1 THEN
        IF i = LEN(InputStr) - 1 THEN
            Fields[FieldCount] := COPY(InputStr, StartPos, i - StartPos + 1);
        ELSE
            Fields[FieldCount] := COPY(InputStr, StartPos, i - StartPos);
        END_IF
        FieldCount := FieldCount + 1;
        StartPos := i + 1;
        IF FieldCount >= 10 THEN EXIT; END_IF // 防溢出
    END_IF
END_FOR

替代4:实时性要求高的循环 → 移至FB内部,用EXECUTE控制

将循环封装为功能块,通过使能信号控制执行时机:

FUNCTION_BLOCK DataAnalyzer
VAR_INPUT
    Enable     : BOOL;
    RawData    : ARRAY[0..999] OF REAL;
    DataLen    : INT;
END_VAR
VAR_OUTPUT
    Result     : REAL;
    Done       : BOOL;
    Error      : BOOL;
END_VAR
VAR
    State      : (stIdle, stProcess, stComplete);
    i          : INT;
END_VAR

CASE State OF
    stIdle:
        IF Enable THEN
            i := 0;
            State := stProcess;
        END_IF
    stProcess:
        Result := Result + RawData[i]; // 示例累加
        i := i + 1;
        IF i >= DataLen THEN
            State := stComplete;
        END_IF
    stComplete:
        Done := TRUE;
        State := stIdle;
END_CASE

调用时:Analyzer(EXECUTE := TriggerSignal); —— 仅在 TriggerSignal 上升沿执行一次完整分析,避免扫描周期污染。

替代5:绝对避免的写法(黑名单)

以下代码模式必须从项目中彻底清除:

危险模式 说明 替代方案
WHILE TRUE DO ... END_WHILE 无退出条件,等同死循环 改为 IF 判断 + 状态机
FOR i := 0 TO 65535 DO 上限硬编码为最大值 上限必须关联有效数据长度变量,并加 MAX 限制
REPEAT ... UNTIL (A AND B AND C) 多条件组合退出,易因信号抖动失效 拆分为独立状态判断,加入防抖计时器
循环内调用 DELAYTON 阻塞整个扫描周期 改用 TP(脉冲定时器)配合状态转移

五、预防机制:构建代码准入防线

排查是救火,预防才是治本。建议在团队开发规范中强制执行:

  1. ST代码审查清单(Checklist)

    • 所有循环必须声明明确的上界变量,且该变量必须经过 MIN(..., MAX_ALLOWED) 校验
    • 所有 WHILE 循环必须包含至少一个在循环体内更新的退出条件变量
    • 禁止在循环内调用任何未标注“实时安全”的自定义函数(需函数文档明确声明最坏执行时间 ≤ 100 μs)。
  2. 编译期强制约束(以Codesys为例)

    • Project → Options → Code Generation 中启用 “Runtime error checking” → 勾选 “Array bounds check”;
    • Tools → Scripting 中部署脚本,自动扫描项目内所有 FOR/WHILE/REPEAT,输出报告含:循环位置、变量名、疑似风险等级(如 i TO 65535 标为“高危”)。
  3. 运行期熔断保护(推荐)
    MAIN 程序开头插入全局防护:

    VAR_GLOBAL
        GlobalWatchdogTimer : TON;
        ScanOverThreshold   : BOOL;
    END_VAR
    
    GlobalWatchdogTimer(IN := TRUE, PT := T#50ms); // 50ms熔断阈值
    ScanOverThreshold := GlobalWatchdogTimer.Q;
    IF ScanOverThreshold THEN
        // 强制进入安全状态:清零输出、置位急停标志
        SAFE_SHUTDOWN();
    END_IF

六、典型案例:某灌装线流量积分超时故障

现象:灌装启停瞬间,PLC每3–5小时随机复位,HMI显示“PLC stopped”。

排查过程

  • 步骤1:启用循环时间监视,复位前3秒捕捉到 427 ms 峰值;
  • 步骤2:隔离发现 FLOW_INTEGRATOR.ST 模块解禁后周期飙升;
  • 步骤3:定位循环 FOR i := 0 TO FlowSamples DOFlowSamples 来自高速计数器 HC0 寄存器;
  • 步骤4:发现 HC0 在电机启停瞬间受电磁干扰,偶发读取到 65535(计数器溢出值),导致循环执行 65536 次;
  • 步骤5:增加防护 FlowSamples := MIN(FlowSamples, 1000); 并添加 HC0 读取有效性校验(比对前后两次读数差值是否合理),问题彻底消失。

根本原因:未对高速硬件寄存器做工程化滤波,将电气噪声直接映射为逻辑灾难。


将循环控制权交还给确定性,而非交给不可预测的输入或硬件噪声。每一次 FOR 的边界校验,都是对产线连续性的无声承诺。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文