ModbusRTU\TCP消息帧解析(C#实现报文发送与解析)
知识点
- PLC寄存器中存储(整型和无符号整型:2字节。长整型:4字节。单精度浮点数:4字节。双精度浮点数:8字节),我们只要知道数据类型,是2个字节一截取,还是4个字节 ,对接收到的报文进行字节截取然后编码成str就行
- 向PLC中写入Float,float占4个字节=2个寄存器,所以要使用功能码“写多寄存器0x10”, 功能码0x06只能写一个寄存器”
- serialPort.write(bytes,0,bytes.Length); thread.sleep(300); serialPort.Read() 发完指令后,要等待从站响应300ms,然后再去读数据
- 主站请求从站有两种方式:主动(手动点击查询线圈状态的按钮)被动(通过委托方式,一件事情的发生触发另外事件。场景:称菜,菜一放上去,触发去查询的功能代码块)
- 一个F要用4个二进制表示,两个F用8个二进制表示,所以 0xFA :表示1个字节
- modbusTCP响应 Tx:00 00 00 00 00 03 01 83 02 【83=1000 0011 (功能码03 的高位为1,就是异常)02是错误码代号要查表】
- send()/recv()和write()/read():发送数据和接收数据 参考链接
- socket原理
- 不同协议图,
- 比如omoronsocekt, modbustcp,他们都是用socket进行数据交互,只是他们在应用层采用不同的协议约定,对报文进行不同方式的解析;明文协议就是直接编码不组包,其他协议都是组包发出去(如明文协议,将字符串编码后直接send
modbustcp协议,要组装发送报文为(从站地址+功能码+等等+字符串数据))
常用链接
虚拟串口调试工具 V6.9 汉化版免费版
串口、Modbus通信协议
一、Modbus
1.ModbusRTU消息帧解析
2.主站poll、从站slave通讯仿真-modbusRTU
从站slave用于模拟PLC中的功能区,一个tab页表示一个功能模块(下图建了两个功能块)
主站poll发送请求,获取PLC中数据。
poll、slave都要设置connection、setup两个区域,只有参数配对了才能正常收发数据
1.功能码=01读线圈状态
报文都是16进制表示
16进制 0X01=0000 0001,一位16进制需要4位二进制表达(F =1111),两个16进制数字表示1个字节
线圈中数据要么是0,要么是1
读取长度:00 0A表示读取10个寄存器
响应字节数(单位是字节):02 表示两个字节,从02往后数两个字节都是数据未位
输出状态:A2 02 这是两字节,解析:先颠倒高低位 02 A2= 0000 0010 1010 0010 再反向读取数据0100 0101 0100 0000
2.功能码=03读保持寄存器
寄存器中数据可以是整数,浮点型 (整型和无符号整型:2字节。长整型:4字节。单精度浮点数:4字节。双精度浮点数:8字节)
报文解析(寄存器存整型)
读取长度:00 0A表示读取10个寄存器,1个寄存器是16位=2个字节,所以返回20个字节,一个整型=2字节,所以返回的是10个数据
响应字节数(单位是字节):14 表示20个字节
输出状态:007B 00 00 00 00 00 00 00 00 00 00 这是20个字节,解析: 第一个数为123
报文解析(寄存器存float)
读取长度:00 0A表示读取10个寄存器,1个寄存器是16位=2个字节,所以返回20个字节,一个float 占4字节,所以返回的是5个数据
响应字节数(单位是字节):14 表示20个字节
输出状态:解析: 42 0A 00 00 通过IEEE转换标准->第一个数为34.5
3.C#模拟主站Poll(ModbusRTU协议-组报文)
说明
1.下面代码模拟的是主站,需要开启小程序mbslave作为从站PLC
2.主站发起的功能码请求有:读线圈,读保持寄存器,写多个寄存器
3.主站发送报文,然后对响应报文按消息帧进行解析
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Threading;
namespace 通讯收发功能解析
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
//Test_0x01();
Test_0x03();
//Test_0x10();
Console.ReadKey();
}
static void Test_0x01()/// 01功能码-读线圈状态测试,PLC中线圈全为0
{
ushort startAddr = 0;
ushort readLen = 10;
// 请求
// byte[] 需要指定长度;不支持Linq
List<byte> command = new List<byte>();
command.Add(0x01);// 1号从站
command.Add(0x01);// 功能码:读线圈状态
// 起始地址
command.Add(BitConverter.GetBytes(startAddr)[1]);
command.Add(BitConverter.GetBytes(startAddr)[0]);
// 读取数量
command.Add(BitConverter.GetBytes(readLen)[1]);
command.Add(BitConverter.GetBytes(readLen)[0]);
// CRC
command = CRC16(command);//command 为长度=8的字节{0x01 0x01 0x00 0x00 0x00 0x0A 0xBC 0x0D}
// 以上报文组装完成
// 发送-》SerialPort
SerialPort serialPort = new SerialPort("COM1", 9600, Parity.None, 8, StopBits.One);
// 打开串口
serialPort.Open();
//将command的第0位->command.count所有数据都发出去
serialPort.Write(command.ToArray(), 0, command.Count);//发送报文为01 01 00 00 00 0A BC 0D 请求10个线圈的状态,响应时1个字节8位接收不够,所以两字节
Thread.Sleep(5);//加上延时等待PLC反应时间
// 进行响应报文的接收和解析
byte[] respBytes = new byte[serialPort.BytesToRead];//缓冲区信息
serialPort.Read(respBytes, 0, respBytes.Length);
// respBytes -> 01 01 02 00 00 B9 FC
// 检查一个校验位
//对报文进行解析数据
List<byte> respList = new List<byte>(respBytes);
respList.RemoveRange(0, 3);//00 00 B9 FC
respList.RemoveRange(respList.Count - 2, 2);//00 00 数据报文
// 1。高低位切换
// 2。从后往前读
respList.Reverse();
var respStrList = respList.Select(r => Convert.ToString(r, 2)).ToList();
var values = string.Join("", respStrList).ToList();
values.Reverse();
values.ForEach(c => Console.WriteLine(Convert.ToBoolean(int.Parse(c.ToString()))));
//Convert.ToBoolean('1');
}
static void Test_0x03()//读保持寄存器 PLC中第一个寄存器为123,其他=0
{
ushort startAddr = 0;
ushort readLen = 10;
// 请求
// byte[] 需要指定长度;不支持Linq
List<byte> command = new List<byte>();
command.Add(0x01);// 1号从站
command.Add(0x03);// 功能码:读保持型寄存器
// 起始地址
command.Add(BitConverter.GetBytes(startAddr)[1]);
command.Add(BitConverter.GetBytes(startAddr)[0]);
// 读取数量
command.Add(BitConverter.GetBytes(readLen)[1]);
command.Add(BitConverter.GetBytes(readLen)[0]);
// CRC
command = CRC16(command);
// 报文组装完成
// 发送-》SerialPort
SerialPort serialPort = new SerialPort("COM1", 9600, Parity.None, 8, StopBits.One);
// 打开串口
serialPort.Open();
serialPort.Write(command.ToArray(), 0, command.Count);
// 进行响应报文的接收和解析
byte[] respBytes = new byte[serialPort.BytesToRead];
serialPort.Read(respBytes, 0, respBytes.Length);
// respBytes -> 01 01 02 00 00 B9 FC
// 检查一个校验位
List<byte> respList = new List<byte>(respBytes);
respList.RemoveRange(0, 3);
respList.RemoveRange(respList.Count - 2, 2);
// 拿到实际的数据部分,进行数据解析
// 明确一点:读的是无符号单精度,占两个字节
//byte[] data = new byte[2];
//for (int i = 0; i < readLen; i++)
//{
// // 字节序问题 小端 大端
// data[0] = respList[i * 2 + 1];
// data[1] = respList[i * 2];
// // 根据此两个字节转换成想要的实际数字
// var value = BitConverter.ToUInt16(data, 0);
// Console.WriteLine(value);
//}
// 明确一点:读的是Float 占4个字节
byte[] data = new byte[4];
for (int i = 0; i < readLen / 2; i++)
{
// 字节序问题 小端 大端
data[0] = respList[i * 4 + 3];
data[1] = respList[i * 4 + 2];
data[2] = respList[i * 4 + 1];
data[3] = respList[i * 4];
// 根据此两个字节转换成想要的实际数字
var value = BitConverter.ToSingle(data, 0);
Console.WriteLine(value);
}
}
//向PLC中写入Float,float占4个字节=2个寄存器,所以要使用功能码“写多寄存器0x10”, 功能码0x06只能写一个寄存器”,
static void Test_0x10()//写多个寄存器功能码0x10
{
ushort startAddr = 2;
ushort writeLen = 4;
float[] values = new float[] { 123.45f, 14.3f };
// 请求
// byte[] 需要指定长度;不支持Linq
List<byte> command = new List<byte>();
command.Add(0x01);// 1号从站
command.Add(0x10);// 功能码:写多个保持型寄存器
// 写入地址
command.Add(BitConverter.GetBytes(startAddr)[1]);
command.Add(BitConverter.GetBytes(startAddr)[0]);
// 写入数量
command.Add(BitConverter.GetBytes(writeLen)[1]);
command.Add(BitConverter.GetBytes(writeLen)[0]);
// 获取数值的byte[]
List<byte> valueBytes = new List<byte>();
for (int i = 0; i < values.Length; i++)
{
List<byte> temp = new List<byte>(BitConverter.GetBytes(values[i]));
temp.Reverse();// 调整字节序
valueBytes.AddRange(temp);
}
// 字节数
command.Add((byte)valueBytes.Count);
command.AddRange(valueBytes);
// CRC
command = CRC16(command);
// 报文组装完成
// 发送-》SerialPort
SerialPort serialPort = new SerialPort("COM1", 9600, Parity.None, 8, StopBits.One);
// 打开串口
serialPort.Open();
serialPort.Write(command.ToArray(), 0, command.Count);
}
static List<byte> CRC16(List<byte> value, ushort poly = 0xA001, ushort crcInit = 0xFFFF)
{
if (value == null || !value.Any())
throw new ArgumentException("");
//运算
ushort crc = crcInit;
for (int i = 0; i < value.Count; i++)
{
crc = (ushort)(crc ^ (value[i]));
for (int j = 0; j < 8; j++)
{
crc = (crc & 1) != 0 ? (ushort)((crc >> 1) ^ poly) : (ushort)(crc >> 1);
}
}
byte hi = (byte)((crc & 0xFF00) >> 8); //高位置
byte lo = (byte)(crc & 0x00FF); //低位置
List<byte> buffer = new List<byte>();
buffer.AddRange(value);
buffer.Add(lo);
buffer.Add(hi);
return buffer;
}
}
}
4.NModbus4模拟主站poll(ModbusRTU协议)
ReadHoldingRegisters(1, 0, 1)# 参数:从站地址,起始地址,读取数量
5.C#模拟主站Poll(ModbusTCP协议-组报文)
03读保持寄存器报文
说明
1.下面代码模拟的是modbusTCP主站,需要开启小程序mbslave作为从站PLC,要设置slave的connection、setup两个区域的TCP相关参数
2.主站发起的功能码请求有:ReadHoldingRegister,ReadInputRegister
3.主站发送报文,然后对响应报文按消息帧进行解析
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace 通讯收发功能解析
{
public class ModbusTcp: ModbusBase
{
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream,ProtocolType.Tcp);
public Result connect(string host, int port)
{
Result result = new Result();
try
{
socket.Connect(host, port);
result.State = true;
}
catch (Exception ex)
{
result.State = false;
result.Exception = ex.Message;
}
return result;
}
// 读保持型寄存器03
// ModbusRTU:0x01(从站地址) 03 (功能码) 0x00 0x0A(起始地址) 0x00 0x05(读取长度) 0xXX 0xXX(CRC16校验码)
// ModbusTCP:请求报文:0x00 0x00(TransationID 最大65535) 0x00 0x00 (Modbus协议标识) 0x00 0x06(后续字节数) => 0x01 (单元标识) 0x03 0x0 0x0A 0x00 0x05
// 响应报文: 0x00 0x00(TransationID 最大65535) 0x00 0x00 (Modbus协议标识) 0x00 0x0D(后续字节数) => 0x01 (单元标识) 0x03(功能码)=>
// 0x0A(返回10个字节数据) 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
/// <summary>
///
/// </summary>
/// <param name="unit_id">从站地址</param>
/// <param name="start_addr">寄存器起始地址</param>
/// <param name="count">读寄存器数量</param>
public Result ReadHoldingRegister(byte unit_id , ushort start_addr,ushort count)
{
Result result = new Result();
try {
ushort tid = 0;
//报文组装
byte[] req_bytes = new byte[]
{
(byte)(tid/256),(byte)(tid%256),
0x00, 0x00,
0x00, 0x06,
unit_id,
0x03,
(byte)(start_addr/256),(byte)(start_addr%256),//10进制转成16
(byte)(count/256),(byte)(count%256),
};
tid++;
tid %= 65535;
//var req_bytes = this.ReadCommandBytes( unit_id, 0x03,start_addr, count);
//发送请求
socket.Send(req_bytes);
//接收响应
byte[] resp_bytes = new byte[6];//由于plc返回的响应字数长度是不一样的,先取前6个字节
socket.Receive(resp_bytes, 0 ,6 ,SocketFlags.None);
var len_bytes = resp_bytes.ToList().GetRange(4, 2);
ushort len = (ushort)(len_bytes[4] * 256 + len_bytes[5]);//解析报文中返回的有多少个字节数
resp_bytes = new byte[len];//这个resp_bytes len 表明数据中有多少个字节数据是有用的数据报文
socket.Receive(resp_bytes, 0, len, SocketFlags.None);//上面从缓存区拿走了6个字节,现在把剩余的都拿走
//检查响应报文是否正常,功能码的高位为1,就是异常
//0x83 1000 0011
if (resp_bytes[1] > 0x80)
{
//说明响应的是异常报文
// 返回异常信息 根据resp_bytes[2]字节进行异常的关联
throw new Exception("错误了");
}
//提取PLC中寄存器中的数据部分报文
var data_bytes = resp_bytes.ToList().GetRange(3, resp_bytes[2]).ToArray();
result.State = true;
result.Datas = data_bytes;
}
catch(Exception ex)
{
result.State = false;
result.Exception = ex.Message;
}
return result;
}
public Result ReadInputRegister(byte unit_id, ushort start_addr, ushort count)
{
Result result = new Result();
try
{
ushort tid = 0;
//报文组装
byte[] req_bytes = new byte[]
{
(byte)(tid/256),(byte)(tid%256),
0x00, 0x00,
0x00, 0x06,
unit_id,
0x04,
(byte)(start_addr/256),(byte)(start_addr%256),//10进制转成16
(byte)(count/256),(byte)(count%256),
};
tid++;
tid %= 65535;
//var req_bytes = this.ReadCommandBytes(unit_id, 0x04, start_addr, count);
//发送请求
socket.Send(req_bytes);
//接收响应
byte[] resp_bytes = new byte[6];//由于plc返回的响应字数长度是不一样的,先取前6个字节
socket.Receive(resp_bytes, 0, 6, SocketFlags.None);
var len_bytes = resp_bytes.ToList().GetRange(4, 2);
ushort len = (ushort)(len_bytes[4] * 256 + len_bytes[5]);//解析报文中返回的有多少个字节数
resp_bytes = new byte[len];//这个resp_bytes len 表明数据中有多少个字节数据是有用的数据报文
socket.Receive(resp_bytes, 0, len, SocketFlags.None);//上面从缓存区拿走了6个字节,现在把剩余的都拿走
//检查响应报文是否正常,功能码的高位为1,就是异常
//0x83 1000 0011
if (resp_bytes[1] > 0x80)
{
//说明响应的是异常报文
// 返回异常信息 根据resp_bytes[2]字节进行异常的关联
throw new Exception("错误了");
}
//解析PLC中寄存器中的数据
var data_bytes = resp_bytes.ToList().GetRange(3, resp_bytes[2]).ToArray();
result.State = true;
result.Datas = data_bytes;
}
catch (Exception ex)
{
result.State = false;
result.Exception = ex.Message;
}
return result;
}
public void write()
{
}
public T[] Getvalues<T>(byte[] data_bytes)//解析报文
{
var type_len = Marshal.SizeOf(typeof(T));//检查类型的长度 , int32是4字节,float是4字节
for (var i =0; i<data_bytes.Length;i+=type_len)
{
//根据数据类型将报文切割,获取一个类型的字节内容,并将字节转换成对应 的str
var temp_bytes = data_bytes.ToList().GetRange(i, type_len);
temp_bytes.Reverse();//字序
short v = BitConverter.ToInt16(temp_bytes.ToArray(), 0);
ushort vv = BitConverter.ToUInt16(temp_bytes.ToArray(), 0);
float vvv = BitConverter.ToSingle(temp_bytes.ToArray(),0);
}
return null;
}
}
}
6.NModbus4模拟从站slave(ModbusTCP协议)
用小工具poll连接自己建立的从站,可读取从站的值
文章
using System.Threading;
using System.Net.Sockets;
using System.Net;
using Modbus.Data;
using Modbus.Device;
public class slave
{
/// <summary>
/// 服务器提供的数据区
/// </summary>
public DataStore Data=DataStoreFactory.CreateDefaultDataStore(); //初始化服务数据区;
/// <summary>
/// Modbus服务器
/// </summary>
public ModbusSlave modbus_tcp_server;
public void modbustcpslave()
{
modbus_tcp_server = ModbusTcpSlave.CreateTcp(1, new TcpListener(IPAddress.Parse("127.0.0.1"), 502)); //创建ModbusTcp服务器
modbus_tcp_server.DataStore = Data;//数据区赋值
Thread th_0 = new Thread(() =>
{
modbus_tcp_server.Listen();//异步 非阻塞 启动服务
})
{
IsBackground = true,
};
th_0.SetApartmentState(ApartmentState.STA);
th_0.Start();
Thread th_1 = new Thread(() =>
{
SetData(); //数据区数据赋值
})
{
IsBackground = true,
};
th_1.SetApartmentState(ApartmentState.STA);
th_1.Start();
}
/// <summary>
/// 设置数据
/// </summary>
public void SetData() //static修饰的函数或变量都是在类初始化的时候加载的,而非静态的变量都是在对象初始化的时候加载。
{
while (true)
{
Data.InputRegisters[1] = (ushort)DateTime.Now.Year; //年
Data.InputRegisters[2] = (ushort)DateTime.Now.Month; //月
Data.InputRegisters[3] = (ushort)DateTime.Now.Day; //日
Data.InputRegisters[4] = (ushort)DateTime.Now.Hour; //时
Data.InputRegisters[5] = (ushort)DateTime.Now.Minute; //分
Data.InputRegisters[6] = (ushort)DateTime.Now.Second; //秒
Data.InputRegisters[7] = (ushort)DateTime.Now.Millisecond; //毫秒
Random ran = new Random();
Data.InputRegisters[8] = (ushort)ran.Next(0, 32767); //产生的随机数
}
}
}
7.NModbus4模拟从站slave(ModbusRTU协议)
public class slave_RTU
{
public ModbusSlave modbus_rtu_server;
public void create()
{
SerialPort slavePort = new SerialPort();
slavePort.PortName = "COM1";
slavePort.BaudRate = 9600;
slavePort.DataBits = 8;
slavePort.Parity = Parity.Even;
slavePort.StopBits = StopBits.One;
slavePort.Open();
byte slaveID =1;
modbus_rtu_server = ModbusSerialSlave.CreateRtu(slaveID, slavePort);
modbus_rtu_server.ModbusSlaveRequestReceived += new EventHandler<ModbusSlaveRequestEventArgs>(Modbus_Request_Event);
modbus_rtu_server.DataStore = Modbus.Data.DataStoreFactory.CreateDefaultDataStore();
modbus_rtu_server.DataStore.DataStoreWrittenTo += new EventHandler<DataStoreEventArgs>(Modbus_DataStoreWriteTo);
modbus_rtu_server.DataStore.InputRegisters[1] = (ushort)DateTime.Now.Year;
modbus_rtu_server.DataStore.InputRegisters[2] = (ushort)DateTime.Now.Year;
modbus_rtu_server.DataStore.InputRegisters[3] = (ushort)DateTime.Now.Year;
modbus_rtu_server.DataStore.CoilDiscretes[1] = true;
modbus_rtu_server.DataStore.CoilDiscretes[2] = false;
modbus_rtu_server.DataStore.CoilDiscretes[3] = false;
modbus_rtu_server.Listen();
}
private void Modbus_Request_Event(object sender, Modbus.Device.ModbusSlaveRequestEventArgs e)
{
try
{
//request from master
byte fc = e.Message.FunctionCode;
byte[] data = e.Message.MessageFrame;
byte[] byteStartAddress = new byte[] { data[3], data[2] };
byte[] byteNum = new byte[] { data[5], data[4] };
Int16 StartAddress = BitConverter.ToInt16(byteStartAddress, 0);
Int16 NumOfPoint = BitConverter.ToInt16(byteNum, 0);
bool BOOL = true;
string FCNUM = fc.ToString();
if (fc.ToString() == "6")
{
//AO
modbus_rtu_server.DataStore.HoldingRegisters[StartAddress] = 16;
modbus_rtu_server.DataStore.HoldingRegisters[StartAddress + 1] = 17;
}
Console.WriteLine(fc.ToString() + "," + StartAddress.ToString() + "," + NumOfPoint.ToString());
}
catch (Exception exc)
{
}
}
private void Modbus_DataStoreWriteTo(object sender, Modbus.Data.DataStoreEventArgs e)
{
//this.Text = "DataType=" + e.ModbusDataType.ToString() + " StartAdress=" + e.StartAddress;
int iAddress = e.StartAddress;//e.StartAddress;
switch (e.ModbusDataType)
{
case ModbusDataType.HoldingRegister:
for (int i = 0; i < e.Data.B.Count; i++)
{
//Set AO
modbus_rtu_server.DataStore.HoldingRegisters[e.StartAddress + i + 1] = e.Data.B[i];
//e.Data.B[i] already write to slave.DataStore.HoldingRegisters[e.StartAddress + i + 1]
//e.StartAddress starts from 0
//You can set AO value to hardware here
//DoAOUpdate(iAddress, e.Data.B[i].ToString());
iAddress++;
}
break;
case ModbusDataType.Coil:
for (int i = 0; i < e.Data.A.Count; i++)
{
//Set DO
modbus_rtu_server.DataStore.CoilDiscretes[e.StartAddress + i + 1] = e.Data.A[i];
//e.Data.A[i] already write to slave.DataStore.CoilDiscretes[e.StartAddress + i + 1]
//e.StartAddress starts from 0
//You can set DO value to hardware here
//DoDOUpdate(iAddress, e.Data.A[i]);
iAddress++;
if (e.Data.A.Count == 1)
{
break;
}
}
break;
}
}
}
8.modbusRTU、modbusTCP报文不同之处
二、明文TCP
using System.Net;
//创建Socket套接字
Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint point = new IPEndPoint(IPAddress.Parse(textBox1.Text), int.Parse(textBox2.Text));
try { server.Bind(point); }
catch (Exception ex)
{
MessageBox.Show("无法启动服务器");
}
server.Listen(3);//
Socket Client = server.Accept();//Accept 抓取的连接请求是客户端发来的
string client = Client.RemoteEndPoint.ToString();
MessageBox.Show(client+"连接了服务器");
byte[] b = new byte[1024 * 1024 * 2];//缓冲器
int length = 0;
try
{
length = Client.Receive(b);
}
catch
{
MessageBox.Show(client + "失去连接");
}
if (length > 0)
{
string msg = Encoding.Default.GetString(b, 0, length);
Client.Send(Encoding.Default.GetBytes(textBox3.Text));
}
else
{
MessageBox.Show(client + "失去连接");
}