在工业自动化领域,PLC(可编程逻辑控制器)作为控制核心,与上位机的通讯是实现数据采集、监控和控制的关键环节。三菱PLC广泛用于各类工业场景,而MC协议(Melsec Communication Protocol)正是三菱为其PLC设备开发的专用通讯协议。
MC协议支持多种物理层连接方式,包括串口(RS232/RS485)和以太网。协议采用主从架构,上位机作为主站主动发起请求,PLC作为从站响应请求。协议帧结构包含帧头、命令代码、地址信息、数据内容和校验等部分,确保通讯的可靠性和安全性。
注意:不同系列的三菱PLC(如FX系列、Q系列)在MC协议实现细节上可能有差异,开发前需确认PLC具体型号和对应的协议手册。
实现C#上位机与三菱PLC通讯,首先需要完成硬件连接。对于串口通讯(RS232)方式,典型连接配置如下:
对于RS485通讯,需注意终端电阻配置和A/B线极性,长距离通讯时建议使用屏蔽双绞线。
在GX Works2等编程软件中,需对PLC进行以下通讯参数配置:
以下是增强版的MitsubishiPLCCommunicator类实现,增加了超时处理、校验计算和异常管理:
csharp复制using System;
using System.IO.Ports;
using System.Text;
using System.Threading;
public class MitsubishiPLCCommunicator : IDisposable
{
private SerialPort serialPort;
private int timeout = 1000; // 默认超时1秒
public MitsubishiPLCCommunicator(string portName, int baudRate,
Parity parity = Parity.None,
int dataBits = 8,
StopBits stopBits = StopBits.One)
{
serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits);
serialPort.Handshake = Handshake.None;
serialPort.ReadTimeout = timeout;
serialPort.WriteTimeout = timeout;
}
public void Open()
{
if (!serialPort.IsOpen)
{
try
{
serialPort.Open();
Thread.Sleep(100); // 等待串口稳定
}
catch (Exception ex)
{
throw new Exception($"串口打开失败: {ex.Message}");
}
}
}
public void SendCommand(byte[] commandBytes)
{
if (!serialPort.IsOpen)
throw new Exception("串口未打开");
try
{
serialPort.DiscardInBuffer();
serialPort.DiscardOutBuffer();
serialPort.Write(commandBytes, 0, commandBytes.Length);
}
catch (Exception ex)
{
throw new Exception($"命令发送失败: {ex.Message}");
}
}
public byte[] ReceiveResponse(int expectedLength)
{
byte[] buffer = new byte[expectedLength];
int bytesRead = 0;
DateTime startTime = DateTime.Now;
while (bytesRead < expectedLength)
{
if ((DateTime.Now - startTime).TotalMilliseconds > timeout)
throw new TimeoutException("接收响应超时");
if (serialPort.BytesToRead > 0)
{
bytesRead += serialPort.Read(buffer, bytesRead,
Math.Min(serialPort.BytesToRead,
expectedLength - bytesRead));
}
Thread.Sleep(10);
}
return buffer;
}
public void Dispose()
{
if (serialPort != null && serialPort.IsOpen)
{
serialPort.Close();
serialPort.Dispose();
}
}
}
MC协议采用固定的帧格式,以下是关键帧构造方法:
csharp复制public class MCProtocolFrameBuilder
{
// 常用指令代码
private const byte CMD_READ = 0x01; // 读命令
private const byte CMD_WRITE = 0x02; // 写命令
public static byte[] BuildReadCommand(string deviceType, int startAddress, int points)
{
// 设备类型转换(如"D"→0xA8)
byte deviceCode = GetDeviceCode(deviceType);
// 构造帧数据部分
byte[] data = new byte[12];
data[0] = 0x01; // 副头部
data[1] = CMD_READ;
data[2] = deviceCode;
// 地址处理(三菱PLC地址为3字节)
byte[] addressBytes = BitConverter.GetBytes(startAddress);
data[3] = addressBytes[1]; // 地址高位
data[4] = addressBytes[0]; // 地址低位
data[5] = 0x00; // 地址扩展
// 点数处理
byte[] pointsBytes = BitConverter.GetBytes((short)points);
data[6] = pointsBytes[1]; // 点数高位
data[7] = pointsBytes[0]; // 点数低位
// 计算校验和
byte checksum = CalculateChecksum(data, 0, 8);
data[8] = checksum;
// 添加帧头帧尾
byte[] frame = new byte[11];
frame[0] = 0x05; // ENQ
Array.Copy(data, 0, frame, 1, 9);
frame[10] = 0x04; // EOT
return frame;
}
private static byte CalculateChecksum(byte[] data, int start, int length)
{
byte sum = 0;
for (int i = start; i < start + length; i++)
{
sum += data[i];
}
return (byte)(sum & 0xFF);
}
private static byte GetDeviceCode(string deviceType)
{
switch (deviceType.ToUpper())
{
case "D": return 0xA8; // 数据寄存器
case "M": return 0x90; // 内部继电器
case "X": return 0x9C; // 输入继电器
case "Y": return 0x9D; // 输出继电器
default: throw new ArgumentException("未知的设备类型");
}
}
}
csharp复制class Program
{
static void Main()
{
try
{
using (var communicator = new MitsubishiPLCCommunicator("COM3", 9600))
{
communicator.Open();
// 构建读取D100开始的两个寄存器的命令帧
byte[] readCommand = MCProtocolFrameBuilder.BuildReadCommand("D", 100, 2);
// 发送命令
communicator.SendCommand(readCommand);
// 接收响应(响应帧长度根据读取点数变化)
byte[] response = communicator.ReceiveResponse(11 + 4*2); // 11字节固定头+4字节/点
// 解析响应数据
if (response[0] == 0x06) // ACK
{
// 提取数据部分(假设为16位有符号整数)
short value1 = BitConverter.ToInt16(new byte[] { response[6], response[5] }, 0);
short value2 = BitConverter.ToInt16(new byte[] { response[10], response[9] }, 0);
Console.WriteLine($"D100: {value1}, D101: {value2}");
}
else if (response[0] == 0x15) // NAK
{
Console.WriteLine($"PLC返回错误: {response[1].ToString("X2")}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"通讯异常: {ex.Message}");
}
}
}
PLC数据存储有以下特点需要注意:
以下是常用数据转换方法:
csharp复制// 16位整数转换(考虑字节顺序)
public static short GetInt16FromPLC(byte[] data, int startIndex)
{
return BitConverter.ToInt16(new byte[] { data[startIndex+1], data[startIndex] }, 0);
}
// 32位浮点数转换
public static float GetFloatFromPLC(byte[] data, int startIndex)
{
byte[] bytes = new byte[4];
bytes[3] = data[startIndex];
bytes[2] = data[startIndex+1];
bytes[1] = data[startIndex+2];
bytes[0] = data[startIndex+3];
return BitConverter.ToSingle(bytes, 0);
}
对于大量数据读写,建议采用批量操作减少通讯次数:
csharp复制// 批量读取多个连续寄存器
public static Dictionary<int, short> BatchReadRegisters(
MitsubishiPLCCommunicator comm,
string deviceType,
int startAddress,
int count)
{
var results = new Dictionary<int, short>();
int remaining = count;
int currentAddr = startAddress;
// 三菱MC协议单次最多可读取64个字
const int MAX_READ = 64;
while (remaining > 0)
{
int thisRead = Math.Min(remaining, MAX_READ);
byte[] cmd = MCProtocolFrameBuilder.BuildReadCommand(
deviceType, currentAddr, thisRead);
comm.SendCommand(cmd);
byte[] resp = comm.ReceiveResponse(11 + 4*thisRead);
// 解析响应并填充字典
for (int i = 0; i < thisRead; i++)
{
int offset = 5 + i*4;
short value = BitConverter.ToInt16(new byte[] { resp[offset+1], resp[offset] }, 0);
results.Add(currentAddr + i, value);
}
currentAddr += thisRead;
remaining -= thisRead;
}
return results;
}
工业现场通讯易受干扰,需建立完善的异常处理机制:
超时重试机制:
数据校验:
连接状态监测:
错误代码处理:
示例重试逻辑实现:
csharp复制public static byte[] SendWithRetry(
MitsubishiPLCCommunicator comm,
byte[] command,
int expectedLength,
int maxRetries = 3)
{
int retryCount = 0;
Exception lastError = null;
while (retryCount < maxRetries)
{
try
{
comm.SendCommand(command);
return comm.ReceiveResponse(expectedLength);
}
catch (TimeoutException ex)
{
lastError = ex;
retryCount++;
Thread.Sleep(100 * retryCount); // 指数退避
}
catch (Exception ex)
{
throw ex; // 非超时异常直接抛出
}
}
throw new Exception($"通讯失败,重试{maxRetries}次后仍不成功", lastError);
}
通讯频率控制:
数据打包策略:
缓存机制:
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 通讯超时 | 1. 物理连接故障 2. 波特率不匹配 3. PLC未进入MC协议模式 |
1. 检查电缆连接 2. 确认双方波特率一致 3. 使用GX Works2确认PLC设置 |
| 收到乱码 | 1. 波特率/数据位设置错误 2. 电气干扰 3. 协议帧格式错误 |
1. 用示波器检查信号质量 2. 尝试降低波特率 3. 检查起始/停止位设置 |
| PLC返回NAK | 1. 指令格式错误 2. 地址越界 3. 写保护 |
1. 分析错误代码 2. 检查地址是否合法 3. 确认PLC是否处于RUN模式 |
| 间歇性通讯失败 | 1. 线路接触不良 2. 强电磁干扰 3. 电源不稳定 |
1. 更换通讯电缆 2. 增加终端电阻 3. 使用屏蔽线并良好接地 |
串口调试助手:
PLC模拟器:
逻辑分析仪:
自定义日志系统:
csharp复制// 简易通讯日志记录方法
public static void LogCommunication(byte[] sent, byte[] received)
{
string logEntry = $"{DateTime.Now:HH:mm:ss.fff}\n" +
$"Sent: {BitConverter.ToString(sent)}\n" +
$"Received: {(received != null ? BitConverter.ToString(received) : "Timeout")}\n";
File.AppendAllText("comm.log", logEntry);
}
在实际项目中,我发现最容易被忽视的是接地问题——不良的接地会导致间歇性通讯故障。曾有一个项目因为PLC和上位机分别接在不同电位的接地线上,导致RS485通讯异常,统一接地后问题立即解决。另一个常见误区是过度频繁地开关串口连接,实际上保持长连接并妥善处理异常才是更可靠的做法。