因业务需要了解Modbus协议的使用,因此对Modbus的协议,以及相应的C#处理应用进行了解,针对协议的几种方式(RTU、ASCII、TCPIP)进行了封装,以及对Modbus的各种功能码的特点进行了详细的了解,本篇随笔基于这些知识进行了一定的梳理和介绍,主要内容包括Modbus协议简要介绍、Modbus模拟工具使用和Modbus应用开发几个部分。
1)Modbus协议简要介绍
Modbus 协议是应用于电子控制器上的一种通用语言。通过此协议,控制器相互之间、控制器经由网络(例如以太网)和其它设备之间可以通信。它已经成为一通用工业标准。有了它,不同厂商生产的控制设备可以连成工业网络,进行集中监控。
此协议定义了一个控制器能认识使用的消息结构,而不管它们是经过何种网络进行通信的。它描述了一控制器请求访问其它设备的过程,如果回应来自其它设备的请求,以及怎样侦测错误并记录。它制定了消息域格局和内容的公共格式。
当在一Modbus网络上通信时,此协议决定了每个控制器须要知道它们的设备地址,识别按地址发来的消息,决定要产生何种行动。如果需要回应,控制器将生成反馈信息并用Modbus协议发出。在其它网络上,包含了Modbus协议的消息转换为在此网络上使用的帧或包结构。这种转换也扩展了根据具体的网络解决节地址、路由路径及错误检测的方法。
Modbus由MODICON公司于1979年开发,是一种工业现场总线协议标准。1996年施耐德公司推出基于以太网TCP/IP的modbus协议:modbusTCP。
Modbus协议是一项应用层报文传输协议,包括ASCII、RTU、TCP三种报文类型。
标准的Modbus协议物理层接口有RS232、RS422、RS485和以太网接口,采用master/slave方式通信。
对于串行连接,存在两个变种,它们在数值数据表示不同和协议细节上略有不同。Modbus RTU是一种紧凑的,采用二进制表示数据的方式,Modbus ASCII是一种人类可读的,冗长的表示方式。这两个变种都使用串行通信(serial communication)方式。RTU格式后续的命令/数据带有循环冗余校验的校验和,而ASCII格式采用纵向冗余校验的校验和。被配置为RTU变种的节点不会和设置为ASCII变种的节点通信,反之亦然。
对于通过TCP/IP(例如以太网)的连接,存在多个Modbus/TCP变种,这种方式不需要校验和计算。
对于所有的这三种通信协议在数据模型和功能调用上都是相同的,只有封装方式是不同的。
Modbus有一个扩展版本Modbus Plus(Modbus+或者MB+),不过此协议是Modicon专有的,和Modbus不同。它需要一个专门的协处理器来处理类似HDLC的高速令牌旋转。它使用1Mbit/s的双绞线,并且每个节点都有转换隔离装置,是一种采用转换/边缘触发而不是电压/水平触发的装置。连接Modbus Plus到计算机需要特别的接口,通常是支持ISA(SA85),PCI或者PMCIA总线的板卡。
MODBUS 协议定义了一个与基础通信层无关的简单协议数据单元(PDU)。
Modbus串行连路上的的PDU如下所示。
错误检验域是对报文内容执行"冗余校验" 的计算结果。根据不同的传输模式(RTU or ASCII) 使用两种不同的计算方法。
RTU的报文格式如下所示。
ASCII码的报文格式如下所示。
在 ASCII 模式, 报文用特殊的字符区分帧起始和帧结束。一个报文必须以一个‘冒号’ ( : ) (ASCII 十六进制3A )起始,以‘回车-换行’ (CR LF) 对(ASCII 十六进制0D 和0A) 结束。
而Modbus TCP数据帧包含报文头、功能代码和数据3部分。
MBAP Header长度共7个字节,分别为Transaction identifier(事务标识符),Protocol identifier(协议标识符),Length(长度),
Unitidentifier(单元标识符)组成,具体如下表所示:
请求和响应带有六个字节的前缀,如下:
byte 0: 事务处理标识符 –由服务器复制 –通常为 0
byte 1: 事务处理标识符 –由服务器复制 –通常为 0
byte 2: 协议标识符= 0
byte 3: 协议标识符= 0
byte 4: 长度字段 (上半部分字节) = 0 (所有的消息长度小于256)
byte 5: 长度字段 (下半部分字节) = 后面字节的数量
byte 6: 单元标识符 (原“从站地址”)
byte 7: MODBUS 功能代码
byte 8 on: 所需的数据
数据区:数据区是根据不同的功能码而不同。数据区可以是实际数值、设置点、主机发送给从机或从机发送给主机的地址。
标准的Modicon控制器使用RS232C实现串行的Modbus。Modbus的ASCII、RTU协议规定了消息、数据的结构、命令和就答的方式,数据通讯采用Maser/Slave方式。
Modbus协议需要对数据进行校验,串行协议中除有奇偶校验外,ASCII模式采用LRC校验,RTU模式采用16位CRC校验.
ModbusTCP模式没有额外规定校验,因为TCP协议是一个面向连接的可靠协议。
对于常规的Modbus串口协议,我们来看看03功能码的读取寄存器的操作请求和响应代码了解下。
请求PDU格式如下所示。
响应的PDU格式如下所示。
一个请求读寄存器108-110 的实例:
可以注意到,很多数据的处理,需要拆分高位低位,高位在前,低位在后的模式。
根据这些RTU、ASCII、TCPIP的Modbus协议的不同,我们可以构建一个通用的处理程序来处理这些操作,在后面的应用开发部分继续介绍。
2)Modbus模拟工具使用
一般在做Mobus前期的开发的时候,一般不是针对具体的Modbus设备进行寄存器的处理,而是使用Modbus模拟工具来进行调试,一般我们需要配合Modbus Slave、Modbus Poll、Virtual Serial Port Driver这几个模拟软件来进行开发的。
Modbus Poll :Modbus主机仿真器,用于测试和调试Modbus从设备。该软件支持Modbus的RTU、ASCII、TCP/IP。用来帮助开发人员测试Modbus从设备,或者其它Modbus协议的测试和仿真。
Modbus Slave: Modbus从设备仿真器,可以仿真32个从设备/地址域。每个接口都提供了对EXCEL报表的OLE自动化支持。主要用来模拟Modbus从站设备,接收主站的命令包,回送数据包。帮助Modbus通讯设备开发人员进行Modbus通讯协议的模拟和测试,用于模拟、测试、调试Modbus通讯设备。
Virtual Serial Port Driver:虚拟串口工具,不需要串口接线,提供虚拟的串口,适合学习和调试使用。
配合这几款软件,我们就可以实现串口Modbus协议的模拟测试了,如果我们使用Modbus的TCPIP协议,那么我们不需要VSPD也可以。
如果我们使用Modbus协议的串口通讯方式,那么我们先要使用VSPD进行串口的配对模拟,模拟出两个通讯的串口端口,端口配对模拟成功后,我们可以看到设备管理器中增加了两个端口了。
接着使用从机模拟器,模拟一个Modbus从机供测试,通过菜单【Connection】【Connect】启动,我们选择连接方式为串口,端口则选择我们配对的其中一个端口即可,如下图所示。
其中模式选择RTU或者ASCII都可以,这两个模式协议有所不同,一旦从机选择RTU模式,那么Modbus主机也需要选择对应的RTU模式,反之亦然。
其他串口设置,如波特率、数据位、奇偶位、停止位等默认配置即可。
如果我们选择TCPIP模式,那么对应Modbus主机也需要选择TCPIP方式。
一旦Modbus从机启动,就会处理来自Modbus主机的指令请求(如果有的话),并做相应处理,我们可以通过【Display】【Communication】菜单弹出的对话框,了解到对应请求和应答的协议详细信息。
Modbus主机的启动和ModBus从机类似,我们根据ModBus从机的配置,选择对应的主机配置,Modbus模拟主机启动和查看通讯记录界面如下所示。
另外我们可以通过【Display】里面选择内容显示的进制格式。
在从机的设置里面,我们可以修改从机的定义信息,以便设置对应的从机ID,功能码,其实地址,长度或者数量的信息,如下界面所示。
我们可以根据实际的寄存器地址和数量,设置对应的数值,如下是显示4个数据的内容设置和显示内容。
设置后正常的内容显示如下。
同时我们也需要设置对应Modbus主机模拟器的地址和数量,正确设置后可以正常显示。
通过更深一步的设置或者调整,我们可以极大程度的进行模拟Modbus实际设备的处理方式,从而在没有实际Modbus硬件设备的情况下尽可能通过前期的模拟完成常规功能的测试和准备。
在我们开发Modbus应用的时候,我们对照相应的主从机Modbus协议请求和应答,能够检查我们程序的输出是否正常,从而可以快速的开发Modbus的应用处理功能。
3)Modbus应用开发
为了模拟对接Modbus的RTU、ASCII、TCP/IP协议处理,我根据不同协议的处理方式定义了一个辅助函数,然后统一进行处理,以便达到统一调用的处理便利。
首先我们来看看使用串口模式下(RTU、ASCII)的处理界面效果,这个直接获取模拟器Modbus Slave从机的数值进行显示的。
TCPIP网络方式对接Modbus界面处理效果如下所示。
两者数据均来源于Modbus Slave从机的数值,只是它们对接的方式不同。
串口的处理,我通过SerialPortUtil类来使用Windows的串口类,处理对应的串口操作,通过定义事件的方式,使得串口收到数据的时候,及时通知调用者进行界面更新处理即可。
//使用字符串参数构造 serial = new SerialPortUtil(portname, this.txtBaudRate.Text, this.txtParity.Text, this.txtDataBits.Text, this.txtStopBit.Text); //收到数据处理的事件 serial.DataReceived += Serial_DataReceived; serial.RTUMode = this.radRTU.Checked;//默认RTU模式为True,否则使用ASCII模式
收到数据后,及时通过委托方式,通知UI进行界面的更新显示。
/// <summary> /// 收到串口响应事件后,及时进行处理(更新在界面上) /// </summary> /// <param name="e"></param> private void Serial_DataReceived(DataReceivedEventArgs e) { //记录在日志,方便复制 LogTextHelper.Info(e.DataReceived); //使用委托进行处理界面控件的数据更新 this.txtResponse.Invoke(new MethodInvoker(()=> { //显示在界面上 this.txtResponse.AppendText(e.DataReceived); this.txtResponse.AppendText(Environment.NewLine); var dataBytes = e.BytesReceived; if(dataBytes != null && dataBytes.Length > 2) { var function = dataBytes[1]; if(function > 0x80)//128 { //Modbus的异常代码大于128,如果是异常,则可以解析错误 var newFunction = function - 0x80; lblTips.Text = "响应有异常,功能代码:" + newFunction.ToString("D2"); lblTips.Text += ",错误描述:" + ((ModBusExceptionCode)newFunction).ToString(); } else { lblTips.Text = "响应正常";//小于128的为正常响应 } } })); }
而对于网络方式,我们先要定义一个Socket通讯的基类,封装相关的通讯处理操作。
然后简单构建一个子类进行使用,如下所示。
/// <summary> /// 通信类子类 /// </summary> public class ModbusClient : BaseSocketClient { public ModbusClient() { this.Name = "ModbusClient"; } }
界面处理的时候,我们只需要初始化一个ModbusClient类来使用即可,如下代码所示。
client = new ModbusClient(); //收到数据处理的事件 client.DataReceived += Client_DataReceived;
收到数据通知界面进行更新的操作如下所示。
private void Client_DataReceived(DataReceivedEventArgs e) { //记录在日志,方便复制 LogTextHelper.Info(e.DataReceived); //使用委托进行处理界面控件的数据更新 this.txtResponse.Invoke(new MethodInvoker(() => { this.txtResponse.AppendText(e.DataReceived); this.txtResponse.AppendText(Environment.NewLine); var dataBytes = e.BytesReceived; if (dataBytes != null && dataBytes.Length > 2) { //串口功能码为第二个字节,TCP/IP功能码为第8个 var function = dataBytes[7]; if (function > 0x80)//128 { //Modbus的异常代码大于128,如果是异常,则可以解析错误 var newFunction = function - 0x80; lblTips.Text = "响应有异常,功能代码:" + newFunction.ToString("D2"); lblTips.Text += ",错误描述:" + ((ModBusExceptionCode)newFunction).ToString(); } else { lblTips.Text = "响应正常";//小于128的为正常响应 } } })); }
不管是串口的RTU或者ASCII,又或者是TCPIP的协议,我们可以通过定义一个协议封装的辅助类ModbusQueryHelper来处理协议的具体细节。
/// <summary> /// Modbus查询消息生成辅助类,可以用于串口RTU/ASCII协议,也可以用于TCPIP协议。 /// 用于生成各种功能代码的消息内容。 /// </summary> public class ModbusQueryHelper { /// <summary> /// 是否为RTU模式,默认为True,否则为ASCII方式 /// </summary> public ModbusProtocol Protocol { get; set; } = ModbusProtocol.RTU; /// <summary> /// 默认函数 /// </summary> public ModbusQueryHelper() { } /// <summary> /// 参数化构造,指定RTU模式 /// </summary> /// <param name="protocal">Modbus协议:ASCII,RTU, TCP,默认为RTU</param> public ModbusQueryHelper(ModbusProtocol protocal) { this.Protocol = protocal; }
而其中ModbusProtocol是一个枚举,定义如下所示。
/// <summary> /// 几种常用的Modbus协议 /// </summary> public enum ModbusProtocol { /// <summary> /// 串口的ASCII模式 /// </summary> ASCII, /// <summary> /// 串口的RTU模式 /// </summary> RTU, /// <summary> /// 网络TCPIP模式 /// </summary> TCP }
我们通过ModbusQueryHelper 类,可以处理不同协议之间的封装细节,并可以对各种功能码的协议进行封装处理。
以上就是 相关Modbus的应用处理和封装,对于常规的Modbus协议可以极大简化对接处理,在实际对接Modbus设备的时候,我们只需要根据对应的说明书,获取对应的内容,就可以把例如温度、湿度、转速等一些设备或者机器人的参数获得,并记录在数据库里面,然后在应用模块中整合一些图表展示就可以很好的实现看板功能了。