西门子TIA Portal 中使用 SCL(Structured Control Language)编写逻辑时,编译报错 Array index out of bounds(数组越界)是高频且易被误判的典型问题。该错误并非运行时异常,而是在编译阶段由 TIA Portal 的静态类型检查器主动捕获的边界违规预警。它不依赖实际数据流,只依据变量声明、索引表达式和编译时可推导的常量范围进行严格校验。理解其触发机制与检查逻辑,是写出健壮、可维护 SCL 代码的基础。
一、SCL 数组声明与索引的本质规则
SCL 中数组是静态分配、边界固定的数据结构。声明语法为:
MyArray : ARRAY [0..9] OF INT;
此处 [0..9] 是显式指定的下标范围,表示该数组合法索引集合为整数闭区间 $[0, 9]$,共 10 个元素。TIA Portal 编译器在解析此声明时,会将 0 记录为 LOWER_BOUND,9 记录为 UPPER_BOUND。所有对该数组的访问(读或写),其索引表达式 i 必须满足:
$$ \text{LOWER\_BOUND} \leq i \leq \text{UPPER\_BOUND} $$
该不等式必须在编译时被证明恒成立。若编译器无法在不执行代码的前提下确认该条件,则报 Array index out of bounds。
关键点在于:SCL 编译器不做运行时猜测,只做编译时确定性推理。它不模拟变量可能的取值范围,而是严格依据以下三类信息进行推理:
- 常量字面量(如
5,MAX_VALUE := 100) - 显式类型范围(如
INT类型的理论范围-32768..32767) - 已知的、由
IF/CASE分支限定的局部约束(需满足特定结构)
二、触发“数组越界”报错的六种典型场景(含逐条解析)
1. 直接使用超限常量索引
MyArray : ARRAY [1..5] OF REAL;
...
Value := MyArray[0]; // 报错:0 < LOWER_BOUND (1)
Value := MyArray[6]; // 报错:6 > UPPER_BOUND (5)
原因:索引 0 和 6 是编译期可知的常量,明显落在 [1, 5] 之外。
修复:修正索引为 1..5 范围内任意整数,如 MyArray[3]。
2. 使用未初始化或范围过宽的变量索引
IndexVar : INT;
MyArray : ARRAY [0..4] OF BOOL;
...
MyArray[IndexVar] := TRUE; // 报错:IndexVar 类型为 INT,其理论范围 [-32768, 32767] 与 [0,4] 无交集
原因:IndexVar 声明为 INT,编译器仅知其类型范围,不知其运行时实际值。由于 [-32768, 32767] 完全覆盖 [0,4] 但又远超之,编译器无法证明 IndexVar 恒在 [0,4] 内,故拒绝访问。
修复:显式约束索引变量范围。推荐两种方式:
-
方式A(推荐):声明为子范围类型
TYPE MyIndexType : INT(0..4); END_TYPE IndexVar : MyIndexType; MyArray[IndexVar] := TRUE; // ✅ 通过:编译器已知 IndexVar ∈ [0,4] -
方式B:用 IF 显式限定(需确保覆盖全部分支)
IF IndexVar >= 0 AND IndexVar <= 4 THEN MyArray[IndexVar] := TRUE; // ✅ 在此分支内,编译器可推导出 IndexVar ∈ [0,4] ELSE // 处理越界情况(如报警、置默认值) END_IF;
3. 使用计算表达式,结果超出声明范围
MyArray : ARRAY [0..9] OF DINT;
Counter : INT := 0;
...
MyArray[Counter + 10] := 1; // 报错:Counter 为 INT,Counter + 10 范围为 [-32758, 32777],超 [0,9]
原因:Counter + 10 是表达式,编译器按类型运算规则推导其范围,而非代入初始值 0。
修复:
- 若
Counter实际只在0..9内变化,应将其声明为子范围类型(同上)。 - 或将计算逻辑包裹在安全边界内:
TempIndex : INT := Counter + 10; IF TempIndex >= 0 AND TempIndex <= 9 THEN MyArray[TempIndex] := 1; END_IF;
4. 循环中索引未受循环变量范围保护
MyArray : ARRAY [0..7] OF BYTE;
i : INT;
...
FOR i := 0 TO 10 DO // i 遍历 0..10
MyArray[i] := BYTE#16#FF; // 报错:i 可能为 8,9,10,超 [0,7]
END_FOR;
原因:FOR 循环的终止值 10 超出数组上限 7,编译器检测到循环体中存在必然越界的路径。
修复:
- 严格匹配循环边界:
FOR i := 0 TO 7 DO // ✅ 终止值 = UPPER_BOUND MyArray[i] := BYTE#16#FF; END_FOR; - 或使用
SIZEOF/LEN获取动态长度(仅适用于部分场景):FOR i := 0 TO (LEN(MyArray) - 1) DO // LEN 返回元素个数,即 8 → 0..7 MyArray[i] := BYTE#16#FF; END_FOR;
⚠️ 注意:
LEN()在 SCL 中是编译期常量函数,对静态数组返回确切元素数,安全可靠。
5. 使用 ADR 或指针间接访问时边界丢失
MyArray : ARRAY [0..3] OF WORD;
pArray : POINTER TO WORD;
...
pArray := ADR(MyArray);
pArray[5] := 16#ABCD; // 报错:指针解引用 pArray[i] 时,编译器无法追溯其原始数组边界
原因:ADR() 生成的指针 pArray 仅携带地址,不携带原数组的维度信息。编译器视 pArray[i] 为对任意内存的访问,其索引 5 无上下文约束。
修复:
- 避免对指针使用非常量索引;若必须,先做显式检查:
IF 5 >= 0 AND 5 <= 3 THEN // 手动写死边界(不推荐,易错) pArray[5] := 16#ABCD; END_IF; - 更优方案:直接操作原数组名,利用其固有边界:
MyArray[5] := 16#ABCD; // 此行本身会报错,从而暴露问题 —— 这正是编译器的价值
6. 结构体嵌套数组时,访问路径未完整声明边界
TYPE MyStruct :
STRUCT
Data : ARRAY [0..2] OF INT;
END_STRUCT
END_TYPE
Instance : MyStruct;
...
Instance.Data[3] := 100; // 报错:3 > UPPER_BOUND (2)
原因:访问路径 Instance.Data[3] 中,Data 成员的边界 [0..2] 是明确的,3 显然越界。
修复:检查结构体定义与访问索引的一致性。若需更大容量,修改声明:
Data : ARRAY [0..5] OF INT; // 调整为所需大小
三、边界检查的“可证明性”核心逻辑(为什么有些看似越界却不报错?)
编译器是否报错,取决于它能否数学上证明索引恒在范围内。以下情形虽“看起来危险”,但因满足可证明性而不报错:
场景A:索引为常量,且在范围内
MyArray : ARRAY [10..19] OF REAL;
MyArray[15] := 3.14; // ✅ 不报错:15 ∈ [10,19]
场景B:索引为子范围类型变量
TYPE ValidIndex : INT(5..15); END_TYPE
i : ValidIndex;
MyArray : ARRAY [5..15] OF BOOL;
MyArray[i] := TRUE; // ✅ 不报错:ValidIndex 的定义即保证 i ∈ [5,15]
场景C:FOR 循环边界与数组完全一致
MyArray : ARRAY [0..99] OF DINT;
FOR i := LOWORD(ADR(MyArray)) TO HIWORD(ADR(MyArray)) DO // ❌ 错误示例:ADR 不提供索引
END_FOR;
// 正确写法(使用 LEN):
FOR i := 0 TO (LEN(MyArray) - 1) DO // ✅ LEN(MyArray) = 100 → i ∈ [0,99]
MyArray[i] := i;
END_FOR;
场景D:CASE 语句中,每个分支的索引均安全
State : INT;
MyArray : ARRAY [0..2] OF BOOL;
CASE State OF
0: MyArray[0] := TRUE; // ✅
1: MyArray[1] := TRUE; // ✅
2: MyArray[2] := TRUE; // ✅
ELSE
// 默认处理
END_CASE;
注意:CASE 必须覆盖所有可能输入(或有 ELSE),且每个分支内的索引必须是常量或受该分支条件严格约束的变量。
四、系统性排查与预防工作流(手把手操作清单)
当遇到 Array index out of bounds 报错时,按以下顺序执行:
-
定位报错行:双击 TIA Portal 编译错误列表中的该条目,光标自动跳转至出问题的 SCL 行。
-
提取索引表达式:识别方括号
[]内的全部内容。例如DataBuffer[Counter MOD 16],则索引表达式为Counter MOD 16。 -
分析索引表达式的编译期可推导范围:
- 若含变量,查看其声明类型(如
INT,USINT, 子范围类型); - 若含运算符(
+,-,MOD,*),回忆 SCL 类型运算规则(如USINT + USINT → UINT); - 查阅《TIA Portal SCL 语言参考》中对应运算符的输出类型定义。
- 若含变量,查看其声明类型(如
-
比对数组声明的
[Lower..Upper]:在变量声明区找到该数组的完整定义,确认Lower和Upper值。 -
判断可证明性:
- 若索引是常量 → 直接比较数值;
- 若索引是子范围类型变量 → 检查其定义是否与数组边界匹配;
- 若索引在
IF/CASE内 → 确认该分支条件是否能唯一确定索引落入[Lower, Upper]; - 其他情况 → 默认视为不可证明,必须重构。
-
选择修复策略:
- ✅ 优先使用子范围类型(最安全、自文档化);
- ✅ 其次使用
LEN()函数控制循环边界; - ✅ 必须用变量索引时,在访问前加
IF 索引 >= Lower AND 索引 <= Upper THEN ... END_IF; - ❌ 禁止依赖注释说明“这里不会越界”;
- ❌ 禁止关闭编译器警告(TIA Portal 不提供此选项,且不应尝试)。
-
验证修复效果:重新编译。若仍有报错,重复步骤 1–6,直至零错误。
五、高级技巧:用 ASSERT 辅助运行时防御(非替代编译检查)
编译期检查无法覆盖所有场景(如索引来自 HMI 输入、通信数据)。此时需运行时防护:
// 假设 IndexFromHMI 来自触摸屏输入,类型为 INT
IF IndexFromHMI >= 0 AND IndexFromHMI <= 99 THEN
SafeArray[IndexFromHMI] := NewValue;
ELSE
// 触发诊断报警,或记录错误日志
DiagError := TRUE;
ErrorCode := 101; // 自定义越界错误码
END_IF;
ASSERT 语句(需启用 SCL 调试支持)可提供额外断言:
ASSERT(IndexFromHMI >= 0 AND IndexFromHMI <= 99) :='Index out of range for SafeArray';
此语句在运行时检查,失败时暂停并显示消息,是编译检查的有力补充,但绝不能替代编译期边界声明。
六、最佳实践总结表
| 类别 | 推荐做法 | 禁止做法 |
|---|---|---|
| 数组声明 | 显式使用有意义的子范围,如 ARRAY [StartPos..EndPos] |
使用 [0..N-1] 后再靠注释说明含义 |
| 索引变量 | 声明为 TYPE MyIndex : INT(Lower..Upper); |
声明为泛型 INT 后靠程序员记忆范围 |
| 循环遍历 | FOR i := 0 TO (LEN(ArrayName) - 1) DO |
FOR i := 0 TO 99 DO(硬编码,易不同步) |
| 指针访问 | 尽量避免;必须时,用 IF 显式校验 |
直接 pArray[i] 且不检查 i |
| HMI/通信输入 | 接收后立即校验并映射到子范围类型变量 | 直接传给数组索引 |
七、常见误区澄清
-
误区1:“我测试过,这个索引永远不会越界,所以报错是编译器 bug。”
→ 错。这是编译器在履行其职责:确保所有可能执行路径都安全。测试只能覆盖有限样本,编译检查覆盖全部逻辑可能性。 -
误区2:“把数组声明成
ARRAY [0..65535] OF ...就一劳永逸。”
→ 错。这浪费内存、降低可读性、掩盖设计缺陷,且可能引入新越界(如ARRAY [0..65535]仍可能被INT类型索引访问,而INT最大为32767)。 -
误区3:“
MOD运算能保证结果在范围内,比如i MOD 10对ARRAY [0..9]安全。”
→ 部分正确,但需前提。i MOD 10的结果确实是0..9,但前提是i为无符号类型(如UINT,UDINT)。若i是INT,MOD在负数时行为依 PLC 标准(IEC 61131-3)定义为“余数符号同被除数”,(-1) MOD 10结果为-1,仍越界。因此,务必确保i为无符号类型,或显式处理负数。 -
误区4:“在
FB中,STAT变量的数组可以随意索引,因为它是静态的。”
→ 错。STAT变量的数组同样受完全相同的编译期边界检查约束,与存储类别无关。
八、调试实例:从报错到修复全程还原
问题代码:
FUNCTION_BLOCK FB_ProcessData
VAR
Buffer : ARRAY [0..15] OF WORD;
WritePtr : INT;
ReadPtr : INT;
END_VAR
// 主逻辑片段
WritePtr := WritePtr + 1;
IF WritePtr > 15 THEN
WritePtr := 0;
END_IF;
Buffer[WritePtr] := NewData; // ← 编译报错:Array index out of bounds
分析过程:
- 数组
Buffer边界:[0..15] WritePtr类型:INT,理论范围[-32768..32767]WritePtr + 1后,即使WritePtr原为15,+1后为16,已超15IF重置逻辑在赋值之后,编译器看到的是Buffer[WritePtr + 1],而非重置后的WritePtr
修复方案(三选一):
-
方案1(推荐):用子范围类型并前置重置
TYPE PtrType : INT(0..15); END_TYPE WritePtr : PtrType; ... WritePtr := WritePtr + 1; IF WritePtr > 15 THEN WritePtr := 0; END_IF; Buffer[WritePtr] := NewData; // ✅ WritePtr 类型保证 ∈ [0,15] -
方案2:合并为单表达式(利用
MOD)WritePtr := (WritePtr + 1) MOD 16; // MOD 16 结果恒为 0..15 Buffer[WritePtr] := NewData; // ✅ -
方案3:访问前校验
TempPtr := WritePtr + 1; IF TempPtr >= 0 AND TempPtr <= 15 THEN Buffer[TempPtr] := NewData; END_IF; WritePtr := TempPtr;
选择方案1,因其最清晰表达了设计意图——写指针必在缓冲区内。

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