Bootstrap

C#实现ModbusRTU详解【六】—— NModbus4报文读写

前言

NModbus4提供了直接读写的方法,但是通过那几个方法,我们无法获取NModbus4生成的报文。如果需要获取报文,则需要使用另外一种方式实现读写。

传送门:

C#实现ModbusRTU详解【一】—— 简介及仿真配置

C#实现ModbusRTU详解【二】—— 生成读取报文

C#实现ModbusRTU详解【三】—— 生成写入报文

C#实现ModbusRTU详解【四】—— 通讯Demo

C#实现ModbusRTU详解【五】—— NModbus4的使用

本专栏的代码已上传至GitHub,项目地址如下:

https://github.com/XMNHCAS/ModbusRtuDemo 


ModbusMaster类

之前我们使用NModbus4的时候,使用的是IModbusMaster这个接口来创建通讯实例。代码如下所示:

//创建串口实例
SerialPort sport = new SerialPort("COM11", 9600, Parity.None, 8, StopBits.One);
//创建ModbusRTU主站实例
IModbusMaster master = ModbusSerialMaster.CreateRtu(sport);

查看IModbusMaster的定义,可以看到,它仅有四种读取方法和四种写入的同步及异步方法。

实际上,NModbus4中存在一个实现了IModbusMaster接口的类,ModbusMaster,它的定义如下图所示:

可以看到,ModbusMaster这个类中,比IModbusMaster多了一个ExecuteCustomMessage的方法。我们可以通过这个方法来发送我们的自定义报文,并获取接收到的相应报文。

但是这个方法接收的参数和返回的值,均为实现了IModbusMessage接口的类的实例,接下来我们来看看IModbusMessage。 


IModbusMessage接口

IModbusMessage是NModbus4的报文定义接口,它的定义如下:

它定义了五个属性和一个方法,但是我们只需要关注的只前面四个,FunctionCode为功能码,SlaveAddress为从站ID,MessageFrame为除校验码外的报文主体,ProtocolDataUnit为除校验码和从站地址外的报文。

我们使用ModbusMaster的ExecuteCustomMessage方法时,可以自行创建实现该接口的类,来生成对应的请求报文和响应报文。不过NModbus4已经为我们创建好了常用的报文类,所以我们在日常使用的时候,无需再自行定义。如果有兴趣学习如何自行创建实现IModbusMessage的类,可以在GitHub上下载NModbus4的源码自行研究。

以下为NModbus4请求报文和相应报文类:

请求报文响应报文
线圈读取Modbus.Message.ReadCoilsInputsRequestModbus.Message.ReadCoilsInputsResponse
写入单个Modbus.Message.WriteSingleCoilRequestResponse
批量Modbus.Message.WriteMultipleCoilsRequestModbus.Message.WriteMultipleCoilsResponse
寄存器读取Modbus.Message.ReadHoldingInputRegistersRequestModbus.Message.ReadHoldingInputRegistersResponse
写入单个Modbus.Message.WriteSingleRegisterRequestResponse
批量Modbus.Message.WriteMultipleRegistersRequestModbus.Message.WriteMultipleRegistersResponse

与写报文

Modbus.Message.ReadWriteMultipleRegistersRequestReadHoldingInputRegistersResponse
WriteMultipleRegistersResponse

读写数据

NModbus4的报文读写的方式,与我们在前面自定义的方式基本相同,同样是通过串口发送我们生成的请求报文以及解析响应报文。

具体实现过程就是先创建对应的请求报文实例,确定从站地址、功能码、读写地址、写入数据等参数,然后使用ModbusMaster实例的ExecuteCustomMessage方法,以请求报文的实例为参数,并规定响应报文的类型,最后获取响应的结果。

读取线圈

通过ReadCoilsInputsRequest类创建对应的请求报文实例,构造函数第一个参数为功能码,第二个为从站地址,第三个为起始地址,第四个为读取的线圈数。

ExecuteCustomMessage<ReadCoilsInputsResponse>规定了返回的值类型为ReadCoilsInputsResponse。

返回的数据的Data属性为读取到的实际数据。

//功能码01 请求报文
ReadCoilsInputsRequest readCoilsReq = new ReadCoilsInputsRequest(0x01, 0x01, 0, 10);
//获取响应报文
var readCoilsRes = master.ExecuteCustomMessage<ReadCoilsInputsResponse>(readCoilsReq);

//功能码02 请求报文
ReadCoilsInputsRequest readInputCoilsReq = new ReadCoilsInputsRequest(0x02, 0x01, 0, 10);
//获取响应报文
var readInputCoilsRes = master.ExecuteCustomMessage<ReadCoilsInputsResponse>(readInputCoilsReq);

读取寄存器

与读取线圈原理相同,此处不多赘述。

//功能码03 请求报文
ReadHoldingInputRegistersRequest readRegistersReq = new ReadHoldingInputRegistersRequest(0x03, 0x01, 0, 10);
//获取响应报文
var readRegistersRes = master.ExecuteCustomMessage<ReadHoldingInputRegistersResponse>(readRegistersReq);

//功能码04 请求报文
ReadHoldingInputRegistersRequest readInputRegistersReq = new ReadHoldingInputRegistersRequest(0x04, 0x01, 0, 10);
//获取响应报文
var readInputRegistersRes = master.ExecuteCustomMessage<ReadHoldingInputRegistersResponse>(readInputRegistersReq);

写入线圈

写入单个线圈时,使用WriteSingleCoilRequestResponse获取请求报文,同样使用该类来获取结果。

而批量写入时则使用WriteMultipleCoilsRequest来获取请求报文。在创建该实例时,需要使用DiscreteCollection来规定写入值。

它们的构造函数的第一个参数是从站地址,第二个参数是写入值的起始地址,第三个参数是写入值。

//写入单个线圈
WriteSingleCoilRequestResponse writeSingleCoilsReq = new WriteSingleCoilRequestResponse(1, 0, true);
//获取响应报文
var writeSingleCoilsRes = master.ExecuteCustomMessage<WriteSingleCoilRequestResponse>(writeSingleCoilsReq);

//批量写入线圈
//写入的值
DiscreteCollection writeMultipleCoilsParam = new DiscreteCollection(new List<bool> { true, true });
//获取请求报文
WriteMultipleCoilsRequest writeMultipleCoilsReq = new WriteMultipleCoilsRequest(1, 1, writeMultipleCoilsParam);
//获取响应报文
var writeMultipleCoilsRes = master.ExecuteCustomMessage<WriteMultipleCoilsResponse>(writeMultipleCoilsReq);

写入寄存器

与写入线圈原理基本相同,此处不多赘述。

//写入单个寄存器
WriteSingleRegisterRequestResponse writeSingleRegisterReq = new WriteSingleRegisterRequestResponse(1, 0, 33);
//获取响应报文
var writeSingleRegisterRes = master.ExecuteCustomMessage<WriteSingleRegisterRequestResponse>(writeSingleRegisterReq);

//批量写入寄存器
//写入的值
RegisterCollection writeMultipleParam = new RegisterCollection(new List<ushort> { 11, 22 });
//获取请求报文
WriteMultipleRegistersRequest writeMultipleRegistersReq = new WriteMultipleRegistersRequest(1, 1, writeMultipleParam);
//获取响应报文
var writeMultipleRegistersRes = master.ExecuteCustomMessage<WriteMultipleRegistersResponse>(writeMultipleRegistersReq);

读与写寄存器

读写寄存器还提供了一个特别的类——ReadWriteMultipleRegistersRequest,这个类不能直接作为ExecuteCustomMessage方法的参数,但是这个类有两个属性,分别为ReadRequest和WriteRequest,这两个属性的类型分别为ReadHoldingInputRegistersRequest和WriteMultipleRegistersRequest,也就是寄存器的读写请求报文类。实际上这个类只是对寄存器的读写请求报文进行了进一步的封装。当我们需要对同一个从站的保持型寄存器既执行读取操作,又进行写入操作的时候,就可以使用该类。

该类的构造函数第一个参数为从站地址,第二个参数为起始的读取地址,第三个参数为读取的寄存器数量,第四个参数为起始的写入地址,第五个参数为写入的值。

//写入的值
RegisterCollection rwMultipleParam = new RegisterCollection(new List<ushort> { 11, 22 });
//获取读写寄存器的读与写的请求报文
ReadWriteMultipleRegistersRequest rwMultipleRegistersReq = new ReadWriteMultipleRegistersRequest(1, 0, 10, 5, rwMultipleParam);

//获取读取的结果
var rMultipleRegistersReq = master.ExecuteCustomMessage<ReadHoldingInputRegistersResponse>(rwMultipleRegistersReq.ReadRequest);

//获取写入的响应报文
var wMultipleRegistersReq = master.ExecuteCustomMessage<WriteMultipleRegistersResponse>(rwMultipleRegistersReq.WriteRequest);

直接执行请求报文

当然我们也可以直接使用我们自己写好的报文来进行发送,下面例子就是使用我们写好的读取报文来进行读取操作。

不过需要注意的是,ExecuteCustomMessage的参数和返回类型都必须实现IModbusMessage接口,所以我们有了报文之后,也依然需要使用ModbusMessageFactory.CreateModbusRequest()方法,来将我们的报文数组转换为实现了IModbusMessage接口的请求报文实例,然后再使用ExecuteCustomMessage。

//01 03 00 00 00 0A C5 CD
//读取报文
byte[] readMsg = new byte[] { 0x01, 0x03, 0x00, 0x00, 0x00, 0x0A, 0xC5, 0xCD };
//获取请求报文
var readMsgReq = ModbusMessageFactory.CreateModbusRequest(readMsg);
//获取响应报文
var readMsgRes = master.ExecuteCustomMessage<ReadHoldingInputRegistersResponse>(readMsgReq);

完整代码

以下以控制台应用为例,实现NModbus4的报文读写操作。

using Modbus.Data;
using Modbus.Device;
using Modbus.Message;
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Text;

namespace NModbus
{
    class Program
    {
        static void Main(string[] args)
        {
            //串口实例
            SerialPort sport = new SerialPort("COM11", 9600, Parity.None, 8, StopBits.One);

            //NModbus4实例
            ModbusMaster master = ModbusSerialMaster.CreateRtu(sport);

            //打开串口
            sport.Open();

            #region 读取线圈

            //功能码01 请求报文
            ReadCoilsInputsRequest readCoilsReq = new ReadCoilsInputsRequest(0x01, 0x01, 0, 10);
            //获取响应报文
            var readCoilsRes = master.ExecuteCustomMessage<ReadCoilsInputsResponse>(readCoilsReq);
            //输出结果
            CWRecvData("读取线圈", readCoilsRes.Data);

            //功能码02 请求报文
            ReadCoilsInputsRequest readInputCoilsReq = new ReadCoilsInputsRequest(0x02, 0x01, 0, 10);
            //获取响应报文
            var readInputCoilsRes = master.ExecuteCustomMessage<ReadCoilsInputsResponse>(readInputCoilsReq);
            //输出结果
            CWRecvData("读取输入线圈", readInputCoilsRes.Data);

            #endregion

            #region 读取寄存器

            //功能码03 请求报文
            ReadHoldingInputRegistersRequest readRegistersReq = new ReadHoldingInputRegistersRequest(0x03, 0x01, 0, 10);
            //获取响应报文
            var readRegistersRes = master.ExecuteCustomMessage<ReadHoldingInputRegistersResponse>(readRegistersReq);
            //输出结果
            CWRecvData("读取保持型寄存器", readRegistersRes.Data);

            //功能码04 请求报文
            ReadHoldingInputRegistersRequest readInputRegistersReq = new ReadHoldingInputRegistersRequest(0x04, 0x01, 0, 10);
            //获取响应报文
            var readInputRegistersRes = master.ExecuteCustomMessage<ReadHoldingInputRegistersResponse>(readInputRegistersReq);
            //输出结果
            CWRecvData("读取输入寄存器", readInputRegistersRes.Data);

            #endregion

            #region 写入线圈

            //写入单个线圈
            WriteSingleCoilRequestResponse writeSingleCoilsReq = new WriteSingleCoilRequestResponse(1, 0, true);
            //获取响应报文
            var writeSingleCoilsRes = master.ExecuteCustomMessage<WriteSingleCoilRequestResponse>(writeSingleCoilsReq);
            //输出响应报文
            CWRecvData("写入单个线圈的响应报文(无校验码)", writeSingleCoilsRes.SlaveAddress, writeSingleCoilsRes.ProtocolDataUnit);

            //批量写入线圈
            //写入的值
            DiscreteCollection writeMultipleCoilsParam = new DiscreteCollection(new List<bool> { true, true });
            //获取请求报文
            WriteMultipleCoilsRequest writeMultipleCoilsReq = new WriteMultipleCoilsRequest(1, 1, writeMultipleCoilsParam);
            //获取响应报文
            var writeMultipleCoilsRes = master.ExecuteCustomMessage<WriteMultipleCoilsResponse>(writeMultipleCoilsReq);
            //输出响应报文
            CWRecvData("批量写入线圈的响应报文(无校验码)", writeMultipleCoilsRes.SlaveAddress, writeMultipleCoilsRes.ProtocolDataUnit);

            #endregion

            #region 写入寄存器

            //写入单个寄存器
            WriteSingleRegisterRequestResponse writeSingleRegisterReq = new WriteSingleRegisterRequestResponse(1, 0, 33);
            //获取响应报文
            var writeSingleRegisterRes = master.ExecuteCustomMessage<WriteSingleRegisterRequestResponse>(writeSingleRegisterReq);
            //输出响应报文
            CWRecvData("写入单个寄存器的响应报文(无校验码)", writeSingleRegisterRes.SlaveAddress, writeSingleRegisterRes.ProtocolDataUnit);

            //批量写入寄存器
            //写入的值
            RegisterCollection writeMultipleParam = new RegisterCollection(new List<ushort> { 11, 22 });
            //获取请求报文
            WriteMultipleRegistersRequest writeMultipleRegistersReq = new WriteMultipleRegistersRequest(1, 1, writeMultipleParam);
            //获取响应报文
            var writeMultipleRegistersRes = master.ExecuteCustomMessage<WriteMultipleRegistersResponse>(writeMultipleRegistersReq);
            //输出响应报文
            CWRecvData("批量写入寄存器的响应报文(无校验码)", writeMultipleRegistersRes.SlaveAddress, writeMultipleRegistersRes.ProtocolDataUnit);

            #endregion

            #region 读与写寄存器

            //写入的值
            RegisterCollection rwMultipleParam = new RegisterCollection(new List<ushort> { 11, 22 });
            //获取读写寄存器的读与写的请求报文
            ReadWriteMultipleRegistersRequest rwMultipleRegistersReq = new ReadWriteMultipleRegistersRequest(1, 0, 10, 5, rwMultipleParam);

            //获取读取的结果
            var rMultipleRegistersReq = master.ExecuteCustomMessage<ReadHoldingInputRegistersResponse>(rwMultipleRegistersReq.ReadRequest);
            //输出结果
            CWRecvData("读取寄存器读取值", rMultipleRegistersReq.Data);

            //获取写入的响应报文
            var wMultipleRegistersReq = master.ExecuteCustomMessage<WriteMultipleRegistersResponse>(rwMultipleRegistersReq.WriteRequest);
            //输出响应报文
            CWRecvData("写入寄存器响应报文(无校验码)", wMultipleRegistersReq.SlaveAddress, wMultipleRegistersReq.ProtocolDataUnit);

            #endregion

            #region 直接使用报文读取

            //01 03 00 00 00 0A C5 CD
            //读取报文
            byte[] readMsg = new byte[] { 0x01, 0x03, 0x00, 0x00, 0x00, 0x0A, 0xC5, 0xCD };
            //获取请求报文
            var readMsgReq = ModbusMessageFactory.CreateModbusRequest(readMsg);
            //获取响应报文
            var readMsgRes = master.ExecuteCustomMessage<ReadHoldingInputRegistersResponse>(readMsgReq);
            //输出结果
            CWRecvData("直接使用报文读取", readMsgRes.Data);

            #endregion

            //关闭串口
            sport.Close();

            Console.ReadKey();
        }

        /// <summary>
        /// 将接收到的数据打印到控制台
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="str">内容说明</param>
        /// <param name="data">接收到的数据</param>
        public static void CWRecvData<T>(string str, ICollection<T> data)
        {
            Console.WriteLine($"{str}:");
            foreach (var item in data)
            {
                Console.Write($"{item} ");
            }

            Console.WriteLine("\n");
        }

        /// <summary>
        /// 显示报文
        /// </summary>
        /// <param name="str">内容说明</param>
        /// <param name="slaveID">从站ID</param>
        /// <param name="msg">报文主体</param>
        public static void CWRecvData(string str, byte slaveID, byte[] msg)
        {
            Console.WriteLine($"{str}:");
            Console.Write($"{slaveID.ToString("X2")} ");
            foreach (var item in msg)
            {
                Console.Write($"{item.ToString("X2")} ");
            }

            Console.WriteLine("\n");
        }
    }
}

执行结果:


结尾

本文介绍了如何使用NModbus4生成请求报文及获取响应报文,这些是NModbus4实现ModbusRTU通讯的底层方法,实际应用时,使用普通的几个读写方法即可。

;