一、核心代码框架
基于System.IO.Ports.SerialPort实现三菱FX系列PLC的串口通信,支持D寄存器、M位元件读写操作,包含指令帧构建、校验和计算、响应解析等关键模块。
csharp
using System;
using System.IO.Ports;
using System.Text;
using System.Threading;
public class MitsubishiPLC
{
private SerialPort _serialPort;
private const byte STX = 0x02;
private const byte ETX = 0x03;
private const byte ENQ = 0x05;
private const byte ACK = 0x06;
private const byte NAK = 0x15;
public MitsubishiPLC(string portName, int baudRate = 9600)
{
_serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One);
_serialPort.DataReceived += SerialPort_DataReceived;
}
// 打开串口
public bool Connect()
{
try
{
if (!_serialPort.IsOpen)
{
_serialPort.Open();
Thread.Sleep(100); // 等待初始化
return true;
}
return false;
}
catch (Exception ex)
{
Log($"串口打开失败: {ex.Message}");
return false;
}
}
// 关闭串口
public void Disconnect()
{
if (_serialPort.IsOpen) _serialPort.Close();
}
// 读取D寄存器(示例读取D0-D9)
public ushort[] ReadDRegisters(string startAddr, int count)
{
byte[] cmd = BuildReadCommand('D', startAddr, count);
byte[] response = SendCommand(cmd);
return ParseResponse(response);
}
// 写入单个D寄存器
public bool WriteDRegister(string address, ushort value)
{
byte[] cmd = BuildWriteCommand('D', address, new ushort[] { value });
byte[] response = SendCommand(cmd);
return CheckAck(response);
}
// 构建读取指令
private byte[] BuildReadCommand(char device, string addr, int count)
{
string cmdStr = $"{ENQ}{0x30}{0x30}{device}{addr.PadLeft(4, '0')}{count:X2}03";
byte[] data = Encoding.ASCII.GetBytes(cmdStr);
byte checksum = CalculateChecksum(data);
return data.Concat(new byte[] { checksum, ETX }).ToArray();
}
// 构建写入指令
private byte[] BuildWriteCommand(char device, string addr, ushort[] values)
{
string valueHex = string.Join("", values.Select(v => v.ToString("X4")));
string cmdStr = $"{ENQ}{0x30}{0x30}{device}{addr.PadLeft(4, '0')}{values.Length:X2}{valueHex}";
byte[] data = Encoding.ASCII.GetBytes(cmdStr);
byte checksum = CalculateChecksum(data);
return data.Concat(new byte[] { checksum, ETX }).ToArray();
}
// 发送指令并接收响应
private byte[] SendCommand(byte[] command)
{
try
{
_serialPort.DiscardInBuffer();
_serialPort.Write(command, 0, command.Length);
Thread.Sleep(100); // 等待响应
if (_serialPort.BytesToRead > 0)
{
byte[] response = new byte[_serialPort.BytesToRead];
_serialPort.Read(response, 0, response.Length);
return response;
}
return null;
}
catch (TimeoutException)
{
Log("通信超时");
return null;
}
}
// 校验和计算
private byte CalculateChecksum(byte[] data)
{
byte sum = 0;
foreach (byte b in data)
{
sum += b;
}
return (byte)(0 - sum); // 补码校验
}
// 解析响应数据
private ushort[] ParseResponse(byte[] response)
{
if (response[0] != STX || response[1] != 0x30) return null;
int length = response[2] - 0x30;
ushort[] data = new ushort[length / 2];
for (int i = 0; i < length; i += 2)
{
data[i / 2] = (ushort)(response[3 + i] << 8 | response[4 + i]);
}
return data;
}
// 校验ACK响应
private bool CheckAck(byte[] response)
{
return response != null && response[0] == ACK;
}
// 日志记录
private void Log(string message)
{
File.AppendAllText("plc_log.txt", $"{DateTime.Now}: {message}\r\n");
}
// 数据接收事件
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
// 处理实时数据流(如心跳检测)
}
}
// 使用示例
var plc = new MitsubishiPLC("COM1");
if (plc.Connect())
{
ushort[] data = plc.ReadDRegisters("0000", 10);
plc.WriteDRegister("0010", 1234);
}
二、关键功能解析
-
指令帧结构
-
起始符 :
0x02 (STX) -
站号 :PLC地址(ASCII码,如
0x30表示站号0) -
指令码 :
0x30表示写操作,0x31表示读操作 -
数据地址 :4位ASCII码(如
D0000表示D寄存器起始地址) -
数据长度 :2位十六进制(如
0A表示读取10个字) -
校验和:所有字节累加取补码
-
结束符 :
0x03 (ETX)
-
-
通信参数配置
csharp_serialPort.BaudRate = 9600; // 波特率 _serialPort.DataBits = 8; // 数据位 _serialPort.StopBits = StopBits.One; // 停止位 _serialPort.Parity = Parity.None; // 校验位 -
异常处理策略
-
超时重试:发送指令后等待100ms,未收到响应则重试3次
-
校验失败:丢弃错误帧并记录日志
-
连接状态检测 :定期发送
ENQ指令检测PLC在线状态
-
三、扩展功能实现
-
批量读写优化
csharppublic ushort[] ReadDBlock(string startAddr, int count) { string cmd = $"{ENQ}00FF{startAddr.PadLeft(4, '0')}{count:X4}03"; byte[] data = Encoding.ASCII.GetBytes(cmd); byte checksum = CalculateChecksum(data); _serialPort.Write(data.Concat(new byte[] { checksum, ETX }).ToArray()); // 等待完整响应(最大3秒) byte[] response = ReadCompleteResponse(); return ParseResponse(response); } -
M位元件操作
csharppublic bool WriteM(string address, bool[] bits) { string hexStr = ""; foreach (bool bit in bits) { hexStr += bit ? "1" : "0"; } byte[] cmd = Encoding.ASCII.GetBytes($"{ENQ}00FFM{address.PadLeft(4, '0')}{hexStr.Length}00{hexStr}"); byte checksum = CalculateChecksum(cmd); _serialPort.Write(cmd.Concat(new byte[] { checksum, ETX }).ToArray()); return CheckAck(_serialPort.ReadExisting().ToByteArray()); }
四、调试与优化建议
-
串口调试工具
-
使用
Putty或SecureCRT验证基础通信,设置参数:csharp波特率: 9600 | 数据位:8 | 停止位:1 | 校验:无
-
-
性能监控
csharppublic class PerformanceMetrics { public long MessagesSent { get; set; } public long MessagesReceived { get; set; } public double CPUUsage { get; set; } public double MemoryUsage { get; set; } } -
日志记录
csharppublic static class Logger { public static void Log(string message) { File.AppendAllText("plc_log.txt", $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}{Environment.NewLine}"); } }
参考代码 C#与三菱PLC通讯源码 串口通讯 www.youwenfan.com/contentcsq/57509.html
五、完整项目结构
bash
PLC_Communication/
├── MitsubishiPLC/ # 核心通信类
│ ├── MitsubishiPLC.cs # 主类
│ └── ProtocolParser.cs # 报文解析工具
├── TestApp/ # 测试程序
│ ├── MainForm.cs # 主界面
│ └── PLCConfigForm.cs # 参数配置界面
└── Resources/ # 配置文件
└── plc_params.json # PLC参数存储
六、注意事项
-
通信参数一致性:确保PLC与上位机的波特率、数据位、停止位、校验位完全一致
-
数据缓冲区管理:批量读写时注意PLC的接收缓冲区大小限制(通常≤256字节)
-
实时性要求:关键控制指令建议采用中断接收方式
-
安全防护:工业环境中需增加光电隔离和防浪涌电路