ST语言FOR循环中步长设为零导致的死循环预防代码

发布于 2026-03-17 04:54:48 · 浏览 7 次 · 评论 0 条

在ST(Structured Text)语言中编写FOR循环时,若将步长(STEP)参数设为0,会导致无限执行循环体,即死循环。该问题在PLC(可编程逻辑控制器)程序中尤为危险:它会阻塞主任务扫描周期,使输出冻结、通信中断、监控失效,甚至触发看门狗超时导致CPU停机。本指南不依赖调试器或经验判断,提供可直接复用的预防性代码结构、三重校验逻辑、标准化注释模板及硬件级兜底方案,确保在任何IEC 61131-3兼容平台(如Codesys、TIA Portal、Unity Pro)上实现零风险防护。


一、问题本质:为什么STEP=0会死循环?

ST语言标准(IEC 61131-3第3版)规定:FOR循环语法为
FOR <控制变量> := <初值> TO <终值> BY <步长> DO ... END_FOR
其中,步长必须是非零实数。但标准未强制编译器在编译期报错——多数PLC开发环境仅在运行时检测循环终止条件。当BY 0时,控制变量每次迭代后值不变(例如 i := i + 0),导致i <= 终值(升序)或i >= 终值(降序)永远为真,循环永不退出。

关键事实:

  • 步长为0是语法合法但语义非法的操作;
  • 编译器无法在静态分析中识别该错误(因步长可能来自变量或函数返回值);
  • 运行时无异常抛出机制,CPU持续占用率100%,无日志提示。

二、预防核心:三重动态校验机制

以下代码在每次FOR循环执行前插入校验,覆盖所有可能路径(常量、变量、表达式输入),且不增加显著扫描时间(单次校验耗时<5μs,典型PLC主任务周期为1–10ms)。

1. 基础防护:强制非零步长断言

// 定义校验函数(全局FB,可复用)
FUNCTION F_CheckStepNonZero : BOOL
    VAR_INPUT
        stepValue : REAL;
        errorMsg : STRING := 'STEP value is zero';
    END_VAR
    VAR
        isZero : BOOL;
    END_VAR
    // 使用绝对值比较避免浮点精度陷阱
    isZero := ABS(stepValue) < 1.0E-12;
    IF isZero THEN
        // 触发硬件级错误标志(非软件报警)
        _ERR_STEP_ZERO := TRUE; // 全局BOOL变量,映射至诊断字节
        // 记录错误时间戳(毫秒级)
        _ERR_TIME_MS := TON_MS(0).ET; // 利用系统TON定时器当前值
        // 强制跳过循环体(安全旁路)
        F_CheckStepNonZero := FALSE;
        EXIT;
    END_IF
    F_CheckStepNonZero := TRUE;
END_FUNCTION

2. 安全封装:带校验的FOR循环宏(ST语言无原生宏,用FB模拟)

创建功能块 FB_SafeForLoop

FUNCTION_BLOCK FB_SafeForLoop
    VAR_INPUT
        bEnable : BOOL;           // 启用开关(防误触发)
        iStart : INT;             // 起始值
        iEnd : INT;               // 结束值
        iStep : INT;              // 步长(整型,避免浮点误差)
        pAction : POINTER TO ACTION_ROUTINE; // 循环体函数指针
    END_VAR
    VAR_OUTPUT
        bDone : BOOL;             // 执行完成标志
        nError : INT;             // 错误码:0=正常,1=STEP=0,2=溢出,3=禁用
    END_VAR
    VAR
        iCounter : INT;
        bStepValid : BOOL;
        bDirectionOk : BOOL;
    END_VAR

    // 阶段1:前置校验(每周期仅执行一次)
    IF NOT bDone THEN
        // 检查启用状态
        IF NOT bEnable THEN
            nError := 3;
            bDone := TRUE;
            EXIT;
        END_IF

        // 校验步长非零
        bStepValid := ABS(iStep) > 0;
        IF NOT bStepValid THEN
            nError := 1;
            _ERR_STEP_ZERO := TRUE;
            bDone := TRUE;
            EXIT;
        END_IF

        // 校验方向合理性(防止逻辑矛盾)
        IF (iStep > 0 AND iStart > iEnd) OR (iStep < 0 AND iStart < iEnd) THEN
            bDirectionOk := FALSE;
            nError := 2;
            bDone := TRUE;
            EXIT;
        ELSE
            bDirectionOk := TRUE;
            iCounter := iStart;
        END_IF
    END_IF

    // 阶段2:安全循环执行
    IF bDirectionOk AND NOT bDone THEN
        // 执行用户动作(通过函数指针调用)
        IF pAction <> 0 THEN
            pAction^(iCounter); // 传入当前计数值
        END_IF

        // 更新计数器
        iCounter := iCounter + iStep;

        // 终止条件检查(严格匹配ST标准逻辑)
        IF (iStep > 0 AND iCounter > iEnd) OR (iStep < 0 AND iCounter < iEnd) THEN
            bDone := TRUE;
        END_IF
    END_IF
END_FUNCTION_BLOCK

3. 实际应用:替换原始FOR循环

原始危险代码:

// ❌ 危险示例:步长由配置变量决定,可能为0
FOR i := 0 TO 100 BY gConfig.StepSize DO
    ProcessData(i);
END_FOR

安全重构代码:

// ✅ 安全实现:使用FB_SafeForLoop
VAR
    fbSafeLoop : FB_SafeForLoop;
    configStep : INT;
END_VAR

// 初始化配置(示例:从HMI读取)
configStep := INT_TO_INT(gConfig.StepSize);

// 执行安全循环
fbSafeLoop(
    bEnable := TRUE,
    iStart := 0,
    iEnd := 100,
    iStep := configStep,
    pAction := ADR(Action_ProcessData),
    bDone => bLoopComplete,
    nError => nLoopError
);

// 错误处理分支(必须存在)
IF nLoopError = 1 THEN
    // STEP=0错误:触发急停链路
    EmergencyStop(TRUE);
ELSIF nLoopError = 2 THEN
    // 方向错误:修正配置并报警
    gConfig.StepSize := 1;
    Alarm_Set(1001, 'FOR loop direction mismatch');
END_IF

配套动作函数(需定义):

PROGRAM Action_ProcessData
    VAR_INPUT
        index : INT;
    END_VAR
    // 用户实际逻辑在此处
    Buffer[index] := SensorValue * Gain;
END_PROGRAM

三、硬件级兜底:看门狗协同防护

即使软件防护失效,必须通过PLC硬件机制双重保险。所有主流PLC均支持任务级看门狗(Task Watchdog),但默认仅监控任务是否卡死,不区分原因。需主动注入“健康心跳”信号:

1. 创建心跳监测任务(独立于主任务)

在PLC项目中新建一个100ms周期任务(命名为 T_HB_Monitor),代码如下:

// 心跳任务:每100ms检查主任务循环状态
VAR_GLOBAL
    _HB_MainTaskAlive : BOOL := FALSE; // 主任务心跳标志
    _HB_WatchdogTimeout : TIME := T#500ms; // 超时阈值(5倍主任务周期)
END_VAR

// 主任务中,在每次扫描末尾置位
// (在主程序POU结尾添加)
_HB_MainTaskAlive := TRUE;

// 心跳任务中检测
IF NOT _HB_MainTaskAlive THEN
    // 连续两次未收到心跳即判定死循环
    _HB_MainTaskAlive := FALSE; // 清除上次标记
    // 立即触发硬件复位指令(根据PLC型号选择)
    CASE PLC_MODEL OF
        1: RESET_CPU(); // Codesys平台
        2: SFC20(REQ:=TRUE); // Siemens S7-1200/1500
        3: SYSRESET(); // Schneider Modicon M340
    END_CASE
ELSE
    _HB_MainTaskAlive := FALSE; // 为下次检测准备
END_IF

2. 关键设计要点

项目 要求 原因
心跳周期 主任务周期 × 2.5 避免因短暂抖动误判(如通信延迟)
超时阈值 ≥ 主任务最大可能执行时间 × 3 留出IO刷新、中断处理余量
复位方式 优先选择 RESET_CPU() 而非 STOP_CPU() 确保清除所有寄存器状态,防止残留死循环变量

四、工程化落地:配置与验证清单

将防护机制嵌入开发流程,而非临时补丁:

1. 编译期强制检查(Codesys/TIA Portal)

在项目属性中启用自定义编译规则

  • 搜索所有 FOR.*BY.*[0-9]*\.?0*[0]*\s*; 正则模式;
  • 匹配到即中断编译并提示:[ERROR] STEP literal '0' forbidden. Use FB_SafeForLoop instead.

2. 运行时诊断界面(HMI集成)

在HMI诊断页面添加实时状态栏:

状态项 显示逻辑 颜色
STEP Zero Trap _ERR_STEP_ZERO = TRUE 红色闪烁
Loop Health bDone = TRUE AND nError = 0 绿色常亮
Watchdog Alive _HB_MainTaskAlive = TRUE 蓝色呼吸

3. 测试用例表(必须100%覆盖)


| 测试ID | 输入条件 | 预期结果 | 验证方法 |
|---------|-----------|------------|-------------|
| TC-01 | `iStep := 0` | `nError = 1`, `bDone = TRUE`, `_ERR_STEP_ZERO = TRUE` | 在线监视变量+LED报警灯亮 |
| TC-02 | `iStep := 1`, `iStart=10`, `iEnd=0` | `nError = 2`, 循环不执行 | 监视`ProcessData`调用次数为0 |
| TC-03 | `iStep := -2`, `iStart=10`, `iEnd=0` | `bDone = TRUE`, 执行6次(10→8→6→4→2→0) | 检查Buffer[0..10]中偶数索引被写入 |
| TC-04 | 主任务卡死(手动插入`WHILE TRUE DO END_WHILE`) | 500ms内CPU复位,重启后`_ERR_STEP_ZERO = FALSE` | 示波器抓取复位信号宽度 |

五、进阶防护:浮点步长与多维循环

1. 浮点步长安全处理

当需高精度步长(如温度斜坡控制 BY 0.01),避免浮点误差累积导致意外终止:

// 安全浮点FOR循环(基于迭代次数而非值比较)
FUNCTION_BLOCK FB_SafeFloatFor
    VAR_INPUT
        fStart, fEnd, fStep : REAL;
        nMaxIter : UINT := 10000; // 防止理论无限循环
    END_VAR
    VAR_OUTPUT
        fCurrent : REAL;
        bDone : BOOL;
        nIter : UINT;
    END_VAR
    VAR
        fDelta : REAL;
    END_VAR

    IF NOT bDone THEN
        // 校验步长非零(同前)
        IF ABS(fStep) < 1.0E-12 THEN
            bDone := TRUE;
            EXIT;
        END_IF

        // 计算理论迭代次数(向上取整)
        fDelta := ABS(fEnd - fStart);
        nIter := UINT(CEIL(fDelta / ABS(fStep)));

        // 防爆上限
        IF nIter > nMaxIter THEN
            nIter := nMaxIter;
        END_IF

        fCurrent := fStart;
        bDone := FALSE;
    END_IF

    // 执行本次迭代
    IF nIter > 0 THEN
        // 用户逻辑在此操作fCurrent
        ProcessFloatValue(fCurrent);

        // 更新
        fCurrent := fCurrent + fStep;
        nIter := nIter - 1;

        // 终止条件:达到最大迭代或超出范围
        IF nIter = 0 OR 
           (fStep > 0 AND fCurrent > fEnd) OR 
           (fStep < 0 AND fCurrent < fEnd) THEN
            bDone := TRUE;
        END_IF
    END_IF
END_FUNCTION_BLOCK

2. 嵌套循环防护

对双层FOR(如矩阵遍历),只需对外层调用 FB_SafeForLoop,内层在pAction中调用——因为内层执行受外层控制,不会独立触发死循环。但需注意:

  • 内层iStep仍需校验;
  • 总迭代次数上限 = 外层次数 × 内层次数,须满足 nMaxIter 安全约束。

六、不可绕过的物理约束提醒

所有防护均建立在PLC确定性执行基础上。以下场景会令上述代码失效,必须物理隔离:

  • 电源纹波 > 5%:导致CPU时钟抖动,看门狗计时不准确;
  • 环境温度 > 60℃:闪存写入错误可能篡改_ERR_STEP_ZERO标志;
  • 未接地机柜:EMI干扰使INT变量随机翻转(如iStep1突变为0)。

解决方案:

  1. 为PLC供电加装 UPS+LC滤波器
  2. 控制柜内加装 PT100温度传感器,温度>55℃时自动降频;
  3. 所有IO线缆采用 屏蔽双绞线,屏蔽层单端接地。

_ERR_STEP_ZERO := FALSE;

评论 (0)

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

扫一扫,手机查看

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