前言
NModbus4提供了直接读写的方法,但是通过那几个方法,我们无法获取NModbus4生成的报文。如果需要获取报文,则需要使用另外一种方式实现读写。
传送门:
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.ReadCoilsInputsRequest | Modbus.Message.ReadCoilsInputsResponse | |
写入 | 单个 | Modbus.Message.WriteSingleCoilRequestResponse | ||
批量 | Modbus.Message.WriteMultipleCoilsRequest | Modbus.Message.WriteMultipleCoilsResponse | ||
寄存器 | 读取 | Modbus.Message.ReadHoldingInputRegistersRequest | Modbus.Message.ReadHoldingInputRegistersResponse | |
写入 | 单个 | Modbus.Message.WriteSingleRegisterRequestResponse | ||
批量 | Modbus.Message.WriteMultipleRegistersRequest | Modbus.Message.WriteMultipleRegistersResponse | ||
读 与写报文 | Modbus.Message.ReadWriteMultipleRegistersRequest | ReadHoldingInputRegistersResponse | ||
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通讯的底层方法,实际应用时,使用普通的几个读写方法即可。