在结构化文本(ST)语言中实现可靠自动化控制,关键在于让程序能预判并妥善应对运行时异常。ST作为IEC 61131-3标准定义的高级编程语言,广泛用于PLC、DCS和边缘控制器中。它支持TRY...CATCH...END_TRY语法块,这是ST中唯一原生的结构化异常处理机制,用于捕获运行时错误(如除零、数组越界、指针空解引用、硬件访问失败等),避免程序崩溃或进入未定义状态。
一、为什么ST需要TRY-CATCH?不处理错误会怎样?
PLC程序通常以固定周期(如10 ms)循环执行。若某次扫描中发生未捕获的运行时错误(例如对一个值为0的变量执行DIV指令),多数符合IEC 61131-3的运行时环境(如Codesys、TwinCAT、Unity Pro)将立即中止当前任务的执行,并可能触发以下连锁反应:
- 当前扫描周期剩余代码不再执行;
- 输出映像区保持上一周期值(导致“输出冻结”);
- 错误被记录到系统诊断缓冲区,但无应用层干预;
- 若错误反复发生,可能使PLC进入“停止模式”或持续报错,中断产线运行。
这与高级语言中“进程崩溃”不同——PLC不会重启,但控制逻辑实质失效。TRY-CATCH的作用,就是把“不可控中断”转化为“可控分支”,让程序在错误发生后仍能执行降级策略(如切换备用通道、置位报警、安全停机)。
二、TRY-CATCH语法结构与执行规则
ST中TRY-CATCH必须成对出现,且遵循严格嵌套规则。基本结构如下:
TRY
// 可能出错的语句(受保护代码)
x := y / z; // 若z=0,此处抛出DivByZero异常
arr[5] := data; // 若arr长度<6,抛出ArrayBounds异常
val := REF_TO(pVar)^; // 若pVar为NULL,抛出NullPointer异常
CATCH e : INT
// 异常处理代码(e接收错误码)
errorID := e;
alarmActive := TRUE;
END_TRY
关键规则:
TRY块内禁止跳转出块:不能在TRY中使用EXIT、RETURN或GOTO跳转到TRY外部(编译器将报错);CATCH必须声明错误变量:CATCH e : INT中的e是只读整型变量,自动接收系统分配的错误代码(非用户自定义值);- 仅捕获运行时异常,不捕获编译错误:类型不匹配、未声明变量等在编译期报错,
CATCH无法拦截; - 异常传播终止:一旦
CATCH块执行完毕,异常即被“消耗”,不会向上传播; CATCH后不可接ELSE或FINALLY:IEC 61131-3 ST标准不支持FINALLY子句(与C#、Java不同),资源清理需手动编码。
三、ST运行时错误代码体系(核心参考表)
不同厂商对错误码定义略有差异,但均遵循IEC 61131-3附录D的通用分类。以下是主流平台(Codesys 3.5+、TwinCAT 3)共用的错误码范围及含义:
| 错误码 | 名称 | 触发条件示例 | 处理建议 |
|---|---|---|---|
1 |
DivByZero |
整数或实数除法中除数为0 | 检查输入有效性,提供默认值 |
2 |
ArrayBounds |
访问数组索引超出声明范围(如arr[10]但arr长5) |
校验索引值,限制在0..SIZEOF(arr)-1 |
3 |
NullPointer |
对空指针(REF_TO(NULL))执行解引用操作 |
使用ISVALID()预检指针 |
4 |
InvalidValue |
向枚举变量赋非法整数值(如state := 99但枚举仅含0..2) |
用CASE校验再赋值 |
5 |
Overflow |
算术运算结果超出目标类型范围(如INT溢出) |
改用更大类型(DINT),或加限幅 |
6 |
HardwareError |
读取I/O模块返回硬件故障(如端子松动、信号丢失) | 切换至冗余通道,触发维护报警 |
⚠️ 注意:错误码
e的值不是字符串,而是整数。不可写作IF e = "DivByZero"(语法错误),必须用数字比较:IF e = 1 THEN ...
四、实战:构建可恢复的PID温度控制器
以下是一个工业级温度控制函数块(FB),演示TRY-CATCH在真实场景中的分层防护设计:
FUNCTION_BLOCK FB_TempController
VAR
// 输入
tempPV : REAL; // 实际温度(来自传感器)
tempSP : REAL; // 设定值
manualMode : BOOL; // 手动/自动切换
manualOut : REAL; // 手动输出值
// 输出
output : REAL; // 控制器输出(0.0~100.0%)
faultActive : BOOL; // 故障标志
// 内部变量
lastError : INT := 0;
integrator : REAL := 0.0;
prevPV : REAL := 0.0;
dt : TIME := T#20ms; // 采样周期
END_VAR
METHOD Execute : REAL
VAR
error : INT;
errCode : INT;
kp, ti, td : REAL;
errorVal : REAL;
derivative : REAL;
pTerm, iTerm, dTerm : REAL;
rawOut : REAL;
maxOut, minOut : REAL := 100.0, 0.0;
END_VAR
// 步骤1:参数校验(预防性防御)
IF tempSP < 0.0 OR tempSP > 300.0 THEN
tempSP := 150.0; // 重置为默认设定值
END_IF
// 步骤2:主控制逻辑(包裹在TRY中)
TRY
// 获取PID参数(假设从配置DB读取,可能因通信中断返回0)
kp := ConfigDB.PID_Kp;
ti := ConfigDB.PID_Ti;
td := ConfigDB.PID_Td;
// 计算误差
errorVal := tempSP - tempPV;
// 比例项
pTerm := kp * errorVal;
// 积分项(防积分饱和)
IF NOT manualMode THEN
integrator := integrator + (errorVal * (TIME_TO_REAL(dt) / ti));
// 限幅积分器
IF integrator > 100.0 THEN integrator := 100.0; END_IF
IF integrator < 0.0 THEN integrator := 0.0; END_IF
END_IF
iTerm := integrator;
// 微分项(带滤波)
derivative := (prevPV - tempPV) / TIME_TO_REAL(dt);
dTerm := td * derivative;
// 合成输出
rawOut := pTerm + iTerm + dTerm;
// 输出限幅
IF rawOut > maxOut THEN
output := maxOut;
ELSIF rawOut < minOut THEN
output := minOut;
ELSE
output := rawOut;
END_IF
// 更新历史值
prevPV := tempPV;
// 步骤3:手动模式覆盖
IF manualMode THEN
output := manualOut;
END_IF
faultActive := FALSE;
CATCH errCode : INT
// 步骤4:异常分类处理
CASE errCode OF
1: // DivByZero → ti=0 或 dt=0
lastError := 1;
faultActive := TRUE;
output := 0.0; // 安全归零
integrator := 0.0;
2: // ArrayBounds → ConfigDB访问越界
lastError := 2;
faultActive := TRUE;
// 加载默认PID参数
kp := 2.0; ti := 120.0; td := 5.0;
3: // NullPointer → ConfigDB为NULL
lastError := 3;
faultActive := TRUE;
// 切换至本地硬编码参数
kp := 1.5; ti := 180.0; td := 8.0;
5: // Overflow → 计算中间值溢出
lastError := 5;
faultActive := TRUE;
// 启用软限幅,降低增益
kp := kp * 0.5;
output := LIMIT(minOut, maxOut, rawOut * 0.8);
ELSE
// 兜底:未知错误,进入安全状态
lastError := errCode;
faultActive := TRUE;
output := 0.0;
integrator := 0.0;
END_CASE
END_TRY
// 步骤5:输出诊断信息(供HMI显示)
diagnosticText := CONCAT('ERR:', INT_TO_STRING(lastError));
Execute := output;
此例体现三大设计原则:
- 分层防护:先做输入校验(步骤1),再执行核心计算(步骤2),最后统一异常兜底(步骤4);
- 故障导向恢复:每个错误码对应具体恢复动作(重置参数、启用备份、安全归零),而非简单报警;
- 状态隔离:
integrator、prevPV等状态变量在异常后被显式重置,防止错误状态污染后续扫描。
五、高级技巧:嵌套TRY-CATCH与错误日志
复杂功能块常需多级异常处理。例如,在数据记录模块中,既要捕获文件写入失败,又要处理时间戳生成异常:
METHOD LogData : BOOL
VAR
fileHandle : FILE_HANDLE;
timestamp : DATE_AND_TIME;
buffer : ARRAY[0..255] OF BYTE;
writeResult : INT;
outerErr, innerErr : INT;
END_VAR
TRY
// 外层:捕获整个日志流程失败
TRY
// 内层:仅捕获时间戳生成异常(极罕见,但可能因RTC电池没电)
timestamp := CURRENT_DATE_AND_TIME();
CATCH innerErr : INT
IF innerErr = 4 THEN // InvalidValue(RTC失效)
timestamp := DT#1970-01-01-00:00:00; // 使用UNIX纪元时间
END_IF
END_TRY
// 打开文件(可能因SD卡满失败)
fileHandle := FOPEN('LOG.TXT', 'a');
IF fileHandle = INVALID_FILE_HANDLE THEN
RAISE(101); // 手动抛出自定义错误(需厂商支持,Codesys可用RAISE)
END_IF
// 构建日志行并写入
buffer := BUILD_LOG_LINE(timestamp, tempPV, output);
writeResult := FWRITE(fileHandle, ADR(buffer), SIZEOF(buffer));
IF writeResult <> SIZEOF(buffer) THEN
RAISE(102);
END_IF
FCLOSE(fileHandle);
LogData := TRUE;
CATCH outerErr : INT
// 统一日志失败处理
IF outerErr IN [101, 102] THEN
// 存储至环形内存缓冲区,待SD卡恢复后补写
RingBuffer_Write(buffer, SIZEOF(buffer));
END_IF
LogData := FALSE;
END_TRY
💡 提示:
RAISE(n)是非标准扩展(Codesys支持),用于主动触发异常;若平台不支持,可用IF FALSE THEN RAISE(1); END_IF模拟抛出。
六、常见陷阱与规避方案
| 陷阱 | 表现 | 解决方案 |
|---|---|---|
| 在CATCH中调用可能出错的函数 | CATCH块内执行FOPEN又失败,导致二次异常未被捕获 |
CATCH内只做状态重置、报警置位、安全输出,严禁调用任何可能抛出异常的函数 |
| 忽略浮点除零 | REAL除法x/y中y=0.0不触发DivByZero(返回INF或NAN) |
用ABS(y) < 1E-9预检,或启用CPU浮点异常中断(需硬件支持) |
| TRY块过大 | 将整个FB逻辑包进一个TRY,导致单个低风险错误(如HMI通信超时)引发全部逻辑降级 |
按功能边界拆分:传感器读取、算法计算、执行器输出各用独立TRY |
| 错误码硬编码 | IF e = 1 THEN ... 降低可读性 |
定义常量:CONST DivByZero := 1; END_CONST,然后IF e = DivByZero THEN |
七、性能影响与实时性保障
TRY-CATCH本身无运行时开销——它只是编译器生成的跳转表和错误处理入口地址。实际性能损耗来自:
- 异常发生时的上下文保存:约200~500 µs(取决于CPU);
- CATCH块执行时间:必须控制在毫秒级,否则影响扫描周期。
实测建议:
- 单个
CATCH块代码行数 ≤ 20行; - 避免在
CATCH中调用STRING操作(如CONCAT)、复杂数学函数(如SQRT); - 将日志记录、网络发送等耗时操作移至后台任务,
CATCH中仅置位标志位。
八、调试与诊断最佳实践
- 强制触发测试:在开发阶段,临时插入
RAISE(1)验证CATCH路径是否生效; - 诊断缓冲区监控:在HMI中实时显示
lastError及diagnosticText,定位首次错误源; - 错误码统计:用数组
errorCount[1..10]累计各错误发生频次,识别高频故障点; - 仿真验证:在Codesys仿真环境中,通过“强制变量值”模拟
z:=0、arr:=NULL等场景。
九、与其他错误处理方式对比
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
TRY-CATCH |
结构清晰、异常隔离、符合标准 | 仅支持运行时错误,无FINALLY |
主要控制逻辑、关键算法 |
IF-THEN预检 |
无异常开销、完全可控 | 代码冗长,易漏检(如并发修改) | 输入校验、边界检查 |
| 系统错误OB(如S7) | 底层硬件错误全覆盖 | 厂商专属、非ST标准、难移植 | I/O模块故障、电源中断 |
| 状态机降级 | 平滑过渡、用户无感 | 开发复杂度高 | 安全完整性等级(SIL)要求场景 |
结论:TRY-CATCH不是万能药,而是与预检、状态机、系统OB协同的防御纵深中的一环。
十、迁移指南:从无错误处理到健壮ST
若现有代码无异常处理,按三步渐进升级:
- 第一周:为所有
/、MOD运算添加IF divisor <> 0 THEN ... END_IF预检; - 第二周:识别5个最高风险函数块(如运动控制、安全门锁),为其核心计算添加
TRY-CATCH; - 第三周:建立项目级错误码字典(
ERROR_CODES全局变量),统一CATCH处理逻辑,输出标准化诊断字符串。
最终目标:任一扫描周期内,即使发生最坏异常,系统仍能维持安全输出、记录故障、通知运维,而非静默失效。
TRY块不是代码的终点,而是控制权移交的起点;CATCH不是失败的句号,而是恢复逻辑的冒号。

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