C#ModBus Tcp Master的实现(1)
modbus已经成为工业领域通信协议的业界标准(de facto),并且现在是工业电子设备之间常用的连接方式。
所以这也是我们工控领域软件开发的所必懂的通讯协议,我也是初次学习,先贴上我的学习笔记
一 .协议概述
(1)modbus协议是应用于控制器上的一种通用语言,实现控制器之间,控制器通过网络和其他设备之间的通信,支持传统rs232/rs422/rs485和以太网设备,它已经成为一种通用的工业标准,有了它不同厂商生产的控制设备可以连成工业网络,进行集中控制,此协议定义了一个控制器能认识使用的消息结构
(2) 如果按照国际 iso/osi 的 7 层网络模型来说,标准 modbus 协议定义了通信物理层、链路层及应用层;
物理层:定义了基于 rs232 和 rs485 的异步串行通信规范;
链路层:规定了基于站号识别、主 / 从方式的介质访问控制;
应用层:规定了信息规范(或报文格式)及通信服务功能;
二. 协议要点
(1) modbus 是主 / 从通信协议。主站主动发送报文 , 只有与主站发送报文中呼叫地址相同的从站才向主站发送回答报文。
(2) 报文以 0 地址发送时为广播模式,无需从站应答,可作为广播报文发送,包括:
①修改线圈状态;
②修改寄存器内容;
③强置多线圈;
④预置多寄存器;
⑤询问诊断;
(3) modbus 规定了 2 种字符传输模式: ascii 模式、 rtu (二进制)模式;两种传输模式不能混用;
(4) 传输错误校验
传输错误校验有奇偶校验、冗余校验检验。
当校验出错时,报文处理停止,从机不再继续通信,不对此报文产生应答;
通信错误一旦发生,报文便被视为不可靠; modbus 主机在一定时间过后仍未收到从站应答,即作出“通信错误已发生”的判断。
(5) 报文级(字符级)采用 crc-16 (循环冗余错误校验)
(6) modbus 报文 rtu 格式
三. 异常应答
(1) 从机接收到的主机报文,没有传输错误,但从机无法正确执行主机命令或无法作出正确应答,从机将以“异常应答”回答之。
(2) 异常应答报文格式
例:主机发请求报文,功能码 01 :读 1 个 04a1 线圈值
由于从机最高线圈地址为 0400 ,则 04a1 超地址上限,从机作出异常应答如下(注意:功能码最高位置 1 ):
(3)异常应答码
四. 寄存器和功能码
modbus的功能码很多,且不同功能码对应的报文也不一致,后续博客我会借用开源库实现一个modbus master 测试功能码 解析报文
下边我用表格总结一下寄存器,功能码,报文格式
注:
(1)报文中的所有字节均为16进制
(2)由上图我们总结出不同的功能码的报文(无论询问报文还是响应报文)前8个字节都是一致的 都是2字节消息号+2字节modbus标识+2字节长度+1字节站号+1字节功能码 后边根据功能码不同而不同
(3)报文中,指定线圈通断标志 ff00 置线圈为on 0000置线圈为off
五.具体实现
接下来我们使用开源库nmodbus库,来实现一个modbus master
创建工程,从nuget管理器安装nmodbusu
先简单介绍一下nmodbus中的几个重要方法
接下来做具体实现
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 }
界面布局
六 功能测试及报文解析
这里功能测试我们需要借助测试工具 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和端口号
选择线圈或寄存器
点击setup->slave definition,这里的function我们需要读/写什么线圈或寄存器就对应选择
测试1 功能码0x01
这里我们所有的测试从站都使用站号1 起始地址0 长度10
功能码0x01 读取线圈 modbus slave的function选择01 coil status(0x)
测试结果:
点击display->communication 可以截取报文,我也不知道为什么他报文字体那么小(绝望ing)
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) (说明一下 线圈和保持寄存器才有写操作)
测试结果
报文
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
以上都为我自己学习总结并实现,有错误之处,希望大家不吝赐教,感谢(抱拳)!