欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C#ModBus Tcp Master的实现(1)

程序员文章站 2022-06-28 20:13:52
Modbus已经成为工业领域通信协议的业界标准(De facto),并且现在是工业电子设备之间常用的连接方式。 所以这也是我们工控领域软件开发的所必懂的通讯协议,我也是初次学习,先贴上我的学习笔记 一 .协议概述 (1)Modbus协议是应用于控制器上的一种通用语言,实现控制器之间,控制器通过网络和 ......

modbus已经成为工业领域通信协议的业界标准(de facto),并且现在是工业电子设备之间常用的连接方式。

所以这也是我们工控领域软件开发的所必懂的通讯协议,我也是初次学习,先贴上我的学习笔记

 

一 .协议概述

(1)modbus协议是应用于控制器上的一种通用语言,实现控制器之间,控制器通过网络和其他设备之间的通信,支持传统rs232/rs422/rs485和以太网设备,它已经成为一种通用的工业标准,有了它不同厂商生产的控制设备可以连成工业网络,进行集中控制,此协议定义了一个控制器能认识使用的消息结构

(2) 如果按照国际 iso/osi 的 7 层网络模型来说,标准 modbus 协议定义了通信物理层、链路层及应用层;

物理层:定义了基于 rs232 和 rs485 的异步串行通信规范;

链路层:规定了基于站号识别、主 / 从方式的介质访问控制;

应用层:规定了信息规范(或报文格式)及通信服务功能;

C#ModBus Tcp Master的实现(1)

 

二. 协议要点

(1) modbus 是主 / 从通信协议。主站主动发送报文 , 只有与主站发送报文中呼叫地址相同的从站才向主站发送回答报文。

(2) 报文以 0 地址发送时为广播模式,无需从站应答,可作为广播报文发送,包括:

  ①修改线圈状态;

  ②修改寄存器内容;

  ③强置多线圈;

  ④预置多寄存器;

  ⑤询问诊断;

(3) modbus 规定了 2 种字符传输模式: ascii 模式、 rtu (二进制)模式;两种传输模式不能混用;

C#ModBus Tcp Master的实现(1)

(4) 传输错误校验

  传输错误校验有奇偶校验、冗余校验检验。

  当校验出错时,报文处理停止,从机不再继续通信,不对此报文产生应答;

  通信错误一旦发生,报文便被视为不可靠; modbus 主机在一定时间过后仍未收到从站应答,即作出“通信错误已发生”的判断。

(5) 报文级(字符级)采用 crc-16 (循环冗余错误校验)

(6) modbus 报文 rtu 格式

C#ModBus Tcp Master的实现(1)

 

三. 异常应答

(1) 从机接收到的主机报文,没有传输错误,但从机无法正确执行主机命令或无法作出正确应答,从机将以“异常应答”回答之。

(2) 异常应答报文格式

例:主机发请求报文,功能码 01 :读 1 个 04a1 线圈值

C#ModBus Tcp Master的实现(1)

  由于从机最高线圈地址为 0400 ,则 04a1 超地址上限,从机作出异常应答如下(注意:功能码最高位置 1 ):

C#ModBus Tcp Master的实现(1)

(3)异常应答码

C#ModBus Tcp Master的实现(1)

四. 寄存器和功能码

modbus的功能码很多,且不同功能码对应的报文也不一致,后续博客我会借用开源库实现一个modbus master 测试功能码 解析报文

下边我用表格总结一下寄存器,功能码,报文格式

C#ModBus Tcp Master的实现(1)

C#ModBus Tcp Master的实现(1)

注:

(1)报文中的所有字节均为16进制

(2)由上图我们总结出不同的功能码的报文(无论询问报文还是响应报文)前8个字节都是一致的 都是2字节消息号+2字节modbus标识+2字节长度+1字节站号+1字节功能码 后边根据功能码不同而不同

(3)报文中,指定线圈通断标志  ff00 置线圈为on  0000置线圈为off

 

五.具体实现

接下来我们使用开源库nmodbus库,来实现一个modbus master

创建工程,从nuget管理器安装nmodbusu

C#ModBus Tcp Master的实现(1)

 

先简单介绍一下nmodbus中的几个重要方法

C#ModBus Tcp Master的实现(1)

 

接下来做具体实现

 

  1 using system;
  2 using system.collections.generic;
  3 using system.componentmodel;
  4 using system.data;
  5 using system.drawing;
  6 using system.linq;
  7 using system.text;
  8 using system.threading.tasks;
  9 using system.windows.forms;
 10 using nmodbus;
 11 using system.net.sockets;
 12 using system.threading;
 13 
 14 namespace modbustcp
 15 {
 16     public partial class form1 : form
 17     {
 18 
 19         private static modbusfactory modbusfactory;
 20         private static imodbusmaster master;
 21         //写线圈或写寄存器数组
 22         bool[] coilsbuffer;
 23         ushort[] registerbuffer;
 24         //功能码
 25         string functioncode;
 26         //参数(分别为站号,起始地址,长度)
 27         byte slaveaddress;
 28         ushort startaddress;
 29         ushort numberofpoints;
 30 
 31         public form1()
 32         {
 33             initializecomponent();
 34 
 35         }
 36         private void form1_load(object sender, eventargs e)
 37         {
 38             //初始化modbusmaster
 39             modbusfactory = new modbusfactory();
 40             //在本地测试 所以使用回环地址,modbus协议规定端口号 502
 41             master = modbusfactory.createmaster(new tcpclient("127.0.0.1", 502));
 42             //设置读取超时时间
 43             master.transport.readtimeout = 2000;
 44             master.transport.retries = 2000;
 45             groupbox1.enabled = false;
 46             groupbox2.enabled = false;
 47         }
 48         /// <summary>
 49         /// 读/写
 50         /// </summary>
 51         /// <param name="sender"></param>
 52         /// <param name="e"></param>
 53         private void button1_click(object sender, eventargs e)
 54         {
 55             executefunction();
 56         }
 57 
 58         private async void executefunction()
 59         {
 60             try
 61             {
 62                 //重新实例化是为了 modbus slave更换连接时不报错 
 63                 master = modbusfactory.createmaster(new tcpclient("127.0.0.1", 502));
 64                 if (functioncode != null)
 65                 {
 66                     switch (functioncode)
 67                     {
 68                         case "01 read coils"://读取单个线圈
 69                             setreadparameters();
 70                             coilsbuffer = master.readcoils(slaveaddress, startaddress, numberofpoints);
 71 
 72                             for (int i = 0; i < coilsbuffer.length; i++)
 73                             {
 74                                 setmsg(coilsbuffer[i] + "");
 75                             }
 76                             break;
 77                         case "02 read discrete inputs"://读取输入线圈/离散量线圈
 78                             setreadparameters();
 79 
 80                             coilsbuffer = master.readinputs(slaveaddress, startaddress, numberofpoints);
 81                             for (int i = 0; i < coilsbuffer.length; i++)
 82                             {
 83                                 setmsg(coilsbuffer[i] + "");
 84                             }
 85                             break;
 86                         case "03 read holding registers"://读取保持寄存器
 87                             setreadparameters();
 88                             registerbuffer = master.readholdingregisters(slaveaddress, startaddress, numberofpoints);
 89                             for (int i = 0; i < registerbuffer.length; i++)
 90                             {
 91                                 setmsg(registerbuffer[i] + "");
 92                             }
 93                             break;
 94                         case "04 read input registers"://读取输入寄存器
 95                             setreadparameters();
 96                             registerbuffer = master.readinputregisters(slaveaddress, startaddress, numberofpoints);
 97                             for (int i = 0; i < registerbuffer.length; i++)
 98                             {
 99                                 setmsg(registerbuffer[i] + "");
100                             }
101                             break;
102                         case "05 write single coil"://写单个线圈
103                             setwriteparametes();
104                             await master.writesinglecoilasync(slaveaddress, startaddress, coilsbuffer[0]);
105                             break;
106                         case "06 write single registers"://写单个输入线圈/离散量线圈
107                             setwriteparametes();
108                             await master.writesingleregisterasync(slaveaddress, startaddress, registerbuffer[0]);
109                             break;
110                         case "0f write multiple coils"://写一组线圈
111                             setwriteparametes();
112                             await master.writemultiplecoilsasync(slaveaddress, startaddress, coilsbuffer);
113                             break;
114                         case "10 write multiple registers"://写一组保持寄存器
115                             setwriteparametes();
116                             await master.writemultipleregistersasync(slaveaddress, startaddress, registerbuffer);
117                             break;
118                         default:
119                             break;
120                     }
121 
122                 }
123                 else
124                 {
125                     messagebox.show("请选择功能码!");
126                 }
127                master.dispose();
128             }
129             catch (exception ex)
130             {
131 
132                 messagebox.show(ex.message);
133             }
134         }
135         private void combobox1_selectedindexchanged(object sender, eventargs e)
136         {
137             if (combobox1.selectedindex >= 4)
138             {
139                 groupbox2.enabled = true;
140                 groupbox1.enabled = false;
141             }
142             else
143             {
144                 groupbox1.enabled = true;
145                 groupbox2.enabled = false;
146             }
147             combobox1.invoke(new action(() => { functioncode = combobox1.selecteditem.tostring(); }));
148         }
149 
150         /// <summary>
151         /// 初始化读参数
152         /// </summary>
153         private void setreadparameters()
154         {
155             if (txt_startaddr1.text == "" || txt_slave1.text == "" || txt_length.text == "")
156             {
157                 messagebox.show("请填写读参数!");
158             }
159             else
160             {
161                 slaveaddress = byte.parse(txt_slave1.text);
162                 startaddress = ushort.parse(txt_startaddr1.text);
163                 numberofpoints = ushort.parse(txt_length.text);
164             }
165         }
166         /// <summary>
167         /// 初始化写参数
168         /// </summary>
169         private void setwriteparametes()
170         {
171             if (txt_startaddr2.text == "" || txt_slave2.text == "" || txt_data.text == "")
172             {
173                 messagebox.show("请填写写参数!");
174             }
175             else
176             {
177                 slaveaddress = byte.parse(txt_slave2.text);
178                 startaddress = ushort.parse(txt_startaddr2.text);
179                 //判断是否写线圈
180                 if (combobox1.selectedindex == 4 || combobox1.selectedindex == 6)
181                 {
182                     string[] strarr = txt_data.text.split(' ');
183                     coilsbuffer = new bool[strarr.length];
184                     //转化为bool数组
185                     for (int i = 0; i < strarr.length; i++)
186                     {
187                         // strarr[i] == "0" ? coilsbuffer[i] = true : coilsbuffer[i] = false;
188                         if (strarr[i] == "0")
189                         {
190                             coilsbuffer[i] = false;
191                         }
192                         else
193                         {
194                             coilsbuffer[i] = true;
195                         }
196                     }
197                 }
198                 else
199                 {
200                     //转化ushort数组
201                     string[] strarr = txt_data.text.split(' ');
202                     registerbuffer = new ushort[strarr.length];
203                     for (int i = 0; i < strarr.length; i++)
204                     {
205                         registerbuffer[i] = ushort.parse(strarr[i]);
206                     }
207                 }
208             }
209         }
210         /// <summary>
211         /// 清除文本
212         /// </summary>
213         /// <param name="sender"></param>
214         /// <param name="e"></param>
215         private void button2_click(object sender, eventargs e)
216         {
217             richtextbox1.clear();
218         }
219         /// <summary>
220         /// setmessage
221         /// </summary>
222         /// <param name="msg"></param>
223         public void setmsg(string msg)
224         {
225             richtextbox1.invoke(new action(() => { richtextbox1.appendtext(msg + "\r\n"); }));
226         }
227 
228     }
229 }

 

界面布局

C#ModBus Tcp Master的实现(1)

 

六 功能测试及报文解析

这里功能测试我们需要借助测试工具  modbus slave(modbus从站客户端)

链接:https://pan.baidu.com/s/1z3bet3l_2a4e6cu_p250tg
提取码:hq1r

简单说明一下,这里我实现了常用的几个功能码

0x01 读一组线圈

0x02 读一组输入线圈/离散量线圈

0x03 读一组保持寄存器

0x04 读一组输入寄存器

0x05 写单个线圈

0x06 写单个保持寄存器

0x0f 写多个线圈

0x10 写多个保持寄存器

简单说一下modbus slave 的操作

打开连接,建立连接,选择连接方式为tcp/ip 设置 ip和端口号

C#ModBus Tcp Master的实现(1)

选择线圈或寄存器

点击setup->slave definition,这里的function我们需要读/写什么线圈或寄存器就对应选择

C#ModBus Tcp Master的实现(1)

 

 

测试1 功能码0x01

这里我们所有的测试从站都使用站号1 起始地址0 长度10

功能码0x01 读取线圈 modbus slave的function选择01 coil status(0x)

测试结果:

C#ModBus Tcp Master的实现(1)

点击display->communication 可以截取报文,我也不知道为什么他报文字体那么小(绝望ing)

C#ModBus Tcp Master的实现(1)

000000-rx:00 01 00 00 00 06 01 01 00 00 00 05
000001-tx:00 01 00 00 00 04 01 01 01 06

测试2  功能码0x10

功能码0x10 写入一组数据到保持寄存器  modbus slave的function选择03 holding register(4x) (说明一下 线圈和保持寄存器才有写操作)

测试结果C#ModBus Tcp Master的实现(1)

 报文

000070-rx:00 01 00 00 00 11 01 10 00 00 00 05 0a 00 0c 00 22 00 38 00 4e 00 5a
000071-tx:00 01 00 00 00 06 01 10 00 00 00 05

上文测试了一个读操作和一个写操作,其他功能码的测试与上文一致,有兴趣的可以自行测试,

下一篇博客我要针对不同的功能码做对应的报文解析

程序源码:

链接:https://pan.baidu.com/s/1549fu65wltnvsxm0bj71da
提取码:1j96

以上都为我自己学习总结并实现,有错误之处,希望大家不吝赐教,感谢(抱拳)!