C# ModBus协议(RTU )详细指南
IC00 2024-07-10 08:35:01 阅读 98
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协议支持多种错误检测和纠正机制,包括CRC校验和奇偶校验等,从而保证了数据传输的可靠性)灵活性好(Modbus协议支持多种数据类型和数据格式,可以满足不同应用场景的需求。)易于维护,成本低(由于Modbus协议是一种标准协议,因此可以使用各种现有的工具和库来进行开发和维护,从而降低了开发和维护成本)最关键一点也是最重要的一点就是:部署简单,易于实现(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 |
<code>01:表示从设备的地址,这里为1。
03:表示Modbus读取保持寄存器的功能码。
00 00:表示要读取的保持寄存器的起始地址,这里为0。
00 0A:表示要读取的保持寄存器的数量,这里为10。 (我们需要读取多少个值)
C5 CD:表示CRC校验码,用于检查命令是否正确。
因此,这个命令的含义是从地址为1的Modbus设备中读取0~9号保持寄存器的值。
注:以11222的格式来看这个命令,只有功能号是需要选择,CRC校验码是计算出来的,其他的都可以根据我们的需求去定义
常用的功能号
<code>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的主站方便通讯
发送命令我们得到了一个答复,我们可以看到我们的服务端的数据
<code>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 =20
00 01 读取到0x0000寄存器地址的数据(两个字节)(高位在前,低位在后)
00 02
00 03
00 04
00 00
00 00
00 07
00 08
00 09
00 04 数据(两个字节)
34 BE CRC16校验码(两个字节)
注意:问询数据的长度最大是127个,它应答最大长度254个
**注意:注意:注意:接收的数据是高位在前低位在后 **
比如我在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
<code>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校验码
<code> 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协议仍然是工业界中最常用的通信协议之一。讲人话:就是规定了一个设备和软件之间发送和接收数据的规则,根据这个规则数据格式去发送数据和接收数据;牢记使用的功能码
上一篇: 【Python】成功解决ValueError: zero-size array to reduction operation minimum which has no identity
下一篇: MATLAB知识点:矩阵的重构和重新排列
本文标签
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。