在结构化文本(ST)编程中,FOR 循环是实现重复逻辑最常用的语句之一。它语法简洁、语义明确,常用于数组遍历、定时扫描、状态机步进等场景。但一个看似微不足道的参数——步长(STEP)——若设置不当,将直接导致 PLC 程序进入不可退出的死循环,表现为 CPU 占用率 100%、周期时间超限、输出冻结、HMI 响应停滞,甚至触发看门狗复位。
这不是理论风险,而是现场高频故障:某汽车焊装线 PLC 因 FOR i := 0 TO 100 BY -1 DO 导致整条工位停机 47 分钟;某水处理自控系统因 FOR j := 10 DOWNTO 0 BY 2 DO 中 BY 2 与 DOWNTO 方向冲突,造成加药泵持续满频运行 3 小时后过载跳闸。这些事故的根源,均指向 ST 语言中 FOR 循环的执行机制与步长语义的深层耦合关系。
一、ST 中 FOR 循环的真实执行逻辑(非直觉!)
许多工程师误以为 FOR 是“从起点到终点,每次加/减步长”,实则 IEC 61131-3 标准定义的执行规则更严格。其核心是三阶段判定:
- 初始化:执行
i := start(例如i := 0); - 循环条件检查:在每次执行循环体前,判断
i是否仍在有效范围内; - 迭代更新:在每次执行循环体后,执行
i := i + step(无论TO或DOWNTO)。
关键点在于:TO 和 DOWNTO 仅影响条件检查的不等式方向,不改变步长的数学符号;步长 BY n 始终以代数加法方式累加。
这意味着:
FOR i := 0 TO 10 BY 2 DO→ 条件为i ≤ 10,更新为i := i + 2FOR 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.3→0。等效于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 > 0,DOWNTO → 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 死循环时,按顺序执行:
- 暂停 PLC:避免设备损坏;
- 打开在线诊断:查看当前扫描周期时间(Cycle Time),若 > 100 ms 且持续上涨,高度可疑;
- 定位高亮行:在编辑器中搜索
FOR,逐行检查其BY值与TO/DOWNTO是否匹配; - 监控循环变量:将循环变量(如
i)拖入监视表,观察其值变化趋势(是否单调增/减且不越界); - 临时注释循环体:将
FOR内所有逻辑注释掉,仅保留i := i;,观察i是否仍失控——若失控,则确认为FOR结构问题; - 替换为
WHILE测试:按本文第三部分步骤 1 改写,验证功能一致性。
FOR 循环不是语法糖,而是带状态机的确定性迭代器。它的安全边界不在编译器报错里,而在程序员对 TO/DOWNTO 与 BY 符号关系的肌肉记忆中。每一次敲下 BY,都必须同步脑内执行一次符号一致性检查:TO → +,DOWNTO → −。省略这一步,就是把产线交给随机性。

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