文章目录

ST浮点数比较陷阱:为什么 IF A = B 永远不要用于 REAL 类型

发布于 2026-03-19 18:33:03 · 浏览 4 次 · 评论 0 条

在结构化文本(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 + yz 在监控界面上都显示为 0.30000001,但它们的二进制位模式完全相同只是巧合;绝大多数情况下,x + yz 的 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.99999237100.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-15REAL_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>tTimerTIME 类型转 REAL TIMEREAL 引入舍入;>= 在边界处不稳定 使用原生 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>(用 >= + 下限,符合工艺逻辑)

六、进阶:何时可以安全使用 =?——仅限三类特例

= 并非完全禁用,满足以下全部条件时方可谨慎使用:

  1. 两个变量引用同一内存地址
    pReal : REFERENCE TO REAL;
    pReal^ := 42.0;
    IF pReal^ = 42.0 THEN  // ✅ 安全:无计算,无赋值传播
  2. 常量与立即数比较,且该立即数可被 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 不可精确表示)
  3. 用于检测特殊值
    IF fSensor = 0.0 THEN  // ✅ 安全(0.0 总是精确)
    IF fResult = 1.#INF THEN  // ✅ 安全(INF 是明确位模式)

其余一切涉及计算、转换、通信、用户输入的场景,一律禁用 =


七、工具链层面的防御建议

  1. 静态代码检查(SAST)
    在 CI/CD 流程中集成规则引擎(如 SonarQube 自定义规则),扫描所有 REAL = REALREAL = <decimal_literal> 模式,自动告警。

  2. TIA Portal 用户自定义数据类型(UDT)
    创建 ST_RealSafe UDT,封装带 epsilon 的比较方法,强制工程师通过 .Equals(other, eps) 调用,从 API 层杜绝裸 =

  3. HMI/SCADA 配置规范
    禁止在画面脚本中对 PLC REAL 变量使用 ==,统一调用 IsEqual(value1, value2, tolerance) 接口。

  4. 新人培训必考题

    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 无相等,只有容差。

评论 (0)

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

扫一扫,手机查看

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