文章目录

ST FOR循环死循环陷阱:步长设置错误导致的程序卡死

发布于 2026-03-19 13:59:44 · 浏览 9 次 · 评论 0 条

在结构化文本(ST)编程中,FOR 循环是实现重复逻辑最常用的语句之一。它语法简洁、语义明确,常用于数组遍历、定时扫描、状态机步进等场景。但一个看似微不足道的参数——步长(STEP)——若设置不当,将直接导致 PLC 程序进入不可退出的死循环,表现为 CPU 占用率 100%、周期时间超限、输出冻结、HMI 响应停滞,甚至触发看门狗复位。

这不是理论风险,而是现场高频故障:某汽车焊装线 PLC 因 FOR i := 0 TO 100 BY -1 DO 导致整条工位停机 47 分钟;某水处理自控系统因 FOR j := 10 DOWNTO 0 BY 2 DOBY 2DOWNTO 方向冲突,造成加药泵持续满频运行 3 小时后过载跳闸。这些事故的根源,均指向 ST 语言中 FOR 循环的执行机制与步长语义的深层耦合关系。


一、ST 中 FOR 循环的真实执行逻辑(非直觉!)

许多工程师误以为 FOR 是“从起点到终点,每次加/减步长”,实则 IEC 61131-3 标准定义的执行规则更严格。其核心是三阶段判定:

  1. 初始化:执行 i := start(例如 i := 0);
  2. 循环条件检查:在每次执行循环体,判断 i 是否仍在有效范围内;
  3. 迭代更新:在每次执行循环体,执行 i := i + step(无论 TODOWNTO)。

关键点在于:TODOWNTO 仅影响条件检查的不等式方向,不改变步长的数学符号;步长 BY n 始终以代数加法方式累加。

这意味着:

  • FOR i := 0 TO 10 BY 2 DO → 条件为 i ≤ 10,更新为 i := i + 2
  • FOR i := 10 DOWNTO 0 BY -2 DO → 条件为 i ≥ 0,更新为 i := i + (-2)
  • FOR i := 0 TO 10 BY -1 DO → 条件为 i ≤ 10,更新为 i := i + (-1)i 持续减小,永远满足 i ≤ 10死循环

下面用表格对比合法与非法组合:

循环语句 初始值 终止条件 步长运算 实际序列 是否终止 原因
FOR i := 0 TO 10 BY 2 DO 0 i ≤ 10 i := i + 2 0, 2, 4, 6, 8, 10, 12(跳出) 12 > 10,条件失败
FOR i := 10 DOWNTO 0 BY -2 DO 10 i ≥ 0 i := i + (-2) 10, 8, 6, 4, 2, 0, -2(跳出) -2 < 0,条件失败
FOR i := 0 TO 10 BY -1 DO 0 i ≤ 10 i := i + (-1) 0, -1, -2, -3, ... → 永远 ≤ 10 条件恒真,无退出可能
FOR i := 10 DOWNTO 0 BY 1 DO 10 i ≥ 0 i := i + 1 10, 11, 12, 13, ... → 永远 ≥ 0 条件恒真,无退出可能

注意:BY 后的值必须是常量整数(如 BY 1, BY -3),不能是变量或表达式(如 BY x, BY n*2),否则编译报错。这是语法硬约束,但恰恰掩盖了步长方向匹配的逻辑风险。


二、四类典型死循环陷阱及现场还原

陷阱 1:正向范围配负步长(最常见)

VAR
  i : INT;
END_VAR

FOR i := 0 TO 100 BY -5 DO
  // 执行电机加速逻辑
  Motor_Speed := i * 10;
END_FOR;
  • 现象:PLC 周期时间从 8 ms 暴涨至 250 ms,CPU 占用率 99%,Motor_Speed 锁定在 0(因循环体未执行即卡死在条件检查)。
  • 真相i 初始化为 0,条件 0 ≤ 100 成立 → 进入循环体;执行后 i := 0 + (-5) = -5;下一轮条件 −5 ≤ 100 仍成立 → 再次进入;i 持续递减(−10, −15, …),永不停止。
  • 验证方法:在循环内添加 i := i;(空赋值),用在线监控观察 i 值是否持续减小且不越界。

陷阱 2:反向范围配正步长

FOR i := 50 DOWNTO 10 BY 3 DO
  // 更新温度采样索引
  Temp_Index[i] := Read_Temp_Sensor(i);
END_FOR;
  • 现象:HMI 温度曲线停止刷新,Temp_Index 数组前 10 个元素全为 0,后续地址被非法写入(如覆盖 Motor_Enable 位)。
  • 真相i 初值 50,条件 50 ≥ 10 成立 → 执行;更新 i := 50 + 3 = 53;下轮 53 ≥ 10 仍成立 → 再执行;i 变为 56, 59, 62...,无限增大,最终 i 超出 Temp_Index 数组边界(假设大小为 60),触发地址越界写入,破坏内存。

陷阱 3:步长为 0(编译器不报错!)

FOR i := 1 TO 10 BY 0 DO
  Counter := Counter + 1;
END_FOR;
  • 现象Counter 在单个扫描周期内暴涨至最大值 32767(INT 上限),随后溢出归零并反复震荡。
  • 真相BY 0 是语法允许的(标准未禁止),i 始终为初始值 1,条件 1 ≤ 10 恒真。循环体被反复执行,直至该扫描周期超时或 Counter 溢出。这是静默型陷阱——编译通过,运行崩溃。

陷阱 4:浮点步长强制转整(隐性截断)

FOR i := 0.0 TO 10.0 BY 0.3 DO
  // 计算 PID 输出
  Output := Kp * Error + Ki * Integral;
END_FOR;
  • 现象:程序不报错,但 i 值在 9.9 后跳变为 10.2,循环提前结束,PID 积分项未覆盖全范围。
  • 真相:ST 中 FOR 的计数器变量 i 必须是整数类型(INT, DINT)。当使用浮点字面量时,编译器自动截断小数部分:0.30。等效于 BY 0,触发陷阱 3。所有浮点步长均非法,必须显式转换为整数增量逻辑。

三、防御性编程:五步杜绝死循环

步骤 1:禁用 BY 子句,改用 WHILE 显式控制

当步长非 1-1 时,优先放弃 FOR,改写为 WHILE

// ❌ 危险
FOR i := 0 TO 100 BY 5 DO
  Process_Array[i];
END_FOR;

// ✅ 安全(清晰暴露边界与更新)
i := 0;
WHILE i <= 100 DO
  Process_Array[i];
  i := i + 5;  // 更新逻辑独立可见
END_WHILE;

优势:条件、执行、更新三者分离,逻辑无歧义;支持任意复杂更新表达式(如 i := i * 2);便于插入防呆校验。

步骤 2:强制添加循环计数保护

FOR 循环内嵌入安全计数器,防止无限执行:

VAR
  i, loop_count : INT;
  MAX_ITERATIONS : INT := 1000;  // 根据业务预估最大合理次数
END_VAR

loop_count := 0;
FOR i := 0 TO 100 BY 2 DO
  loop_count := loop_count + 1;
  IF loop_count > MAX_ITERATIONS THEN
    // 触发报警并跳出
    Alarm_Code := 1201;  // FOR 循环超限
    EXIT;
  END_IF;
  Process_Data[i];
END_FOR;

步骤 3:建立步长-方向匹配检查表(开发时手写核查)

在编写 FOR 前,立即填写下表并打钩确认:

项目 是否合规 检查说明
起始值 start 0
终止值 end 100
关键字 TO/DOWNTO TO 若选 TO,步长必须 > 0
步长 BY n -5 n 必须与方向一致:TO → n > 0DOWNTO → n < 0
步长是否为 0 BY 0 绝对禁止
循环变量类型 INT 禁用 REAL 作计数器

步骤 4:启用编译器静态分析(TIA Portal / Codesys 设置)

  • TIA Portal V18+:在“项目属性 → 编译器 → 静态代码分析”中启用 Rule_613_FOR_loop_step_sign_mismatch(检测步长符号错误);
  • Codesys:在“工具 → 选项 → 编译器 → 诊断”中勾选 Warning on suspicious FOR loop step
  • 效果FOR i := 0 TO 10 BY -1 DO 将直接报黄灯警告:“Step value -1 contradicts direction TO”。

步骤 5:循环体首行插入调试标记

在所有 FOR 循环第一行加入唯一标识符,便于在线追踪:

FOR i := 0 TO 50 BY 2 DO
  Debug_Marker := 401;  // 独立整数,对应此循环ID
  // ... 实际逻辑
END_FOR;

当程序卡死时,监控 Debug_Marker 值即可 100% 定位到具体哪一行 FOR 导致问题,无需猜测。


四、高级场景:如何安全实现非线性步进?

工业现场常需非等距采样(如温度补偿查表、多段速切换)。此时绝不可强行用 BY,而应采用索引数组驱动:

// ✅ 推荐:预定义步进点阵列
VAR
  Speed_Steps : ARRAY[0..4] OF INT := [0, 1000, 2500, 4000, 5000];  // 非线性速度点
  idx : INT;
END_VAR

FOR idx := 0 TO 4 DO
  Set_Motor_Speed(Speed_Steps[idx]);
  WAIT_FOR_STABLE();  // 等待电机稳定
END_FOR;

若需动态计算步进(如 f(x) = x^2),则用 WHILE + 函数封装:

FUNCTION GetNextStep : INT
VAR_INPUT
  current : INT;
END_VAR
GetNextStep := current * current;  // 示例:平方增长
step := 1;
WHILE step <= 100 DO
  Process_At_Step(step);
  step := GetNextStep(step);  // 动态生成下一点
END_WHILE;

五、故障排查清单(现场立即执行)

当怀疑 FOR 死循环时,按顺序执行:

  1. 暂停 PLC:避免设备损坏;
  2. 打开在线诊断:查看当前扫描周期时间(Cycle Time),若 > 100 ms 且持续上涨,高度可疑;
  3. 定位高亮行:在编辑器中搜索 FOR,逐行检查其 BY 值与 TO/DOWNTO 是否匹配;
  4. 监控循环变量:将循环变量(如 i)拖入监视表,观察其值变化趋势(是否单调增/减且不越界);
  5. 临时注释循环体:将 FOR 内所有逻辑注释掉,仅保留 i := i;,观察 i 是否仍失控——若失控,则确认为 FOR 结构问题;
  6. 替换为 WHILE 测试:按本文第三部分步骤 1 改写,验证功能一致性。

FOR 循环不是语法糖,而是带状态机的确定性迭代器。它的安全边界不在编译器报错里,而在程序员对 TO/DOWNTOBY 符号关系的肌肉记忆中。每一次敲下 BY,都必须同步脑内执行一次符号一致性检查:TO+DOWNTO。省略这一步,就是把产线交给随机性。

评论 (0)

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

扫一扫,手机查看

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