PLC轮询太慢的三个解决方法

描述

项目初期只接 5 台 PLC,采用串行轮询,一轮采集只要 800 毫秒,系统流畅稳定,一切很顺利现场毫无异议。

后来设备加到 20 台,代码一点没改,但一轮轮询直接涨到 3.2 秒。操作工点完确认键迟迟看不到反馈,以为没触发成功反复点击,频繁出现重复提交的故障。

等到设备扩容到 50 台,问题彻底爆发:大量 PLC 通讯超时,数据采集跟不上节拍,采集服务持续丢数据,产出的数据完全不可用。逐行排查后确认代码不存在 bug,核心症结不在代码,而是串行轮询的采集模型本身承载不了多设备规模。

串行轮询 = 用最慢的那台设备,拖垮整个系统。
设备越加越多,延迟线性增长,这是顺序轮询的天花板。

为什么顺序轮询一定会越来越慢

假设单台 PLC 平均响应 160ms,串行轮询总耗时 = 设备台数 ×160ms:

5 台 → 5 × 160ms = 800ms,采集周期短,系统响应流畅,完全满足现场使用;

20 台 → 20 × 160ms = 3.2s,操作交互延迟明显,现场操作工频繁反馈卡顿;

50 台 → 50 × 160ms = 8s,系统采集效率严重不足。

实际工况下情况会比理论计算更恶劣:现场网络存在抖动,部分老旧或远距离 PLC 响应延迟偏高,通讯超时后程序还会自动重试。一旦某一台 PLC 通讯卡顿 2 秒,整条采集队列都会停滞等待,拖慢全部设备的数据读取。

顺序串行轮询是早期单线程架构的设计思路,面对当下工业现场数十台 PLC 同步采集的并发需求,这套底层采集模型存在根本性瓶颈,天然无法承载大规模设备。

方法一:并发轮询——立竿见影

最直接的优化:把串行改成并发,所有 PLC 同时发请求,总时间取决于最慢那台,不再是所有台的总和。

原来的写法(串行):

 


// 20 台 PLC,一台一台等,总时间 = 所有台的总和
foreach (var plc in plcs)
{
    var data = await plc.ReadAsync();
    Process(data);
}

 

改成并发:

 


// 20 台同时发请求,总时间 = 最慢那台的时间
var tasks = plcs.Select(async plc =>
{
    var data = await plc.ReadAsync();
    Process(data);
});
await Task.WhenAll(tasks);

 

改造优化后成效显著:20 台 PLC 的整体采集延迟从原先 3.2 秒降至约 200ms。但是有两处关键风险落地时务必要规避:

坑一:不能无限并发

50 台 PLC 同时发请求,网络瞬间峰值会很高,容易打爆交换机。必须用信号量限制并发数。

// 最多同时请求 10 台,其余排队等var semaphore = new SemaphoreSlim(10);

坑二:老 PLC 扛不住高并发

部分老款 PLC 或者串口转 TCP 的设备,同时收到多个请求会直接卡死或重启。并发数建议从 5 开始测,根据现场实际情况调整,切勿直接设置 20 及以上并发值。

方法二:分级采集——最被低估的优化

不少项目会统一采用相同采集周期读取全部测点,这是错的,也是浪费。

不同数据对实时性要求差异极大:报警信号、操作按钮状态要求100ms快速响应;温度类趋势数据 1 秒采集一次即可满足需求;设备静态运行参数甚至 10 秒更新一次就足够。把它们放在同一个轮询循环里,高频数据在等低频数据,低频数据又在拖高频数据。

高优先级 报警信号、按钮、急停 100ms 中优先级 运行状态、计数器、工步 500ms 低优先级 温度、电流、设备参数 1s~10s


// 三组测点,三个独立循环,互不干扰
var highPriority = points.Where(p => p.Level == 1);
var midPriority  = points.Where(p => p.Level == 2);
var lowPriority  = points.Where(p => p.Level == 3);
_ = Task.Run(() => PollLoopAsync(highPriority, intervalMs: 100,  ct));
_ = Task.Run(() => PollLoopAsync(midPriority,  intervalMs: 500,  ct));
_ = Task.Run(() => PollLoopAsync(lowPriority,  intervalMs: 5000, ct));
实际项目里,分级采集能让轮询总压力下降 50%~80%。因为大多数测点都是低频的,高频轮询只针对少数关键信号。

 

方法三:变化检测——减少无效写入

你会发现一个现实:大部分采集到的数据,和上一次相比没有变化。温度还是 23.1,压力还是 47.3,设备还在运行。但你还是在每个周期都写入 MES,既浪费资源,也增加了数据库的压力。

只处理"有意义的变化":

 


// 变化超过阈值才写入 MES
if (Math.Abs(newValue - lastValue) > threshold)
{
    WriteToMes(newValue);
    lastValue = newValue;
}
// 或者直接接入上一篇的 DataQualityChecker
// Frozen 状态 = 值没有变化,不写入
var quality = checker.Check(newValue);
if (quality == DataQuality.Good)
    WriteToMes(newValue);
变化检测还有一个好处:上一篇的数据质量检测天然集成进来了——值冻结就是"没有有意义的变化",一套逻辑同时解决两个问题。

 

完整调度器:三个方法合在一起

 


public class PlcPollingScheduler
{
    private readonly SemaphoreSlim _semaphore;
    private readonly int _intervalMs;
    public PlcPollingScheduler(int maxConcurrency = 10, int intervalMs = 200)
    {
        _semaphore  = new SemaphoreSlim(maxConcurrency);
        _intervalMs = intervalMs;
    }
    /// 并发轮询一组 PLC,内置变化检测和数据质量过滤
    public async Task PollOnceAsync(
        IEnumerable points,
        CancellationToken ct)
    {
        var tasks = points.Select(async point =>
        {
            await _semaphore.WaitAsync(ct);
            try
            {
                double value;
                try
                {
                    value = await point.Device.ReadAsync(point.Address, ct);
                }
                catch (Exception ex)
                {
                    // 单台读取失败,记录日志,不影响其他设备
                    Console.WriteLine($"[读取失败] {point.Name}: {ex.Message}");
                    return;
                }
                // 数据质量检测(接上一篇 DataQualityChecker)
                var quality = point.Checker.Check(value);
                if (quality != DataQuality.Good) return;
                // 变化检测:变化超过阈值才写入
                if (Math.Abs(value - point.LastValue) < point.ChangeThreshold)
                    return;
                point.LastValue = value;
                await point.MesWriter.WriteAsync(point.Name, value);
            }
            finally
            {
                _semaphore.Release();
            }
        });
        await Task.WhenAll(tasks);
    }
    /// 持续轮询循环
    public async Task RunAsync(
        IEnumerable points,
        CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            var sw = Stopwatch.StartNew();
            await PollOnceAsync(points, ct);
            sw.Stop();
            // 扣除已花时间,保证轮询间隔稳定
            var remaining = _intervalMs - (int)sw.ElapsedMilliseconds;
            if (remaining > 0)
                await Task.Delay(remaining, ct)
                          .ContinueWith(_ => { });
        }
    }
}
// 使用:三组分级,各自独立跑
var highScheduler = new PlcPollingScheduler(maxConcurrency: 10, intervalMs: 100);
var midScheduler  = new PlcPollingScheduler(maxConcurrency: 10, intervalMs: 500);
var lowScheduler  = new PlcPollingScheduler(maxConcurrency: 5,  intervalMs: 5000);
_ = highScheduler.RunAsync(highPriorityPoints, ct);
_ = midScheduler.RunAsync(midPriorityPoints,  ct);
_ = lowScheduler.RunAsync(lowPriorityPoints,  ct);

 

三个方法组合的实际效果

20 台 PLC,优化前延迟稳定在 3 秒以上。加了并发轮询、分级采集、变化过滤三层之后:

并发轮询:3.2s → ~200ms

分级采集:轮询总压力下降 ~70%

变化过滤:MES 写入量下降 ~60%

最终延迟:稳定在 150ms ~ 300ms 采集慢,不是因为你写得慢。

而是你还在用串行思维解决并发问题。
模型换了,延迟自然就下去了。

— 完 —

来源 :工业软件架构笔记

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分