C# NModbus RTU通信实现

Modbus协议时应用于电子控制器上的一种通用语言。通过此协议,控制器相互之间、控制器经由网络/串口和其它设备之间可以进行通信。它已经成为了一种工业标准。有了这个通信协议,不同的厂商生成的控制设备就可以连城工业网络,进行集中监控。

本文实现需要借用一个开源的NModbus库来完成,通过在菜单栏,工具-----NuGet包管理器-----管理解决方案的NuGet程序包,安装NModbus的开源库。

C# NModbus RTU通信实现

本次实例的基本框架和实现效果如下所示:

C# NModbus RTU通信实现

可自动识别当前设备的可用串口。

C# NModbus RTU通信实现

Modbus RTU通信的具体的实现如下:

C# NModbus RTU通信实现C# NModbus RTU通信实现
  1 using System;
2 using System.Collections;
3 using System.Collections.Generic;
4 using System.ComponentModel;
5 using System.Data;
6 using System.Drawing;
7 using System.Linq;
8 using System.Text;
9 using System.Threading.Tasks;
10 using System.Windows.Forms;
11 using Modbus.Device;
12 using System.Net.Sockets;
13 using System.Threading;
14 using System.IO.Ports;
15 using System.Drawing.Text;
16 using System.Windows.Forms.VisualStyles;
17 using System.Timers;
18 using System.CodeDom.Compiler;
19
20 namespace ModbusRtuMaster
21 {
22 public partial class Form1 : Form
23 {
24 #region 参数配置
25 private static IModbusMaster master;
26 private static SerialPort port;
27 //写线圈或写寄存器数组
28 private bool[] coilsBuffer;
29 private ushort[] registerBuffer;
30 //功能码
31 private string functionCode;
32 //功能码序号
33 private int functionOder;
34 //参数(分别为从站地址,起始地址,长度)
35 private byte slaveAddress;
36 private ushort startAddress;
37 private ushort numberOfPoints;
38 //串口参数
39 private string portName;
40 private int baudRate;
41 private Parity parity;
42 private int dataBits;
43 private StopBits stopBits;
44 //自动测试标志位
45 private bool AutoFlag = false;
46 //获取当前时间
47 private System.DateTime Current_time;
48
49 //定时器初始化
50 private System.Timers.Timer t = new System.Timers.Timer(1000);
51
52 private const int WM_DEVICE_CHANGE = 0x219; //设备改变
53 private const int DBT_DEVICEARRIVAL = 0x8000; //设备插入
54 private const int DBT_DEVICE_REMOVE_COMPLETE = 0x8004; //设备移除
55
56 #endregion
57
58
59 public Form1()
60 {
61 InitializeComponent();
62 GetSerialLstTb1();
63 }
64
65 private void Form1_Load(object sender, EventArgs e)
66 {
67 //界面初始化
68 cmb_portname.SelectedIndex = 0;
69 cmb_baud.SelectedIndex = 5;
70 cmb_parity.SelectedIndex = 2;
71 cmb_databBits.SelectedIndex = 1;
72 cmb_stopBits.SelectedIndex = 0;
73
74 }
75
76 #region 定时器
77 //定时器初始化,失能状态
78 private void init_Timer()
79 {
80 t.Elapsed += new System.Timers.ElapsedEventHandler(Execute);
81 t.AutoReset = true;//设置false定时器执行一次,设置true定时器一直执行
82 t.Enabled = false;//定时器使能true,失能false
83 //t.Start();
84 }
85
86 private void Execute(object source,System.Timers.ElapsedEventArgs e)
87 {
88 //停止定时器后再打开定时器,避免重复打开
89 t.Stop();
90 //ExecuteFunction();可添加执行操作
91 t.Start();
92 }
93 #endregion
94
95 #region 串口配置
96 /// <summary>
97 /// 串口参数获取
98 /// </summary>
99 /// <returns></返回串口配置参数>
100 private SerialPort InitSerialPortParameter()
101 {
102 if (cmb_portname.SelectedIndex < 0 || cmb_baud.SelectedIndex < 0 || cmb_parity.SelectedIndex < 0 || cmb_databBits.SelectedIndex < 0 || cmb_stopBits.SelectedIndex < 0)
103 {
104 MessageBox.Show("请选择串口参数");
105 return null;
106 }
107 else
108 {
109 portName = cmb_portname.SelectedItem.ToString();
110 baudRate = int.Parse(cmb_baud.SelectedItem.ToString());
111
112 switch (cmb_parity.SelectedItem.ToString())
113 {
114 case "奇":
115 parity = Parity.Odd;
116 break;
117 case "偶":
118 parity = Parity.Even;
119 break;
120 case "无":
121 parity = Parity.None;
122 break;
123 default:
124 break;
125 }
126 dataBits = int.Parse(cmb_databBits.SelectedItem.ToString());
127 switch (cmb_stopBits.SelectedItem.ToString())
128 {
129 case "1":
130 stopBits = StopBits.One;
131 break;
132 case "2":
133 stopBits = StopBits.Two;
134 break;
135 default:
136 break;
137 }
138
139 port = new SerialPort(portName, baudRate, parity, dataBits, stopBits);
140 return port;
141
142 }
143 }
144 #endregion
145
146 #region 串口收/发
147 private async void ExecuteFunction()
148 {
149 Current_time = System.DateTime.Now;
150 try
151 {
152
153 if (port.IsOpen == false)
154 {
155 port.Open();
156 }
157 if (functionCode != null)
158 {
159 switch (functionCode)
160 {
161 case "01 Read Coils"://读取单个线圈
162 SetReadParameters();
163 try
164 {
165 coilsBuffer = master.ReadCoils(slaveAddress, startAddress, numberOfPoints);
166 }
167 catch(Exception)
168 {
169 MessageBox.Show("参数配置错误");
170 //MessageBox.Show(e.Message);
171 AutoFlag = false;
172 break;
173 }
174 SetMsg("[" + Current_time.ToString("yyyy-MM-dd HH:mm:ss" + "]" + " "));
175 for (int i = 0; i < coilsBuffer.Length; i++)
176 {
177 SetMsg(coilsBuffer[i] + " ");
178 }
179 SetMsg("\r\n");
180 break;
181 case "02 Read DisCrete Inputs"://读取输入线圈/离散量线圈
182 SetReadParameters();
183 try
184 {
185 coilsBuffer = master.ReadInputs(slaveAddress, startAddress, numberOfPoints);
186 }
187 catch(Exception)
188 {
189 MessageBox.Show("参数配置错误");
190 AutoFlag = false;
191 break;
192 }
193 SetMsg("[" + Current_time.ToString("yyyy-MM-dd HH:mm:ss" + "]" + " "));
194 for (int i = 0; i < coilsBuffer.Length; i++)
195 {
196 SetMsg(coilsBuffer[i] + " ");
197 }
198 SetMsg("\r\n");
199 break;
200 case "03 Read Holding Registers"://读取保持寄存器
201 SetReadParameters();
202 try
203 {
204 registerBuffer = master.ReadHoldingRegisters(slaveAddress, startAddress, numberOfPoints);
205 }
206 catch (Exception)
207 {
208 MessageBox.Show("参数配置错误");
209 AutoFlag = false;
210 break;
211 }
212 SetMsg("[" + Current_time.ToString("yyyy-MM-dd HH:mm:ss" + "]" + " "));
213 for (int i = 0; i < registerBuffer.Length; i++)
214 {
215 SetMsg(registerBuffer[i] + " ");
216 }
217 SetMsg("\r\n");
218 break;
219 case "04 Read Input Registers"://读取输入寄存器
220 SetReadParameters();
221 try
222 {
223 registerBuffer = master.ReadInputRegisters(slaveAddress, startAddress, numberOfPoints);
224 }
225 catch (Exception)
226 {
227 MessageBox.Show("参数配置错误");
228 AutoFlag = false;
229 break;
230 }
231 SetMsg("[" + Current_time.ToString("yyyy-MM-dd HH:mm:ss" + "]" + " "));
232 for (int i = 0; i < registerBuffer.Length; i++)
233 {
234 SetMsg(registerBuffer[i] + " ");
235 }
236 SetMsg("\r\n");
237 break;
238 case "05 Write Single Coil"://写单个线圈
239 SetWriteParametes();
240 await master.WriteSingleCoilAsync(slaveAddress, startAddress, coilsBuffer[0]);
241 break;
242 case "06 Write Single Registers"://写单个输入线圈/离散量线圈
243 SetWriteParametes();
244 await master.WriteSingleRegisterAsync(slaveAddress, startAddress, registerBuffer[0]);
245 break;
246 case "0F Write Multiple Coils"://写一组线圈
247 SetWriteParametes();
248 await master.WriteMultipleCoilsAsync(slaveAddress, startAddress, coilsBuffer);
249 break;
250 case "10 Write Multiple Registers"://写一组保持寄存器
251 SetWriteParametes();
252 await master.WriteMultipleRegistersAsync(slaveAddress, startAddress, registerBuffer);
253 break;
254 default:
255 break;
256 }
257
258 }
259 else
260 {
261 MessageBox.Show("请选择功能码!");
262 }
263 port.Close();
264 }
265 catch (Exception ex)
266 {
267 port.Close();
268 MessageBox.Show(ex.Message);
269 }
270 }
271 #endregion
272
273 /// <summary>
274 /// 设置读参数
275 /// </summary>
276 private void SetReadParameters()
277 {
278 if (txt_startAddr1.Text == "" || txt_slave1.Text == "" || txt_length.Text == "")
279 {
280 MessageBox.Show("请填写读参数!");
281 }
282 else
283 {
284 slaveAddress = byte.Parse(txt_slave1.Text);
285 startAddress = ushort.Parse(txt_startAddr1.Text);
286 numberOfPoints = ushort.Parse(txt_length.Text);
287 }
288 }
289
290 /// <summary>
291 /// 设置写参数
292 /// </summary>
293 private void SetWriteParametes()
294 {
295 if (txt_startAddr2.Text == "" || txt_slave2.Text == "" || txt_data.Text == "")
296 {
297 MessageBox.Show("请填写写参数!");
298 }
299 else
300 {
301 slaveAddress = byte.Parse(txt_slave2.Text);
302 startAddress = ushort.Parse(txt_startAddr2.Text);
303 //判断是否写线圈
304 if (functionOder == 4 || functionOder == 6)
305 {
306 string[] strarr = txt_data.Text.Split(' ');
307 coilsBuffer = new bool[strarr.Length];
308 //转化为bool数组
309 for (int i = 0; i < strarr.Length; i++)
310 {
311 // strarr[i] == "0" ? coilsBuffer[i] = false : coilsBuffer[i] = true;
312 if (strarr[i] == "0")
313 {
314 coilsBuffer[i] = false;
315 }
316 else
317 {
318 coilsBuffer[i] = true;
319 }
320 }
321 }
322 else
323 {
324 //转化ushort数组
325 string[] strarr = txt_data.Text.Split(' ');
326 registerBuffer = new ushort[strarr.Length];
327 for (int i = 0; i < strarr.Length; i++)
328 {
329 registerBuffer[i] = ushort.Parse(strarr[i]);
330 }
331 }
332 }
333 }
334
335 /// <summary>
336 /// 创建委托,打印日志
337 /// </summary>
338 /// <param name="msg"></param>
339 public void SetMsg(string msg)
340 {
341 richTextBox1.Invoke(new Action(() => { richTextBox1.AppendText(msg); }));
342 }
343
344 /// <summary>
345 /// 清空日志
346 /// </summary>
347 /// <param name="sender"></param>
348 /// <param name="e"></param>
349 private void button2_Click(object sender, EventArgs e)
350 {
351 richTextBox1.Clear();
352 }
353
354 /// <summary>
355 /// 单击button1事件,串口完成一次读/写操作
356 /// </summary>
357 /// <param name="sender"></param>
358 /// <param name="e"></param>
359 private void button1_Click(object sender, EventArgs e)
360 {
361 //AutoFlag = false;
362 //button_AutomaticTest.Enabled = true;
363
364 try
365 {
366 //初始化串口参数
367 InitSerialPortParameter();
368
369 master = ModbusSerialMaster.CreateRtu(port);
370
371
372 ExecuteFunction();
373
374 }
375 catch (Exception)
376 {
377 MessageBox.Show("初始化异常");
378 }
379 }
380
381 /// <summary>
382 /// 自动测试初始化
383 /// </summary>
384 private void AutomaticTest()
385 {
386 AutoFlag = true;
387 button1.Enabled = false;
388
389 InitSerialPortParameter();
390 master = ModbusSerialMaster.CreateRtu(port);
391
392 Task.Factory.StartNew(() =>
393 {
394 //初始化串口参数
395
396 while (AutoFlag)
397 {
398
399 try
400 {
401
402 ExecuteFunction();
403
404 }
405 catch (Exception)
406 {
407 MessageBox.Show("初始化异常");
408 }
409 Thread.Sleep(500);
410 }
411 });
412 }
413
414 /// <summary>
415 /// 读取数据时,失能写数据;写数据时,失能读数据
416 /// </summary>
417 /// <param name="sender"></param>
418 /// <param name="e"></param>
419 private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
420 {
421 if (comboBox1.SelectedIndex >= 4)
422 {
423 groupBox2.Enabled = true;
424 groupBox1.Enabled = false;
425 }
426 else
427 {
428 groupBox1.Enabled = true;
429 groupBox2.Enabled = false;
430 }
431 //委托事件,在主线程中创建的控件,在子线程中读取设置控件的属性会出现异常,使用Invoke方法可以解决
432 comboBox1.Invoke(new Action(() => { functionCode = comboBox1.SelectedItem.ToString(); functionOder = comboBox1.SelectedIndex; }));
433 }
434
435 /// <summary>
436 /// 将打印日志显示到最新接收到的符号位置
437 /// </summary>
438 /// <param name="sender"></param>
439 /// <param name="e"></param>
440 private void richTextBox1_TextChanged(object sender, EventArgs e)
441 {
442 this.richTextBox1.SelectionStart = int.MaxValue;
443 this.richTextBox1.ScrollToCaret();
444 }
445
446 /// <summary>
447 /// 自动化测试
448 /// </summary>
449 /// <param name="sender"></param>
450 /// <param name="e"></param>
451 private void button_AutomaticTest_Click(object sender, EventArgs e)
452 {
453 AutoFlag = false;
454 button_AutomaticTest.Enabled = false; //自动收发按钮失能,避免从复开启线程
455 if (AutoFlag == false)
456 {
457 AutomaticTest();
458
459 }
460
461 }
462
463 /// <summary>
464 /// 串口关闭,停止读/写
465 /// </summary>
466 /// <param name="sender"></param>
467 /// <param name="e"></param>
468 private void button_ClosePort_Click(object sender, EventArgs e)
469 {
470 AutoFlag = false;
471 button1.Enabled = true;
472 button_AutomaticTest.Enabled = true;
473 t.Enabled = false;//失能定时器
474
475 if (port.IsOpen)
476 {
477 port.Close();
478 }
479
480 }
481
482 #region 串口下拉列表刷新
483 /// <summary>
484 /// 刷新下拉列表显示
485 /// </summary>
486 private void GetSerialLstTb1()
487 {
488 //清除cmb_portname显示
489 cmb_portname.SelectedIndex = -1;
490 cmb_portname.Items.Clear();
491 //获取串口列表
492 string[] serialLst = SerialPort.GetPortNames();
493 if (serialLst.Length > 0)
494 {
495 //取串口进行排序
496 Array.Sort(serialLst);
497 //将串口列表输出到cmb_portname
498 cmb_portname.Items.AddRange(serialLst);
499 cmb_portname.SelectedIndex = 0;
500 }
501 }
502
503 /// <summary>
504 /// 消息处理
505 /// </summary>
506 /// <param name="m"></param>
507 protected override void WndProc(ref Message m)
508 {
509 switch (m.Msg) //判断消息类型
510 {
511 case WM_DEVICE_CHANGE: //设备改变消息
512 {
513 GetSerialLstTb1(); //设备改变时重新花去串口列表
514 }
515 break;
516 }
517 base.WndProc(ref m);
518 }
519 #endregion
520
521 private void label11_Click(object sender, EventArgs e)
522 {
523
524 }
525
526 private void txt_slave1_TextChanged(object sender, EventArgs e)
527 {
528
529 }
530
531 private void label7_Click(object sender, EventArgs e)
532 {
533
534 }
535
536 private void txt_startAddr1_TextChanged(object sender, EventArgs e)
537 {
538
539 }
540
541 private void label8_Click(object sender, EventArgs e)
542 {
543
544 }
545
546 private void txt_length_TextChanged(object sender, EventArgs e)
547 {
548
549 }
550
551 }
552 }

在线程中对控件的属性进行操作可能会出现代码异常,可以使用Invoke委托方法完成相应的操作:

 public void SetMsg(string msg)
{
richTextBox1.Invoke(new Action(() => { richTextBox1.AppendText(msg); }));
}

在进行自动读/写操作时,为避免多次点击按键控件,多次重复建立新线程;在进入自动读写线程中时,将对应的按键控件失能,等待停止读写操作时再使能:

private void AutomaticTest()
{
AutoFlag = true;
button1.Enabled = false; InitSerialPortParameter();
master = ModbusSerialMaster.CreateRtu(port); Task.Factory.StartNew(() =>
{
//初始化串口参数 while (AutoFlag)
{ try
{ ExecuteFunction(); }
catch (Exception)
{
MessageBox.Show("初始化异常");
}
Thread.Sleep(500);
}
});
}

自动获取当前设备的可用串口实现如下:

#region 串口下拉列表刷新
/// <summary>
/// 刷新下拉列表显示
/// </summary>
private void GetSerialLstTb1()
{
//清除cmb_portname显示
cmb_portname.SelectedIndex = -1;
cmb_portname.Items.Clear();
//获取串口列表
string[] serialLst = SerialPort.GetPortNames();
if (serialLst.Length > 0)
{
//取串口进行排序
Array.Sort(serialLst);
//将串口列表输出到cmb_portname
cmb_portname.Items.AddRange(serialLst);
cmb_portname.SelectedIndex = 0;
}
} /// <summary>
/// 消息处理
/// </summary>
/// <param name="m"></param>
protected override void WndProc(ref Message m)
{
switch (m.Msg) //判断消息类型
{
case WM_DEVICE_CHANGE: //设备改变消息
{
GetSerialLstTb1(); //设备改变时重新花去串口列表
}
break;
}
base.WndProc(ref m);
}
#endregion

对本次实例进行测试需要使用到串口模拟软件,串口模拟器可以到网上下载,也可以通过以下链接进行下载:

链接:https://pan.baidu.com/s/1XRUIqTqZ9rwnYowyVyn4cQ
提取码:xy4m

C# NModbus RTU通信实现

Modbus从站模拟器下载链接:

链接:https://pan.baidu.com/s/1Bf0Qg50_d-XYlwQfzEY8ag
提取码:06i9

Modbus从站需要完成一下两步操作:

一、菜单栏Connection-----Connect

C# NModbus RTU通信实现

二、菜单栏Setup-----Slave Definition

C# NModbus RTU通信实现

最后需要运行自己创建的Modbus RTU Master上位机,完成相应的配置:

C# NModbus RTU通信实现

实现的最终效果:

C# NModbus RTU通信实现

完整的工程可通过以下链接下载:

链接:https://pan.baidu.com/s/1XkRAF6yxs19tu-LYLraCgA
提取码:s2m6

本人初次学习Modbus通信,相关方面的解析可能还不够到位,如存在相关问题,欢迎一块讨论完成,一起学习一起进步!

上一篇:一个完善的ActiveX Web控件教程


下一篇:log4cplus 简单记录