在工业自动化领域,PLC(可编程逻辑控制器)与上位机的数据交互一直是系统集成中的关键环节。三菱FX5U作为一款广泛应用于中小型自动化设备的PLC,其以太网通讯功能的实现对于设备监控、数据采集和远程控制具有重要意义。这个开源项目提供了一个基于C#的FX5U以太网通讯库,实现了对PLC内部寄存器的读写操作。
核心功能可以概括为:
这个工具特别适合需要开发以下场景的应用:
FX5U通过以太网通讯时使用的是三菱专用的MC协议(MELSEC Communication Protocol),这是一种基于TCP/IP的工业通讯协议。协议帧结构主要包含以下几个部分:
子头部(8字节):
报文头部(12字节):
请求数据部分:
提示:实际编程时需要特别注意字节序问题,三菱PLC采用大端序(Big-Endian),而x86架构的PC是小端序(Little-Endian),需要进行转换。
要实现C#程序与FX5U的通讯,需要先完成以下硬件配置:
PLC端设置:
PC端要求:
物理连接:
项目中的核心类是MitsubishiFX5U,它封装了与PLC通讯的所有功能。类的主要结构如下:
csharp复制public class MitsubishiFX5U : IDisposable
{
private TcpClient _tcpClient;
private NetworkStream _stream;
private string _ipAddress;
private int _port;
private int _timeout;
// 构造函数
public MitsubishiFX5U(string ip, int port = 5002, int timeout = 1000)
{
_ipAddress = ip;
_port = port;
_timeout = timeout;
}
// 连接PLC
public bool Connect()
{
try {
_tcpClient = new TcpClient();
var result = _tcpClient.BeginConnect(_ipAddress, _port, null, null);
var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(_timeout));
if (!success || !_tcpClient.Connected)
return false;
_stream = _tcpClient.GetStream();
_stream.ReadTimeout = _timeout;
return true;
}
catch {
return false;
}
}
// 断开连接
public void Disconnect()
{
_stream?.Close();
_tcpClient?.Close();
}
// 实现IDisposable
public void Dispose()
{
Disconnect();
}
// 其他读写方法...
}
读取不同寄存器的核心方法采用统一的处理流程,以下是读取D寄存器的典型实现:
csharp复制public short[] ReadDRegisters(int startAddress, int count)
{
// 构造读取命令帧
byte[] command = BuildReadCommand(0x9C, startAddress, count);
// 发送命令
_stream.Write(command, 0, command.Length);
// 接收响应
byte[] responseHeader = new byte[22];
_stream.Read(responseHeader, 0, 22);
// 验证响应头
if (responseHeader[11] != 0 || responseHeader[19] != 0)
throw new Exception("PLC returned error status");
// 读取数据部分
int dataLength = BitConverter.ToInt16(new byte[] { responseHeader[21], responseHeader[20] }, 0);
byte[] responseData = new byte[dataLength];
_stream.Read(responseData, 0, dataLength);
// 转换数据为short数组
short[] result = new short[count];
for (int i = 0; i < count; i++)
{
int offset = i * 2;
result[i] = BitConverter.ToInt16(new byte[] { responseData[offset+1], responseData[offset] }, 0);
}
return result;
}
private byte[] BuildReadCommand(byte deviceType, int startAddress, int count)
{
// 子头部
byte[] subHeader = new byte[] { 0x50, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00 };
// 报文头部
byte[] header = new byte[12];
header[0] = 0x10; // 监视定时器低位
header[1] = 0x00; // 监视定时器高位
header[2] = 0x01; // 指令代码低位(0401表示批量读取)
header[3] = 0x04; // 指令代码高位
header[4] = 0x00; // 子指令代码低位
header[5] = 0x00; // 子指令代码高位
// 请求数据部分
byte[] requestData = new byte[6];
requestData[0] = deviceType; // 设备类型
// 起始地址(3字节,大端序)
requestData[1] = (byte)((startAddress >> 16) & 0xFF);
requestData[2] = (byte)((startAddress >> 8) & 0xFF);
requestData[3] = (byte)(startAddress & 0xFF);
// 点数(2字节)
requestData[4] = (byte)(count & 0xFF);
requestData[5] = (byte)((count >> 8) & 0xFF);
// 设置数据长度
byte[] dataLength = BitConverter.GetBytes(requestData.Length);
header[6] = dataLength[0];
header[7] = dataLength[1];
// 合并所有部分
byte[] command = new byte[subHeader.Length + header.Length + requestData.Length];
Array.Copy(subHeader, 0, command, 0, subHeader.Length);
Array.Copy(header, 0, command, subHeader.Length, header.Length);
Array.Copy(requestData, 0, command, subHeader.Length + header.Length, requestData.Length);
return command;
}
注意:读取X/Y/M/S寄存器时,设备类型代码不同(X=9C,Y=9D,M=90,S=98),但基本流程相同。这些寄存器通常按位操作,需要特别处理位与字节的转换。
写入操作与读取类似,但需要构造不同的指令代码。以下是写入D寄存器的实现:
csharp复制public void WriteDRegisters(int startAddress, short[] values)
{
// 构造写入数据部分
byte[] dataBytes = new byte[values.Length * 2];
for (int i = 0; i < values.Length; i++)
{
byte[] bytes = BitConverter.GetBytes(values[i]);
dataBytes[i*2] = bytes[1]; // 大端序调整
dataBytes[i*2+1] = bytes[0];
}
// 构造写入命令帧
byte[] command = BuildWriteCommand(0x9C, startAddress, values.Length, dataBytes);
// 发送命令
_stream.Write(command, 0, command.Length);
// 接收响应
byte[] response = new byte[22];
_stream.Read(response, 0, 22);
// 验证响应
if (response[11] != 0 || response[19] != 0)
throw new Exception("PLC returned error status");
}
private byte[] BuildWriteCommand(byte deviceType, int startAddress, int count, byte[] data)
{
// 子头部和报文头部与读取类似
byte[] subHeader = new byte[] { 0x50, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00 };
byte[] header = new byte[12];
header[0] = 0x10; // 监视定时器低位
header[1] = 0x00; // 监视定时器高位
header[2] = 0x01; // 指令代码低位(1401表示批量写入)
header[3] = 0x14; // 指令代码高位
header[4] = 0x00; // 子指令代码低位
header[5] = 0x00; // 子指令代码高位
// 请求数据部分
byte[] requestData = new byte[6 + data.Length];
requestData[0] = deviceType; // 设备类型
// 起始地址(3字节,大端序)
requestData[1] = (byte)((startAddress >> 16) & 0xFF);
requestData[2] = (byte)((startAddress >> 8) & 0xFF);
requestData[3] = (byte)(startAddress & 0xFF);
// 点数(2字节)
requestData[4] = (byte)(count & 0xFF);
requestData[5] = (byte)((count >> 8) & 0xFF);
// 写入数据
Array.Copy(data, 0, requestData, 6, data.Length);
// 设置数据长度
byte[] dataLength = BitConverter.GetBytes(requestData.Length);
header[6] = dataLength[0];
header[7] = dataLength[1];
// 合并所有部分
byte[] command = new byte[subHeader.Length + header.Length + requestData.Length];
Array.Copy(subHeader, 0, command, 0, subHeader.Length);
Array.Copy(header, 0, command, subHeader.Length, header.Length);
Array.Copy(requestData, 0, command, subHeader.Length + header.Length, requestData.Length);
return command;
}
下面是一个完整的控制台应用示例,展示如何使用这个库与FX5U进行交互:
csharp复制class Program
{
static void Main(string[] args)
{
// 创建PLC连接实例
using (var plc = new MitsubishiFX5U("192.168.1.10"))
{
try
{
// 连接PLC
if (!plc.Connect())
{
Console.WriteLine("无法连接到PLC");
return;
}
Console.WriteLine("成功连接PLC");
// 读取D100开始的5个寄存器
short[] dRegisters = plc.ReadDRegisters(100, 5);
Console.WriteLine("D100-D104的值:");
for (int i = 0; i < dRegisters.Length; i++)
{
Console.WriteLine($"D{100 + i} = {dRegisters[i]}");
}
// 读取X0-X7的状态
bool[] xInputs = plc.ReadXBits(0, 8);
Console.WriteLine("X0-X7的状态:");
for (int i = 0; i < xInputs.Length; i++)
{
Console.WriteLine($"X{i} = {xInputs[i]}");
}
// 设置Y0-Y3的输出
plc.WriteYBits(0, new bool[] { true, false, true, false });
Console.WriteLine("已设置Y0-Y3输出");
// 写入D200-D202的值
plc.WriteDRegisters(200, new short[] { 100, 200, 300 });
Console.WriteLine("已写入D200-D202的值");
}
catch (Exception ex)
{
Console.WriteLine($"发生错误:{ex.Message}");
}
}
Console.WriteLine("按任意键退出...");
Console.ReadKey();
}
}
在实际工业环境中使用时,可以考虑以下优化措施:
连接池管理:
批量操作优化:
异常处理增强:
异步操作支持:
csharp复制public async Task<short[]> ReadDRegistersAsync(int startAddress, int count)
{
byte[] command = BuildReadCommand(0x9C, startAddress, count);
await _stream.WriteAsync(command, 0, command.Length);
byte[] responseHeader = new byte[22];
await _stream.ReadAsync(responseHeader, 0, 22);
if (responseHeader[11] != 0 || responseHeader[19] != 0)
throw new Exception("PLC returned error status");
int dataLength = BitConverter.ToInt16(new byte[] { responseHeader[21], responseHeader[20] }, 0);
byte[] responseData = new byte[dataLength];
await _stream.ReadAsync(responseData, 0, dataLength);
short[] result = new short[count];
for (int i = 0; i < count; i++)
{
int offset = i * 2;
result[i] = BitConverter.ToInt16(new byte[] { responseData[offset+1], responseData[offset] }, 0);
}
return result;
}
数据缓存策略:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 连接超时 | 网络不通 | 检查网线连接,ping测试PLC IP |
| PLC IP配置错误 | 使用GX Works3确认PLC网络设置 | |
| 端口被占用 | 确认5002端口未被其他程序使用 | |
| 连接被拒绝 | 未启用MC协议 | 在GX Works3中启用MC协议通讯 |
| 防火墙阻挡 | 关闭防火墙或添加端口例外 |
数据读取不完整:
写入操作失败:
通讯速度慢:
位寄存器操作:
csharp复制public bool[] ReadXBits(int startBit, int count)
{
// FX5U中X寄存器每16位为一个单元
int wordCount = (count + 15) / 16;
short[] words = ReadDRegisters(0x300 + startBit/16, wordCount);
bool[] bits = new bool[count];
for (int i = 0; i < count; i++)
{
int wordIndex = i / 16;
int bitIndex = i % 16;
bits[i] = (words[wordIndex] & (1 << bitIndex)) != 0;
}
return bits;
}
32位数据处理:
csharp复制public int ReadDRegister32(int startAddress)
{
short[] values = ReadDRegisters(startAddress, 2);
return (values[1] << 16) | (values[0] & 0xFFFF);
}
字符串处理:
csharp复制public string ReadString(int startAddress, int length)
{
int registerCount = (length + 1) / 2;
short[] registers = ReadDRegisters(startAddress, registerCount);
StringBuilder sb = new StringBuilder();
foreach (short reg in registers)
{
sb.Append((char)(reg & 0xFF));
sb.Append((char)((reg >> 8) & 0xFF));
}
return sb.ToString().Substring(0, length);
}
支持更多PLC型号:
添加协议解析工具:
集成OPC UA支持:
开发可视化配置工具:
依赖注入支持:
跨平台支持:
性能监控:
安全增强:
设备监控系统:
生产数据采集:
远程维护工具:
自动化测试平台:
在工业现场实际使用这个通讯库时,建议先进行充分的测试,特别是在写入操作前务必确认目标寄存器的用途,避免误操作导致设备异常。对于关键生产设备,考虑添加硬件写保护或软件权限控制,确保操作安全。