三菱FX3U系列PLC作为工业自动化领域的经典控制器,在中小型产线中占据重要市场份额。传统上,工程师需要通过专用编程电缆(如USB-SC09)连接PLC进行调试,这种点对点连接方式在需要远程监控或多设备集成的场景中显得力不从心。以太网MC协议(MELSEC Communication Protocol)的引入,使得通过标准以太网接口访问PLC内部寄存器成为可能。
我在去年参与的一个食品包装产线改造项目中,就遇到了需要将12台FX3U PLC数据集中采集到MES系统的需求。如果采用传统的串口服务器方案,不仅布线复杂,而且实时性难以保证。最终我们通过MC协议实现PLC直连交换机,数据采集周期从原来的2秒缩短到200毫秒以内。这个案例让我深刻认识到掌握MC协议开发的重要性。
MC协议采用典型的请求-响应模型,通信帧由三部分组成:
以读取D100-D109这10个数据寄存器为例,完整的请求帧如下(十六进制表示):
code复制50 00 FF FF 03 00 0C 00 00 01 04 01 00 00 A8 01 0A 00
其中关键字段解析:
三菱PLC采用独特的地址编码系统,需要特别注意:
元件类型码 + 地址/8
重要提示:FX3U的元件地址范围与Q/L系列不同,例如D寄存器在FX3U上最大只能到D7999,超出范围会导致通信错误。
采用异步Socket实现基础通信,关键类设计如下:
csharp复制public class McProtocolClient : IDisposable
{
private Socket _socket;
private readonly IPEndPoint _endPoint;
private const int ReceiveTimeout = 1000;
public McProtocolClient(string ip, int port = 5002)
{
_endPoint = new IPEndPoint(IPAddress.Parse(ip), port);
_socket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp);
_socket.ReceiveTimeout = ReceiveTimeout;
}
public async Task ConnectAsync()
{
await _socket.ConnectAsync(_endPoint);
}
private async Task<byte[]> SendAndReceive(byte[] request)
{
await _socket.SendAsync(new ArraySegment<byte>(request),
SocketFlags.None);
var buffer = new byte[256];
var received = await _socket.ReceiveAsync(
new ArraySegment<byte>(buffer),
SocketFlags.None);
return buffer.Take(received).ToArray();
}
// 其他协议方法...
}
设计指令构造类处理地址转换和帧组装:
csharp复制public static class McCommandBuilder
{
public static byte[] BuildReadCommand(
DeviceType device,
int address,
int points)
{
var stream = new MemoryStream();
using (var writer = new BinaryWriter(stream))
{
// 子头部
writer.Write(new byte[] { 0x50, 0x00, 0xFF, 0xFF, 0x03, 0x00 });
// 报文头部
writer.Write((short)0x000C); // 请求数据长度
writer.Write((short)0x0000); // 监控定时器
writer.Write((short)0x0401); // 读指令
// 报文数据
writer.Write((byte)GetDeviceCode(device));
writer.Write((short)address); // 小端序
writer.Write((short)points); // 小端序
return stream.ToArray();
}
}
private static byte GetDeviceCode(DeviceType device)
{
return device switch
{
DeviceType.X => 0x9C,
DeviceType.Y => 0x9D,
DeviceType.M => 0x90,
DeviceType.D => 0xA8,
_ => throw new ArgumentException("Unsupported device type")
};
}
}
扩展McProtocolClient类添加读操作:
csharp复制public async Task<short[]> ReadWordsAsync(
DeviceType device,
int address,
int points)
{
var command = McCommandBuilder.BuildReadCommand(
device, address, points);
var response = await SendAndReceive(command);
// 解析响应帧
if (response.Length < 11 || response[9] != 0)
throw new McProtocolException("Invalid response");
var result = new short[points];
for (int i = 0; i < points; i++)
{
int offset = 11 + i * 2;
result[i] = BitConverter.ToInt16(response, offset);
}
return result;
}
字元件写入方法示例:
csharp复制public async Task WriteWordsAsync(
DeviceType device,
int address,
short[] values)
{
var stream = new MemoryStream();
using (var writer = new BinaryWriter(stream))
{
// 子头部和报文头部
writer.Write(new byte[] { 0x50, 0x00, 0xFF, 0xFF, 0x03, 0x00 });
writer.Write((short)(9 + values.Length * 2)); // 数据长度
writer.Write((short)0x0000); // 监控定时器
writer.Write((short)0x1401); // 写字指令
// 报文数据
writer.Write((byte)GetDeviceCode(device));
writer.Write((short)address);
writer.Write((short)values.Length);
foreach (var value in values)
writer.Write(value);
}
var response = await SendAndReceive(stream.ToArray());
if (response.Length < 11 || response[9] != 0)
throw new McProtocolException("Write operation failed");
}
批量读取优化:
csharp复制// 推荐方式 - 单次读取多个寄存器
var batchData = await client.ReadWordsAsync(DeviceType.D, 100, 50);
// 不推荐 - 多次单独读取
for (int i = 0; i < 50; i++)
{
var singleData = await client.ReadWordsAsync(DeviceType.D, 100 + i, 1);
}
连接池管理:
csharp复制public class McConnectionPool
{
private readonly ConcurrentDictionary<string, Lazy<McProtocolClient>> _pool;
public async Task<McProtocolClient> GetClientAsync(string ip)
{
var lazyClient = _pool.GetOrAdd(ip,
new Lazy<McProtocolClient>(() => new McProtocolClient(ip)));
var client = lazyClient.Value;
if (!client.IsConnected)
await client.ConnectAsync();
return client;
}
}
超时重试机制:
csharp复制public async Task<T> RetryAsync<T>(
Func<Task<T>> operation,
int maxRetries = 3)
{
int retryCount = 0;
while (true)
{
try
{
return await operation();
}
catch (SocketException ex) when (retryCount < maxRetries)
{
retryCount++;
await Task.Delay(100 * retryCount);
_socket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp);
await ConnectAsync();
}
}
}
心跳检测实现:
csharp复制public async Task StartHeartbeatAsync(
TimeSpan interval,
CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
try
{
await ReadWordsAsync(DeviceType.D, 0, 1);
await Task.Delay(interval, ct);
}
catch
{
// 触发重连逻辑
await ReconnectAsync();
}
}
}
| 错误代码 | 含义 | 解决方案 |
|---|---|---|
| 0x00C1 | 目标不可用 | 检查PLC运行状态,确认IP地址正确 |
| 0x00C2 | 指令不支持 | 确认FX3U支持该指令(如不支持批量位操作) |
| 0x00C4 | 数据格式错误 | 检查元件地址是否超出范围 |
| 0x00C5 | 通信超时 | 检查网线连接,调整监控定时器值 |
Wireshark抓包分析:
tcp.port == 5002PLC侧诊断:
C#调试技巧:
csharp复制// 在SendAndReceive方法中添加日志
Console.WriteLine($"Sent: {BitConverter.ToString(request)}");
Console.WriteLine($"Received: {BitConverter.ToString(response)}");
通过将MC协议客户端封装为OPC UA服务器,实现标准化接入:
csharp复制public class McOpcUaServer : IDisposable
{
private readonly McProtocolClient _mcClient;
private readonly ApplicationInstance _application;
public McOpcUaServer(string plcIp)
{
_mcClient = new McProtocolClient(plcIp);
_application = new ApplicationInstance {
ApplicationName = "FX3U OPC UA Server",
ApplicationType = ApplicationType.Server
};
// 配置服务器证书和端点
var serverConfig = new ApplicationConfiguration {
// ... 省略配置细节
};
}
public async Task StartAsync()
{
await _mcClient.ConnectAsync();
// 创建地址空间
var namespaceManager = new NamespaceManager();
var factory = new Opc.Ua.Server.StandardServer();
// 映射PLC数据到OPC节点
var variables = new List<BaseDataVariableState>();
for (int i = 0; i < 100; i++)
{
var variable = new BaseDataVariableState {
NodeId = new NodeId($"D{i}", 2),
DisplayName = $"D{i}",
DataType = DataTypeIds.Int16,
ValueRank = ValueRanks.Scalar,
AccessLevel = AccessLevels.CurrentRead | AccessLevels.CurrentWrite
};
variables.Add(variable);
}
// 启动数据同步任务
_ = Task.Run(async () => {
while (true)
{
var values = await _mcClient.ReadWordsAsync(DeviceType.D, 0, 100);
for (int i = 0; i < values.Length; i++)
{
variables[i].Value = values[i];
}
await Task.Delay(200);
}
});
}
}
基于MC协议构建的工业物联网架构示例:
code复制[FX3U PLC] ←MC协议→ [边缘网关(C#)] ←MQTT→ [云平台]
↳ 本地缓存
↳ 协议转换
↳ 断线续传
边缘网关核心功能实现:
csharp复制public class EdgeGateway
{
private readonly McProtocolClient _plcClient;
private readonly IMqttClient _mqttClient;
private readonly TimeSeriesDbClient _dbClient;
public async Task RunAsync()
{
// 初始化连接
await _plcClient.ConnectAsync();
await _mqttClient.ConnectAsync();
// 创建数据采集管道
var transformBlock = new TransformBlock<short[], MqttMessage>(
data => new MqttMessage {
Topic = "plc/d100",
Payload = JsonConvert.SerializeObject(data)
});
var broadcastBlock = new BroadcastBlock<MqttMessage>(msg => msg);
// 数据流处理
var processingPipeline = new ActionBlock<MqttMessage>(async msg => {
await Task.WhenAll(
_mqttClient.PublishAsync(msg),
_dbClient.InsertAsync("plc_data", msg.Payload)
);
});
// 构建管道链路
transformBlock.LinkTo(broadcastBlock);
broadcastBlock.LinkTo(processingPipeline);
// 启动采集循环
while (true)
{
var data = await _plcClient.ReadWordsAsync(DeviceType.D, 100, 10);
await transformBlock.SendAsync(data);
await Task.Delay(500);
}
}
}
在实际项目中,这种架构可以将PLC数据同时发送到云端和本地时序数据库,确保在网络波动时数据不丢失。我曾在一个水处理项目中采用类似方案,实现了98.7%的数据完整率。