赞
踩
虚拟串口调试工具 V6.9 汉化版免费版
串口、Modbus通信协议
从站slave用于模拟PLC中的功能区,一个tab页表示一个功能模块(下图建了两个功能块)
主站poll发送请求,获取PLC中数据。
poll、slave都要设置connection、setup两个区域,只有参数配对了才能正常收发数据
报文都是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字节。长整型: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
读取长度:00 0A表示读取10个寄存器,1个寄存器是16位=2个字节,所以返回20个字节,一个float 占4字节,所以返回的是5个数据
响应字节数(单位是字节):14 表示20个字节
输出状态:解析: 42 0A 00 00 通过IEEE转换标准->第一个数为34.5
说明
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; } } }
ReadHoldingRegisters(1, 0, 1)# 参数:从站地址,起始地址,读取数量
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; } } }
用小工具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); //产生的随机数 } } }
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; } } }
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 + "失去连接"); }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。