欢迎回来请牢记网站域名西安百度seo代理
一、Modbus通信协议是什么?
关于Mod,因为这种协议最早被用在PLC控制器中,准确的说是Modicon公司的PLC控制器,这也是Mod-Bus名称的由来。后来Modicon被施耐德(Schneider)收购,Modbus协议广泛应用在工业控制器、HMI和传感器上,逐渐被其他厂商所接受,成为了一种主流的通讯协议,用于和外围设备进行通讯。
Modbus协议作为当今工业控制领域的通用通讯协议,在无数物联网产品中得到应用,工业、农业等物联网解决方案中都有其身影。ModBus网络是一个工业通信系统,由智能终端的可编程序控制器和计算机,通过公用线路或局部专用线路连接而成。其系统结构既包括硬件、亦包括软件,应用于各种数据采集和过程监控。
通过Modbus协议,控制器相互之间、或控制器经由网络(如以太网)可以和其它设备之间进行通信。
Modbus协议使用的是主从通讯技术,即由主设备主动查询和操作从设备。一般将主控设备方所使用的协议称为Modbus Master
,从设备方使用的协议称为Modbus Slave
。典型的主设备包括工控机和工业控制器等;典型的从设备如PLC可编程控制器等。Modbus通讯物理接口可以选用串口(包括RS232和RS485
),也可以选择以太网口。
Modbus在7层OSI参考模型中属于第七层应用层,数据链路层有两种:基于标准串口协议和TCP协议,物理层可使用3线232、2线485、4线422,或光纤、网线、无线等多种传输介质。
ModBus网络只有一个主机,发出通讯信号,多个从机,网络可支持247个之多的远程从属控制器,但实际所支持的从机数要由所用通信设备决定。采用这个系统,各PC可以和中心主机交换信息而不影响各PC执行本身的控制任务。
Modbus协议包括ASCII、RTU、TCP等,并没有规定物理层。此协议定义了控制器能够认识和使用的消息结构,而不管它们是经过何种网络进行通信的。
二、三种传输模式
1979年,Modicon 首先推出了串行Modbus标准,后来由于网络的普及,需要更高的传输速度,1997年制定了基于TCP网络的Modbus标准。
所以总的可分为两个传输模式:基于串行链路的和基于以太网TCP/IP的。但是我总结为3种传输模式,方便理解
🔹 基于串口的Modbus-RTU 数据按照标准串口协议进行编码,是使用最广泛的一种Modbus协议,采用CRC-16_Modbus校验算法。
🔹 基于串口的Modbus-ASCII 所有数据都是ASCII格式,一个字节的原始数据需要两个字符来表示,效率低,采用LRC校验算法。
🔹 基于网口的Modbus-TCP Modbus-TCP基于TCP/IP协议,占用502端口,数据帧主要包括两部分:MBAP(报文头)+PDU(帧结构),数据块与串行链路是一致的。
所以当我们提及Modbus协议时,要确定是哪种模式:RTU、ASCII或TCP,3种模式区别还是很大的。
有些设备支持多种Modbus模式,有些设备只支持其中一种,实际使用要根据设备使用手册来选择采用哪种模式。
对于所有的这三种通信协议在数据模型和功能调用上都是相同的,只有封装方式是不同的。
Modbus的ASCII、RTU协议规定了消息、数据的结构、命令和就答的方式,数据通讯采用Maser/Slave方式,Master端发出数据请求消息,Slave端接收到正确消息后就可以发送数据到Master端以响应请求;
Master端也可以直接发消息修改Slave端的数据,实现双向读写。
Modbus协议需要对数据进行校验,串行协议中除有奇偶校验外,ASCII模式采用LRC校验,RTU模式采用16位CRC校验,但TCP模式没有额外规定校验,因为TCP协议是一个面向连接的可靠协议。
另外,Modbus采用主从方式定时收发数据,在实际使用中如果某Slave站点断开后(如故障或关机),Master端可以诊断出来,而当故障修复后,网络又可自动接通,Modbus协议的可靠性较好。
对于Modbus的ASCII、RTU和TCP协议来说,其中TCP和RTU协议非常类似,只要把RTU协议的两个字节的校验码去掉,然后在RTU协议的开始加上5个0和一个6并通过TCP/IP网络协议发送出去即可。
三、Modbus 通信协议特点
(1)标准、开放,用户可以免费、放心地使用Modbus协议,不需要交纳许可证费,也不会侵犯知识产权。
(2)Modbus可以支持多种电气接口,如RS-232、RS-485等,还可以在各种介质上传送,如双绞线、光纤、无线等。
(3)Modbus的帧格式简单、紧凑,通俗易懂。用户使用容易,厂商开发简单。
四、Modbus4种数据类型和功能码
Modbus协议规定,进行读写操作的数据类型,按照读写属性和类型可分为以下4种:
离散量输入(Discretes Input )
:1位,只读
线圈(Coils)
:1位,读写
输入寄存器(Input Registers )
:16位,只读
保持寄存器(Holding Registers)
:16位,读写
主设备通过功能码指定操作类型,常见功能码
包括:
01:读线圈
02:读离散输入
03:读保持寄存器
04:读输入寄存器
05:写单个线圈
06:写单个保持寄存器
15:写多个线圈
16:写多个保持寄存器
五、常用的MODBUS通讯方式
有两种,一种是MODBUS ASCII
,一种是MODBUS RTU
。每个设备必须都有相同的传输模式。所有设备都支持RTU模式,ASCII传输模式是选项。
ASCII传输方式
Modbus串行链路的设备被配置为使用ASCII模式通信时,报文中的每8位字节以两个ASCII字符
发送。例:字节0X5B会被编码为两个字符:0x35和0x42进行传送(ASCII编码0x35=“5”,0x42=“B”),这样传输效率会降低。
在ASCII模式,报文用特殊的字符区分帧起始和帧结束。一个报文必须以一个‘冒号’(:)(ASCII十六进制3A)起始,以‘回车-换行’(CRLF)对(ASCII十六进制0D和0A)结束
。设备连续的监视总线上的‘冒号’字符。当收到这个字符后,每个设备解码后续的字符一直到帧结束。报文中字符间的时间间隔可以达一秒。如果有更大的间隔,则接受设备认为发生了错误。
RTU传输方式
当设备使用RTU(RemoteTerminalUnit)模式在Modbus串行链路通信,报文中每个8位字节
含有两个4位十六进制字符
。这种模式的主要优点是较高的数据密度,在相同的波特率下比ASCII模式有更高的传输效率。每个报文必须以连续的字符流传送。
六、Modbus总结
总的来说,简单是 Modbus 如此普遍的主要原因。消息检查是 Modbus 如此受欢迎的另一个原因。CRC 和 LRC 检查意味着检查传输错误的准确率达到 99%。概括来讲,Modbus就是一种用于工业控制的协议,具有免费使用、上手简单、修改方便三大特点,已经被广泛使用。
此协议支持传统的RS-232、RS-485、RS-422和以太网设备
。许多工业设备,包括PLC,DCS,智能仪表,变频器等都在使用Modbus协议作为他们之间的通讯标准。有了它,不同厂商生产的控制设备可以连成工业网络,进行集中监控。
七、读写寄存器示例
/// <summary>
/// Modbus通信方式
/// </summary>
internal class Program
{static void Main(string[] args){int flag = 7;#region NModbus4if (flag == 1){//SerialPort serialPort = new SerialPort(); 创建串口对象//serialPort = new SerialPort(); 设置连接串口名称//serialPort.PortName = "COM2"; 设置波特率//serialPort.BaudRate = 9600; 设置数据位//serialPort.DataBits = 8; 设置停止位//serialPort.StopBits = StopBits.One; 设置校验位//serialPort.Parity = Parity.None; 打开串口//serialPort.Open();//ModbusSerialMaster Master = ModbusSerialMaster.CreateRtu(serialPort); 读取保持寄存器//ushort[] values = Master.ReadHoldingRegisters(1, 0, 1);}#endregion#region ModbusRTU 读取保持寄存器报文处理if (flag == 2){List<byte> registers = new List<byte>();// 组装报文 (报文什么样子,不知道怎么组装呢?)registers.Add(0x01); // 从站地址registers.Add(0x03); // 功能码ushort startAddr = 4; // 起始地址// 0100 0010// 0001 0000registers.Add((byte)(startAddr / 256));registers.Add((byte)(startAddr % 256));ushort len = 2; // 读取寄存器数量// 0100 0010// 0001 0000registers.Add((byte)(len / 256));registers.Add((byte)(len % 256));// CRC校验registers = CRC16(registers);// 发送报文给串口SerialPort serialPort = new SerialPort("COM2", 9600, Parity.None, 8, StopBits.One);serialPort.Open();serialPort.Write(registers.ToArray(), 0, registers.Count);Thread.Sleep(1000);// 获取串口响应给我们结果byte[] buffer = new byte[len * 2 + 5];serialPort.Read(buffer, 0, registers.Count);#region 读取整数// 通过响应结果解析我们的数据//for (int i = 3; i < buffer.Length - 2; i += 2)//{// // 因为一个寄存器是2个2个字节,所有读取一个数据需要2个字节组成// // Modbus slave大端处理方式// byte[] value = new byte[] { buffer[i + 1], buffer[i] };// ushort v = BitConverter.ToUInt16(value);// Console.WriteLine($"结果为:{v}");//}#endregion#region 读取浮点数 读取温度计数据// 选择是 AB CD // BA DC// Slave 是大端处理: 高位放前面for (int i = 3; i < buffer.Length - 2; i += 4){byte[] vb = new byte[]{buffer[i + 2], // Cbuffer[i + 3], // Dbuffer[i], // Abuffer[i+1], // B};float v = BitConverter.ToSingle(vb);Console.WriteLine($"结果为:{v}");}#endregion}#endregion#region 保持寄存器写入单值if (flag == 3){var bytes = new List<byte>();bytes.Add(0x01);bytes.Add(0x06);// 定义起始地址ushort startAddr = 3;bytes.Add((byte)(startAddr / 256));bytes.Add((byte)(startAddr % 256));// 定义写入寄存器的值ushort value = 88;bytes.Add((byte)(value / 256));bytes.Add((byte)(value % 256));// CRC16校验bytes = CRC16(bytes);// 连接串口SerialPort serialPort = new SerialPort("COM2", 9600, Parity.None, 8, StopBits.One);serialPort.Open();serialPort.Write(bytes.ToArray(), 0, bytes.Count);}#endregion#region 保持寄存器写入多个值if (flag == 4){/*请求报文从站地址:01 功能码:10 起始地址: 00 03写人寄存器的数量: 00 04 需要写入的数据字节数:08 写入内容:00 16 00 21 00 2C 00 37 CRC16校验: F9 A7响应报文从站地址:01 功能码:10 起始地址: 00 03写人寄存器的数量: 00 04 CRC16校验:31 CA*/var bytes = new List<byte>();bytes.Add(0x01); // 从站地址bytes.Add(0x10); // 功能码// 定义起始地址ushort startAddr = 3;bytes.Add((byte)(startAddr / 256));bytes.Add((byte)(startAddr % 256));#region 写入多个整数 定义写入寄存器的值//var values = new List<ushort>();//values.Add(10);//values.Add(20);//values.Add(30);//values.Add(40); 定义写入寄存器的数量//bytes.Add((byte)(values.Count / 256));//bytes.Add((byte)(values.Count % 256)); 定需要写入字节数大小//bytes.Add((byte)(values.Count * 2)); 处理写入数据大小端数据问题//for (int i = 0; i < values.Count; i++)//{// // 把一个ushort拆成高低位两个字节// bytes.Add((byte)(values[i] / 256));// bytes.Add((byte)(values[i] % 256));//}#endregion#region 写入多个float数 定义写入寄存器的值//var values = new List<float>();//values.Add(3.2f);//values.Add(4.2f);//values.Add(2.3f); 定义写入寄存器的数量//bytes.Add((byte)(values.Count * 2 / 256));//bytes.Add((byte)(values.Count * 2 % 256)); 定需要写入字节数大小//bytes.Add((byte)(values.Count * 4)); 处理写入数据大小端数据问题//for (int i = 0; i < values.Count; i++)//{// // 把一个float拆成四个个字节// bytes.Add(BitConverter.GetBytes(values[i])[3]);// bytes.Add(BitConverter.GetBytes(values[i])[2]);// bytes.Add(BitConverter.GetBytes(values[i])[1]);// bytes.Add(BitConverter.GetBytes(values[i])[0]);//}#endregion#region 写入数据类型不确定 ushort, float// 定义写入寄存器的值var values = new List<dynamic>();ushort v1 = 24;values.Add(v1);values.Add(41.2f);// 获取集合中值总共字节数var temp = new List<byte>();for (int i = 0; i < values.Count; i++){var dBytes = new List<byte>(BitConverter.GetBytes(values[i]));// 0x50 0x00 =>0x00 0x50dBytes.Reverse();temp.AddRange(dBytes);}// 定义写入寄存器的数量bytes.Add((byte)(temp.Count / 2 / 256));bytes.Add((byte)(temp.Count / 2 % 256));// 定需要写入字节数大小bytes.Add((byte)(temp.Count));// 需要写入数据字节数据bytes.AddRange(temp);#endregion// CRC16校验bytes = CRC16(bytes);// 连接串口SerialPort serialPort = new SerialPort("COM2", 9600, Parity.None, 8, StopBits.One);serialPort.Open();serialPort.Write(bytes.ToArray(), 0, bytes.Count);}#endregion#region 读线圈状态 (开关量,是位方式操作)if (flag == 5){var datas = new List<byte>();datas.Add(0x01);datas.Add(0x01);ushort startAddr = 0;datas.Add((byte)(startAddr / 256));datas.Add((byte)(startAddr % 256));ushort len = 9;datas.Add((byte)(len / 256));datas.Add((byte)(len % 256));datas = CRC16(datas);// 发送报文给串口SerialPort serialPort = new SerialPort("COM2", 9600, Parity.None, 8, StopBits.One);serialPort.Open();serialPort.Write(datas.ToArray(), 0, datas.Count);Thread.Sleep(1000);// 获取串口响应给我们结果byte[] buffer = new byte[(int)(Math.Ceiling(len * 1.0 / 8)+5)];serialPort.Read(buffer, 0, buffer.Length);// 解析结果var temp = new List<byte>();for (int i = 3; i < buffer.Length - 2; i++){temp.Add(buffer[i]); // 拿出2个字节}int index = 0;for (int i = 0; i < temp.Count; i++){// 把一个字节拆成八个位for (int k = 0; k < 8; k++){byte tempByte = (byte)(1 << k);// 0010 0101// 0000 0100// 101// bool result = (temp[i] & tempByte) != 0;Console.WriteLine(result? "1": "0");index++;if (index == len){break;}}}}#endregion#region 写单线圈if (flag == 6){var datas = new List<byte>();datas.Add(0x01);datas.Add(0x05);ushort startAddr = 10;datas.Add((byte)(startAddr / 256));datas.Add((byte)(startAddr % 256));// 规定,事件情况根据你的设备参数而定// on 0xFF 0x00// off 0x00 0x00datas.Add(0xFF);datas.Add(0x00);datas = CRC16(datas);// 发送报文给串口SerialPort serialPort = new SerialPort("COM2", 9600, Parity.None, 8, StopBits.One);serialPort.Open();serialPort.Write(datas.ToArray(), 0, datas.Count);}#endregion#region 写多线圈if (flag == 7){var datas = new List<byte>();datas.Add(0x01); // 从站地址datas.Add(0x0F); // 功能码15ushort startAddr = 10; // 写入起始地址datas.Add((byte)(startAddr / 256));datas.Add((byte)(startAddr % 256));// 准备写入的值var status = new List<bool>{ true, false, true, true, true, false, true, false, false };// 写入寄存器的数量datas.Add((byte)(status.Count / 256));datas.Add((byte)(status.Count % 256));var temp = new List<byte>();int index = 0;for (int i = 0; i < status.Count; i++){if (i % 8 ==0){temp.Add(0x00);}index = temp.Count - 1;if (status[i]){byte tempByte = (byte)(1 << i);temp[index] |= tempByte;}}// 设置写入字节数datas.Add((byte)temp.Count);// 写入字节数组datas.AddRange(temp);datas = CRC16(datas);// 发送报文给串口SerialPort serialPort = new SerialPort("COM2", 9600, Parity.None, 8, StopBits.One);serialPort.Open();serialPort.Write(datas.ToArray(), 0, datas.Count);}#endregion}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;}
}