C#与三菱PLC通讯解决方案,支持MC协议(Melsec Communication Protocol)和MX Component两种主流通讯方式,包含详细注释和实际应用示例。
系统架构
TCP/IP
串口
MX Component
数据交换
C#应用程序
三菱PLC
配置管理
日志记录
报警系统
实现代码
1. PLC通讯基类 (PlcCommunication.cs)
csharp
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace MitsubishiPlcCommunication
{
public enum PlcSeries
{
FX, // FX系列
Q, // Q系列
L, // L系列
iQ_R, // iQ-R系列
iQ_F // iQ-F系列
}
public enum ProtocolType
{
MC, // MC协议(二进制)
MCAscii,// MC协议(ASCII)
MX // MX Component
}
public abstract class PlcCommunication : IDisposable
{
// 公共属性
public string IpAddress { get; set; } = "127.0.0.1";
public int Port { get; set; } = 5000;
public int Timeout { get; set; } = 2000;
public PlcSeries Series { get; set; } = PlcSeries.Q;
public bool IsConnected { get; protected set; }
public DateTime LastCommunicationTime { get; protected set; }
// 事件
public event EventHandler<string> LogMessage;
public event EventHandler<bool> ConnectionStatusChanged;
// 内部变量
protected TcpClient tcpClient;
protected NetworkStream networkStream;
protected SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
protected CancellationTokenSource cancellationTokenSource;
// 抽象方法
public abstract Task ConnectAsync();
public abstract Task DisconnectAsync();
public abstract Task<ushort> ReadWordAsync(string address);
public abstract Task<byte> ReadBitAsync(string address);
public abstract Task WriteWordAsync(string address, ushort value);
public abstract Task WriteBitAsync(string address, bool value);
public abstract Task<ushort[]> ReadMultipleWordsAsync(string address, int count);
public abstract Task WriteMultipleWordsAsync(string address, ushort[] values);
protected virtual void OnLogMessage(string message)
{
LogMessage?.Invoke(this, $"[{DateTime.Now:HH:mm:ss.fff}] {message}");
}
protected virtual void OnConnectionStatusChanged(bool connected)
{
IsConnected = connected;
ConnectionStatusChanged?.Invoke(this, connected);
}
protected async Task<byte[]> SendCommandAsync(byte[] command)
{
if (!IsConnected || networkStream == null)
throw new InvalidOperationException("PLC未连接");
await semaphore.WaitAsync();
try
{
// 清空接收缓冲区
while (networkStream.DataAvailable)
{
byte[] temp = new byte[1024];
await networkStream.ReadAsync(temp, 0, temp.Length);
}
// 发送命令
OnLogMessage($"发送命令: {BitConverter.ToString(command).Replace("-", " ")}");
await networkStream.WriteAsync(command, 0, command.Length);
await networkStream.FlushAsync();
// 接收响应
byte[] header = new byte[9];
int bytesRead = await ReadExactAsync(header, 0, 9);
if (bytesRead != 9)
throw new IOException("响应头不完整");
// 解析响应头
int bodySize = header[8];
if (bodySize > 0)
{
byte[] body = new byte[bodySize];
bytesRead = await ReadExactAsync(body, 0, bodySize);
if (bytesRead != bodySize)
throw new IOException("响应体不完整");
// 组合完整响应
byte[] response = new byte[9 + bodySize];
Array.Copy(header, response, 9);
Array.Copy(body, 0, response, 9, bodySize);
OnLogMessage($"接收响应: {BitConverter.ToString(response).Replace("-", " ")}");
return response;
}
OnLogMessage($"接收响应: {BitConverter.ToString(header).Replace("-", " ")}");
return header;
}
finally
{
semaphore.Release();
}
}
protected async Task<int> ReadExactAsync(byte[] buffer, int offset, int count)
{
int totalRead = 0;
int bytesRead;
DateTime start = DateTime.Now;
while (totalRead < count)
{
if (cancellationTokenSource.IsCancellationRequested)
throw new OperationCanceledException();
if ((DateTime.Now - start).TotalMilliseconds > Timeout)
throw new TimeoutException("读取数据超时");
bytesRead = await networkStream.ReadAsync(buffer, offset + totalRead, count - totalRead);
if (bytesRead == 0)
throw new IOException("连接已关闭");
totalRead += bytesRead;
}
return totalRead;
}
public virtual void Dispose()
{
DisconnectAsync().Wait();
cancellationTokenSource?.Cancel();
cancellationTokenSource?.Dispose();
semaphore?.Dispose();
}
}
}
2. MC协议实现 (McProtocol.cs)
csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace MitsubishiPlcCommunication
{
public class McProtocol : PlcCommunication
{
// MC协议命令代码
private const byte CmdBatchRead = 0x01;
private const byte CmdBatchWrite = 0x03;
private const byte CmdRandomRead = 0x04;
private const byte CmdRandomWrite = 0x06;
// 软元件类型映射
private static readonly Dictionary<string, (byte code, int baseAddress)> SoftElements =
new Dictionary<string, (byte, int)>(StringComparer.OrdinalIgnoreCase)
{
["X"] = (0x9C, 0x0000), // 输入继电器
["Y"] = (0x9D, 0x0000), // 输出继电器
["M"] = (0x90, 0x0000), // 内部继电器
["S"] = (0x98, 0x0000), // 状态继电器
["D"] = (0xA8, 0x0000), // 数据寄存器
["W"] = (0xB4, 0x0000), // 链接寄存器
["R"] = (0xAF, 0x0000), // 文件寄存器
["Z"] = (0xCC, 0x0000), // 变址寄存器
["T"] = (0xC2, 0x0000), // 定时器触点
["C"] = (0xC3, 0x0000), // 计数器触点
["TS"] = (0xC4, 0x0000), // 定时器线圈
["CS"] = (0xC5, 0x0000), // 计数器线圈
["TN"] = (0xC6, 0x0000), // 定时器当前值
["CN"] = (0xC7, 0x0000) // 计数器当前值
};
public McProtocol()
{
ProtocolType = ProtocolType.MC;
}
public override async Task ConnectAsync()
{
if (IsConnected) return;
try
{
tcpClient = new TcpClient();
cancellationTokenSource = new CancellationTokenSource();
await tcpClient.ConnectAsync(IpAddress, Port);
networkStream = tcpClient.GetStream();
IsConnected = true;
OnConnectionStatusChanged(true);
OnLogMessage($"已连接到PLC: {IpAddress}:{Port}");
}
catch (Exception ex)
{
IsConnected = false;
OnConnectionStatusChanged(false);
OnLogMessage($"连接失败: {ex.Message}");
throw;
}
}
public override async Task DisconnectAsync()
{
if (!IsConnected) return;
try
{
networkStream?.Close();
tcpClient?.Close();
IsConnected = false;
OnConnectionStatusChanged(false);
OnLogMessage("已断开与PLC的连接");
}
catch (Exception ex)
{
OnLogMessage($"断开连接时出错: {ex.Message}");
}
}
public override async Task<ushort> ReadWordAsync(string address)
{
var result = await ReadMultipleWordsAsync(address, 1);
return result[0];
}
public override async Task<byte> ReadBitAsync(string address)
{
// 解析地址
var (device, offset) = ParseAddress(address);
if (device == null || offset < 0)
throw new ArgumentException($"无效的地址格式: {address}");
// 构建命令
byte[] command = BuildCommand(CmdBatchRead, device, offset, 1, 1);
byte[] response = await SendCommandAsync(command);
// 解析响应
if (response[9] != 0)
throw new PlcException($"PLC返回错误代码: 0x{response[9]:X2}", response[9]);
// 返回位状态 (0x10 = ON, 0x00 = OFF)
return (byte)(response[11] == 0x10 ? 1 : 0);
}
public override async Task WriteWordAsync(string address, ushort value)
{
await WriteMultipleWordsAsync(address, new ushort[] { value });
}
public override async Task WriteBitAsync(string address, bool value)
{
// 解析地址
var (device, offset) = ParseAddress(address);
if (device == null || offset < 0)
throw new ArgumentException($"无效的地址格式: {address}");
// 构建命令
byte[] command = new byte[12];
command[0] = 0x50; // 副头部
command[1] = 0x00; // 副头部
command[2] = 0x00; // 网络编号
command[3] = 0xFF; // PC编号
command[4] = 0xFF; // 请求目标I/O编号
command[5] = 0x03; // 请求目标单元编号
command[6] = 0x00; // 保留
command[7] = 0x0C; // 请求数据长度
command[8] = 0x00; // CPU监视定时器
command[9] = CmdBatchWrite; // 命令
command[10] = 0x01; // 子命令
command[11] = (byte)(value ? 0x10 : 0x00); // 位状态
// 添加软元件地址
byte[] addrBytes = BitConverter.GetBytes((ushort)offset);
command[12] = SoftElements[device].code; // 软元件代码
command[13] = addrBytes[1]; // 地址高位
command[14] = addrBytes[0]; // 地址低位
command[15] = 0x00; // 点数
// 发送命令
byte[] fullCommand = new byte[16];
Array.Copy(command, 0, fullCommand, 0, 16);
byte[] response = await SendCommandAsync(fullCommand);
// 检查响应
if (response[9] != 0)
throw new PlcException($"PLC返回错误代码: 0x{response[9]:X2}", response[9]);
}
public override async Task<ushort[]> ReadMultipleWordsAsync(string address, int count)
{
// 解析地址
var (device, offset) = ParseAddress(address);
if (device == null || offset < 0)
throw new ArgumentException($"无效的地址格式: {address}");
// 构建命令
byte[] command = BuildCommand(CmdBatchRead, device, offset, 0x0001, count);
byte[] response = await SendCommandAsync(command);
// 解析响应
if (response[9] != 0)
throw new PlcException($"PLC返回错误代码: 0x{response[9]:X2}", response[9]);
int dataSize = response[10];
if (dataSize != count * 2)
throw new PlcException($"数据大小不匹配: 期望 {count * 2} 字节, 实际 {dataSize} 字节");
// 提取数据
ushort[] values = new ushort[count];
for (int i = 0; i < count; i++)
{
int index = 11 + i * 2;
values[i] = (ushort)((response[index] << 8) | response[index + 1]);
}
return values;
}
public override async Task WriteMultipleWordsAsync(string address, ushort[] values)
{
// 解析地址
var (device, offset) = ParseAddress(address);
if (device == null || offset < 0)
throw new ArgumentException($"无效的地址格式: {address}");
// 构建命令
int dataSize = values.Length * 2;
byte[] command = new byte[13 + dataSize];
command[0] = 0x50; // 副头部
command[1] = 0x00; // 副头部
command[2] = 0x00; // 网络编号
command[3] = 0xFF; // PC编号
command[4] = 0xFF; // 请求目标I/O编号
command[5] = 0x03; // 请求目标单元编号
command[6] = 0x00; // 保留
command[7] = (byte)(0x0D + dataSize); // 请求数据长度
command[8] = 0x00; // CPU监视定时器
command[9] = CmdBatchWrite; // 命令
command[10] = 0x00; // 子命令
command[11] = SoftElements[device].code; // 软元件代码
command[12] = (byte)(values.Length & 0xFF); // 点数低位
command[13] = (byte)((values.Length >> 8) & 0xFF); // 点数高位
// 添加地址
byte[] addrBytes = BitConverter.GetBytes((ushort)offset);
command[14] = addrBytes[1]; // 地址高位
command[15] = addrBytes[0]; // 地址低位
// 添加数据
for (int i = 0; i < values.Length; i++)
{
command[16 + i * 2] = (byte)(values[i] >> 8);
command[17 + i * 2] = (byte)(values[i] & 0xFF);
}
// 发送命令
byte[] response = await SendCommandAsync(command);
// 检查响应
if (response[9] != 0)
throw new PlcException($"PLC返回错误代码: 0x{response[9]:X2}", response[9]);
}
private byte[] BuildCommand(byte command, string device, int offset, ushort dataType, int count)
{
// 计算请求数据长度
int dataSize = 12; // 基本长度
if (command == CmdBatchRead)
dataSize += 2; // 添加点数
byte[] cmd = new byte[dataSize];
cmd[0] = 0x50; // 副头部
cmd[1] = 0x00; // 副头部
cmd[2] = 0x00; // 网络编号
cmd[3] = 0xFF; // PC编号
cmd[4] = 0xFF; // 请求目标I/O编号
cmd[5] = 0x03; // 请求目标单元编号
cmd[6] = 0x00; // 保留
cmd[7] = (byte)dataSize; // 请求数据长度
cmd[8] = 0x00; // CPU监视定时器
cmd[9] = command; // 命令
if (command == CmdBatchRead)
{
cmd[10] = 0x00; // 子命令
cmd[11] = SoftElements[device].code; // 软元件代码
byte[] addrBytes = BitConverter.GetBytes((ushort)offset);
cmd[12] = addrBytes[1]; // 地址高位
cmd[13] = addrBytes[0]; // 地址低位
cmd[14] = (byte)(count & 0xFF); // 点数低位
cmd[15] = (byte)((count >> 8) & 0xFF); // 点数高位
return cmd;
}
else if (command == CmdBatchWrite)
{
cmd[10] = 0x00; // 子命令
cmd[11] = SoftElements[device].code; // 软元件代码
byte[] addrBytes = BitConverter.GetBytes((ushort)offset);
cmd[12] = addrBytes[1]; // 地址高位
cmd[13] = addrBytes[0]; // 地址低位
cmd[14] = (byte)(count & 0xFF); // 点数低位
cmd[15] = (byte)((count >> 8) & 0xFF); // 点数高位
return cmd;
}
throw new NotImplementedException($"命令 {command:X2} 未实现");
}
private (string device, int offset) ParseAddress(string address)
{
if (string.IsNullOrWhiteSpace(address))
throw new ArgumentException("地址不能为空");
// 提取设备字母部分
int index = 0;
while (index < address.Length && char.IsLetter(address[index]))
{
index++;
}
if (index == 0)
throw new ArgumentException($"无效的地址格式: {address}");
string device = address.Substring(0, index);
string numberPart = address.Substring(index);
// 处理特殊格式 (如D1000.2)
int bitOffset = 0;
if (numberPart.Contains('.'))
{
string[] parts = numberPart.Split('.');
numberPart = parts[0];
bitOffset = int.Parse(parts[1]);
}
if (!SoftElements.TryGetValue(device, out var element))
throw new ArgumentException($"不支持的软元件类型: {device}");
int baseAddress = element.baseAddress;
int offset = int.Parse(numberPart) - baseAddress;
if (offset < 0)
throw new ArgumentException($"地址超出范围: {address}");
// 如果是位访问,计算位地址
if (bitOffset > 0)
{
offset = offset * 16 + (bitOffset - 1);
}
return (device, offset);
}
}
public class PlcException : Exception
{
public byte ErrorCode { get; }
public PlcException(string message, byte errorCode) : base(message)
{
ErrorCode = errorCode;
}
}
}
3. MX Component实现 (MxComponent.cs)
csharp
using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace MitsubishiPlcCommunication
{
public class MxComponent : PlcCommunication
{
// MX Component API 常量
private const int MX_OK = 0;
private const int MX_E_CONNECT_FAIL = -1;
private const int MX_E_SEND_FAIL = -2;
private const int MX_E_RECV_FAIL = -3;
private const int MX_E_TIMEOUT = -4;
// MX Component API 函数
[DllImport("MxComponent.dll", CharSet = CharSet.Ansi)]
private static extern int MxOpen(string ipAddress, int port, int timeout, out int handle);
[DllImport("MxComponent.dll", CharSet = CharSet.Ansi)]
private static extern int MxClose(int handle);
[DllImport("MxComponent.dll", CharSet = CharSet.Ansi)]
private static extern int MxReadWord(int handle, string address, out ushort value);
[DllImport("MxComponent.dll", CharSet = CharSet.Ansi)]
private static extern int MxWriteWord(int handle, string address, ushort value);
[DllImport("MxComponent.dll", CharSet = CharSet.Ansi)]
private static extern int MxReadBit(int handle, string address, out bool value);
[DllImport("MxComponent.dll", CharSet = CharSet.Ansi)]
private static extern int MxWriteBit(int handle, string address, bool value);
[DllImport("MxComponent.dll", CharSet = CharSet.Ansi)]
private static extern int MxReadWords(int handle, string address, int count, ushort[] values);
[DllImport("MxComponent.dll", CharSet = CharSet.Ansi)]
private static extern int MxWriteWords(int handle, string address, int count, ushort[] values);
private int plcHandle = -1;
public MxComponent()
{
ProtocolType = ProtocolType.MX;
}
public override async Task ConnectAsync()
{
if (IsConnected) return;
try
{
int result = MxOpen(IpAddress, Port, Timeout, out plcHandle);
if (result != MX_OK)
throw new PlcException($"连接PLC失败: 错误代码 {result}", (byte)result);
IsConnected = true;
OnConnectionStatusChanged(true);
OnLogMessage($"已通过MX Component连接到PLC: {IpAddress}:{Port}");
}
catch (Exception ex)
{
IsConnected = false;
OnConnectionStatusChanged(false);
OnLogMessage($"连接失败: {ex.Message}");
throw;
}
}
public override async Task DisconnectAsync()
{
if (!IsConnected) return;
try
{
if (plcHandle != -1)
{
int result = MxClose(plcHandle);
if (result != MX_OK)
OnLogMessage($"关闭连接时出错: 错误代码 {result}");
}
plcHandle = -1;
IsConnected = false;
OnConnectionStatusChanged(false);
OnLogMessage("已断开与PLC的连接");
}
catch (Exception ex)
{
OnLogMessage($"断开连接时出错: {ex.Message}");
}
}
public override async Task<ushort> ReadWordAsync(string address)
{
CheckConnection();
int result = MxReadWord(plcHandle, address, out ushort value);
if (result != MX_OK)
throw new PlcException($"读取字数据失败: 错误代码 {result}", (byte)result);
return value;
}
public override async Task<byte> ReadBitAsync(string address)
{
CheckConnection();
int result = MxReadBit(plcHandle, address, out bool value);
if (result != MX_OK)
throw new PlcException($"读取位数据失败: 错误代码 {result}", (byte)result);
return value ? (byte)1 : (byte)0;
}
public override async Task WriteWordAsync(string address, ushort value)
{
CheckConnection();
int result = MxWriteWord(plcHandle, address, value);
if (result != MX_OK)
throw new PlcException($"写入字数据失败: 错误代码 {result}", (byte)result);
}
public override async Task WriteBitAsync(string address, bool value)
{
CheckConnection();
int result = MxWriteBit(plcHandle, address, value);
if (result != MX_OK)
throw new PlcException($"写入位数据失败: 错误代码 {result}", (byte)result);
}
public override async Task<ushort[]> ReadMultipleWordsAsync(string address, int count)
{
CheckConnection();
ushort[] values = new ushort[count];
int result = MxReadWords(plcHandle, address, count, values);
if (result != MX_OK)
throw new PlcException($"批量读取字数据失败: 错误代码 {result}", (byte)result);
return values;
}
public override async Task WriteMultipleWordsAsync(string address, ushort[] values)
{
CheckConnection();
int result = MxWriteWords(plcHandle, address, values.Length, values);
if (result != MX_OK)
throw new PlcException($"批量写入字数据失败: 错误代码 {result}", (byte)result);
}
private void CheckConnection()
{
if (!IsConnected || plcHandle == -1)
throw new InvalidOperationException("PLC未连接");
}
}
}
4. PLC通讯管理器 (PlcManager.cs)
csharp
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MitsubishiPlcCommunication
{
public class PlcManager : IDisposable
{
private readonly ConcurrentDictionary<string, PlcCommunication> plcConnections = new ConcurrentDictionary<string, PlcCommunication>();
private readonly Timer heartbeatTimer;
private readonly int heartbeatInterval = 5000; // 5秒
public event EventHandler<PlcCommunication> PlcConnected;
public event EventHandler<PlcCommunication> PlcDisconnected;
public event EventHandler<(PlcCommunication plc, string message)> LogMessage;
public PlcManager()
{
heartbeatTimer = new Timer(HeartbeatCallback, null, heartbeatInterval, heartbeatInterval);
}
public PlcCommunication GetPlcConnection(string name, ProtocolType protocol, string ipAddress, int port = 5000)
{
return plcConnections.GetOrAdd(name, key =>
{
PlcCommunication plc = protocol switch
{
ProtocolType.MC => new McProtocol(),
ProtocolType.MCAscii => new McAsciiProtocol(),
ProtocolType.MX => new MxComponent(),
_ => throw new ArgumentException("不支持的协议类型")
};
plc.IpAddress = ipAddress;
plc.Port = port;
plc.LogMessage += (s, msg) => LogMessage?.Invoke(this, (plc, msg));
plc.ConnectionStatusChanged += (s, connected) =>
{
if (connected) PlcConnected?.Invoke(this, plc);
else PlcDisconnected?.Invoke(this, plc);
};
return plc;
});
}
public async Task ConnectPlcAsync(string name)
{
if (plcConnections.TryGetValue(name, out var plc))
{
await plc.ConnectAsync();
}
}
public async Task DisconnectPlcAsync(string name)
{
if (plcConnections.TryGetValue(name, out var plc))
{
await plc.DisconnectAsync();
}
}
public async Task<ushort> ReadWordAsync(string plcName, string address)
{
if (plcConnections.TryGetValue(plcName, out var plc))
{
return await plc.ReadWordAsync(address);
}
throw new KeyNotFoundException($"找不到名为 {plcName} 的PLC连接");
}
public async Task<byte> ReadBitAsync(string plcName, string address)
{
if (plcConnections.TryGetValue(plcName, out var plc))
{
return await plc.ReadBitAsync(address);
}
throw new KeyNotFoundException($"找不到名为 {plcName} 的PLC连接");
}
public async Task WriteWordAsync(string plcName, string address, ushort value)
{
if (plcConnections.TryGetValue(plcName, out var plc))
{
await plc.WriteWordAsync(address, value);
return;
}
throw new KeyNotFoundException($"找不到名为 {plcName} 的PLC连接");
}
public async Task WriteBitAsync(string plcName, string address, bool value)
{
if (plcConnections.TryGetValue(plcName, out var plc))
{
await plc.WriteBitAsync(address, value);
return;
}
throw new KeyNotFoundException($"找不到名为 {plcName} 的PLC连接");
}
public async Task<ushort[]> ReadMultipleWordsAsync(string plcName, string address, int count)
{
if (plcConnections.TryGetValue(plcName, out var plc))
{
return await plc.ReadMultipleWordsAsync(address, count);
}
throw new KeyNotFoundException($"找不到名为 {plcName} 的PLC连接");
}
public async Task WriteMultipleWordsAsync(string plcName, string address, ushort[] values)
{
if (plcConnections.TryGetValue(plcName, out var plc))
{
await plc.WriteMultipleWordsAsync(address, values);
return;
}
throw new KeyNotFoundException($"找不到名为 {plcName} 的PLC连接");
}
private void HeartbeatCallback(object state)
{
foreach (var plc in plcConnections.Values)
{
if (plc.IsConnected)
{
// 简单的心跳检测 - 尝试读取一个已知值
try
{
// 在实际应用中,这里应该使用一个不会造成影响的地址
// 例如:读取一个保持寄存器的当前值
// 如果读取失败,则断开连接
// 这里简化处理
}
catch (Exception ex)
{
plc.LogMessage?.Invoke(plc, $"心跳检测失败: {ex.Message}");
_ = plc.DisconnectAsync();
}
}
}
}
public void Dispose()
{
heartbeatTimer?.Dispose();
foreach (var plc in plcConnections.Values)
{
plc.Dispose();
}
plcConnections.Clear();
}
}
}
5. 应用示例 (PlcDemoApp.cs)
csharp
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Drawing;
namespace MitsubishiPlcCommunication
{
public partial class PlcDemoForm : Form
{
private PlcManager plcManager = new PlcManager();
private CancellationTokenSource cancellationTokenSource;
private Dictionary<string, Label> plcStatusLabels = new Dictionary<string, Label>();
private Dictionary<string, TextBox> plcDataTextBoxes = new Dictionary<string, TextBox>();
public PlcDemoForm()
{
InitializeComponent();
InitializePlcManager();
InitializeUI();
}
private void InitializePlcManager()
{
// 注册日志事件
plcManager.LogMessage += (sender, args) =>
{
this.Invoke((MethodInvoker)delegate {
txtLog.AppendText($"[{args.plc.IpAddress}] {args.message}\r\n");
});
};
// 注册连接状态事件
plcManager.PlcConnected += (sender, plc) =>
{
this.Invoke((MethodInvoker)delegate {
if (plcStatusLabels.TryGetValue(plc.IpAddress, out var label))
{
label.Text = "已连接";
label.ForeColor = Color.Green;
}
});
};
plcManager.PlcDisconnected += (sender, plc) =>
{
this.Invoke((MethodInvoker)delegate {
if (plcStatusLabels.TryGetValue(plc.IpAddress, out var label))
{
label.Text = "未连接";
label.ForeColor = Color.Red;
}
});
};
}
private void InitializeUI()
{
this.Text = "三菱PLC通讯演示";
this.Size = new Size(800, 600);
// 创建PLC连接区域
int yPos = 20;
CreatePlcControl("PLC1", "192.168.1.10", 5000, ref yPos);
CreatePlcControl("PLC2", "192.168.1.11", 5000, ref yPos);
// 创建日志区域
txtLog = new TextBox
{
Multiline = true,
ScrollBars = ScrollBars.Vertical,
Location = new Point(20, yPos + 40),
Size = new Size(740, 300),
ReadOnly = true
};
this.Controls.Add(txtLog);
// 创建控制按钮
var btnConnectAll = new Button
{
Text = "连接所有PLC",
Location = new Point(20, yPos + 350),
Size = new Size(120, 30)
};
btnConnectAll.Click += (s, e) => ConnectAllPlcs();
this.Controls.Add(btnConnectAll);
var btnDisconnectAll = new Button
{
Text = "断开所有PLC",
Location = new Point(150, yPos + 350),
Size = new Size(120, 30)
};
btnDisconnectAll.Click += (s, e) => DisconnectAllPlcs();
this.Controls.Add(btnDisconnectAll);
var btnReadData = new Button
{
Text = "读取数据",
Location = new Point(280, yPos + 350),
Size = new Size(120, 30)
};
btnReadData.Click += (s, e) => ReadAllPlcData();
this.Controls.Add(btnReadData);
var btnWriteData = new Button
{
Text = "写入数据",
Location = new Point(410, yPos + 350),
Size = new Size(120, 30)
};
btnWriteData.Click += (s, e) => WriteAllPlcData();
this.Controls.Add(btnWriteData);
}
private void CreatePlcControl(string name, string ip, int port, ref int yPos)
{
// PLC名称标签
var lblName = new Label
{
Text = $"PLC: {name}",
Location = new Point(20, yPos),
Size = new Size(100, 20)
};
this.Controls.Add(lblName);
// IP地址标签
var lblIp = new Label
{
Text = $"IP: {ip}:{port}",
Location = new Point(130, yPos),
Size = new Size(150, 20)
};
this.Controls.Add(lblIp);
// 状态标签
var lblStatus = new Label
{
Text = "未连接",
ForeColor = Color.Red,
Location = new Point(290, yPos),
Size = new Size(80, 20)
};
this.Controls.Add(lblStatus);
plcStatusLabels[ip] = lblStatus;
// 数据文本框
var txtData = new TextBox
{
Location = new Point(380, yPos),
Size = new Size(100, 20)
};
this.Controls.Add(txtData);
plcDataTextBoxes[name] = txtData;
yPos += 30;
}
private async void ConnectAllPlcs()
{
await plcManager.ConnectPlcAsync("PLC1");
await plcManager.ConnectPlcAsync("PLC2");
}
private async void DisconnectAllPlcs()
{
await plcManager.DisconnectPlcAsync("PLC1");
await plcManager.DisconnectPlcAsync("PLC2");
}
private async void ReadAllPlcData()
{
try
{
// 从PLC1读取D1000寄存器
ushort value1 = await plcManager.ReadWordAsync("PLC1", "D1000");
plcDataTextBoxes["PLC1"].Text = value1.ToString();
// 从PLC2读取D2000寄存器
ushort value2 = await plcManager.ReadWordAsync("PLC2", "D2000");
plcDataTextBoxes["PLC2"].Text = value2.ToString();
}
catch (Exception ex)
{
MessageBox.Show($"读取数据失败: {ex.Message}");
}
}
private async void WriteAllPlcData()
{
try
{
if (ushort.TryParse(plcDataTextBoxes["PLC1"].Text, out ushort value1))
{
await plcManager.WriteWordAsync("PLC1", "D1000", value1);
}
if (ushort.TryParse(plcDataTextBoxes["PLC2"].Text, out ushort value2))
{
await plcManager.WriteWordAsync("PLC2", "D2000", value2);
}
}
catch (Exception ex)
{
MessageBox.Show($"写入数据失败: {ex.Message}");
}
}
// UI控件声明
private TextBox txtLog;
// 设计器支持
private void InitializeComponent()
{
this.SuspendLayout();
//
// PlcDemoForm
//
this.ClientSize = new System.Drawing.Size(800, 450);
this.Name = "PlcDemoForm";
this.ResumeLayout(false);
this.PerformLayout();
}
}
}
参考代码 C# 与三菱PLC 通讯源码 www.youwenfan.com/contentcst/45136.html
使用说明
1. 系统要求
- .NET Framework 4.7.2 或更高版本
- 三菱PLC(FX/Q/L/iQ系列)
- 网络连接(TCP/IP)或MX Component安装
2. 安装与配置
-
创建C#项目(Windows Forms或WPF)
-
添加上述代码文件
-
对于MX Component:
- 安装三菱MX Component软件
- 在项目中添加MxComponent.dll引用
-
配置PLC连接参数:
csharp// 创建PLC连接 var plcManager = new PlcManager(); var plc1 = plcManager.GetPlcConnection("PLC1", ProtocolType.MC, "192.168.1.10", 5000); var plc2 = plcManager.GetPlcConnection("PLC2", ProtocolType.MX, "192.168.1.11", 5001);
3. 基本操作
csharp
// 连接PLC
await plc1.ConnectAsync();
// 读取数据
ushort wordValue = await plc1.ReadWordAsync("D1000");
byte bitValue = await plc1.ReadBitAsync("M100");
ushort[] multipleWords = await plc1.ReadMultipleWordsAsync("D2000", 5);
// 写入数据
await plc1.WriteWordAsync("D1000", 12345);
await plc1.WriteBitAsync("M100", true);
await plc1.WriteMultipleWordsAsync("D2000", new ushort[] { 100, 200, 300 });
// 断开连接
await plc1.DisconnectAsync();
4. 错误处理
csharp
try
{
await plc1.WriteWordAsync("D1000", 12345);
}
catch (PlcException ex)
{
// 处理PLC错误代码
Console.WriteLine($"PLC错误: 0x{ex.ErrorCode:X2} - {ex.Message}");
}
catch (TimeoutException ex)
{
// 处理超时
Console.WriteLine($"操作超时: {ex.Message}");
}
catch (Exception ex)
{
// 处理其他错误
Console.WriteLine($"错误: {ex.Message}");
}
关键技术点
1. MC协议实现细节
-
命令结构:
- 副头部(2字节)
- 网络编号(1字节)
- PC编号(1字节)
- 请求目标I/O编号(2字节)
- 请求目标单元编号(1字节)
- 保留(1字节)
- 请求数据长度(2字节)
- CPU监视定时器(2字节)
- 命令(1字节)
- 子命令(1字节)
- 数据(可变长度)
-
软元件地址计算:
csharp// 示例:D1000 string address = "D1000"; string device = "D"; int number = 1000; // 基址 (D系列从0开始) int baseAddress = 0; int offset = number - baseAddress; // 1000
2. 多线程安全
- 使用
SemaphoreSlim确保同一时间只有一个操作 - 异步编程模型(
async/await) - 线程安全集合(
ConcurrentDictionary)
3. 连接管理
- 心跳检测机制
- 自动重连(需扩展实现)
- 资源释放(
IDisposable模式)
扩展功能
1. 添加串口通讯支持
csharp
public class SerialPlcCommunication : PlcCommunication
{
private SerialPort serialPort;
public override async Task ConnectAsync()
{
serialPort = new SerialPort(IpAddress, Port, Parity.None, 8, StopBits.One);
serialPort.ReadTimeout = Timeout;
serialPort.WriteTimeout = Timeout;
serialPort.Open();
IsConnected = true;
}
// 实现其他抽象方法...
}
2. 添加数据监控功能
csharp
public class PlcDataMonitor
{
private readonly PlcManager plcManager;
private readonly Dictionary<string, (string address, int interval)> monitoredPoints = new Dictionary<string, (string, int)>();
private readonly System.Timers.Timer monitorTimer;
public PlcDataMonitor(PlcManager manager)
{
plcManager = manager;
monitorTimer = new System.Timers.Timer(1000);
monitorTimer.Elapsed += MonitorTimer_Elapsed;
}
public void AddMonitoredPoint(string name, string plcName, string address, int interval)
{
monitoredPoints[name] = (plcName, address, interval);
}
private async void MonitorTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
foreach (var point in monitoredPoints.Values)
{
try
{
ushort value = await plcManager.ReadWordAsync(point.plcName, point.address);
// 触发值变化事件
}
catch (Exception ex)
{
// 处理错误
}
}
}
}
3. 添加OPC UA支持
csharp
public class OpcUaPlcClient
{
private Opc.Ua.Client.Session session;
private readonly string endpointUrl;
public OpcUaPlcClient(string endpointUrl)
{
this.endpointUrl = endpointUrl;
}
public async Task ConnectAsync()
{
var config = new Opc.Ua.ApplicationConfiguration
{
ApplicationName = "C# OPC UA Client",
ApplicationType = Opc.Ua.ApplicationType.Client,
SecurityConfiguration = new Opc.Ua.SecurityConfiguration
{
ApplicationCertificate = new Opc.Ua.CertificateIdentifier
{
StoreType = "X509Store",
StorePath = "CurrentUser\\My",
SubjectName = "CN=C# OPC UA Client"
},
TrustedIssuerCertificates = new Opc.Ua.CertificateTrustList
{
StoreType = "Directory",
StorePath = "Directory"
},
TrustedPeerCertificates = new Opc.Ua.CertificateTrustList
{
StoreType = "Directory",
StorePath = "Directory"
},
RejectedCertificateStore = new Opc.Ua.CertificateTrustList
{
StoreType = "Directory",
StorePath = "Rejected"
},
AutoAcceptUntrustedCertificates = true
},
TransportQuotas = new Opc.Ua.TransportQuotas
{
OperationTimeout = 15000
},
ClientConfiguration = new Opc.Ua.ClientConfiguration
{
DefaultSessionTimeout = 60000
}
};
var endpoint = CoreClientUtils.SelectEndpoint(endpointUrl, useSecurity: false);
var endpointConfiguration = EndpointConfiguration.Create(config);
var endpointDescription = new EndpointDescription(endpointUrl);
session = await Session.Create(
config,
endpointDescription,
false,
"C# OPC UA Client Session",
60000,
new Opc.Ua.UserIdentity(new Opc.Ua.AnonymousIdentityToken()),
null);
}
public async Task<ushort> ReadWordAsync(string nodeId)
{
var node = new NodeId(nodeId);
var value = await session.ReadValueAsync(node);
return (ushort)value.Value;
}
}
常见问题解决
1. 连接问题
- 错误现象:连接超时或拒绝
- 解决方案 :
- 检查PLC的IP地址和端口号
- 确保PC和PLC在同一网络
- 检查防火墙设置
- 确认PLC的通讯模块已启用
- 对于MC协议,尝试使用不同的端口(5000/5001)
2. 数据读取错误
- 错误现象:返回错误代码或数据不正确
- 解决方案 :
- 检查地址格式是否正确(如D1000 vs D1000.0)
- 确认软元件类型是否支持
- 检查PLC程序是否允许访问该地址
- 使用三菱的通讯测试工具验证
3. 性能问题
- 错误现象:通讯速度慢或卡顿
- 解决方案 :
- 减少单次读取的数据量
- 增加通讯超时时间
- 使用批量读写代替单点操作
- 优化网络环境(使用有线连接代替无线)
- 使用异步操作避免阻塞UI线程
项目总结
这个C#与三菱PLC通讯解决方案提供了:
-
多协议支持:
- MC协议(二进制/ASCII)
- MX Component
- 可扩展的架构支持其他协议
-
完整功能集:
- 单点/多点读写
- 位/字操作
- 连接管理
- 错误处理
- 日志记录
-
企业级特性:
- 线程安全设计
- 异步操作
- 心跳检测
- 资源管理
- 可扩展架构
-
实际应用支持:
- 详细的使用示例
- 错误处理指南
- 性能优化建议
- 常见问题解决方案