简易虚拟 PLC 服务器-流水线自动化,上位机程序维护升级,西门子PLC仿真

找能模拟 PLC 行为的虚拟软件,重点是能复现多线程 / 多客户端高频读写时的并发冲突、脏数据等 BUG,而且普通的快速读写模拟程序因为速度太快看不到问题,需要能精准控制读写时序的虚拟 PLC 工具。

推荐的虚拟 PLC 软件 & 定制化模拟方案

以下方案从 "即用型工具" 到 "定制化开发" 分层,既能快速复现问题,也能精准模拟脏数据 / 并发冲突场景:


方案 1:即用型虚拟 PLC(快速上手,无需大量开发)
1.1 Modbus Slave(最推荐,精准模拟读写冲突)
  • 核心优势:这是专业的 Modbus 从站模拟器(对应 PLC 角色),支持 TCP/UDP,能手动设置寄存器读写延迟、模拟 "半写状态",完美复现脏数据和并发冲突。
  • 操作步骤(复现脏数据)
    1. 下载安装:Modbus Slave 官网(有试用版,足够测试);
    2. 配置:新建 Modbus TCP 从站(端口 502),添加保持寄存器(对应 PLC 的 D 区),比如地址 100-105;
    3. 关键设置:
      • 开启「Write Delay」(写延迟):设置写操作需要 500ms 完成(模拟 PLC 写寄存器的耗时);
      • 开启「Partial Write」(部分写):允许寄存器只写入一半数据(模拟脏数据);
    4. 测试:用你之前的 C# 多线程程序同时读写地址 100,此时能清晰看到:线程 A 写了一半数据时,线程 B 读取到 "脏数据"(比如高 8 位已更新、低 8 位还是旧值)。
1.2 S7-PLCSIM(西门子虚拟 PLC,通用兼容性好)
  • 适用场景:如果你的项目后续可能对接西门子 PLC,用这个更贴近工业场景;
  • 核心优势:支持多客户端连接,能模拟 PLC 的 CPU 负载、通讯缓冲区溢出,甚至可以设置 "读写互斥延迟";
  • 关键操作:在仿真界面中,给指定寄存器(比如 DB1.DBW100)设置「写保护延迟」,当一个客户端写操作未完成时,另一个客户端读会获取到中间值(脏数据)。

方案 2:定制化虚拟 PLC(精准控制时序,复现任意 BUG)

如果现成工具满足不了你的 "时序精准控制" 需求(比如要模拟 "每 10 次写操作出现 1 次脏数据"),可以用 C#/VB.NET自己开发一个简易虚拟 PLC 服务器,核心是:

  • 模拟 PLC 的寄存器内存;
  • 给读写操作加「可控延迟」和「随机中断」;
  • 记录所有读写请求的时序,方便定位 BUG。
完整代码示例(C# 版,VB.NET/VBA可参考逻辑)
cs 复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Collections.Generic;
using System.Threading.Tasks;

/// <summary>
/// 定制化虚拟PLC服务器(模拟Modbus TCP,支持可控延迟、脏数据、并发冲突)
/// </summary>
public class VirtualPLCServer
{
    // 模拟PLC的寄存器内存(D区,地址0-999)
    private readonly ushort[] _registers = new ushort[1000];
    // 读写锁(模拟PLC的CPU调度,可手动控制锁的释放时机)
    private readonly object _lockObj = new object();
    // 配置项:写操作延迟(毫秒),可动态调整
    public int WriteDelayMs { get; set; } = 300;
    // 配置项:是否模拟脏数据(写一半中断)
    public bool SimulateDirtyData { get; set; } = true;
    // 记录所有读写日志,方便分析时序
    private readonly List<string> _log = new List<string>();

    private TcpListener _listener;
    private bool _isRunning = false;

    // 启动虚拟PLC服务器(端口502,Modbus默认端口)
    public void Start(int port = 502)
    {
        _listener = new TcpListener(IPAddress.Any, port);
        _listener.Start();
        _isRunning = true;
        Console.WriteLine($"虚拟PLC已启动,端口:{port}");

        // 异步接收客户端连接
        Task.Run(async () =>
        {
            while (_isRunning)
            {
                var client = await _listener.AcceptTcpClientAsync();
                // 每个客户端分配独立线程处理
                Task.Run(() => HandleClient(client));
            }
        });
    }

    // 处理单个客户端的读写请求
    private void HandleClient(TcpClient client)
    {
        var clientIp = ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString();
        Console.WriteLine($"客户端连接:{clientIp}");

        using (var stream = client.GetStream())
        {
            byte[] buffer = new byte[1024];
            while (_isRunning && client.Connected)
            {
                try
                {
                    int bytesRead = stream.Read(buffer, 0, buffer.Length);
                    if (bytesRead == 0) break;

                    // 解析Modbus请求(简化版:只处理读/写保持寄存器)
                    var request = ParseModbusRequest(buffer, bytesRead);
                    if (request == null) continue;

                    // 处理读/写请求(核心:模拟PLC的延迟和脏数据)
                    byte[] response = ProcessRequest(request, clientIp);
                    if (response != null)
                    {
                        stream.Write(response, 0, response.Length);
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"客户端{clientIp}处理失败:{ex.Message}");
                    break;
                }
            }
        }

        client.Close();
        Console.WriteLine($"客户端断开:{clientIp}");
    }

    // 解析Modbus请求(简化版,只处理03读保持寄存器、16写多个寄存器)
    private ModbusRequest ParseModbusRequest(byte[] buffer, int length)
    {
        // Modbus TCP帧结构:MBAP(7字节) + PDU
        if (length < 8) return null;

        var request = new ModbusRequest
        {
            TransactionId = BitConverter.ToUInt16(buffer, 0),
            ProtocolId = BitConverter.ToUInt16(buffer, 2),
            Length = BitConverter.ToUInt16(buffer, 4),
            UnitId = buffer[6],
            FunctionCode = buffer[7]
        };

        // 解析读请求(03:读保持寄存器)
        if (request.FunctionCode == 0x03)
        {
            request.StartAddress = BitConverter.ToUInt16(new[] { buffer[9], buffer[8] }, 0); // 高低位互换
            request.Quantity = BitConverter.ToUInt16(new[] { buffer[11], buffer[10] }, 0);
        }
        // 解析写请求(16:写多个寄存器)
        else if (request.FunctionCode == 0x10)
        {
            request.StartAddress = BitConverter.ToUInt16(new[] { buffer[9], buffer[8] }, 0);
            request.Quantity = BitConverter.ToUInt16(new[] { buffer[11], buffer[10] }, 0);
            int byteCount = buffer[12];
            request.WriteData = new ushort[request.Quantity];
            for (int i = 0; i < request.Quantity; i++)
            {
                request.WriteData[i] = BitConverter.ToUInt16(new[] { buffer[13 + i * 2 + 1], buffer[13 + i * 2] }, 0);
            }
        }

        return request;
    }

    // 处理Modbus请求(核心:模拟延迟、脏数据)
    private byte[] ProcessRequest(ModbusRequest request, string clientIp)
    {
        lock (_lockObj) // 模拟PLC的单线程处理,但可手动释放锁制造冲突
        {
            string logMsg = $"{DateTime.Now:HH:mm:ss.fff} | {clientIp} | ";
            byte[] response = null;

            if (request.FunctionCode == 0x03)
            {
                // 处理读请求
                logMsg += $"读寄存器 | 起始地址:{request.StartAddress} | 数量:{request.Quantity}";
                ushort[] data = new ushort[request.Quantity];
                for (int i = 0; i < request.Quantity; i++)
                {
                    int addr = request.StartAddress + i;
                    if (addr < _registers.Length)
                    {
                        data[i] = _registers[addr];
                    }
                }

                // 构建读响应
                response = BuildReadResponse(request, data);
            }
            else if (request.FunctionCode == 0x10)
            {
                // 处理写请求(模拟延迟 + 脏数据)
                logMsg += $"写寄存器 | 起始地址:{request.StartAddress} | 数量:{request.Quantity} | 数据:{string.Join(",", request.WriteData)}";
                
                // 模拟写操作延迟(关键:制造并发冲突的窗口)
                Thread.Sleep(WriteDelayMs);

                // 模拟脏数据:只写一半数据(随机触发,更贴近真实场景)
                int writeCount = SimulateDirtyData 
                    ? new Random().Next(1, request.Quantity) // 随机写1~n个,剩下的未更新
                    : request.Quantity;

                for (int i = 0; i < writeCount; i++)
                {
                    int addr = request.StartAddress + i;
                    if (addr < _registers.Length)
                    {
                        _registers[addr] = request.WriteData[i];
                    }
                }

                if (writeCount < request.Quantity)
                {
                    logMsg += $" | 模拟脏数据:仅写入{writeCount}个寄存器";
                }

                // 构建写响应
                response = BuildWriteResponse(request);
            }

            _log.Add(logMsg);
            Console.WriteLine(logMsg);
            return response;
        }
    }

    // 构建读响应
    private byte[] BuildReadResponse(ModbusRequest request, ushort[] data)
    {
        int responseLength = 5 + data.Length * 2; // MBAP(7) + PDU(1 + 1 + n*2)
        byte[] response = new byte[7 + 1 + 1 + data.Length * 2];

        // MBAP头
        BitConverter.GetBytes(request.TransactionId).CopyTo(response, 0);
        BitConverter.GetBytes(request.ProtocolId).CopyTo(response, 2);
        BitConverter.GetBytes((ushort)(1 + 1 + data.Length * 2)).CopyTo(response, 4);
        response[6] = request.UnitId;

        // PDU
        response[7] = request.FunctionCode;
        response[8] = (byte)(data.Length * 2); // 字节数
        for (int i = 0; i < data.Length; i++)
        {
            byte[] bytes = BitConverter.GetBytes(data[i]);
            response[9 + i * 2] = bytes[1]; // 高字节
            response[10 + i * 2] = bytes[0]; // 低字节
        }

        return response;
    }

    // 构建写响应
    private byte[] BuildWriteResponse(ModbusRequest request)
    {
        byte[] response = new byte[12];

        // MBAP头
        BitConverter.GetBytes(request.TransactionId).CopyTo(response, 0);
        BitConverter.GetBytes(request.ProtocolId).CopyTo(response, 2);
        BitConverter.GetBytes((ushort)5).CopyTo(response, 4);
        response[6] = request.UnitId;

        // PDU
        response[7] = request.FunctionCode;
        BitConverter.GetBytes(request.StartAddress).CopyTo(response, 8);
        Array.Reverse(response, 8, 2); // 高低位互换
        BitConverter.GetBytes(request.Quantity).CopyTo(response, 10);
        Array.Reverse(response, 10, 2);

        return response;
    }

    // 停止服务器
    public void Stop()
    {
        _isRunning = false;
        _listener?.Stop();
        Console.WriteLine("虚拟PLC已停止");
    }

    // Modbus请求模型
    private class ModbusRequest
    {
        public ushort TransactionId { get; set; }
        public ushort ProtocolId { get; set; }
        public ushort Length { get; set; }
        public byte UnitId { get; set; }
        public byte FunctionCode { get; set; }
        public ushort StartAddress { get; set; }
        public ushort Quantity { get; set; }
        public ushort[] WriteData { get; set; }
    }
}

// 测试调用
class Program
{
    static void Main(string[] args)
    {
        // 创建虚拟PLC服务器
        var virtualPlc = new VirtualPLCServer();
        // 配置:写延迟300ms,模拟脏数据
        virtualPlc.WriteDelayMs = 300;
        virtualPlc.SimulateDirtyData = true;
        // 启动服务器(端口502)
        virtualPlc.Start(502);

        Console.WriteLine("按任意键停止虚拟PLC...");
        Console.ReadKey();
        virtualPlc.Stop();
    }
}
代码核心功能解释
  1. 模拟 PLC 内存 :用_registers数组模拟 PLC 的 D 区寄存器;
  2. 可控写延迟 :通过WriteDelayMs设置写操作耗时(比如 300ms),制造 "写未完成时读" 的窗口;
  3. 脏数据模拟SimulateDirtyData开启后,写操作会随机只写入部分寄存器,复现 "半写" 的脏数据;
  4. 详细日志:记录每个客户端的读写时序,方便分析 "哪个线程在什么时候读 / 写了哪个地址";
  5. 标准 Modbus TCP :兼容所有支持 Modbus 的客户端(C#/VBA/VB.NET都能连接)。

方案 3:VBA/VB.NET轻量模拟(无需服务器,直接模拟冲突)

如果只想用 VBA/VB.NET快速模拟,不用完整 PLC 协议,可以写一个「共享内存 + 延迟锁」的模拟类:

vbnet 复制代码
' VB.NET版 轻量PLC内存模拟类(复现脏数据)
Public Class SimpleVirtualPLC
    ' 模拟寄存器内存
    Private Registers As New Dictionary(Of Integer, Integer)
    ' 读写锁
    Private ReadWriteLock As New Object
    ' 写操作延迟(毫秒)
    Public WriteDelayMs As Integer = 200
    ' 是否模拟脏数据
    Public SimulateDirtyData As Boolean = True

    ' 读寄存器
    Public Function ReadRegister(ByVal addr As Integer) As Integer
        SyncLock ReadWriteLock
            ' 模拟读操作:可能读到未写完的数据
            If Registers.ContainsKey(addr) Then
                Return Registers(addr)
            Else
                Return 0
            End If
        End SyncLock
    End Function

    ' 写寄存器(模拟延迟+脏数据)
    Public Sub WriteRegister(ByVal addr As Integer, ByVal value As Integer)
        ' 不锁定整个写过程,制造并发窗口
        If SimulateDirtyData Then
            ' 模拟:先写低8位,延迟后写高8位(脏数据)
            Dim dirtyValue As Integer = (value And &HFF) Or (Registers(addr) And &HFF00)
            Registers(addr) = dirtyValue ' 写入一半数据
            Threading.Thread.Sleep(WriteDelayMs) ' 延迟
            Registers(addr) = value ' 补全数据
        Else
            Threading.Thread.Sleep(WriteDelayMs)
            SyncLock ReadWriteLock
                Registers(addr) = value
            End SyncLock
        End If
    End Sub
End Class

' 测试调用
Sub Test()
    Dim plc As New SimpleVirtualPLC
    plc.WriteDelayMs = 200
    plc.SimulateDirtyData = True

    ' 线程1:写数据
    Dim t1 As New Threading.Thread(Sub()
                                      For i As Integer = 1 To 100
                                          plc.WriteRegister(100, i * 100)
                                          Threading.Thread.Sleep(50)
                                      Next
                                  End Sub)

    ' 线程2:读数据(会读到脏数据)
    Dim t2 As New Threading.Thread(Sub()
                                      For i As Integer = 1 To 200
                                          Dim val As Integer = plc.ReadRegister(100)
                                          Console.WriteLine($"读取D100:{val}(可能是脏数据)")
                                          Threading.Thread.Sleep(100)
                                      Next
                                  End Sub)

    t1.Start()
    t2.Start()
    t1.Join()
    t2.Join()
End Sub

关键使用技巧(复现 BUG 的核心)

  1. 控制延迟:写操作延迟设置为 200-500ms,读操作频率设为 100ms / 次,刚好能覆盖 "写一半" 的窗口;
  2. 随机脏数据:不要每次都触发脏数据,设置 10%~20% 的触发概率,更贴近真实 PLC 的偶发 BUG;
  3. 日志分析:记录每个读写操作的时间戳、地址、值,BUG 出现后能回溯 "哪个线程在什么时候操作了地址"。

总结

  1. 快速复现:优先用「Modbus Slave」,开箱即用,能精准模拟 PLC 的读写延迟和脏数据;
  2. 精准定制:用 C# 开发「虚拟 PLC 服务器」,可自定义延迟、脏数据触发规则,适配任意测试场景;
  3. 轻量测试 :VBA/VB.NET写「共享内存模拟类」,无需网络,直接复现多线程读写冲突。

用这些工具,你能清晰看到 "多线程快速读写时,读线程获取到未写完的脏数据",完美复现真实 PLC 可能出现的并发 BUG,方便调试你的 C# 多线程访问逻辑。

相关推荐
安科士andxe3 小时前
深入解析|安科士1.25G CWDM SFP光模块核心技术,破解中长距离传输痛点
服务器·网络·5g
2601_949146536 小时前
Shell语音通知接口使用指南:运维自动化中的语音告警集成方案
运维·自动化
0思必得06 小时前
[Web自动化] Selenium无头模式
前端·爬虫·selenium·自动化·web自动化
儒雅的晴天6 小时前
大模型幻觉问题
运维·服务器
Gofarlic_OMS7 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
通信大师7 小时前
深度解析PCC策略计费控制:核心网产品与应用价值
运维·服务器·网络·5g
dixiuapp7 小时前
智能工单系统如何选,实现自动化与预测性维护
运维·自动化
默|笙9 小时前
【Linux】fd_重定向本质
linux·运维·服务器
叫我龙翔10 小时前
【计网】从零开始掌握序列化 --- JSON实现协议 + 设计 传输\会话\应用 三层结构
服务器·网络·c++·json
“αβ”10 小时前
网络层协议 -- ICMP协议
linux·服务器·网络·网络协议·icmp·traceroute·ping