为实现上位机与贴片机或晶圆老化设备的联调测试,下面提供一个基于C#的模拟器,模拟贴片机和晶圆老化设备的Modbus TCP和自定义串口协议行为。模拟器将与之前实现的上位机驱动进行通信,支持寄存器读写、协议解析和状态反馈。同时,模拟器将记录通信日志,便于调试,并支持性能测试。以下是详细实现和说明。
设计目标
模拟器功能:
模拟贴片机(SMT Placement Machine)的运动控制(X/Y/Z坐标、吸嘴)和状态反馈。
模拟晶圆老化设备(Wafer Aging Equipment)的温度/电压控制和数据采集。
支持Modbus TCP和自定义串口协议,响应上位机的寄存器读写或帧命令。
通信兼容:与之前提供的 SmtPlacementDriver 和寄存器映射无缝对接。
日志记录:使用Serilog记录模拟器的通信和状态变化。
测试支持:提供简单的测试用例,确保模拟器与上位机正确联调。
可扩展性:支持动态配置寄存器和协议,适配不同设备。
寄存器映射(参考前文)
使用之前定义的寄存器映射:
贴片机寄存器映射
寄存器类型 |
地址 |
功能 |
数据类型 |
描述 |
---|---|---|---|---|
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"
}
}
测试步骤:
启动模拟器(Modbus TCP或串口)。
运行上位机程序,执行 ExecuteAsync,设置X轴坐标和速度。
模拟器响应请求,更新寄存器并记录日志。
上位机读取状态,验证返回数据。
预期输出:
[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. 优化建议
模拟器优化:
状态模拟:增加动态行为(如温度渐变、坐标移动仿真)。
csharp
private async Task SimulateTemperatureAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
_inputRegisters[0] += 10; // 模拟温度每秒升高1°C
UpdateDataStore();
await Task.Delay(1000, ct);
}
}
错误模拟:随机触发超时或校验错误,测试上位机容错能力。
通信优化:
使用连接池管理Modbus TCP连接。
优化串口波特率(如115200)提高吞吐量。
日志优化:
添加性能指标(如每次操作的耗时)。
csharp
var stopwatch = Stopwatch.StartNew();
await _modbusMaster.WriteSingleRegisterAsync(1, address, value);
Log.Information("Write register {Address} took {ElapsedMs}ms", address, stopwatch.ElapsedMilliseconds);
测试扩展:
使用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. 联调测试步骤
环境准备:
安装NuGet包:NModbus、Serilog、BenchmarkDotNet。
配置appsettings.json,指定Modbus TCP(127.0.0.1:502)或串口(COM2)。
运行模拟器:
启动SmtModbusSimulator或SmtSerialSimulator。
运行上位机:
执行Program.Main,测试寄存器设置和状态读取。
验证日志:
检查logs/app-.log和logs/simulator-.log,确认通信正常。
性能测试:
运行SmtCommunicationBenchmarks,分析延迟和内存。
总结
本方案实现了:
Modbus TCP模拟器:响应寄存器读写,模拟贴片机坐标和状态。
串口模拟器:处理自定义协议帧,支持设置和状态查询。
联调测试:上位机与模拟器通过Modbus TCP或串口通信,验证功能。
性能测试:使用BenchmarkDotNet评估通信性能,优化延迟。
下一步建议:
添加晶圆老化设备模拟器,模拟温度/电压动态变化。
实现多设备模拟(多Modbus从站或多COM端口)。
扩展错误场景测试(如断线、超时)。
如果需要晶圆老化设备模拟器代码、具体协议细节或更复杂的测试用例,请提供进一步需求,我可以定制实现!