C# ModBus协议RTU 通讯详解
前言
ModBus协议:官方的解释是Modbus协议是一种通信协议,用于在自动化设备之间进行数据传输。它最初是由Modicon公司于1979年开发的,现在已成为工业界的一种通用协议。Modbus协议有多种变体,包括Modbus-RTU、Modbus-TCP和Modbus-ASCII等,其中Modbus-RTU是最常用的变体之一。Modbus协议基于主从结构进行通信。主设备通过发送读写请求来与从设备进行通信,从设备则响应这些请求。Modbus协议支持多种数据类型,包括线圈、离散输入、保持寄存器和输入寄存器等。在Modbus协议中,每个数据帧都包含了设备地址、功能码、数据和错误检查等信息。设备地址用于标识从设备,功能码用于指定读写请求的类型,数据则包含了读取或写入的寄存器地址和数据值等信息。错误检查通常使用CRC校验码来确保数据的完整性。Modbus协议广泛应用于工业自动化领域,可以用于控制器之间的通信、传感器的数据采集和PLC与HMI之间的通信等。由于其简单、可靠和易于实现等特点,Modbus协议仍然是工业界中最常用的通信协议之一。
讲人话就是:规定了一个设备和软件之间发送和接收数据的规则,根据这个规则数据格式去发送数据和接收数据
那么为什么要用这个协议呢:
就是因为它部署简单容易实现才使得ModBus被广泛运用
ModBus-RTU
自我理解:
Modbus-RTU是一个串口通讯的方式,主要分为五个部分一个设备ID号(占一个字节)、功能号(占一个字节)、寄存器地址(占两个字节)、读取数据长度(占两个字节)、CRC16校验码(占两个字节),通过对前四个设置值计算出CRC16 的检验码记住(11222这个规则,代表字节)发送的规则就是这样,下面我们举例说明更好的理解
官方解释:
Modbus-RTU:RTU是Modbus-RTU协议的一部分,它代表“Remote Terminal Unit”,即远程终端单元。在Modbus-RTU协议中,RTU是指一种串行通信方式,通常在RS-485物理层上运行。在这种通信方式中,数据以二进制编码的形式传输,并使用16位CRC错误检查。RTU协议是Modbus协议的一种变体,通常在RS-485物理层上运行。它是Modbus协议的一种子集,使用二进制编码,支持16位CRC错误检查。Modbus-RTU使用主从结构进行通信。主设备通过发送读写请求来与从设备进行通信,从设备则响应这些请求。Modbus-RTU支持读写线圈、离散输入、保持寄存器和输入寄存器等四种数据类型。在Modbus-RTU中,每个数据帧都包含了设备地址、功能码、数据和CRC校验。设备地址用于标识从设备,功能码用于指定读写请求的类型,数据则包含了读取或写入的寄存器地址和值,CRC校验用于检查数据传输的正确性。由于Modbus-RTU是一种比较简单的协议,因此它在工业自动化领域得到了广泛的应用。
我们来解析一下这个命令的值
设备ID | 功能号选择 | 寄存器地址 | 读数据长度 | CRC16 |
---|---|---|---|---|
01 | 03 | 00 00 | 00 0A | C5 CD |
1 | 1 | 2 | 2 | 2 |
01:表示从设备的地址,这里为1。 03:表示Modbus读取保持寄存器的功能码。 00 00:表示要读取的保持寄存器的起始地址,这里为0。 00 0A:表示要读取的保持寄存器的数量,这里为10。 (我们需要读取多少个值)C5 CD:表示CRC校验码,用于检查命令是否正确。 因此,这个命令的含义是从地址为1的Modbus设备中读取0~9号保持寄存器的值。
注:以11222的格式来看这个命令,只有功能号是需要选择,CRC校验码是计算出来的,其他的都可以根据我们的需求去定义
常用的功能号
Modbus-RTU的功能码是用于指示Modbus协议进行何种数据操作的标识符。以下是常用的Modbus-RTU功能码及其含义:01:读取线圈状态,用于读取开关量输入(DO)。02:读取离散输入状态,用于读取开关量输入(DI)。03:读取保持寄存器,用于读取模拟量输入(AI)。04:读取输入寄存器,用于读取模拟量输入(AI)。05:写单个线圈,用于控制开关量输出(DO)。06:写单个保持寄存器,用于控制模拟量输出(AO)。15:写多个线圈,用于控制多个开关量输出(DO)。16:写多个保持寄存器,用于控制多个模拟量输出(AO)。需要注意的是,不同设备支持的功能码可能不同,因此在使用Modbus-RTU通信时需要根据实际情况选择合适的功能码。
注: ModBus通讯是基于一个主站和从站的基础,我需要一个服务端,一般我们使用PLC为主站,也就是服务端,而我们的软件是需要连接主站的服务器进行通讯的,我采用一个模拟的ModBus的主站方便通讯
发送命令我们得到了一个答复,我们可以看到我们的服务端的数据
01 03 14 00 01 00 02 00 03 00 04 00 00 00 00 00 07 00 08 00 09 00 04 34 BE 01 设备ID号03 功能号14 数据长度 这里是16进制 14===》20 ,有20个数据因为我们收到数据是两个字节,所有是2*10 =2000 01 读取到0x0000寄存器地址的数据(两个字节)(高位在前,低位在后)00 0200 0300 0400 0000 00 00 07 00 08 00 0900 04 数据(两个字节)34 BE CRC16校验码(两个字节)
注意:接收数据的长度最大是127个
**注意:注意:注意:接收的数据是高位在前低位在后 **
比如我在0x1000 地址取一个,得到的数据是2个字节 比如接收的是01 03 02 0x01 0x02 CRC CRC 那么 高字节就是01 低字节就是02
就是0x0102 我们在计算的时候要么就用 int num = 0x01; num = (num<<8)+0x2;或者你把这两个字节赋值给一个2个为的byte数组,再用数组反转,使用BitConverter.ToUInt16(数组,起始位置);
byte[] num = new byte[2]; Array.Copy(data, 0, num, 0, 2);//将需要的数据复制 num.Reverse();//将需要的数据反转,因为BitConverter 是低位在前高位在后 UInt16 number = BitConverter.ToUInt16(num,0); //从0位置默认取两个,不会的BitConverter的可以看我前面的文章
ModBus-RTU报错代码及解决办法
错误代码 01:读取离散输入量时,请求地址错误或无法访问该地址。解决方法:检查请求地址和设备是否正确连接。如果地址正确,可能是设备故障或通信线路问题。错误代码 02:读取线圈时,请求地址错误或无法访问该地址。解决方法:检查请求地址和设备是否正确连接。如果地址正确,可能是设备故障或通信线路问题。错误代码 03:读取保持寄存器时,请求地址错误或无法访问该地址。解决方法:检查请求地址和设备是否正确连接。如果地址正确,可能是设备故障或通信线路问题。错误代码 04:读取输入寄存器时,请求地址错误或无法访问该地址。解决方法:检查请求地址和设备是否正确连接。如果地址正确,可能是设备故障或通信线路问题。错误代码 05:写单个线圈时,请求地址错误或无法访问该地址。解决方法:检查请求地址和设备是否正确连接。如果地址正确,可能是设备故障或通信线路问题。错误代码 06:写单个保持寄存器时,请求地址错误或无法访问该地址。解决方法:检查请求地址和设备是否正确连接。如果地址正确,可能是设备故障或通信线路问题。错误代码 07:读取异常状态时,请求的数据值无效。解决方法:检查请求的数据值是否有效,并且设备是否正确响应请求。错误代码 08:写多个线圈时,请求的数据值无效。解决方法:检查请求的数据值是否有效,并且设备是否正确响应请求。错误代码 09:写多个保持寄存器时,请求的数据值无效。解决方法:检查请求的数据值是否有效,并且设备是否正确响应请求。错误代码 10:读取文件记录时,请求的文件号无效。解决方法:检查请求的文件号是否有效,并且设备是否正确响应请求。错误代码 11:写文件记录时,请求的文件号无效。解决方法:检查请求的文件号是否有效,并且设备是否正确响应请求。错误代码 12:屏蔽写寄存器时,请求地址错误或无法访问该地址。解决方法:检查请求地址和设备是否正确连接。如果地址正确,可能是设备故障或通信线路问题。错误代码 13:读/写多个寄存器时,请求的数据值无效。解决方法:检查请求的数据值是否有效,并且设备是否正确响应请求。错误代码 14:ModBus 从站设备忙。解决方法:等待从站设备空闲,并重新发送请求。错误代码 15:ModBus 从站设备返回错误异常码。解决方法:参考 ModBus 协议文档中的异常码表,并进行相应的处理。错误代码 16:设备返回的数据长度错误。解决方法:检查设备是否正确响应请求,并且返回的数据长度是否与请求匹配。如果不匹配,可能是设备故障或通信线路问题。错误代码 17:设备返回的数据值错误。解决方法:检查设备是否正确响应请求,并且返回的数据值是否正确。如果不正确,可能是设备故障或通信线路问题。
C# 连接ModBus-RTU详解
我是使用第三方库,使用NModBus4
using System;using System.IO.Ports;using NModbus;namespace ModbusRtuExample{ class Program { static void Main(string[] args) { // 创建SerialPort对象来配置串口参数 SerialPort serialPort = new SerialPort("COM1"); serialPort.BaudRate = 9600; serialPort.DataBits = 8; serialPort.Parity = Parity.None; serialPort.StopBits = StopBits.One; // 创建ModbusFactory对象来进行Modbus RTU通信 IModbusFactory modbusFactory = new ModbusFactory(); IModbusMaster modbusMaster = modbusFactory.CreateRtuMaster(serialPort); try { // 打开串口连接 serialPort.Open(); // 使用Modbus函数来读取和写入数据 ushort startAddress = 0; ushort numRegisters = 10; ushort[] registers = modbusMaster.ReadHoldingRegisters(1, startAddress, numRegisters); Console.WriteLine($"读取位保持寄存器地址 {startAddress} 开始的 {numRegisters} 个寄存器:"); for (int i = 0; i < registers.Length; i++) { Console.WriteLine($"寄存器 {startAddress + i}: {registers[i]}"); } // 写入保持寄存器的值 ushort[] writeValues = new ushort[] { 10, 20, 30, 40, 50 }; modbusMaster.WriteMultipleRegisters(1, startAddress, writeValues); Console.WriteLine($"将 {writeValues.Length} 个值写入位保持寄存器地址 {startAddress} 开始的寄存器。"); // 读取离散输入的值 bool[] inputs = modbusMaster.ReadInputs(1, startAddress, numRegisters); Console.WriteLine($"读取离散输入地址 {startAddress} 开始的 {numRegisters} 个输入:"); for (int i = 0; i < inputs.Length; i++) { Console.WriteLine($"输入 {startAddress + i}: {inputs[i]}"); } // 关闭串口连接 serialPort.Close(); } catch (Exception ex) { Console.WriteLine($"发生错误:{ex.Message}"); } } }}
以下是NModbus4库中常用的一些函数:
01:读取线圈状态02:读取离散输入状态03:读取保持寄存器的值04:读取输入寄存器的值05:写单个线圈状态06:写单个保持寄存器的值0F:写多个线圈状态10:写多个保持寄存器的值
读取线圈状态:
bool[] coils = modbusMaster.ReadCoils(slaveAddress, startAddress, numCoils);
写入单个线圈状态:
modbusMaster.WriteSingleCoil(slaveAddress, coilAddress, value);
写入多个线圈状态:
bool[] coilValues = new bool[] { true, false, true };
modbusMaster.WriteMultipleCoils(slaveAddress, startAddress, coilValues);
读取离散输入状态:
bool[] inputs = modbusMaster.ReadInputs(slaveAddress, startAddress, numInputs);
读取保持寄存器的值:
ushort[] registers = modbusMaster.ReadHoldingRegisters(slaveAddress, startAddress, numRegisters);
写入单个保持寄存器的值:
modbusMaster.WriteSingleRegister(slaveAddress, registerAddress, value);
写入多个保持寄存器的值:
ushort[] registerValues = new ushort[] { 100, 200, 300 };
modbusMaster.WriteMultipleRegisters(slaveAddress, startAddress, registerValues);
读取输入寄存器的值:
ushort[] inputs = modbusMaster.ReadInputRegisters(slaveAddress, startAddress, numInputs);
使用自定义方式发送
我们在使用自定义的方式的时候,需要知道设备号,功能码,地址,数据个数,CRC16校验,比如我们只想使用03功能码,我们就可以自己封装一个03功能码发送的方法,比如下图,我的实参是地址和长度,里面的设备号,功能码我都是固定的,然后计算CRC16,最后使用Serport的write发送,read接收数据校验CRC16码,不会serport的看我之前的文章。
注意:CRC16 校验是从设备号开始到CRC校验码之前,也就是最后两个字节除外,因为用前面的才能算出后两个字节的CRC16校验码
public byte[] SendGen(UInt16 address,UInt16 number) { try { byte[] data1 = new byte[6]; data1[0] = 0x01; data1[1] = 0x03; data1[2] = Convert.ToByte((address >> 8) & 0xff); data1[3] = Convert.ToByte(address & 0xff); data1[4] = Convert.ToByte((number >> 8) & 0xff); data1[5] = Convert.ToByte(number & 0xff); byte[] data2 = new byte[2]; data2 = CRC16(data1); byte[] data3 = new byte[8]; Array.Copy(data1, 0, data3, 0, 6);//CRC16校验,从前面设备号一直到CRC码之前的数据 Array.Copy(data2, 0, data3, 6, 2); return data3; } catch (Exception ex) { Console.WriteLine(ex.ToString()); return null; } }
CRC16 校验
public static byte[] CRC16(byte[] data) { ushort crc = 0xFFFF; for (int i = 0; i < data.Length; i++) { crc ^= (ushort)data[i]; for (int j = 0; j < 8; j++) { if ((crc & 0x0001) != 0) { crc >>= 1; crc ^= 0xA001; } else { crc >>= 1; } } } return new byte[] { (byte)(crc & 0xFF), (byte)(crc >> 8) }; }
使用自定义封装的功能方法,比较好查BUG,灵活度就高一点,我们在使用第三方库的时候,有时数据不对,第三方库不好打断点,所以我们用自定义封装的就不会出现这种情况,可以清楚知道哪里的问题。
总结
ModBus RTU 是我们广泛使用的,在我们的PLC或者上位机里面用的比较多。Modbus协议广泛应用于工业自动化领域,可以用于控制器之间的通信、传感器的数据采集和PLC与HMI之间的通信等。由于其简单、可靠和易于实现等特点,Modbus协议仍然是工业界中最常用的通信协议之一。讲人话:就是规定了一个设备和软件之间发送和接收数据的规则,根据这个规则数据格式去发送数据和接收数据;牢记使用的功能码