C# 基于以太网 MC 协议的三菱 FX3U PLC 通信客户端实现
基于以太网 MC 协议的三菱 FX3U 通信架构
在工业自动化系统中,上位机与三菱 FX3U 系列 PLC 的数据交互通常依赖于 MELSEC 通信协议(简称 MC 协议)。通过以太网模块,C# 应用程序可以利用 TCP/IP 套接字直接与 PLC 建立连接,实现对内部寄存器(如 D、M、X、Y)的高效读写。
环境准备与基础依赖
开发该通信客户端无需引入庞大的第三方工业库,.NET 基础类库中的 System.Net.Sockets 即可满足底层 TCP 通信需求。确保项目目标框架为 .NET Core 3.1 或更高版本,以获得更好的异步 I/O 性能支持。
客户端连接管理设计
为了保证通信的稳定性,我们需要对 TcpClient 进行封装,处理连接建立、异常捕获以及网络流的管理。以下重构后的连接管理类采用了异步编程模型,避免在 UI 线程中阻塞。
using System;
using System.Net.Sockets;
using System.Threading.Tasks;
public class McProtocolClient : IDisposable
{
private TcpClient _tcpClient;
private NetworkStream _dataStream;
private bool _isDisposed;
public async Task<bool> EstablishConnectionAsync(string ipAddress, int port)
{
try
{
_tcpClient = new TcpClient();
// 设置合理的超时时间,防止网络异常导致长时间挂起
_tcpClient.ReceiveTimeout = 3000;
_tcpClient.SendTimeout = 3000;
await _tcpClient.ConnectAsync(ipAddress, port);
_dataStream = _tcpClient.GetStream();
return true;
}
catch (SocketException ex)
{
Console.WriteLine($"[网络错误] 无法连接到 PLC {ipAddress}:{port} - {ex.Message}");
return false;
}
}
MC 协议指令构建与数据交互
MC 协议支持二进制和 ASCII 两种编码格式。在实际工程中,二进制格式(如 3E 帧)因其数据体积小、解析效率高而被广泛使用。以下代码展示了如何发送构建好的 MC 协议字节帧,并同步接收 PLC 的响应数据。
public byte[] TransmitMcFrame(byte[] requestFrame)
{
if (_dataStream == null || !_dataStream.CanWrite)
throw new InvalidOperationException("通信链路未建立或已断开。");
// 发送请求帧
_dataStream.Write(requestFrame, 0, requestFrame.Length);
_dataStream.Flush();
// 接收响应数据
// 实际应用中应根据 3E 帧头部的"请求数据长度"字段进行精确读取
byte[] responseBuffer = new byte[1024];
int bytesRead = _dataStream.Read(responseBuffer, 0, responseBuffer.Length);
if (bytesRead == 0)
throw new IOException("PLC 意外关闭了连接。");
byte[] actualResponse = new byte[bytesRead];
Array.Copy(responseBuffer, actualResponse, bytesRead);
return actualResponse;
}
资源释放与生命周期管理
在工业控制场景中,连接的优雅释放与建立同等重要。未正确关闭的 Socket 会导致 PLC 端口被占用或连接数耗尽。通过实现 IDisposable 接口,可以确保网络资源被及时回收。
public void Disconnect()
{
if (_dataStream != null)
{
_dataStream.Close();
_dataStream = null;
}
if (_tcpClient != null)
{
_tcpClient.Close();
_tcpClient = null;
}
}
public void Dispose()
{
if (!_isDisposed)
{
Disconnect();
_isDisposed = true;
}
GC.SuppressFinalize(this);
}
}