在电气自动化领域,结构化文本(Structured Text,简称 ST)是 IEC 61131-3 标准定义的五种编程语言之一,广泛用于 PLC(可编程逻辑控制器)开发。它语法接近 Pascal 和 C,支持变量声明、条件判断、循环、函数调用和数组操作,特别适合处理批量数据搬运、算法计算和状态机建模等任务。其中,FOR 循环配合数组索引实现批量复制,是最基础也最常被误用的操作之一。本文将完全围绕这一行典型代码展开:
FOR i := 0 TO N DO Dest[i] := Src[i]; END_FOR;
不讲概念堆砌,不列标准条文,只讲你写这行代码时真正需要知道的每一步细节——从语法合法性、边界安全、内存对齐,到实际工程中必须规避的陷阱、可替代的更优写法,以及如何在主流 PLC 平台(如 Siemens TIA Portal、Rockwell Studio 5000、Codesys)中验证与调试。
一、先看这行代码到底“合法”吗?
严格来说,该语句在 IEC 61131-3 标准下不完全合法,需分三部分校验:
-
循环变量
i的声明
ST 要求FOR循环的控制变量必须是预先声明的整型变量(INT,DINT,USINT等),不能在FOR语句中隐式声明。以下写法错误:FOR i := 0 TO N DO ... // ❌ i 未声明正确做法是:在
VAR区显式声明i:VAR i : INT; N : INT := 99; // 假设复制 100 个元素 Src : ARRAY[0..99] OF REAL; Dest : ARRAY[0..99] OF REAL; END_VAR然后才能使用:
FOR i := 0 TO N DO Dest[i] := Src[i]; END_FOR; -
TO的终点值N必须是确定的整数表达式
N可以是常量(N := 99)、已初始化的变量(N := Length - 1),或编译期可计算的表达式(N := SIZEOF(Src)/SIZEOF(REAL) - 1)。但以下写法危险或非法:N是未初始化变量 → 运行时值不确定,可能为负数或超大值,导致越界访问;N是浮点数(如REAL类型)→ ST 不允许FOR循环使用非整型终点;N是函数返回值且含副作用(如N := ReadSensorAndReset())→ 标准未规定TO表达式是否每次迭代重算,不同厂商实现不一致,绝对禁止。
-
数组下标
i是否在有效范围内?
Src[i]和Dest[i]的合法性取决于i的实时值是否落在各自数组声明的索引区间内。例如:Src : ARRAY[0..99] OF INT; // 合法下标:0 ~ 99 Dest : ARRAY[10..109] OF INT; // 合法下标:10 ~ 109若
N := 99,则Dest[i]在i = 0时访问Dest[0]—— 越界!崩溃或静默错误。
因此,N的值必须同时满足:
$$ N \leq \text{UPPER\_BOUND(Src)} \quad \text{且} \quad N \leq \text{UPPER\_BOUND(Dest)} \quad \text{且} \quad 0 \geq \text{LOWER\_BOUND(Src)} \quad \text{且} \quad 0 \geq \text{LOWER\_BOUND(Dest)} $$
更实用的写法是用LEN函数(若平台支持)或显式计算:N := 99; // 确保 <= (SIZEOF(Src)/SIZEOF(INT)) - 1 且 <= (SIZEOF(Dest)/SIZEOF(INT)) - 1
二、为什么不能直接抄这行代码?4 个真实工程陷阱
陷阱 1:TO vs DOWNTO 的方向错配
FOR i := 0 TO N 是升序,适用于绝大多数情况。但如果 Src 和 Dest 是同一块内存的重叠区域(如实现类似 memmove() 的移动),升序复制会导致数据覆盖。例如:
Src : ARRAY[0..4] OF INT := [1,2,3,4,5];
Dest : ARRAY[0..4] OF INT := [0,0,0,0,0];
// 想把 Src[1..4] 复制到 Dest[0..3] → 即 Dest := [2,3,4,5,0]
若用 FOR i := 0 TO 3 DO Dest[i] := Src[i+1];,执行过程:
- i=0: Dest[0] = Src[1] = 2 → Dest=[2,0,0,0,0]
- i=1: Dest[1] = Src[2] = 3 → Dest=[2,3,0,0,0]
- i=2: Dest[2] = Src[3] = 4 → Dest=[2,3,4,0,0]
- i=3: Dest[3] = Src[4] = 5 → ✅ 成功
但若目标是 Src[0..3] 复制到 Dest[1..4](即 Dest[1]←Src[0], Dest[2]←Src[1]…),则:
- i=0: Dest[1] = Src[0] = 1 → Dest=[0,1,0,0,0]
- i=1: Dest[2] = Src[1] = 2 → Dest=[0,1,2,0,0]
- i=2: Dest[3] = Src[2] = 3 → Dest=[0,1,2,3,0]
- i=3: Dest[4] = Src[3] = 4 → ✅ 仍成功
然而,若 Src 和 Dest 完全重叠(如 Dest 就是 Src 自身偏移),升序会出错:
// 错误场景:把 Src[1..4] 复制回 Src[0..3](左移一位)
// Src 初始:[1,2,3,4,5]
// i=0: Src[0] := Src[1] → [2,2,3,4,5]
// i=1: Src[1] := Src[2] → [2,3,3,4,5]
// i=2: Src[2] := Src[3] → [2,3,4,4,5]
// i=3: Src[3] := Src[4] → [2,3,4,5,5] → ✅ 表面正确,但这是巧合
本质风险:当源起始地址 < 目标起始地址 且 内存重叠时,升序复制会提前覆盖尚未读取的源数据。此时必须用 DOWNTO 降序:
FOR i := 3 DOWNTO 0 DO
Src[i] := Src[i + 1]; // 从高位向低位赋值,避免覆盖
END_FOR;
陷阱 2:数据类型隐式转换导致精度丢失
Dest[i] := Src[i]; 看似简单,但若 Src 是 REAL,Dest 是 INT,ST 会执行截断式转换(非四舍五入):
Src : ARRAY[0..1] OF REAL := [3.7, -2.9];
Dest : ARRAY[0..1] OF INT;
// 执行后 Dest[0] = 3, Dest[1] = -2 (不是 -3!)
更隐蔽的是字长不匹配:Src 是 DINT(32 位),Dest 是 INT(16 位),超出范围时行为由 PLC 厂商定义(多数截断低 16 位,而非报错)。务必检查变量声明中的数据类型是否严格一致。必要时显式转换:
Dest[i] := INT_TO_DINT(Src[i]); // 明确意图
陷阱 3:循环体为空或含多语句时的分号规则
ST 中,FOR 循环体若只有一条语句,可省略 BEGIN...END;但若有多个语句,必须用 BEGIN...END 包裹,且分号仅用于语句间分隔,不放在 END 后:
// 正确(单语句)
FOR i := 0 TO N DO Dest[i] := Src[i]; END_FOR;
// 正确(多语句)
FOR i := 0 TO N DO
BEGIN
Dest[i] := Src[i];
Status[i] := TRUE;
END_FOR;
// 错误(END_FOR 后多余分号)
FOR i := 0 TO N DO Dest[i] := Src[i]; END_FOR;; // ❌ 编译失败
陷阱 4:循环次数超限引发运行时异常
多数 PLC 对单次扫描周期内执行的指令数有限制(如 Siemens S7-1500 默认单周期最多 10M 条指令)。若 N = 10000,且 Dest[i] := Src[i] 每次耗 10 条指令,则循环占 100k 条——安全。但若 N = 1000000,则可能触发“Watchdog timeout”(看门狗超时),CPU 进入 STOP 模式。永远不要假设 N 是小数字。必须加保护:
IF N > 1000 THEN
N := 1000; // 或触发报警
END_IF;
FOR i := 0 TO N DO
Dest[i] := Src[i];
END_FOR;
三、比 FOR 循环更高效、更安全的 3 种替代方案
方案 1:使用 MEMCPY 类系统函数(推荐)
几乎所有主流 PLC 平台都提供内存块复制函数,底层用汇编优化,速度远超 ST 循环,且自动处理对齐、长度校验、重叠检测。
| 平台 | 函数名 | 典型调用方式 |
|---|---|---|
| Siemens TIA | MOVE_BLOCK |
MOVE_BLOCK(IN:=ADR(Src), OUT:=ADR(Dest), LEN:=100); |
| Rockwell | COP (Copy) |
COP Src Dest 100; (FBD/LAD 中常用,ST 中需封装为函数块) |
| Codesys | MEMCPY |
MEMCPY(ADR(Dest), ADR(Src), 100 * SIZEOF(REAL)); |
关键点:
- 参数为地址指针(
ADR()),非变量名; LEN单位是字节(MEMCPY)或元素个数(MOVE_BLOCK),务必查手册;- 无需循环变量,无越界风险,执行时间恒定。
方案 2:数组整体赋值(语法糖,非万能)
若 Src 和 Dest 类型、尺寸完全相同,ST 支持直接赋值:
Dest := Src; // ✅ 编译器自动生成最优复制代码
限制:
- 仅适用于同构数组(类型、维数、大小全部一致);
- 部分老旧 PLC(如早期 S7-300)不支持,需确认固件版本;
- 无法指定复制长度(只能全量)。
方案 3:分段循环 + 扫描周期切片(应对大数据)
当必须用 FOR 且 N 极大(如 100k 点通信缓冲区)时,避免单周期阻塞:
VAR
i : INT := 0;
N_total : INT := 100000;
N_step : INT := 100; // 每次复制 100 个
CopyDone : BOOL := FALSE;
END_VAR
IF NOT CopyDone THEN
// 本次循环复制 min(N_step, N_total - i) 个
FOR j := 0 TO MIN(N_step - 1, N_total - i - 1) DO
Dest[i + j] := Src[i + j];
END_FOR;
i := i + N_step;
IF i >= N_total THEN
CopyDone := TRUE;
i := 0;
END_IF;
END_IF;
原理:每次扫描只执行固定小量操作,剩余工作留待下次扫描,彻底规避看门狗超时。
四、调试这行代码的 3 个硬核技巧
-
强制监控循环变量:在 TIA Portal 中,右键变量
i→ “Add to Watch Table”,勾选 “Update on every scan”;观察i是否从 0 递增到N,中间是否跳变(说明被其他逻辑篡改)。 -
用
ASSERT插桩校验:在循环体内加入断言(需 PLC 支持):FOR i := 0 TO N DO ASSERT(i >= 0 AND i <= N, 'Loop index out of range!'); ASSERT(i <= UPPER_BOUND(Src) AND i <= UPPER_BOUND(Dest), 'Array bound violation!'); Dest[i] := Src[i]; END_FOR;触发时 CPU 进入 STOP 并记录故障码,精准定位问题。
-
对比内存快照:使用 PLC 的“Memory Comparison”工具,在复制前后分别导出
Src和Dest的内存块(十六进制),逐字节比对,确认无一字节偏差。
五、终极检查清单(写完这行代码后,逐项打钩)
- [ ]
i已在VAR区声明为整型(INT/DINT) - [ ]
N是已初始化的整数,且值 ≤UPPER_BOUND(Src)和UPPER_BOUND(Dest) - [ ]
Src和Dest数据类型完全一致(包括有符号/无符号、字长) - [ ] 若内存重叠且源地址 < 目标地址,已改用
DOWNTO - [ ]
N值经MIN(N, MAX_ALLOWED)限制,防看门狗超时 - [ ] 已评估
MEMCPY/MOVE_BLOCK等系统函数是否可用并优先选用 - [ ] 在调试模式下,已用监控表验证
i的完整遍历过程
FOR i := 0 TO N DO Dest[i] := Src[i]; END_FOR; 这行代码本身不是魔法,它是杠杆——撬动正确性的支点,不在语法,而在你写它之前想清楚的每一个约束条件。

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