ST语言(Structured Text)是IEC 61131-3标准定义的五大PLC编程语言之一,广泛用于工业自动化控制系统中。其语法接近Pascal和C,支持结构化逻辑、函数调用、数组操作和复杂数据类型。但正因为其“类高级语言”的灵活性,开发者容易忽略底层运行时约束——尤其是数组下标越界访问这一类看似微小、实则致命的错误。
这类错误在调试阶段往往不暴露:程序能编译通过,仿真运行正常,甚至上电初期也无异常。但一旦实际工况触发越界读写(例如传感器突发脉冲导致索引计算偏移、HMI手动输入非法值、循环计数器未重置),轻则数据错乱、控制失准;重则覆盖关键内存区域(如任务堆栈、系统变量区),引发PLC硬复位、通信中断或I/O模块锁死。更隐蔽的是,某些PLC固件对越界访问不做拦截,而是静默读取相邻内存单元,造成“偶发性故障”,极难复现与定位。
本指南聚焦ST语言中数组下标越界访问的成因、检测机制、防护策略与现场排查方法,所有内容均基于主流PLC平台(如倍福TwinCAT、罗克韦尔Logix、施耐德Unity Pro、西门子S7-1500 TIA Portal)的共性行为,不依赖特定品牌扩展指令。
一、ST语言数组的底层存储模型与越界定义
ST语言中的数组声明语法为:
ARRAY[下界..上界] OF 数据类型
例如:
VAR
TempBuffer : ARRAY[0..9] OF REAL; // 10个元素,索引0~9
MotorCmd : ARRAY[1..4] OF INT; // 4个元素,索引1~4
StatusLog : ARRAY[-2..2] OF BOOL; // 5个元素,索引-2,-1,0,1,2
END_VAR
关键点在于:ST数组是静态分配的连续内存块,编译时即确定总字节数。以 ARRAY[0..9] OF REAL 为例:
- 每个
REAL占4字节(IEEE 754单精度); - 总长度 = 10 × 4 = 40字节;
- 内存地址假设起始为
0x1000,则有效访问范围为0x1000~0x1027(含)。
所谓“越界访问”,指代码中使用了超出声明范围的下标值进行读或写操作。例如:
TempBuffer[10] := 25.5; // ❌ 越界写:索引10 > 上界9
MotorCmd[0] := 1; // ❌ 越界写:索引0 < 下界1
StatusLog[-3] := TRUE; // ❌ 越界读:索引-3 < 下界-2
此时,PLC运行时的行为取决于固件是否启用边界检查(详见第二部分),而非语言标准本身强制要求。IEC 61131-3标准未规定必须进行运行时边界检查,仅要求编译器报告明显静态越界(如常量下标超限),而动态计算下标(如 i+1、j*2)完全交由厂商实现决定。
二、不同PLC平台的边界检查机制对比
是否执行运行时下标检查,由PLC固件/运行时环境(Runtime)控制,与ST编译器无关。下表总结主流平台默认行为与开关路径:
| PLC平台 | 默认是否启用数组边界检查 | 如何启用/禁用 | 越界时典型表现 |
|---|---|---|---|
| 西门子 S7-1500 (TIA Portal) | 否(编译期仅警告,运行时不检查) | 在项目属性 → “编译” → 勾选 启用运行时检查 → 数组访问检查 |
若启用:触发 OB121 编程错误组织块,CPU进入停止模式,诊断缓冲区记录 F3001 错误码 |
| 罗克韦尔 Logix (Studio 5000) | 否 | 在控制器属性 → “高级” → 勾选 Enable array bounds checking |
若启用:触发严重故障(Major Fault),CPU停机,事件日志记录 Array Index Out of Bounds |
| 倍福 TwinCAT 3 | 是(默认启用) | 在项目设置 → “Build” → “Runtime Checks” → 可单独关闭 Array Bounds Check |
抛出 TC_RuntimeError 异常,可被 TRY...CATCH 捕获,或触发 ErrorHandling 例程 |
| 施耐德 Unity Pro | 否 | 无全局开关;需手动插入 CHECK_ARRAY_BOUNDS() 函数(非标准,依赖库版本) |
静默越界(读取随机值/写入相邻变量) |
✅ 核心结论:不能依赖平台默认开启边界检查。生产环境必须显式配置并验证,否则越界即等于“未定义行为”。
三、四类高发越界场景及对应防护代码模板
以下场景在电气自动化项目中占比超85%,全部提供零依赖、跨平台兼容的ST防护写法。
场景1:循环遍历中计数器未重置或步长错误
危险代码:
FOR i := 0 TO 10 DO // 错误:上界应为9(对应ARRAY[0..9])
TempBuffer[i] := SensorValue[i];
END_FOR;
防护方案:永远用 LOW() 和 HIGH() 内置函数获取边界
FOR i := LOW(TempBuffer) TO HIGH(TempBuffer) DO
TempBuffer[i] := SensorValue[i];
END_FOR;
LOW(数组名)返回声明下界(如ARRAY[1..4]返回1);HIGH(数组名)返回声明上界(如ARRAY[1..4]返回4);- 此写法自动适配任意声明形式(
[0..9]、[1..4]、[-5..5]),杜绝硬编码。
场景2:索引由外部输入(HMI/Modbus/OPC UA)直接赋值
危险代码:
TempBuffer[InputIndex] := NewValue; // InputIndex可能为-1、15、100等任意值
防护方案:显式范围校验 + 默认兜底
IF (InputIndex >= LOW(TempBuffer)) AND (InputIndex <= HIGH(TempBuffer)) THEN
TempBuffer[InputIndex] := NewValue;
ELSE
// 写入安全值或触发报警
AlarmFlag := TRUE;
AlarmCode := 101; // 自定义越界错误码
END_IF;
场景3:多维数组嵌套访问时维度混淆
危险代码:
VAR
Matrix : ARRAY[0..2, 0..3] OF INT; // 3行×4列
END_VAR
// 错误:将行列顺序颠倒,或越界行索引
Matrix[5, 2] := 1; // 行越界(最大行索引为2)
防护方案:为每维定义命名常量,强制语义清晰
VAR
MATRIX_ROWS : INT := 3; // 行数
MATRIX_COLS : INT := 4; // 列数
Matrix : ARRAY[0..MATRIX_ROWS-1, 0..MATRIX_COLS-1] OF INT;
END_VAR
// 使用时明确维度含义
IF (RowIdx >= 0) AND (RowIdx < MATRIX_ROWS) AND
(ColIdx >= 0) AND (ColIdx < MATRIX_COLS) THEN
Matrix[RowIdx, ColIdx] := Value;
END_IF;
场景4:指针解引用后未验证目标有效性(仅适用于支持指针的平台如TwinCAT)
危险代码:
pBuf := ADR(TempBuffer);
pBuf^[15] := 99.9; // 直接解引用越界地址
防护方案:指针运算前计算绝对地址并比对数组首尾
VAR
pBuf : POINTER TO REAL;
pStart, pEnd : POINTER TO REAL;
END_VAR
pBuf := ADR(TempBuffer);
pStart := ADR(TempBuffer[LOW(TempBuffer)]);
pEnd := ADR(TempBuffer[HIGH(TempBuffer)]) + SIZEOF(REAL); // 指向末尾后一地址
IF (pBuf >= pStart) AND (pBuf < pEnd) THEN
pBuf^ := 99.9;
ELSE
// 拒绝非法指针操作
END_IF;
四、实战:构建可复用的数组安全访问函数库
为避免重复编写校验逻辑,建议创建如下ST函数块(Function Block),供全项目调用:
FUNCTION_BLOCK SafeArrayWrite
VAR_INPUT
pArray : POINTER TO ANY; // 通用指针,指向任意数组首地址
Index : DINT; // 待写入索引
Value : ANY; // 待写入值(需与数组类型一致)
ArraySize : DINT; // 数组总元素数(编译期已知,传入常量)
LowerBound : DINT; // 数组下界(如0或1)
END_VAR
VAR_OUTPUT
Success : BOOL; // TRUE=写入成功,FALSE=越界拒绝
END_VAR
VAR
pTarget : POINTER TO ANY;
END_VAR
// 计算目标地址:pArray + Index * 元素字节数(此处简化,实际需按Value类型推导)
// 实际工程中,推荐为每种类型(INT/REAL/BOOL)单独建立专用函数,避免ANY类型陷阱
IF (Index >= LowerBound) AND (Index < LowerBound + ArraySize) THEN
pTarget := pArray + (Index - LowerBound) * SIZEOF(Value);
pTarget^ := Value;
Success := TRUE;
ELSE
Success := FALSE;
END_IF;
调用示例:
// 对 TempBuffer[0..9] OF REAL 写入
SafeArrayWrite(
pArray := ADR(TempBuffer),
Index := InputIndex,
Value := NewValue,
ArraySize := 10,
LowerBound := 0,
Success => WriteOK
);
IF NOT WriteOK THEN
TriggerAlarm('TempBuffer Index Out of Range');
END_IF;
⚠️ 注意:
ANY类型在部分PLC中存在类型擦除风险,强烈建议优先使用类型特化函数(如SafeWrite_REAL_Array,SafeWrite_INT_Array),确保编译期类型安全。
五、现场故障排查:三步法定位越界源头
当系统出现间歇性崩溃或数据异常,按以下步骤快速锁定越界点:
第一步:启用运行时诊断(以西门子S7-1500为例)
- 在线连接PLC → 打开“诊断缓冲区”;
- 复现故障(如点击HMI越界按钮);
- 查看最新条目中是否含
F3001(数组访问错误)或F3002(指针访问错误); - 记录触发时的 OB编号、块名、网络号、时间戳。
第二步:反向追踪索引来源
- 在诊断提示的块内,搜索所有对该数组的写操作(
:=); - 对每个写操作,向上追溯索引变量(如
i,Index,Pos)的所有赋值路径; - 重点检查:
- 是否来自
FB或FC的IN参数?→ 检查调用处传入值; - 是否来自
FOR/WHILE循环?→ 核查循环条件边界; - 是否来自
HMI变量绑定?→ 检查HMI画面中该变量的输入限制(如数值框设置最小/最大值)。
- 是否来自
第三步:注入临时断点验证
在疑似越界写操作前插入诊断代码:
// 在 TempBuffer[Index] := X; 前插入
IF NOT ((Index >= LOW(TempBuffer)) AND (Index <= HIGH(TempBuffer))) THEN
DebugLog := CONCAT('越界! Index=', DINT_TO_STRING(Index));
DebugLog := CONCAT(DebugLog, ', Low=', DINT_TO_STRING(LOW(TempBuffer)));
DebugLog := CONCAT(DebugLog, ', High=', DINT_TO_STRING(HIGH(TempBuffer)));
// 将DebugLog发送至HMI文本框或写入DB供上位机读取
END_IF;
通过实时查看 DebugLog 内容,100%确认越界值及上下文。
六、设计阶段防御:自动化代码审查清单
在编码规范中强制加入以下检查项,从源头阻断越界:
| 检查项 | 检查方式 | 不合规示例 | 合规写法 |
|---|---|---|---|
| 禁止硬编码数组上界 | 人工审查 / 静态扫描工具 | FOR i:=0 TO 9 DO |
FOR i:=LOW(arr) TO HIGH(arr) DO |
| 所有外部输入索引必须校验 | 代码走查 | arr[Inp] := val; |
IF Inp IN [LOW(arr)..HIGH(arr)] THEN arr[Inp]:=val; END_IF; |
| 多维数组访问必须标注维度语义 | 评审会议 | mat[i,j] := x; |
mat[RowIdx, ColIdx] := x; 并声明 RowIdx/ColIdx 范围 |
| 指针操作必须配套地址范围验证 | 单元测试覆盖 | p^ := 1; |
IF p IN [pStart..pEnd] THEN p^:=1; END_IF; |
✅ 最佳实践:将上述规则集成至CI/CD流水线,使用开源工具(如
plcopenxml-parser+ 自定义Python脚本)自动扫描ST源码,对违规行标记告警。
七、边界检查的性能代价与取舍建议
启用运行时边界检查会带来约3%~8%的扫描周期延长(取决于数组访问频次)。对于毫秒级响应的高速运动控制,需权衡:
- 必须开启:安全相关功能(SIL2/SIL3)、数据记录、配方管理、HMI交互逻辑;
- 可关闭:纯本地高速循环(如PID内环计算),但需满足:
① 索引由内部确定且逻辑简单(如i := i + 1; IF i > 9 THEN i := 0; END_IF;);
② 已通过静态分析证明无越界路径;
③ 关键变量增加看门狗校验(如每周期检查i是否 ∈[0,9],越界则复位)。
终极原则:宁可牺牲微秒级性能,不可容忍一次越界导致的产线停机。
在ST代码中写下第一个 LOW() 和 HIGH() 函数调用,就是为控制系统筑起第一道内存防火墙。

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