C# 与三菱PLC通讯解决方案

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. 安装与配置

  1. 创建C#项目(Windows Forms或WPF)

  2. 添加上述代码文件

  3. 对于MX Component:

    • 安装三菱MX Component软件
    • 在项目中添加MxComponent.dll引用
  4. 配置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. 连接问题

  • 错误现象:连接超时或拒绝
  • 解决方案
    1. 检查PLC的IP地址和端口号
    2. 确保PC和PLC在同一网络
    3. 检查防火墙设置
    4. 确认PLC的通讯模块已启用
    5. 对于MC协议,尝试使用不同的端口(5000/5001)

2. 数据读取错误

  • 错误现象:返回错误代码或数据不正确
  • 解决方案
    1. 检查地址格式是否正确(如D1000 vs D1000.0)
    2. 确认软元件类型是否支持
    3. 检查PLC程序是否允许访问该地址
    4. 使用三菱的通讯测试工具验证

3. 性能问题

  • 错误现象:通讯速度慢或卡顿
  • 解决方案
    1. 减少单次读取的数据量
    2. 增加通讯超时时间
    3. 使用批量读写代替单点操作
    4. 优化网络环境(使用有线连接代替无线)
    5. 使用异步操作避免阻塞UI线程

项目总结

这个C#与三菱PLC通讯解决方案提供了:

  1. 多协议支持

    • MC协议(二进制/ASCII)
    • MX Component
    • 可扩展的架构支持其他协议
  2. 完整功能集

    • 单点/多点读写
    • 位/字操作
    • 连接管理
    • 错误处理
    • 日志记录
  3. 企业级特性

    • 线程安全设计
    • 异步操作
    • 心跳检测
    • 资源管理
    • 可扩展架构
  4. 实际应用支持

    • 详细的使用示例
    • 错误处理指南
    • 性能优化建议
    • 常见问题解决方案
相关推荐
2501_933329552 小时前
技术架构深度解析:Infoseek舆情监测系统的全链路设计与GEO时代的技术实践
开发语言·人工智能·分布式·架构
Tong Z2 小时前
常见的限流算法和实现原理
java·开发语言
凭君语未可2 小时前
Java 中的实现类是什么
java·开发语言
wearegogog1232 小时前
离散系统参数辨识与广义预测控制MATLAB实现
开发语言·matlab
史迪仔01122 小时前
[QML] QML IMage图像处理
开发语言·前端·javascript·c++·qt
还在忙碌的吴小二3 小时前
Harness 最佳实践:Java Spring Boot 项目落地 OpenSpec + Claude Code
java·开发语言·spring boot·后端·spring
liliangcsdn3 小时前
mstsc不在“C:\Windows\System32“下在C:\windows\WinSxS\anmd64xxx“问题分析
开发语言·windows