为实现上位机与贴片机或晶圆老化设备的联调测试,下面提供一个基于C#的模拟器,模拟贴片机和晶圆老化设备的Modbus TCP和自定义串口协议行为

为实现上位机与贴片机或晶圆老化设备的联调测试,下面提供一个基于C#的模拟器,模拟贴片机和晶圆老化设备的Modbus TCP和自定义串口协议行为。模拟器将与之前实现的上位机驱动进行通信,支持寄存器读写、协议解析和状态反馈。同时,模拟器将记录通信日志,便于调试,并支持性能测试。以下是详细实现和说明。


设计目标

  1. 模拟器功能:

    • 模拟贴片机(SMT Placement Machine)的运动控制(X/Y/Z坐标、吸嘴)和状态反馈。

    • 模拟晶圆老化设备(Wafer Aging Equipment)的温度/电压控制和数据采集。

    • 支持Modbus TCP和自定义串口协议,响应上位机的寄存器读写或帧命令。

  2. 通信兼容:与之前提供的 SmtPlacementDriver 和寄存器映射无缝对接。

  3. 日志记录:使用Serilog记录模拟器的通信和状态变化。

  4. 测试支持:提供简单的测试用例,确保模拟器与上位机正确联调。

  5. 可扩展性:支持动态配置寄存器和协议,适配不同设备。


寄存器映射(参考前文)

使用之前定义的寄存器映射:

贴片机寄存器映射

寄存器类型

地址

功能

数据类型

描述

Holding Register

0

X轴坐标

UInt16

X轴位置(单位:0.01mm)

Holding Register

1

Y轴坐标

UInt16

Y轴位置(单位:0.01mm)

Holding Register

2

Z轴高度

UInt16

吸嘴Z轴高度(单位:0.01mm)

Holding Register

3

贴装速度

UInt16

贴装速度(mm/s)

Coil

0

吸嘴开关

Bool

吸嘴拾取/释放(true=拾取,false=释放)

Input Register

0

状态

UInt16

设备状态(0=空闲,1=运行,2=故障)

晶圆老化设备寄存器映射

寄存器类型

地址

功能

数据类型

描述

Holding Register

0

目标温度

UInt16

老化温度(单位:0.1°C)

Holding Register

1

电压

UInt16

施加电压(单位:0.01V)

Holding Register

2

升温速率

UInt16

升温速率(单位:0.1°C/min)

Input Register

0

当前温度

UInt16

当前温度(单位:0.1°C)

Input Register

1

当前电压

UInt16

当前电压(单位:0.01V)

Coil

0

加热开关

Bool

加热器开关(true=开启,false=关闭)


模拟器实现

模拟器将实现Modbus TCP服务器和串口服务器,响应上位机的请求。以下以贴片机为例,晶圆老化设备类似。

1. Modbus TCP 模拟器

使用 NModbus 库实现Modbus TCP服务器,模拟寄存器读写。

csharp

using System;
using System.Net;
using System.Threading.Tasks;
using Modbus.Device;
using Serilog;
using System.Threading;

public class SmtModbusSimulator
{
    private readonly ModbusSlave _modbusSlave;
    private readonly ushort[] _holdingRegisters = new ushort[10]; // 模拟Holding Registers
    private readonly bool[] _coils = new bool[10]; // 模拟Coils
    private readonly ushort[] _inputRegisters = new ushort[10]; // 模拟Input Registers
    private readonly CancellationTokenSource _cts = new();

    public SmtModbusSimulator(string ipAddress = "127.0.0.1", int port = 502)
    {
        Log.Logger = new LoggerConfiguration()
            .MinimumLevel.Debug()
            .WriteTo.Console()
            .WriteTo.File("logs/simulator-.log", rollingInterval: RollingInterval.Day)
            .CreateLogger();

        // 初始化默认值
        _holdingRegisters[0] = 0; // X轴坐标
        _holdingRegisters[1] = 0; // Y轴坐标
        _holdingRegisters[2] = 0; // Z轴高度
        _holdingRegisters[3] = 100; // 贴装速度
        _coils[0] = false; // 吸嘴关闭
        _inputRegisters[0] = 0; // 状态:空闲

        // 创建Modbus TCP服务器
        var tcpListener = new TcpListener(IPAddress.Parse(ipAddress), port);
        _modbusSlave = ModbusTcpSlave.CreateTcp(1, tcpListener);
        _modbusSlave.DataStore = DataStoreFactory.CreateDefaultDataStore();
        UpdateDataStore(); // 初始化数据存储
        _modbusSlave.DataStore.DataStoreWrittenTo += OnDataStoreWritten;
    }

    private void UpdateDataStore()
    {
        for (ushort i = 0; i < _holdingRegisters.Length; i++)
        {
            _modbusSlave.DataStore.HoldingRegisters[i + 1] = _holdingRegisters[i];
        }
        for (ushort i = 0; i < _coils.Length; i++)
        {
            _modbusSlave.DataStore.Coils[i + 1] = _coils[i];
        }
        for (ushort i = 0; i < _inputRegisters.Length; i++)
        {
            _modbusSlave.DataStore.InputRegisters[i + 1] = _inputRegisters[i];
        }
    }

    private void OnDataStoreWritten(object sender, DataStoreEventArgs e)
    {
        if (e.ModbusDataType == ModbusDataType.HoldingRegister)
        {
            for (int i = 0; i < e.Data.B.Count; i++)
            {
                _holdingRegisters[e.StartAddress - 1 + i] = e.Data.B[i];
                Log.Information("Holding Register {Address} written: {Value}", e.StartAddress + i, e.Data.B[i]);
            }
            // 模拟状态更新
            _inputRegisters[0] = 1; // 运行状态
            UpdateDataStore();
        }
        else if (e.ModbusDataType == ModbusDataType.Coil)
        {
            for (int i = 0; i < e.Data.A.Count; i++)
            {
                _coils[e.StartAddress - 1 + i] = e.Data.A[i];
                Log.Information("Coil {Address} written: {Value}", e.StartAddress + i, e.Data.A[i]);
            }
            _inputRegisters[0] = _coils[0] ? (ushort)1 : (ushort)0; // 吸嘴开启->运行状态
            UpdateDataStore();
        }
    }

    public void Start()
    {
        Log.Information("Starting Modbus TCP simulator on {IpAddress}:{Port}", "127.0.0.1", 502);
        _modbusSlave.Listen();
    }

    public void Stop()
    {
        _cts.Cancel();
        _modbusSlave.Dispose();
        Log.Information("Modbus TCP simulator stopped.");
    }
}

说明:

  • Modbus服务器:监听 127.0.0.1:502,响应上位机的寄存器读写请求。

  • 数据存储:使用 _holdingRegisters、_coils 和 _inputRegisters 模拟寄存器状态。

  • 状态反馈:写入寄存器或Coil时更新状态(如吸嘴开启触发运行状态)。

  • 日志:记录每次寄存器写入操作。

2. 自定义串口协议模拟器

模拟串口通信,响应 [STX][Command][Data][Checksum][ETX] 格式的帧。

csharp

using System;
using System.IO.Ports;
using System.Threading.Tasks;
using Serilog;
using System.Threading;
using System.Linq;

public class SmtSerialSimulator
{
    private readonly SerialPort _serialPort;
    private readonly CancellationTokenSource _cts = new();
    private readonly ushort[] _registers = new ushort[10]; // 模拟寄存器
    private bool _nozzleState = false; // 模拟吸嘴状态

    public SmtSerialSimulator(string portName = "COM2")
    {
        Log.Logger = new LoggerConfiguration()
            .MinimumLevel.Debug()
            .WriteTo.Console()
            .WriteTo.File("logs/simulator-.log", rollingInterval: RollingInterval.Day)
            .CreateLogger();

        _serialPort = new SerialPort(portName, 9600, Parity.None, 8, StopBits.One);
        _serialPort.DataReceived += OnDataReceived;
        _registers[0] = 0; // X轴
        _registers[1] = 0; // Y轴
        _registers[2] = 0; // Z轴
        _registers[3] = 100; // 速度
    }

    private void OnDataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        try
        {
            var buffer = new byte[1024];
            int bytesRead = _serialPort.Read(buffer, 0, buffer.Length);
            var frame = buffer.Take(bytesRead).ToArray();

            // 解析协议帧
            if (frame.Length < 4 || frame[0] != 0x02 || frame[^1] != 0x03)
            {
                Log.Error("Invalid frame format: {Frame}", BitConverter.ToString(frame));
                return;
            }

            byte checksum = frame.Take(frame.Length - 1).Aggregate((a, b) => (byte)(a ^ b));
            if (checksum != frame[^2])
            {
                Log.Error("Checksum mismatch: {Frame}", BitConverter.ToString(frame));
                return;
            }

            byte command = frame[1];
            var data = frame.Skip(2).Take(frame.Length - 4).ToArray();
            Log.Information("Received frame: Command={Command}, Data={Data}", command, BitConverter.ToString(data));

            // 处理命令
            switch (command)
            {
                case 0x01: // 设置坐标或速度
                    if (data.Length >= 4)
                    {
                        ushort address = BitConverter.ToUInt16(data, 0);
                        ushort value = BitConverter.ToUInt16(data, 2);
                        _registers[address] = value;
                        Log.Information("Set register {Address} to {Value}", address, value);
                    }
                    break;
                case 0x02: // 设置吸嘴
                    _nozzleState = data[0] != 0;
                    Log.Information("Set nozzle state: {State}", _nozzleState);
                    break;
                case 0x03: // 读取状态
                    var response = new List { 0x02, 0x03, (byte)(_nozzleState ? 1 : 0) };
                    response.AddRange(BitConverter.GetBytes(_registers[0])); // X轴
                    response.AddRange(BitConverter.GetBytes(_registers[1])); // Y轴
                    byte respChecksum = response.Aggregate((a, b) => (byte)(a ^ b));
                    response.Add(respChecksum);
                    response.Add(0x03);
                    _serialPort.Write(response.ToArray(), 0, response.Count);
                    Log.Information("Sent response: {Response}", BitConverter.ToString(response.ToArray()));
                    break;
            }
        }
        catch (Exception ex)
        {
            Log.Error(ex, "Error processing serial data");
        }
    }

    public void Start()
    {
        Log.Information("Starting Serial simulator on {Port}", _serialPort.PortName);
        _serialPort.Open();
    }

    public void Stop()
    {
        _cts.Cancel();
        _serialPort.Close();
        Log.Information("Serial simulator stopped.");
    }
}

说明:

  • 串口服务器:监听指定COM端口,响应上位机的帧请求。

  • 协议解析:验证STX、ETX和Checksum,处理设置坐标、吸嘴和读取状态的命令。

  • 状态模拟:维护寄存器和吸嘴状态,返回动态响应。

  • 日志:记录接收和发送的帧。


3. 上位机与模拟器联调测试

使用之前实现的 SmtPlacementDriver 和 HardwareController,与模拟器进行联调。

测试代码

csharp

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Serilog;

class Program
{
    static async Task Main(string[] args)
    {
        // 初始化日志
        Log.Logger = new LoggerConfiguration()
            .MinimumLevel.Debug()
            .WriteTo.Console()
            .WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day)
            .CreateLogger();

        // 启动模拟器
        var simulator = new SmtModbusSimulator();
        simulator.Start();
        // var simulator = new SmtSerialSimulator("COM2"); // 切换到串口模拟器
        // simulator.Start();

        try
        {
            // 加载配置
            var config = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .Build()
                .GetSection("HardwareSettings")
                .Get();

            // 创建驱动
            var driver = await DriverFactory.CreateDriverAsync(config.DriverType, config.ProtocolType, config.AssemblyName);
            if (driver == null)
            {
                Log.Fatal("Failed to initialize driver.");
                return;
            }

            // 初始化控制器并执行
            using var controller = new HardwareController(driver);
            await controller.ExecuteAsync(new Dictionary
            {
                { "XPosition", 1000 }, // 设置X轴坐标为10mm
                { "Speed", 200 } // 设置速度为200mm/s
            });

            // 额外测试:读取状态
            var data = await driver.ReceiveDataAsync();
            Log.Information("Received data: {Data}", BitConverter.ToString(data));
        }
        catch (Exception ex)
        {
            Log.Fatal(ex, "Application failed.");
        }
        finally
        {
            simulator.Stop();
            Log.CloseAndFlush();
        }
    }
}

public class HardwareConfig
{
    public string DriverType { get; set; } = "SmtPlacementDriver";
    public string ProtocolType { get; set; } = "ModbusTCP";
    public string AssemblyName { get; set; }
    public string ConnectionString { get; set; } = "127.0.0.1:502";
}

配置文件(appsettings.json):

json

{
  "HardwareSettings": {
    "DriverType": "SmtPlacementDriver",
    "ProtocolType": "ModbusTCP",
    "AssemblyName": null,
    "ConnectionString": "127.0.0.1:502"
  }
}

串口测试配置:

json

{
  "HardwareSettings": {
    "DriverType": "SmtPlacementDriver",
    "ProtocolType": "Serial",
    "AssemblyName": null,
    "ConnectionString": "COM2"
  }
}

测试步骤:

  1. 启动模拟器(Modbus TCP或串口)。

  2. 运行上位机程序,执行 ExecuteAsync,设置X轴坐标和速度。

  3. 模拟器响应请求,更新寄存器并记录日志。

  4. 上位机读取状态,验证返回数据。

预期输出:

[INFO] Starting Modbus TCP simulator on 127.0.0.1:502
[INFO] Connected to SMT via ModbusTCP at 127.0.0.1:502
[INFO] Set SMT parameter XPosition to 1000 at address 0
[INFO] Set SMT parameter Speed to 200 at address 3
[INFO] SMT wrote register 0 with value 1000
[INFO] SMT wrote register 3 with value 200
[INFO] SMT received registers: E8-03-C8-00

4. 性能测试用例

使用 BenchmarkDotNet 评估上位机与模拟器的通信性能。

测试代码

csharp

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Threading.Tasks;

[MemoryDiagnoser]
public class SmtCommunicationBenchmarks
{
    private readonly SmtPlacementDriver _driver;
    private readonly SmtModbusSimulator _simulator;

    public SmtCommunicationBenchmarks()
    {
        _driver = new SmtPlacementDriver();
        _simulator = new SmtModbusSimulator();
        _simulator.Start();
    }

    [Benchmark]
    public async Task WriteRegisterAsync()
    {
        await _driver.ConnectAsync("127.0.0.1:502", "ModbusTCP");
        await _driver.SendDataAsync(BitConverter.GetBytes((ushort)0).Concat(BitConverter.GetBytes((ushort)1000)).ToArray());
        await _driver.DisconnectAsync();
    }

    [Benchmark]
    public async Task ReadRegistersAsync()
    {
        await _driver.ConnectAsync("127.0.0.1:502", "ModbusTCP");
        await _driver.ReceiveDataAsync();
        await _driver.DisconnectAsync();
    }

    [GlobalCleanup]
    public void Cleanup()
    {
        _simulator.Stop();
    }
}

class Program
{
    static void Main(string[] args)
    {
        var summary = BenchmarkRunner.Run();
    }
}

性能结果(示例):

方法

平均时间

内存分配

WriteRegisterAsync

25ms

1.0KB

ReadRegistersAsync

30ms

1.2KB

分析:

  • 延迟:Modbus TCP通信延迟约为25-30ms,受网络栈和NModbus库影响。

  • 内存:每次操作分配约1KB,主要用于字节数组和日志。

  • 优化建议:

    • 复用TcpClient连接,减少初始化开销。

    • 批量读写寄存器,降低通信频率。


5. 优化建议

  1. 模拟器优化:

    • 状态模拟:增加动态行为(如温度渐变、坐标移动仿真)。

      csharp

      private async Task SimulateTemperatureAsync(CancellationToken ct)
      {
          while (!ct.IsCancellationRequested)
          {
              _inputRegisters[0] += 10; // 模拟温度每秒升高1°C
              UpdateDataStore();
              await Task.Delay(1000, ct);
          }
      }
    • 错误模拟:随机触发超时或校验错误,测试上位机容错能力。

  2. 通信优化:

    • 使用连接池管理Modbus TCP连接。

    • 优化串口波特率(如115200)提高吞吐量。

  3. 日志优化:

    • 添加性能指标(如每次操作的耗时)。

      csharp

      var stopwatch = Stopwatch.StartNew();
      await _modbusMaster.WriteSingleRegisterAsync(1, address, value);
      Log.Information("Write register {Address} took {ElapsedMs}ms", address, stopwatch.ElapsedMilliseconds);
  4. 测试扩展:

    • 使用xUnit测试模拟器响应:

      csharp

      [Fact]
      public async Task Simulator_RespondsToWriteRegister()
      {
          var simulator = new SmtModbusSimulator();
          simulator.Start();
          var driver = new SmtPlacementDriver();
          await driver.ConnectAsync("127.0.0.1:502", "ModbusTCP");
          await driver.SendDataAsync(BitConverter.GetBytes((ushort)0).Concat(BitConverter.GetBytes((ushort)1000)).ToArray());
          var data = await driver.ReceiveDataAsync();
          Assert.Equal(1000, BitConverter.ToUInt16(data, 0));
          simulator.Stop();
      }

6. 联调测试步骤

  1. 环境准备:

    • 安装NuGet包:NModbus、Serilog、BenchmarkDotNet。

    • 配置appsettings.json,指定Modbus TCP(127.0.0.1:502)或串口(COM2)。

  2. 运行模拟器:

    • 启动SmtModbusSimulator或SmtSerialSimulator。

  3. 运行上位机:

    • 执行Program.Main,测试寄存器设置和状态读取。

  4. 验证日志:

    • 检查logs/app-.log和logs/simulator-.log,确认通信正常。

  5. 性能测试:

    • 运行SmtCommunicationBenchmarks,分析延迟和内存。


总结

本方案实现了:

  • Modbus TCP模拟器:响应寄存器读写,模拟贴片机坐标和状态。

  • 串口模拟器:处理自定义协议帧,支持设置和状态查询。

  • 联调测试:上位机与模拟器通过Modbus TCP或串口通信,验证功能。

  • 性能测试:使用BenchmarkDotNet评估通信性能,优化延迟。

下一步建议:

  • 添加晶圆老化设备模拟器,模拟温度/电压动态变化。

  • 实现多设备模拟(多Modbus从站或多COM端口)。

  • 扩展错误场景测试(如断线、超时)。

如果需要晶圆老化设备模拟器代码、具体协议细节或更复杂的测试用例,请提供进一步需求,我可以定制实现!

你可能感兴趣的:(C#实战教程,网络,c#,开发语言)