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 }
View Code
界面布局
六 功能测试及报文解析
这里功能测试我们需要借助测试工具 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
上文测试了一个读操作和一个写操作,其他功能码的测试与上文一致,有兴趣的可以自行测试,
下一篇博客我要针对不同的功能码做对应的报文解析