在结构化文本(ST)编程中,IF A = B 看似直白的浮点数相等判断,实则是电气自动化系统中最隐蔽、最常被忽视的故障源头之一。它不会报错,不会崩溃,却可能让温度控制偏差 ±5℃、让变频器输出突跳 20Hz、让安全联锁在关键毫秒失效——而所有日志都显示“逻辑执行正常”。
根本原因不是你写错了代码,而是 REAL 类型在 PLC 中的底层表示方式与人类直觉存在不可调和的矛盾。
一、REAL 不是数学上的实数,而是 IEEE 754 单精度二进制浮点数
PLC(如西门子 S7-1200/1500、倍福 TwinCAT、罗克韦尔 ControlLogix)中的 REAL 类型严格遵循 IEEE 754-1985 标准的 32 位单精度格式:1 位符号 + 8 位指数 + 23 位尾数(实际精度为 24 位,因隐含最高位 1)。
这意味着:
- 它能精确表示的十进制数极少,仅限于形如 $\frac{m}{2^n}$ 的有理数($m, n$ 为整数);
- 所有带小数的常用十进制数(如
0.1,3.14,100.55,0.001)在存入 REAL 时,必然发生舍入误差。
例如,在 ST 中执行:
VAR
x : REAL := 0.1;
y : REAL := 0.2;
z : REAL := 0.3;
END_VAR
表面上 x + y 应等于 z,但实际内存值为:
| 变量 | 十进制显示值(HMI/监控) | 实际二进制近似值(IEEE 754 解码) | 与理想值绝对误差 |
|---|---|---|---|
x |
0.10000000149011612 |
$0.100000001490116119384765625$ | $1.49 \times 10^{-9}$ |
y |
0.20000000298023224 |
$0.20000000298023223876953125$ | $2.98 \times 10^{-9}$ |
z |
0.30000001192092896 |
$0.300000011920928955078125$ | $1.19 \times 10^{-8}$ |
x + y |
0.30000001192092896 |
$0.300000011920928955078125$ | — |
注意:x + y 和 z 在监控界面上都显示为 0.30000001,但它们的二进制位模式完全相同只是巧合;绝大多数情况下,x + y 与 z 的 IEEE 754 位模式不同,x + y = z 判断将返回 FALSE。
这就是陷阱的起点:显示值 ≠ 存储值 ≠ 数学值。
二、“= ”操作符比较的是位模式,而非数值意义
ST 中的 = 对 REAL 类型不做任何容差处理,它直接比较两个变量的 32 位内存字节是否逐比特相等。
以下代码在绝大多数 PLC 上永远不进入 THEN 分支:
IF (0.1 + 0.2) = 0.3 THEN
// 这段代码永远不会执行
bAlarm := TRUE;
END_IF;
原因:(0.1 + 0.2) 计算结果经 IEEE 754 舍入后,其 32 位字节序列 ≠ 0.3 的字节序列。
更危险的是动态计算场景。例如 PID 控制器输出限幅:
// 错误示范:用 = 判断是否达到上限
IF fOutput = 100.0 THEN
bAtLimit := TRUE; // 极大概率永远为 FALSE
END_IF;
即使 HMI 显示 fOutput = 100.00,其真实值可能是 99.99999237 或 100.0000076,= 比较失败,导致 bAtLimit 始终为 FALSE,上位机无法正确触发饱和报警或切换控制模式。
三、为什么“四舍五入后再比较”也不可靠?
有人尝试绕过问题:
// ❌ 仍然错误:ROUND() 返回 INT,REAL 转 INT 会截断,且 ROUND 本身受浮点误差影响
IF ROUND(fValue * 100.0) = ROUND(5.55 * 100.0) THEN // 期望匹配 5.55
...
END_IF;
问题在于:
5.55本身已失真(真实存储为5.550000190734863),乘以100.0后误差放大;ROUND()函数输入已是近似值,输出 INT 无法还原原始意图;ROUND()在不同 PLC 厂商实现中舍入规则可能不同(银行家舍入 vs 正无穷舍入)。
同样,TRUNC(), CEIL(), FLOOR() 均无法解决根本问题——它们操作的对象已经是失真的浮点数。
四、正确解法:使用“绝对误差比较”(Epsilon 比较)
唯一可靠的方法是:判断两 REAL 值之差的绝对值是否小于一个预设的小阈值 epsilon。
1. 选择合适的 epsilon
epsilon 不是固定常数,必须根据应用场景的物理精度要求设定:
| 应用场景 | 典型物理分辨率 | 推荐 epsilon 值 | 说明 |
|---|---|---|---|
| 温度控制(PT100) | ±0.1 ℃ | 1.0E-1 |
大于传感器精度即可 |
| 流量计量(涡街) | ±0.01 m³/h | 1.0E-2 |
匹配仪表最小分度 |
| 伺服位置(脉冲当量 1μm) | ±1 μm | 1.0E-6 |
注意单位换算一致性 |
| 通用调试/中间计算 | — | 1.0E-6 |
保守起见,避免过度敏感 |
⚠️ 绝对禁止使用 1.0E-15 或 REAL_EPSILON(如 C 语言中的 FLT_EPSILON ≈ 1.19E-7)——它代表的是相邻可表示数的间距量级,而非应用容差。在 REAL 值较大时(如 1.0E6),相邻数间距已达 0.0625,此时 1.0E-15 比较毫无意义。
2. 封装为可复用函数块(推荐)
在 TIA Portal 或 CoDeSys 中,创建函数块 FB_RealEqual:
FUNCTION_BLOCK FB_RealEqual
VAR_INPUT
a : REAL;
b : REAL;
epsilon : REAL := 1.0E-6; // 默认容差
END_VAR
VAR_OUTPUT
equal : BOOL;
END_VAR
equal := ABS(a - b) <= epsilon;
// 可选:增加防溢出保护(当 a 或 b 为 INF/NAN 时)
IF NOT IS_VALID(a) OR NOT IS_VALID(b) THEN
equal := FALSE;
END_IF;
使用方式:
fbEq(a:=fSpeedActual, b:=fSpeedSet, epsilon:=1.0E-2);
IF fbEq.equal THEN
bSpeedMatch := TRUE;
END_IF;
3. 直接内联写法(轻量级场景)
若不需复用,直接写:
// ✅ 正确:显式容差,语义清晰
IF ABS(fCurrent - fTripThreshold) <= 0.05 THEN // 允许 ±50mA 误差
bOvercurrent := TRUE;
END_IF;
注意:ABS() 是标准 ST 函数,所有主流 PLC 均支持。
五、高频陷阱场景与规避清单
以下是在工程中反复出现的高危模式,务必逐条核查:
| 场景 | 错误写法 | 风险 | 正确写法 |
|---|---|---|---|
| 定时器超时判断 | IF tTimer >= tDelay THEN<br>tTimer 为 TIME 类型转 REAL |
TIME 转 REAL 引入舍入;>= 在边界处不稳定 |
使用原生 TIME 比较:<br>IF tTimer >= tDelay THEN(无需转换) |
| HMI 输入校验 | IF fInput = 0.0 THEN(判断空输入) |
HMI 发送 0.0 经网络传输、协议解析后失真 |
改用范围:<br>IF ABS(fInput) <= 1.0E-5 THEN |
| 多 PLC 数据同步 | IF fMasterVal = fSlaveVal THEN |
两边计算路径不同,舍入累积差异 | 设定同步容差:<br>IF ABS(fMasterVal - fSlaveVal) <= 0.1 THEN |
| PID 输出限幅标志 | IF fOut = fMax THEN |
永远无法置位 bAtMax |
IF fOut >= (fMax - 1.0E-3) THEN<br>(注意:用 >= 配合下偏移,确保进入限幅区即触发) |
| 状态机跳转条件 | IF fTemp = 100.0 THEN state := STATE_BOIL; |
沸腾温度判断失效 | IF fTemp >= 99.5 THEN state := STATE_BOIL;<br>(用 >= + 下限,符合工艺逻辑) |
六、进阶:何时可以安全使用 =?——仅限三类特例
= 并非完全禁用,满足以下全部条件时方可谨慎使用:
- 两个变量引用同一内存地址:
pReal : REFERENCE TO REAL; pReal^ := 42.0; IF pReal^ = 42.0 THEN // ✅ 安全:无计算,无赋值传播 - 常量与立即数比较,且该立即数可被 IEEE 754 精确表示:
可精确表示的数包括:- 整数:
0.0,1.0,2.0, ...,16777216.0(≤ $2^{24}$ 的整数); - 二进制小数:
0.5,0.25,0.125,0.75,1.5等(分母为 $2^n$)。IF fState = 1.0 THEN // ✅ 安全(1.0 精确) IF fRatio = 0.5 THEN // ✅ 安全(0.5 = 1/2) IF fValue = 0.1 THEN // ❌ 危险(0.1 不可精确表示)
- 整数:
- 用于检测特殊值:
IF fSensor = 0.0 THEN // ✅ 安全(0.0 总是精确) IF fResult = 1.#INF THEN // ✅ 安全(INF 是明确位模式)
其余一切涉及计算、转换、通信、用户输入的场景,一律禁用 =。
七、工具链层面的防御建议
-
静态代码检查(SAST):
在 CI/CD 流程中集成规则引擎(如 SonarQube 自定义规则),扫描所有REAL = REAL或REAL = <decimal_literal>模式,自动告警。 -
TIA Portal 用户自定义数据类型(UDT):
创建ST_RealSafeUDT,封装带 epsilon 的比较方法,强制工程师通过.Equals(other, eps)调用,从 API 层杜绝裸=。 -
HMI/SCADA 配置规范:
禁止在画面脚本中对 PLCREAL变量使用==,统一调用IsEqual(value1, value2, tolerance)接口。 -
新人培训必考题:
VAR a : REAL := 0.1; b : REAL := 0.2; c : REAL := 0.3; END_VAR // 问:以下表达式值为 TRUE 还是 FALSE? (a + b) = c; // → FALSE ABS((a + b) - c) < 1.0E-6; // → TRUE
八、一个真实故障案例:锅炉水位失控
某电厂 DCS 系统(基于 ABB 800xA)发生多次水位骤升超限,安全阀频繁启跳。日志显示水位调节阀指令已输出至 0%,但水位仍上涨。
根因分析发现,水位控制器中存在:
// 控制器内部逻辑(简化)
IF fWaterLevel = fSetpoint THEN
fValveCmd := 0.0; // 期望水位到位即关闭阀门
ELSE
fValveCmd := PID_Calc(...);
END_IF;
由于 fWaterLevel 来自差压变送器(4-20mA → REAL),fSetpoint 来自操作员设定(HMI 输入 → REAL),二者均含独立舍入误差,= 几乎永不成立,阀门永远得不到“关闭”指令,仅靠 PID 微调,响应滞后导致超调。
修复方案:
// 替换为带死区的区间判断
IF ABS(fWaterLevel - fSetpoint) <= 5.0 THEN // ±5mm 死区
fValveCmd := 0.0;
ELSE
fValveCmd := PID_Calc(...);
END_IF;
上线后,水位波动幅度下降 72%,安全阀动作频次归零。
浮点比较不是编程技巧问题,而是对数字本质的认知问题。在电气自动化领域,每一次 = 的滥用,都是在用确定性逻辑去驾驭不确定性表示。真正的鲁棒性,始于放弃“相等”的执念,转向“足够接近”的工程思维。
用 ABS(a - b) <= epsilon 替代 a = b,不是妥协,而是尊重硬件的物理约束;不是增加复杂度,而是消除隐性故障源。当你下次敲下 = 键前,请默念:REAL 无相等,只有容差。

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