在电气自动化系统中,ST(Structured Text)语言是IEC 61131-3标准定义的高级文本编程语言,广泛用于PLC逻辑开发。它语法接近Pascal,支持变量声明、条件判断、循环、函数调用等结构化特性,适合实现复杂控制算法。然而,其对浮点数(REAL 类型)的处理存在一个隐蔽但高危的陷阱:当参与比较运算的浮点数为 NaN(Not a Number)或 Infinity(正/负无穷)时,ST语言标准未强制要求运行时进行自动过滤或异常拦截,所有关系运算符(=, <>, <, <=, >, >=)均返回 FALSE。这一行为看似“安全”,实则导致逻辑判断彻底失效——本该触发的安全停机不动作,本该跳转的工艺分支被跳过,本该报警的异常状态静默消失。
一、问题本质:ST语言中NaN与Infinity的布尔语义是“全假”
IEC 61131-3 第3版标准第8.3.2节明确规定:对于 REAL 类型的操作数,若任一操作数为 NaN,则所有关系运算的结果均为 FALSE。同样,+INF 与 -INF 在比较中也遵循特殊规则:
+INF > x对任意有限x返回TRUE;-INF < x对任意有限x返回TRUE;- 但
+INF = +INF返回FALSE(注意:这与IEEE 754标准一致,但极易被误认为应为TRUE); NaN = NaN返回FALSE;NaN > 0,NaN < 0,NaN <> NaN全部返回FALSE。
这意味着:任何包含 NaN 或 Infinity 的 IF 判断条件,只要该值参与了关系运算,整个条件表达式恒为 FALSE,无论原逻辑意图如何。
例如,一段典型的安全联锁代码:
IF (MotorSpeed > 0.0) AND (MotorTemp < 120.0) THEN
RunMotor := TRUE;
ELSE
RunMotor := FALSE;
TriggerAlarm := TRUE;
END_IF;
若传感器故障导致 MotorSpeed 变为 NaN(如模拟量输入模块通信中断后返回无效浮点值),则 (MotorSpeed > 0.0) 计算结果为 FALSE,整个 IF 条件为 FALSE,程序进入 ELSE 分支——表面看“触发了报警”,但真实风险在于:RunMotor 被强制置 FALSE 是结果,而非原因;而更危险的是,某些场景下开发者会将 ELSE 仅用于日志记录,不执行停机动作。更隐蔽的是如下写法:
IF MotorSpeed > MaxSafeSpeed THEN
EmergencyStop();
END_IF;
一旦 MotorSpeed 为 NaN,MotorSpeed > MaxSafeSpeed 永远为 FALSE,EmergencyStop() 永不执行——系统在“无声中失控”。
二、NaN与Infinity的常见来源(非理论,全部来自现场)
以下来源均在实际工程项目中复现并造成停机事故:
-
除零运算
SpeedRatio := ActualSpeed / SetpointSpeed; // 当SetpointSpeed=0.0时,结果为±INF或NaN(取决于ActualSpeed符号) -
浮点计算溢出
LargeValue := 1E30 * 1E10; // 超出REAL范围(通常为±3.4E38),结果为+INF -
模数转换(ADC)故障值透传
某些PLC模拟量模块在通道断线时,固件直接向用户寄存器写入0x7FC00000(IEEE 754单精度NaN编码),而ST程序未做校验即读取为REAL变量。 -
外部数据接口污染
OPC UA客户端从上位机接收REAL值时,若上位机使用JSON传输,null或字符串"NaN"被错误解析为浮点NaN;或Modbus TCP响应中,故障寄存器被填充为0xFFFF,按浮点解释即为NaN。 -
数学函数返回值未检查
SqrtResult := SQRT(Operand); // Operand < 0.0 → 返回NaN(ST标准未规定SQRT负数行为,多数厂商实现为NaN) LogResult := LN(Input); // Input ≤ 0.0 → 返回NaN或-INF -
初始化未赋值变量
VAR区域声明的REAL变量在PLC首次上电时,内存内容为随机值,其中约1/256概率为NaN编码(因IEEE 754 NaN有大量有效bit模式)。
三、检测:三步定位NaN/Infinity(无需额外硬件)
步骤1:编写通用检测函数块(FB)
创建函数块 FB_IsValidReal,输入 IN : REAL,输出 IsValid : BOOL:
FUNCTION_BLOCK FB_IsValidReal
VAR_INPUT
IN : REAL;
END_VAR
VAR_OUTPUT
IsValid : BOOL;
END_VAR
VAR
u32Bits : DWORD; // 用于提取REAL的32位二进制表示
expBits : WORD; // 指数字段(bits 30..23)
fracBits : DWORD; // 尾数字段(bits 22..0)
END_VAR
// 将REAL按位转换为DWORD(需PLC支持字节序一致的类型转换,如TIA Portal中用REAL_TO_DWORD)
u32Bits := REAL_TO_DWORD(IN);
// 提取指数:掩码 0x7F800000 → 右移23位得8位指数
expBits := WORD((u32Bits AND 16#7F800000#) / 16#800000#);
// 提取尾数:掩码 0x007FFFFF
fracBits := u32Bits AND 16#007FFFFF#;
// IEEE 754规则:指数全1(255)且尾数非零 → NaN;指数全1且尾数为零 → INF
IsValid := NOT ((expBits = 16#FF#) AND (fracBits <> 0))
AND NOT ((expBits = 16#FF#) AND (fracBits = 0));
✅ 说明:该函数严格依据IEEE 754单精度定义。
expBits = 255(十六进制FF)且fracBits ≠ 0→NaN;expBits = 255且fracBits = 0→±INF。其余情况均为有效数。
步骤2:在关键变量读取后立即调用检测
// 读取物理输入
RawSpeed := IW100; // 假设为整型原始值
MotorSpeed := INT_TO_REAL(RawSpeed) * 0.1; // 转换为工程量
// 立即检测
fbValidSpeed(IN := MotorSpeed);
IF NOT fbValidSpeed.IsValid THEN
MotorSpeed := 0.0; // 安全默认值
FaultCode := 16#0001; // 设置故障码
END_IF;
步骤3:全局监控(适用于TIA Portal/CoDeSys等支持诊断缓冲区的平台)
启用PLC运行时诊断,在 System Configuration → Diagnostics 中勾选:
Enable floating-point exception trapping(若支持)Log invalid floating-point operations
当发生 NaN/INF 运算时,诊断缓冲区将记录事件ID 0x8012(浮点无效操作)及触发地址,可导出CSV快速定位问题代码行。
四、防御:五层过滤策略(从硬件到逻辑)
| 层级 | 措施 | 实施要点 | 防御效果 |
|---|---|---|---|
| L1:硬件层 | 选用带“断线检测”和“超限标记”功能的模拟量模块 | 如西门子SM1231 AI 8x13bit,配置通道参数时启用 Breakwire detection,模块自动将断线通道置为特定状态字,而非无效浮点值 |
阻断90%以上NaN源头 |
| L2:驱动层 | 在设备驱动或IO访问函数中嵌入有效性检查 | 所有 READ_AI 函数在返回前执行 FB_IsValidReal;无效值返回预设安全值(如0.0)并置位 IO_Fault 标志 |
隔离硬件异常,业务逻辑无感 |
| L3:数据层 | 为所有 REAL 变量定义“受控别名” |
使用 TYPE 定义:TYPE T_SafeReal : STRUCT Value : REAL; Valid : BOOL; END_STRUCT;所有运算前检查 .Valid 字段 |
强制契约:无效值不可参与计算 |
| L4:逻辑层 | 关键判断前插入显式有效性守卫 | pascal IF NOT fbValidSpeed.IsValid THEN<br> RunMotor := FALSE;<br> RETURN;<br>END_IF; |
最小侵入式修复,适用于遗留代码 |
| L5:测试层 | 单元测试注入边界值 | 使用PLC仿真器(如S7-PLCSIM Advanced)在测试用例中强制将变量设为 NaN/INF,验证 IF 分支覆盖完整性 |
验证防御措施真实生效 |
✅ 强烈推荐组合使用L2+L4:驱动层拦截硬件异常,逻辑层守卫核心判断,兼顾鲁棒性与可维护性。
五、重构示例:从脆弱到健壮
原始脆弱代码(某输送带速度联锁):
// 主程序循环
IF (BeltSpeed > 0.0) AND (BeltLoad < 150.0) AND (NoJams) THEN
DriveEnable := TRUE;
ELSIF (BeltSpeed = 0.0) AND (BeltLoad = 0.0) THEN
DriveEnable := FALSE;
ELSE
DriveEnable := FALSE;
BeltFault := TRUE;
END_IF;
重构后健壮代码:
// 1. 驱动层封装(在独立FB中)
fbBeltSpeed(IN := IW200, OUT => BeltSpeedRaw);
fbBeltLoad(IN := IW202, OUT => BeltLoadRaw);
// 2. 数据层转换(带有效性)
fbValidSpeed(IN := BeltSpeedRaw);
fbValidLoad(IN := BeltLoadRaw);
BeltSpeed.Value := BeltSpeedRaw;
BeltSpeed.Valid := fbValidSpeed.IsValid;
BeltLoad.Value := BeltLoadRaw;
BeltLoad.Valid := fbValidLoad.IsValid;
// 3. 逻辑层守卫判断
IF NOT BeltSpeed.Valid OR NOT BeltLoad.Valid THEN
DriveEnable := FALSE;
BeltFault := TRUE;
RETURN;
END_IF;
// 4. 原逻辑(此时BeltSpeed.Value和BeltLoad.Value必为有效REAL)
IF (BeltSpeed.Value > 0.0) AND (BeltLoad.Value < 150.0) AND (NoJams) THEN
DriveEnable := TRUE;
ELSIF (BeltSpeed.Value = 0.0) AND (BeltLoad.Value = 0.0) THEN
DriveEnable := FALSE;
ELSE
DriveEnable := FALSE;
BeltFault := TRUE;
END_IF;
六、调试技巧:快速识别运行时NaN/Infinity
当系统异常且怀疑浮点问题时,执行以下操作:
-
在线监控变量值:在PLC编程软件中右键目标
REAL变量 →Add to Watch Table→ 启用Hex Display模式。观察其DWORD值:NaN典型值:7FC00000,FFC00000,7F800001(任意尾数非零且指数全1)+INF:7F800000-INF:FF800000
-
添加临时诊断输出:在疑似问题区域插入:
// 临时诊断:将REAL转为HEX字符串(需PLC支持STRING操作)
HexStr := DWORD_TO_STRING(REAL_TO_DWORD(BeltSpeed));
// 输出HexStr至HMI诊断页或PLC日志
- 使用断点条件:在TIA Portal中,为关键
IF行设置断点,条件设为REAL_TO_DWORD(BeltSpeed) = 16#7FC00000#,直接捕获NaN时刻。
七、供应商差异提醒(避免踩坑)
不同PLC厂商对NaN/Infinity的默认处理存在细微差别,务必查阅具体型号手册:
| 厂商 | 型号示例 | NaN比较行为 | 特殊说明 |
|---|---|---|---|
| 西门子 | S7-1200/1500 | 严格遵循IEC 61131-3:所有比较返回FALSE |
TIA Portal V17+新增IS_NAN()系统函数,推荐直接使用 |
| 罗克韦尔 | ControlLogix | NaN = NaN 返回FALSE,但NaN <> NaN 返回TRUE(与标准一致) |
某些旧固件版本对LN(-1)返回0.0而非NaN,需升级 |
| 倍福 | CX9020 | 支持编译期警告:#pragma diag_default "1043" 可开启NaN使用警告 |
TwinCAT 3.1中REAL默认初始化为0.0,降低随机NaN风险 |
| 施耐德 | M340 | SQRT(-1.0) 返回0.0(非NaN),属非标行为 |
必须手动检查输入符号,不可依赖函数返回值 |
⚠️ 关键结论:绝不能假设所有平台行为一致。必须以IEC 61131-3标准为底线,自行实现检测,而非依赖厂商扩展。
八、终极防护:编译期强制检查(适用于支持静态分析的平台)
在支持ST静态分析的IDE(如Codesys 3.5+、TIA Portal V18)中,启用以下检查项:
Enable NaN usage warningWarn on uninitialized REAL variablesDetect division by zero in constant expressions
并在项目属性中设置:
Initialization value for REAL variables: 0.0(覆盖默认随机值)Enable runtime NaN trap: Enabled(若硬件支持)
此举可将80%的NaN隐患消灭在编码阶段。
在电气自动化领域,逻辑的确定性就是安全的生命线。NaN与Infinity不是数学边缘案例,而是传感器失效、通信中断、配置错误的必然产物。放弃“它不会出现”的侥幸,代之以“它一定会来”的预设,通过分层检测与强制守卫,让每一行ST代码都成为可靠防线。

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