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协议仍然是工业界中最常用的通信协议之一。讲人话:就是规定了一个设备和软件之间发送和接收数据的规则,根据这个规则数据格式去发送数据和接收数据;牢记使用的功能码

image.png



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。