using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Wsfly
{
/// <summary>
/// 委托-回调
/// </summary>
/// <param name="receiveData">十进制列表</param>
/// <param name="receiveHexData">十六进制字符串</param>
public delegate void ModbusSL_Callback(bool success, string msg, List<int> receiveData, string receiveHexData, string resultData);
/// <summary>
/// 三菱PLC Modbus 指令
/// </summary>
public class ModbusSLHandler
{
/*
通讯格式
命令 命令码 目标设备
DEVICE READ CMD "0" X,Y,M,S,T,C,D
DEVICE WRITE CMD "1" X,Y,M,S,T,C,D
FORCE ON CMD "7" X,Y,M,S,T,C
FORCE OFF CMD "8" X,Y,M,S,T,C
————————————————————————————————
16进制代码:
ENQ 05H 请求
ACK 06H PLC正确响应
NAK 15H PLC错误响应
STX 02H 报文开始
ETX 03H 报文结束
————————————————————————————————
发出的命令为11个两位数(如:02 30 31 30 31 34 30 32 03 35 41),并且这些两位数必须为16进制(H)的ASCII码
命令格式Demo:
请求 命令 元件首地址 BYTE数 结束 校验总和
1 2 3 4 5 6 7 8 9 10 11
STX CMD 16(3) 16(2) 16(1) 16(0) 16(1) 16(0) ETX 16(1) 16(0)
02 30 31 30 31 34 30 32 03 35 42
解释说明:
STX: 报文开始为 02
命令: 30 30 为从PLC读取数据,31为写入数据;0的ASCII码16进制表示为30,1的ASCII码为31
元件首地址: 31 30 31 34 D10查表可得其地址的首地址为1014(行为1010,列4,1010+4=1014 16进制的加法),1位数字对应1位ASCII码的16进制表示,1014即为31 30 31 34(1为31,0为30,4为34)
BYTE数: 30 32 即 02 因在三菱PLC中数据寄存器D为两个字节的存储,所以读取时必须为2个字节,即02,0对应30,2对应32,即30 32
ETX: 报文结束为 03
校验总和: 35 42 按照前述照片协议校验和为30+31+30+31+34+30+32+03=15B(16进制加法),取15B后边两位即5B,5对应ASCII中对应16进制为35,B为42
*/
/// <summary>
/// 串口 SerialPort
/// </summary>
private SerialPort _serialPort = null;
/// <summary>
/// 回调函数
/// </summary>
private ModbusSL_Callback _callback = null;
/// <summary>
/// 收到的数据 DataReceive
/// </summary>
private string _receiveHexData = null;
/// <summary>
/// 最后发送的数据
/// </summary>
private string _lastSendData = "";
/*三菱Fx系列PLC地址对应表
Public Const PLC_D_Base_AddRess = 4096
Public Const PLC_D_Special_Base_AddRess = 3584
Public Const PLC_Y_Group_Base_AddRess = 160
Public Const PLC_PY_Group_Base_AddRess = 672
Public Const PLC_T_Group_Base_AddRess = 192
Public Const PLC_OT_Group_Base_AddRess = 704
Public Const PLC_RT_Group_Base_AddRess = 1216
Public Const PLC_M_SINGLE_Base_AddRess = 2048(命令为7或8时)
Public Const PLC_M_Group_Base_AddRess = 256
Public Const PLC_PM_Group_Base_AddRess = 768
Public Const PLC_S_Group_Base_AddRess = 0
Public Const PLC_X_Group_Base_AddRess = 128
Public Const PLC_C_Group_Base_AddRess = 448
Public Const PLC_OC_Group_Base_AddRess = 960
Public Const PLC_RC_Group_Base_AddRess = 1472
Public Const PLC_TV_Group_Base_AddRess = 2048
Public Const PLC_CV16_Group_Base_AddRess = 2560
Public Const PLC_CV32_Group_Base_AddRess = 3072
当我们用DEVICE READ命令时,D100地址=100*2+4096;M100地址=100+256;
不同的是D类型寄存器存放的是字,M寄存器存放的是位,
同样是读两个字节,D100返回的就是PLC中D100地址的值,M类型寄存器返回的是M100到M116的值。
所以当我们用FORCE ON 命令时,M100寄存器地址=100+2048;
FORCE ON/Off命令中地址排列与DEVICE READ/WRITE不同,是低位在前高位在后。
如Y20,地址是0510H,代码中4个字节地址表示为:1005。(注意:Y寄存器为八进制,如Y20地址=16+1280=0510H)
*/
#region 构造函数
/// <summary>
/// 构造函数
/// </summary>
/// <param name="serialPort"></param>
public ModbusSLHandler(SerialPort serialPort)
{
_serialPort = serialPort;
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name="port">端口号</param>
/// <param name="baudRate">波特率</param>
/// <param name="dataBits">数据位</param>
/// <param name="parity">奇偶检验位</param>
/// <param name="stopBits">停止位</param>
/// <param name="handshake">握手协议</param>
public ModbusSLHandler(int port, int baudRate = 9600, int dataBits = 8, Parity parity = Parity.Even, StopBits stopBits = StopBits.One, Handshake handshake = Handshake.None, bool dtrEnable = true, bool rtsEnable = true)
{
//实例化串口
_serialPort = new SerialPort();
_serialPort.PortName = "COM" + port; //端口号
_serialPort.BaudRate = baudRate; //波特率
_serialPort.DataBits = dataBits; //数据位
_serialPort.Parity = parity; //奇偶检验位
_serialPort.StopBits = stopBits; //停止位
_serialPort.Handshake = handshake; //握手协议
_serialPort.DtrEnable = dtrEnable;
_serialPort.RtsEnable = rtsEnable;
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name="port">端口号</param>
/// <param name="baudRate">波特率</param>
/// <param name="dataBits">数据位</param>
/// <param name="parity">奇偶检验位</param>
/// <param name="stopBits">停止位</param>
/// <param name="handshake">握手协议</param>
public ModbusSLHandler(int port, int baudRate, int dataBits, int parity, int stopBits, int handshake, bool dtrEnable = true, bool rtsEnable = true)
{
//实例化串口
_serialPort = new SerialPort();
_serialPort.PortName = "COM" + port; //端口号
_serialPort.BaudRate = baudRate; //波特率
_serialPort.DataBits = dataBits; //数据位
_serialPort.Parity = (Parity)parity; //奇偶检验位
_serialPort.StopBits = (StopBits)stopBits; //停止位
_serialPort.Handshake = (Handshake)handshake; //握手协议
_serialPort.DtrEnable = dtrEnable;
_serialPort.RtsEnable = rtsEnable;
}
#endregion
#region 打开、关闭串口
/// <summary>
/// 打开串口
/// </summary>
public void Open()
{
//是否有串口
if (_serialPort == null) return;
//打开连接
if (!_serialPort.IsOpen)
{
//打开串口
_serialPort.Open();
//串口收到数据处理
_serialPort.DataReceived += _serialPort_DataReceived;
}
}
/// <summary>
/// 关闭串口
/// </summary>
public void Close()
{
//是否有串口
if (_serialPort == null) return;
//关闭连接
if (_serialPort.IsOpen)
{
//串口收到数据处理
_serialPort.DataReceived -= _serialPort_DataReceived;
_serialPort.Close();
//_serialPort.Dispose();
}
}
#endregion
#region 接收数据
/// <summary>
/// 接收数据处理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void _serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
//暂停0.1秒
System.Threading.Thread.Sleep(100);
//发回的字符数
int bytesLength = _serialPort.BytesToRead;
if (bytesLength <= 0) return;
//发回的数据
byte[] bytes = new byte[bytesLength];
//读取数据
int readDataLength = _serialPort.Read(bytes, 0, bytesLength);
if (readDataLength <= 2 || bytes == null) return;
//对数据进行转换
string resultData = Encoding.ASCII.GetString(bytes, 0, bytes.Length);
//要返回的十进制列表
List<int> results = new List<int>();
//转换为10进制、16进制
foreach (byte b in bytes)
{
results.Add(Convert.ToInt32(b));
_receiveHexData += b.ToString("X2") + " ";
}
_receiveHexData = _receiveHexData.Trim();
//验证CRC是否正确
string rhd = _receiveHexData.Replace(" ", "");
string rhd_Check = bytes[bytes.Length - 2].ToString("X2") + " " + bytes.Last().ToString("X2");
string rhd_Data = rhd.Replace(rhd_Check, "");
//验证校验码
bool isRightCRC = false;
string rhdCRC = CRC16(rhd_Data);
if (rhdCRC == rhd_Check)
{
//校验码是正确的
isRightCRC = true;
}
//回调通知
_callback?.Invoke(isRightCRC, "OK", results, _receiveHexData, resultData);
}
#endregion
#region 发送数据
/// <summary>
/// 读数据
/// </summary>
public void DeviceRead(DeviceType type, int addr, int len, ModbusSL_Callback callback)
{
//读寄存器:STX 02H + CMD 30H + 寄存器首地址 + 寄存器位数 + 终止ETX 03H + CRC
switch (type)
{
case DeviceType.S:
addr = addr / 8;
break;
case DeviceType.X:
addr = addr / 8 + 128; //0x0080
break;
case DeviceType.Y:
addr = addr / 8 + 160;//0x00A0
break;
case DeviceType.T:
addr = addr / 8 + 192;//0x00C0
break;
case DeviceType.M:
addr = addr / 8 + 256;//0x0100
break;
case DeviceType.C:
addr = addr / 8 + 448;//0x01C0
break;
case DeviceType.D:
addr = addr * 2 + 4096;//0x1000
break;
}
if (len > 32)
{
callback?.Invoke(false, "读取长度不可大于32", null, null, null);
return;
}
//开始STX + CMD
string zl = "02 30 ";
//寄存器地址
string addrStr = addr.ToString("x").ToUpper().PadLeft(4, '0');
zl += ToASCIIHex(addrStr[0]) + " ";
zl += ToASCIIHex(addrStr[1]) + " ";
zl += ToASCIIHex(addrStr[2]) + " ";
zl += ToASCIIHex(addrStr[3]) + " ";
//寄存器位数
string byteLength = Convert.ToString(len * 2, 16).PadLeft(2, '0').ToUpper();
zl += ToASCIIHex(byteLength[0]) + " ";
zl += ToASCIIHex(byteLength[1]) + " ";
//结束ETX
zl += "03 ";
//校验CRC
zl += CRC16(zl);
//发送数据
SendData(zl, callback);
}
/// <summary>
/// 写数据
/// </summary>
public void DeviceWrite(DeviceType type, int addr, int val, ModbusSL_Callback callback)
{
DeviceWrite(type, addr, new int[] { val }, callback);
}
/// <summary>
/// 写数据
/// </summary>
public void DeviceWrite(DeviceType type, int addr, int[] vals, ModbusSL_Callback callback)
{
//写寄存器:STX 02H+ CMD 31H+ 寄存器首地址 + 寄存器位数 + 写入数据 + 终止ETX 03H + CRC
int len = vals.Length;
switch (type)
{
case DeviceType.S:
addr = addr / 8;
break;
case DeviceType.X:
addr = addr / 8 + 128; //0x0080
break;
case DeviceType.Y:
addr = addr / 8 + 160;//0x00A0
break;
case DeviceType.T:
addr = addr / 8 + 192;//0x00C0
break;
case DeviceType.M:
addr = addr / 8 + 256;//0x0100
break;
case DeviceType.C:
addr = addr / 8 + 448;//0x01C0
break;
case DeviceType.D:
addr = addr * 2 + 4096;//0x1000
break;
}
if (len > 32)
{
callback?.Invoke(false, "写入长度不可大于32", null, null, null);
return;
}
//开始STX + CMD
string zl = "02 31 ";
//寄存器地址
string addrStr = addr.ToString("x").ToUpper().PadLeft(4, '0');
zl += ToASCIIHex(addrStr[0]) + " ";
zl += ToASCIIHex(addrStr[1]) + " ";
zl += ToASCIIHex(addrStr[2]) + " ";
zl += ToASCIIHex(addrStr[3]) + " ";
//寄存器位数
string byteLength = Convert.ToString(len * 2, 16).PadLeft(2, '0').ToUpper();
zl += ToASCIIHex(byteLength[0]) + " ";
zl += ToASCIIHex(byteLength[1]) + " ";
//写入的数据
for (int i = 0; i < len; i++)
{
int val = vals[i];
string valStr = val.ToString("x").PadLeft(4, '0').ToUpper();
//高低字节交换
valStr = valStr.Substring(2, 2) + valStr.Substring(0, 2);
zl += ToASCIIHex(valStr[0]) + " ";
zl += ToASCIIHex(valStr[1]) + " ";
zl += ToASCIIHex(valStr[2]) + " ";
zl += ToASCIIHex(valStr[3]) + " ";
}
//结束ETX
zl += "03 ";
//校验CRC
zl += CRC16(zl);
//发送数据
SendData(zl, callback);
}
/// <summary>
/// 置位
/// </summary>
/// <param name="type"></param>
/// <param name="addr"></param>
/// <param name="callback"></param>
/// <returns></returns>
public void ForceOn(DeviceType type, int addr, ModbusSL_Callback callback)
{
/*
FORCE ON 置位
始命令 地址 终和校验
STX CMD ADDRESS ETX SUM
02H 37H ADDRESS 03H SUM
*/
switch (type)
{
case DeviceType.S:
break;
case DeviceType.X:
addr = addr + 1024; //0x0400
break;
case DeviceType.Y:
addr = addr + 1280; //0x0500
break;
case DeviceType.T:
addr = addr + 1536; //0x0600
break;
case DeviceType.M:
addr = addr + 2048; //0x0800
break;
case DeviceType.C:
addr = addr + 3584; //0x0E00
break;
}
//开始STX + CMD
string zl = "02 37 ";
//寄存器地址
string addrStr = addr.ToString("x").ToUpper().PadLeft(4, '0');
zl += ToASCIIHex(addrStr[2]) + " ";
zl += ToASCIIHex(addrStr[3]) + " ";
zl += ToASCIIHex(addrStr[0]) + " ";
zl += ToASCIIHex(addrStr[1]) + " ";
//结束ETX
zl += "03 ";
//校验CRC
zl += CRC16(zl);
//发送数据
SendData(zl, callback);
}
/// <summary>
/// 复位
/// </summary>
/// <param name="type"></param>
/// <param name="addr"></param>
/// <param name="callback"></param>
/// <returns></returns>
public void ForceOff(DeviceType type, int addr, ModbusSL_Callback callback)
{
/*
FORCE OFF 复位
始 命令 地址 终 和校验
STX CMD ADDRESS ETX SUM
02H 38H ADDRESS 03H SUM
*/
switch (type)
{
case DeviceType.S:
break;
case DeviceType.X:
addr = addr + 1024; //0x0400
break;
case DeviceType.Y:
addr = addr + 1280; //0x0500
break;
case DeviceType.T:
addr = addr + 1536; //0x0600
break;
case DeviceType.M:
addr = addr + 2048; //0x0800
break;
case DeviceType.C:
addr = addr + 3584; //0x0E00
break;
}
//开始STX + CMD
string zl = "02 38 ";
//寄存器地址
string addrStr = addr.ToString("x").ToUpper().PadLeft(4, '0');
zl += ToASCIIHex(addrStr[2]) + " ";
zl += ToASCIIHex(addrStr[3]) + " ";
zl += ToASCIIHex(addrStr[0]) + " ";
zl += ToASCIIHex(addrStr[1]) + " ";
//结束ETX
zl += "03 ";
//校验CRC
zl += CRC16(zl);
//发送数据
SendData(zl, callback);
}
/// <summary>
/// 发送数据
/// </summary>
/// <param name="data"></param>
/// <param name="callback"></param>
public void SendData(string data, ModbusSL_Callback callback)
{
//检查参数
if (_serialPort == null)
{
//回调
callback?.Invoke(false, "串口未定义", null, null, null);
return;
}
if (string.IsNullOrWhiteSpace(data))
{
//回调
callback?.Invoke(false, "发送的数据不可为空", null, null, null);
return;
}
try
{
//回调函数
_callback = callback;
//最后发送的数据
_lastSendData = data;
//清空回传数据
_receiveHexData = string.Empty;
//发送十六进制
SendHexadecimalData(data);
}
catch (Exception ex)
{
throw ex;
}
}
/// <summary>
/// 发送16进制数据
/// </summary>
/// <param name="sendData">发送的十六进制的数据 如:01 06 01 00 00 01</param>
private void SendHexadecimalData(string sendData)
{
//发送的十六进制的数据
sendData = sendData.Replace(" ", "");
//定义发送的字节大小除以2是因为两个字位一个字节,加2是因为RTU发送后面还要加两个字节的校验码
byte[] sendBytes = new byte[sendData.Length / 2 + 2];
int k = 0;
//截取待发送的数据每次截取两位转换成10进制放到一个字节中
for (int i = 0; i < sendData.Length - 1; i = i + 2)
{
//第I个字截取两个
string str = sendData.Substring(i, 2);
//将截取到的两个字符转换成10进制方便异或
sendBytes[k] = Convert.ToByte(str, 16);
k++;
}
//发送数据
_serialPort.Write(sendBytes, 0, sendBytes.Length);
}
#endregion
#region 转换
/// <summary>
/// 转为ASCII的16进制
/// </summary>
/// <param name="c"></param>
/// <returns></returns>
private string ToASCIIHex(char c)
{
return ((int)c).ToString("X2");
}
/// <summary>
/// 生成校验码
/// </summary>
/// <param name="zl"></param>
/// <returns></returns>
private string CRC16(string zl)
{
int value = 0;
string[] zlArr = zl.Trim().Substring(3).Split(' ');
foreach (string x in zlArr)
{
value += Convert.ToInt32(x, 16);
}
var crc = value.ToString("X");
crc = crc.Length > 2 ? crc.Substring(crc.Length - 2) : crc;
var crcZL = "";
crcZL += ToASCIIHex(crc[0]) + " ";
crcZL += ToASCIIHex(crc[1]);
return crcZL;
}
#endregion
#region 处理结果
/// <summary>
/// 结果转整数
/// </summary>
/// <param name="result"></param>
/// <returns></returns>
public int ResultToInt(string result)
{
var vals = ResultToInts(result);
if (vals != null && vals.Length > 0) return vals[0];
return -1;
}
/// <summary>
/// 结果转整数数组
/// </summary>
/// <param name="result"></param>
/// <returns></returns>
public int[] ResultToInts(string result)
{
string[] values = result.Trim().Split(' ');
int datalen = values.Length - 4;
//结果不正确
if (datalen % 4 > 0) return null;
List<int> resultInts = new List<int>();
for (int i = 1; i < datalen; i += 4)
{
//高低位转换
byte[] buff = new byte[4];
buff[0] = Convert.ToByte(values[i + 2], 16);
buff[1] = Convert.ToByte(values[i + 3], 16);
buff[2] = Convert.ToByte(values[i], 16);
buff[3] = Convert.ToByte(values[i + 1], 16);
//转10进制数据
string hex = Encoding.Default.GetString(buff);
int res = Convert.ToInt32(hex, 16);
resultInts.Add(res);
}
return resultInts.ToArray();
}
#endregion
#region DEMO
/// <summary>
/// 转字符
/// </summary>
/// <param name="asciiCode"></param>
/// <returns></returns>
private string Chr(int asciiCode)
{
if (asciiCode >= 0 && asciiCode <= 255)
{
System.Text.ASCIIEncoding asciiEncoding = new System.Text.ASCIIEncoding();
byte[] byteArray = new byte[] { (byte)asciiCode };
return asciiEncoding.GetString(byteArray);
}
else
{
throw new Exception("ASCII Code is not valid.");
}
}
/// <summary>
/// 转ASCII
/// </summary>
/// <param name="character"></param>
/// <returns></returns>
private int Asc(string character)
{
if (character.Length == 1)
{
System.Text.ASCIIEncoding asciiEncoding = new System.Text.ASCIIEncoding();
int intAsciiCode = (int)asciiEncoding.GetBytes(character)[0];
return (intAsciiCode);
}
else
{
throw new Exception("Character is not valid.");
}
}
/// <summary>
/// 和校验
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public string SumCheck(string data)
{
int sum = 0;
for (int i = 0; i < data.Length; i++)
{
sum += Asc(data.Substring(i, 1));
}
string res = sum.ToString("X");
res = res.Substring(res.Length - 2, 2);
return res;
}
public void DeviceWriteDemo()
{
string[] write = new string[] { "1234", "4567" }; //将准备写入PLC的值
//将要写入的值转换成16进制数,补齐两个字节,注意高低字节需要交换
string sWriteData = "";
for (int i = 0; i < write.Length; i++)
{
int val = Convert.ToInt32(write[i].Length > 0 ? write[i] : "0");
string s = val.ToString("X");
while (s.Length < 4)
{
s = "0" + s;
}
sWriteData += s.Substring(2, 2) + s.Substring(0, 2);
}
//写入命令,1表示写入,1194表示D202这个地址的16进制,04表示D202,D203为4个BYTE,1194=(202*2)+4096的16进制数,至于用它表示D202的起始位置,三菱故意要这么麻烦了.
sWriteData = "1119404" + sWriteData + Chr(3);
//chr(2)和chr(3)是构成命令的标志字符,然后加上校验和,命令组织完成
sWriteData = Chr(2) + sWriteData + SumCheck(sWriteData);
//写入串口
//com.Write(sWriteData);
}
private void DeviceReadDemo()
{
string sReadData = "";
//在读PLC中的数据之前,需要先发个指令给它,让它将数据发送到串口,下面的字符串中,chr(2),chr(3)为PLC命令的格式标志,0119404中,0表示读,1194表示D202的起始地址,04表示读D202,D203两个字,共4个字节,66为0119404和chr(3)的校验和,向串口写入"读"命令,其实和向plc地址中写入数据是一样的,只是没有数据,用0表示读
string sReadCmd = Chr(2) + "0119404" + Chr(3) + "66";
//com.Write(sReadCmd);
//等待1秒钟
System.Threading.Thread.Sleep(1000);
// 从串口读数据
byte[] data = new byte[1024];
//com.Read(data, 0, 1024);
//如果首位为2,则表示数据有效.这里有个问题,在第二次读,第2位才为'2',第三次又是首位为2,需要再测试
if (data[0] == 2)
{
string sReceiveData = System.Text.Encoding.ASCII.GetString(data);
//解析命令,将读到的字符解析成数字,注意高低位的转换
for (int i = 1; i < 8; i += 4)
{
string sLow = sReceiveData.Substring(i, 2);
string sHigh = sReceiveData.Substring(i + 2, 2);
int res = Convert.ToInt32(sHigh, 16) + Convert.ToInt32(sLow, 16);
//this.txtRead0.Text += res.ToString() + ",";
}
}
}
#endregion
/// <summary>
/// 目标设备
/// </summary>
public enum DeviceType
{
X,
Y,
M,
S,
T,
C,
D
}
}
}
DEMO:
//实例化
ModbusSLHandler handler = new ModbusSLHandler(3, 19200, 8, 2, 1, 0);
//打开串口
handler.Open();
//读D100
handler.DeviceRead(ModbusSLHandler.DeviceType.D, 100, 1, new ModbusSL_Callback(delegate (bool success, string msg2, List<int> receiveData, string receiveHexData, string resultData)
{
if (success)
{
//成功
//receiveHexData 十六进制数据
}
}));