ST错误处理机制:TRY-CATCH在ST语言中的异常捕获

发布于 2026-03-18 09:23:48 · 浏览 4 次 · 评论 0 条

在结构化文本(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

关键规则:

  1. TRY块内禁止跳转出块:不能在TRY中使用EXITRETURNGOTO跳转到TRY外部(编译器将报错);
  2. CATCH必须声明错误变量CATCH e : INT中的e只读整型变量,自动接收系统分配的错误代码(非用户自定义值);
  3. 仅捕获运行时异常,不捕获编译错误:类型不匹配、未声明变量等在编译期报错,CATCH无法拦截;
  4. 异常传播终止:一旦CATCH块执行完毕,异常即被“消耗”,不会向上传播;
  5. CATCH后不可接ELSEFINALLY: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);
  • 故障导向恢复:每个错误码对应具体恢复动作(重置参数、启用备份、安全归零),而非简单报警;
  • 状态隔离integratorprevPV等状态变量在异常后被显式重置,防止错误状态污染后续扫描。

五、高级技巧:嵌套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/yy=0.0不触发DivByZero(返回INFNAN 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中仅置位标志位。

八、调试与诊断最佳实践

  1. 强制触发测试:在开发阶段,临时插入RAISE(1)验证CATCH路径是否生效;
  2. 诊断缓冲区监控:在HMI中实时显示lastErrordiagnosticText,定位首次错误源;
  3. 错误码统计:用数组errorCount[1..10]累计各错误发生频次,识别高频故障点;
  4. 仿真验证:在Codesys仿真环境中,通过“强制变量值”模拟z:=0arr:=NULL等场景。

九、与其他错误处理方式对比

方式 优点 缺点 适用场景
TRY-CATCH 结构清晰、异常隔离、符合标准 仅支持运行时错误,无FINALLY 主要控制逻辑、关键算法
IF-THEN预检 无异常开销、完全可控 代码冗长,易漏检(如并发修改) 输入校验、边界检查
系统错误OB(如S7) 底层硬件错误全覆盖 厂商专属、非ST标准、难移植 I/O模块故障、电源中断
状态机降级 平滑过渡、用户无感 开发复杂度高 安全完整性等级(SIL)要求场景

结论:TRY-CATCH不是万能药,而是与预检、状态机、系统OB协同的防御纵深中的一环


十、迁移指南:从无错误处理到健壮ST

若现有代码无异常处理,按三步渐进升级:

  1. 第一周:为所有/MOD运算添加IF divisor <> 0 THEN ... END_IF预检;
  2. 第二周:识别5个最高风险函数块(如运动控制、安全门锁),为其核心计算添加TRY-CATCH
  3. 第三周:建立项目级错误码字典(ERROR_CODES全局变量),统一CATCH处理逻辑,输出标准化诊断字符串。

最终目标:任一扫描周期内,即使发生最坏异常,系统仍能维持安全输出、记录故障、通知运维,而非静默失效。


TRY块不是代码的终点,而是控制权移交的起点;CATCH不是失败的句号,而是恢复逻辑的冒号。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文