工业现场的软件,最怕的往往不是程序报错,而是程序没有报错,数据却悄悄变错了。
比如,PLC 每隔 100ms 上传一次温度,正常值在 35℃ 左右,某次通信受到干扰,突然读到 3276.7℃。程序没有异常,数据类型转换也成功了,界面却显示了一个离谱的数值。如果这份数据直接参与报警判断、设备联锁或控制计算,问题就不再是显示异常那么简单。
类似情况在工控项目中并不少见:
- 通信抖动: Modbus TCP 偶尔超时,数据更新出现间隔不均。
- 传感器噪声: 模拟量采集值不断小幅波动,导致报警反复触发。
- 瞬时尖峰: 某一帧数据突然偏离正常范围,随后又恢复。
- 设备掉线: 上位机仍然显示最后一次读数,让操作员误以为设备正常。
- 线程竞争: 采集线程和 UI 线程同时读写数据,偶尔出现不一致。
- 异常恢复: 通信恢复后,旧数据、缓存数据和新数据混在一起,造成状态误判。
这些问题不能仅靠一个try-catch解决。真正可靠的工控软件,需要把数据校验、滤波、通信状态、故障降级和恢复策略组合起来。
本文以 C#、.NET 8、PLC 周期采集为例,拆解 7 种可以落地的软件层容错手段,并提供可组合的代码实现。
先明确一个原则:软件滤波用于抑制噪声、识别异常和改善数据质量,不能替代电气隔离、屏蔽接地、硬件保护和 PLC 安全联锁。涉及人身安全或设备安全的动作,必须由经过验证的安全机制保障。
一、先设计整体架构:不要让原始数据直接进入控制逻辑
很多项目最初采用的是一条直线式处理链:
读取 PLC → 更新界面 → 判断报警 → 执行控制问题在于,采集到的数据默认被认为是正确的。只要通信成功、数据类型转换成功,后续模块就会继续使用它。
更合理的方式,是在原始数据与业务逻辑之间增加一层数据质量处理。
① 数据采集层
PLC / Modbus TCP / 串口 / 传感器
② 数据校验层
数值范围、通信状态、时间戳、变化速率
③ 滤波与状态判定层
中值滤波、指数平滑、报警滞回、故障状态机
④ 业务应用层
界面显示、趋势曲线、报警、控制决策
这几个层次解决的问题并不相同:
| 处理手段 | 主要解决的问题 | 是否引入延迟 |
|---|---|---|
| 数值范围校验 | 超量程、非法值 | 基本没有 |
| 时间戳与新鲜度检查 | 数据过期、通信中断 | 基本没有 |
| 中值滤波 | 孤立尖峰、脉冲噪声 | 有窗口延迟 |
| 指数移动平均 | 连续小幅波动 | 有平滑延迟 |
| 变化速率限制 | 突变、跳变 | 可能延迟真实变化 |
| 报警滞回 | 报警反复开关 | 可能延迟报警恢复 |
| 超时、重试与熔断 | 短暂故障、持续掉线 | 取决于超时与重试策略 |
核心思路:先判断数据是否可信,再决定是否需要滤波,最后才交给业务逻辑。
二、手段一:数据范围校验,先拦住明显错误
假设一个温度传感器的工程量程是 -20℃~150℃。采集到的数据如果是 3276.7℃,就应该先检查,而不是直接拿去计算。
public static bool IsValidValue( double value, double min, double max) { return double.IsFinite(value) && value >= min && value <= max; }这里使用double.IsFinite(),可以同时排除NaN和正负无穷大。它们虽然是合法的double值,却不适合直接进入普通业务计算。
使用示例:
double temperature = ReadTemperature(); if (!IsValidValue(temperature, -20, 150)) { Console.WriteLine("温度数据超出有效范围"); return; } Console.WriteLine($"当前温度:{temperature:F2} ℃");需要注意:ReadTemperature()只是示意,实际项目应由具体的 PLC 驱动实现。
工程实践中的两个陷阱
陷阱一:量程范围不等于正常运行范围。
传感器的测量范围可能是 -20℃~150℃,但设备正常运行范围也许是 30℃~80℃。超出正常运行范围未必意味着数据无效,它可能恰恰意味着设备真的发生了异常。
建议将范围拆成三类:
- 传输有效性:数据格式正确,转换没有溢出。
- 传感器有效性:数值位于传感器允许测量的范围。
- 工艺告警范围:数值是否符合设备当前工况要求。
不要把三种判断混为一谈。
陷阱二:不要把异常数据悄悄替换成 0。
// 不推荐:错误数据被伪装成正常数据 if (!IsValidValue(value, -20, 150)) { value = 0; }如果 0℃ 是一个有效读数,这种做法就会掩盖问题。
更合理的设计是保留数据有效性状态,让下游明确知道这次采样不可用,而不是制造一个看似正常的数值。
三、手段二:检查数据新鲜度,避免拿旧值冒充实时值
工业通信中有一种非常隐蔽的问题:通信线程已经停止更新,但界面仍显示最后一次成功读取的数据。
例如,温度最后一次读取是 45℃,随后 PLC 掉线。界面如果一直显示 45℃,操作员可能误认为设备仍处于正常状态。
因此,数据值之外,还应维护采样时间和通信状态。
public sealed record Sample( double Value, DateTimeOffset Timestamp, bool IsValid); public static bool IsFresh( Sample sample, DateTimeOffset now, TimeSpan maxAge) { if (!sample.IsValid) return false; var age = now - sample.Timestamp; return age >= TimeSpan.Zero && age <= maxAge; }调用方式:
var sample = new Sample( Value: 45.2, Timestamp: DateTimeOffset.UtcNow, IsValid: true); bool fresh = IsFresh( sample, DateTimeOffset.UtcNow, TimeSpan.FromMilliseconds(500)); Console.WriteLine($"数据是否新鲜:{fresh}");这里的 500ms 仅用于演示,生产环境需要根据设备的采样周期、通信周期和允许的控制延迟确定。
如果 PLC 每 100ms 更新一次数据,允许连续丢失几次采样后才判定超时,那么阈值可以根据这一要求设置;但不能把所有设备统一设为 500ms。
为什么时间戳很重要?
因为“值为 45℃”和“最新值为 45℃”是两件不同的事。
推荐在数据对象中至少维护以下信息:
Value 当前数值 Timestamp 采样时间 IsValid 本次采样是否有效 Quality 数据质量状态 ErrorCode 最近一次采集错误对于跨设备时间同步不可靠的场景,应优先使用本机接收时间判断通信新鲜度,同时单独记录设备时间。不要直接比较未经同步的 PLC 时间与上位机时间。
数据过期时,应将状态标记为未知或失效,而不是继续将旧值当作实时值使用。
四、手段三:中值滤波,专门对付偶发尖峰
如果采集数据大致如下:
50.1、50.2、50.1、73.8、50.2、50.1、50.3其中73.8是一次孤立尖峰。简单平均会受到这个异常值的影响,而中值滤波可以有效抑制它。
原理很简单:取最近 N 个样本,排序后取中间值。
例如,窗口内有 5 个样本:
原始数据:50.1、50.2、73.8、50.2、50.1 排序后: 50.1、50.1、50.2、50.2、73.8 中值: 50.2C# 实现:固定窗口中值滤波
using System; using System.Collections.Generic; using System.Linq; public sealed class MedianFilter { private readonly int _windowSize; private readonly Queue<double> _window = new(); public MedianFilter(int windowSize) { if (windowSize < 1 || windowSize % 2 == 0) { throw new ArgumentException( "窗口长度必须是正奇数。", nameof(windowSize)); } _windowSize = windowSize; } public double Add(double value) { if (!double.IsFinite(value)) throw new ArgumentOutOfRangeException(nameof(value)); _window.Enqueue(value); if (_window.Count > _windowSize) _window.Dequeue(); double[] sorted = _window.OrderBy(x => x).ToArray(); return sorted[sorted.Length / 2]; } }使用示例:
var filter = new MedianFilter(5); double[] values = { 50.1, 50.2, 73.8, 50.2, 50.1, 50.3 }; foreach (double value in values) { double filtered = filter.Add(value); Console.WriteLine( $"原始值:{value:F2},滤波值:{filtered:F2}"); }时间复杂度与性能分析
假设窗口长度为 N。
- 入队、移除旧样本:均摊 O(1)。
- 每次重新排序:O(N log N)。
- 窗口内存:O(N)。
当窗口只有 3~9 个样本时,这种实现通常足够直观。若需要同时处理数千个测点、窗口较大且采样频率很高,可以考虑维护双堆或平衡有序结构,降低重复排序开销,但实现复杂度也会增加。
使用中值滤波的注意事项
- 窗口越大,对孤立尖峰的抑制通常越明显,但对真实变化的响应也可能越慢。
- 窗口刚开始填充时,输出基于已有样本,并不代表窗口已经完整。
- 中值滤波不是万能的。如果异常值连续出现并占据窗口多数,滤波结果仍会被带偏。
- 对快速变化的压力、位置或速度信号,不能仅凭滤波效果好就使用大窗口。
中值滤波适合处理脉冲噪声,不应该用来掩盖真实的工艺突变。
五、手段四:指数移动平均,让趋势更平稳
中值滤波擅长去掉孤立尖峰,而指数移动平均(EMA)更适合平滑连续的小幅波动。
C# 实现
public sealed class EmaFilter { private readonly double _alpha; private bool _initialized; private double _value; public EmaFilter(double alpha) { if (!double.IsFinite(alpha) || alpha <= 0 || alpha > 1) { throw new ArgumentOutOfRangeException( nameof(alpha), "alpha 必须大于 0 且不超过 1。"); } _alpha = alpha; } public double Add(double input) { if (!double.IsFinite(input)) throw new ArgumentOutOfRangeException(nameof(input)); if (!_initialized) { _value = input; _initialized = true; } else { _value = _alpha * input + (1 - _alpha) * _value; } return _value; } }使用方式:
var ema = new EmaFilter(alpha: 0.2); foreach (double raw in new[] { 50.0, 51.0, 50.5, 52.0, 51.2 }) { Console.WriteLine( $"原始值:{raw:F2},平滑值:{ema.Add(raw):F2}"); }EMA 为什么适合工控软件?
它不需要保存全部历史数据,每次只保留一个状态值。
- 每个样本计算复杂度:O(1)。
- 额外存储空间:O(1)。
对于大量测点的持续采集,这种低开销很有优势。
但需要特别注意:EMA 会引入滞后。 如果压力突然升高,滤波值不会立即跟上。对趋势显示而言,这可能是优点;对超压保护而言,这种延迟可能不可接受。
工程上可以采用双路数据:
- 原始有效值用于快速异常检测和安全相关判断。
- 滤波值用于趋势展示、噪声较大的非安全业务计算。
具体如何分流,必须由工艺要求和风险评估决定。
六、手段五:变化速率检查,识别不合理跳变
数值即使没有超过量程,也可能存在问题。
例如,温度从 45℃ 突然跳到 90℃,仍然处于 -20℃~150℃ 的有效量程内,但对于某些设备而言,100ms 内上升 45℃ 极不合理。
C# 实现
public sealed class RateOfChangeChecker { private readonly double _maxRate; private bool _initialized; private double _lastValue; private DateTimeOffset _lastTime; public RateOfChangeChecker(double maxRate) { if (!double.IsFinite(maxRate) || maxRate <= 0) throw new ArgumentOutOfRangeException(nameof(maxRate)); _maxRate = maxRate; } public bool Accept(double value, DateTimeOffset timestamp) { if (!double.IsFinite(value)) return false; if (!_initialized) { Save(value, timestamp); return true; } double seconds = (timestamp - _lastTime).TotalSeconds; if (seconds <= 0) return false; double rate = Math.Abs(value - _lastValue) / seconds; if (rate > _maxRate) return false; Save(value, timestamp); return true; } private void Save(double value, DateTimeOffset timestamp) { _lastValue = value; _lastTime = timestamp; _initialized = true; } }示例:
var checker = new RateOfChangeChecker(maxRate: 10); var t0 = DateTimeOffset.UtcNow; Console.WriteLine(checker.Accept(45, t0)); // true Console.WriteLine(checker.Accept(90, t0.AddMilliseconds(100))); // false这里的最大变化速率10仅为演示值,不应直接复制到实际设备。
一个非常重要的工程细节
上面的实现中,遇到可疑样本时,不会更新上一条已接受的样本。
这样可以避免单次尖峰污染参考值,但也存在一个风险:如果真实过程确实快速变化,后续连续样本可能一直被拒绝。
因此,生产项目不应把“变化率超限”简单等同于“数据一定错误”。建议区分以下状态:
正常数据 ↓ 变化率异常 ↓ 暂时标记可疑、记录日志 ↓ 连续异常或超过等待阈值 ↓ 升级为数据质量故障还可以结合设备工况、传感器类型和连续样本数量进行判断。
对于运动控制、压力突变或快速温升设备,变化率阈值尤其需要结合真实工艺验证。
七、手段六:报警滞回与持续时间确认,解决报警抖动
假设设备温度超过 80℃ 就报警,低于 80℃ 就解除报警。
当温度在阈值附近波动时:
79.9 → 80.1 → 79.8 → 80.2 → 79.9报警就可能反复触发和解除,产生大量日志,甚至让操作员无法判断故障是否真正恢复。
解决方法之一是滞回:设置不同的报警触发阈值和解除阈值。
例如:
- 温度达到或超过 80℃:触发报警。
- 温度下降到或低于 77℃:解除报警。
- 77℃~80℃:保持之前的报警状态。
C# 实现
public sealed class HysteresisAlarm { private readonly double _high; private readonly double _low; public bool IsAlarmed { get; private set; } public HysteresisAlarm(double low, double high) { if (!double.IsFinite(low) || !double.IsFinite(high) || low >= high) { throw new ArgumentException( "解除阈值必须小于触发阈值。"); } _low = low; _high = high; } public bool Update(double value) { if (!double.IsFinite(value)) throw new ArgumentOutOfRangeException(nameof(value)); if (!IsAlarmed && value >= _high) IsAlarmed = true; else if (IsAlarmed && value <= _low) IsAlarmed = false; return IsAlarmed; } }调用示例:
var alarm = new HysteresisAlarm(low: 77, high: 80); foreach (double temperature in new[] { 79.9, 80.1, 79.8, 78.5, 76.9 }) { bool active = alarm.Update(temperature); Console.WriteLine( $"温度:{temperature:F1},报警状态:{active}"); }滞回不等于延时确认
滞回主要解决阈值附近的反复切换。如果现场存在持续时间很短的干扰,还可以增加持续时间确认:
- 温度连续超过报警阈值 500ms,才产生普通业务报警。
- 温度恢复到解除阈值以下后,再执行恢复逻辑。
但是,不能为了减少误报而随意延迟安全相关报警。普通 HMI 告警、工艺报警与安全联锁的时间要求并不相同。
此外,不要只对滤波后的数据做安全判断。如果滤波窗口导致真实异常被延迟,可能错过需要及时处理的事件。
八、手段七:通信超时、有限重试与故障降级
前面六种手段主要针对数据本身,而现场的另一类常见问题是通信异常。
例如:
- PLC 暂时繁忙,单次读取超时。
- 交换机链路短暂中断。
- 设备断电,通信持续失败。
- 采集线程反复重连,导致日志暴涨、线程堆积。
- 网络恢复后,程序立即对所有请求进行重试,引起请求风暴。
此时,仅靠滤波没有用。需要为通信层建立明确的超时和恢复策略。
1. 每次通信都要有超时边界
不要让一次 PLC 读取无限等待。否则,一个卡住的请求可能长期占用采集任务,影响其他测点。
对于支持取消令牌的驱动,可以采用类似方式:
public static async Task<T> ReadWithTimeoutAsync<T>( Func<CancellationToken, Task<T>> readOperation, TimeSpan timeout, CancellationToken cancellationToken) { using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken); timeoutCts.CancelAfter(timeout); return await readOperation(timeoutCts.Token); }使用示例:
double value = await ReadWithTimeoutAsync( ct => plc.ReadTemperatureAsync(ct), TimeSpan.FromMilliseconds(500), cancellationToken);其中plc.ReadTemperatureAsync()代表实际 PLC 驱动提供的方法。
注意: 取消令牌只有在底层驱动支持取消时,才能真正中止通信操作。若驱动忽略取消请求,上层的超时控制并不保证底层 I/O 已停止。此时需要使用驱动自身的超时配置,并防止超时后无限创建新的并发请求。
2. 重试必须有上限
并非所有失败都适合重试。对于临时网络抖动,有限重试可能有效;对于地址错误、非法参数或权限错误,重复发送通常没有意义。
一种简单的指数退避策略是:
public static TimeSpan GetRetryDelay( int retryAttempt, int baseDelayMs = 200, int maxDelayMs = 5000) { if (retryAttempt < 0) throw new ArgumentOutOfRangeException(nameof(retryAttempt)); double delay = baseDelayMs * Math.Pow(2, Math.Min(retryAttempt, 20)); return TimeSpan.FromMilliseconds( Math.Min(delay, maxDelayMs)); }延迟示例:
| 重试次数 | 理论等待时间 |
|---|---|
| 第 1 次 | 200ms |
| 第 2 次 | 400ms |
| 第 3 次 | 800ms |
| 第 4 次 | 1600ms |
| 后续 | 逐渐增加,最高不超过 5000ms |
这里的表格是按公式计算的示例,并非现场实测结果。
实际实现还应增加随机抖动,避免多个设备同时恢复时集中重试。更重要的是,重试次数、单次超时和整体恢复时间必须有明确上限。
3. 区分通信失败与业务数据异常
建议将通信状态定义为一个简单的状态机:
恢复时不要只检查“连接成功”。至少还要确认:
- 读取请求能够连续成功。
- 数据时间戳已更新。
- 关键测点通过基本有效性校验。
- 缓存中没有被误当成最新数据的旧结果。
- 业务层已正确收到状态恢复通知。
如果某些 PLC 命令具有非幂等性,例如触发一次性动作,不能把它们当成普通读取请求进行无条件重试。重试可能造成重复动作,应结合命令编号、确认机制和设备端状态设计。
九、把这些手段组合起来:一份可运行的 .NET 8 示例
前面的代码分别解决不同问题。实际项目中,建议将它们组合成统一的数据处理器,避免采集线程、UI 线程和报警逻辑各自维护一套判断规则。
下面给出一个可以直接运行的控制台示例。它演示:
- 检查数值是否合法。
- 对输入值执行工程量程校验。
- 使用中值滤波去除孤立尖峰。
- 使用 EMA 平滑趋势。
- 使用滞回逻辑处理普通业务报警。
新建项目:
dotnet new console -n IndustrialDataFilter cd IndustrialDataFilter确认项目使用 .NET 8,然后将Program.cs替换为以下内容。
using System; using System.Collections.Generic; using System.Linq; public sealed record ProcessedSample( double RawValue, double MedianValue, double FilteredValue, bool IsValid, bool IsAlarmed, string Message); public sealed class MedianFilter { private readonly int _size; private readonly Queue<double> _window = new(); public MedianFilter(int size) { if (size < 1 || size % 2 == 0) throw new ArgumentException("窗口长度必须是正奇数。"); _size = size; } public double Add(double value) { _window.Enqueue(value); if (_window.Count > _size) _window.Dequeue(); var sorted = _window.OrderBy(x => x).ToArray(); return sorted[sorted.Length / 2]; } } public sealed class EmaFilter { private readonly double _alpha; private bool _initialized; private double _value; public EmaFilter(double alpha) { if (!double.IsFinite(alpha) || alpha <= 0 || alpha > 1) throw new ArgumentOutOfRangeException(nameof(alpha)); _alpha = alpha; } public double Add(double input) { if (!_initialized) { _value = input; _initialized = true; } else { _value = _alpha * input + (1 - _alpha) * _value; } return _value; } } public sealed class HysteresisAlarm { private readonly double _low; private readonly double _high; public bool IsAlarmed { get; private set; } public HysteresisAlarm(double low, double high) { if (!double.IsFinite(low) || !double.IsFinite(high) || low >= high) { throw new ArgumentException("low 必须小于 high。"); } _low = low; _high = high; } public bool Update(double value) { if (!double.IsFinite(value)) throw new ArgumentOutOfRangeException(nameof(value)); if (!IsAlarmed && value >= _high) IsAlarmed = true; else if (IsAlarmed && value <= _low) IsAlarmed = false; return IsAlarmed; } } public sealed class TemperatureProcessor { private readonly double _min; private readonly double _max; private readonly MedianFilter _median; private readonly EmaFilter _ema; private readonly HysteresisAlarm _alarm; public TemperatureProcessor( double min, double max, int medianWindow, double alpha, double alarmLow, double alarmHigh) { if (!double.IsFinite(min) || !double.IsFinite(max) || min >= max) { throw new ArgumentException("温度有效范围配置错误。"); } _min = min; _max = max; _median = new MedianFilter(medianWindow); _ema = new EmaFilter(alpha); _alarm = new HysteresisAlarm(alarmLow, alarmHigh); } public ProcessedSample Process(double raw) { if (!double.IsFinite(raw) || raw < _min || raw > _max) { return new ProcessedSample( raw, double.NaN, double.NaN, false, _alarm.IsAlarmed, "无效数据:数值非有限或超出量程"); } double median = _median.Add(raw); double filtered = _ema.Add(median); bool alarmed = _alarm.Update(filtered); return new ProcessedSample( raw, median, filtered, true, alarmed, "OK"); } } public static class Program { public static void Main() { // 以下参数仅用于演示,不应直接作为设备参数。 var processor = new TemperatureProcessor( min: -20, max: 150, medianWindow: 5, alpha: 0.3, alarmLow: 77, alarmHigh: 80); double[] samples = { 49.8, 50.1, 50.2, 73.8, 50.0, 50.3, 51.0, 80.5, 80.2, 76.5, 3276.7, 76.0 }; foreach (double raw in samples) { ProcessedSample result = processor.Process(raw); Console.WriteLine( $"原始={result.RawValue,8:F2} | " + $"中值={result.MedianValue,8:F2} | " + $"EMA={result.FilteredValue,8:F2} | " + $"有效={result.IsValid,-5} | " + $"报警={result.IsAlarmed,-5} | " + $"{result.Message}"); } } }运行:
dotnet run这份代码解决了什么,又没有解决什么?
它提供了一个清晰的处理链,但并不是完整的工业级数据质量系统。
它尚未实现:
- 通信超时与重试。
- 数据时间戳与过期判定。
- 变化速率检查。
- 报警持续时间确认。
- 多测点隔离、日志持久化和恢复状态机。
- 多线程并发保护。
此外,示例中无效样本不会进入滤波窗口,但滤波器会保留此前的状态。因此,滤波器输出不等于最新原始采样。真实系统必须同时传递数据有效性、采样时间和通信状态,不能只把FilteredValue交给所有业务模块。
如果某个测点发生了长时间掉线,重新上线后还需要根据业务规则决定是否重置滤波状态、丢弃旧缓存或等待若干个有效样本。不能假设旧的滤波状态永远适用于恢复后的数据。
十、工控项目中最容易踩的 5 个坑
1. 滤波窗口越大越好
不是。窗口越大,通常越平滑,但响应越慢。对于快速变化的测量值,过大的窗口可能掩盖真实变化。
2. 所有异常都直接丢弃
孤立尖峰可以暂时标记为可疑,但连续异常可能意味着真实故障。应根据连续次数、时间跨度和设备工况升级异常状态,而不是无限期丢弃数据。
3. 通信超时后继续显示最后一次读数,却不标记过期
这样会让操作员误认为设备仍然在线。界面应同时显示数值和数据状态,例如“45.2℃,数据已过期”。
4. 在 UI 线程中执行通信和滤波
网络读取、串口读取或大规模数据处理如果占用 UI 线程,可能导致界面卡顿。建议把采集、处理和显示解耦,并通过线程安全的数据交换机制更新界面。
对于持续采集场景,队列应设置容量上限。如果生产速度长期高于消费速度,无界队列会不断增长,最终消耗大量内存。实时监控类数据可以考虑只保留最新值;需要完整追溯的数据则应采用有界缓冲、明确的溢出策略和持久化机制。
5. 仅依靠上位机软件保障安全
上位机可能死机、断网、重启或发生资源耗尽。急停、危险运动保护以及安全联锁不能仅依赖普通 C# 程序。相关保护应在适当的安全 PLC、硬件回路或经过验证的安全系统中实现。
十一、如何选择方案:不要把 7 种手段全部叠加
| 现场问题 | 优先采用 | 需要额外关注 |
|---|---|---|
| 读数偶尔出现极端值 | 范围校验、中值滤波 | 连续异常识别 |
| 数值持续小幅波动 | EMA | 响应延迟 |
| 温度或压力突然跳变 | 变化速率检查 | 真实快速变化 |
| 报警在阈值附近反复切换 | 滞回、持续时间确认 | 安全报警时限 |
| 通信间歇性超时 | 超时、有限重试 | 底层驱动是否支持取消 |
| 设备断线但界面数值不变 | 时间戳、新鲜度检查 | 数据状态同步 |
| 通信恢复后数据不稳定 | 恢复状态机、重新确认数据质量 | 旧缓存与重复命令 |
一个实用的实施顺序是:
- 先做好数值校验、时间戳和通信状态。
- 再根据数据特征选择中值滤波或 EMA。
- 对变化速率和报警逻辑设置独立规则。
- 为通信失败配置超时、有限重试和降级策略。
- 最后通过现场日志、趋势数据和故障注入测试验证参数。
不要一开始就给所有测点套上同一组滤波参数。温度、压力、液位、编码器位置和高速运动信号具有不同的动态特性,应分别设计并验证。
结语
工业现场的数据异常,往往不是一个算法就能解决的。真正可靠的 C# 工控软件,需要把数据有效性、采样新鲜度、滤波算法、报警状态和通信容错看成一个完整体系。
这 7 种手段可以分工协作:
- 范围校验拦截明显非法值。
- 新鲜度检查识别过期数据。
- 中值滤波抑制孤立尖峰。
- EMA 平滑连续噪声。
- 变化速率检查识别可疑跳变。
- 滞回减少普通报警抖动。
- 超时、有限重试与降级机制应对通信故障。
最后记住一个工程原则:不要为了让曲线更平滑,就让异常更难被发现;也不要为了让程序看起来稳定,就把故障伪装成正常数据。
工业软件真正需要的不是“永远不报错”,而是出现问题时能够识别问题、限制影响、保留证据,并按照明确的规则恢复。