C# NModbus RTU通信实现

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

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

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

 

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

 

 

 

 

 Modbus 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 }
View Code

在线程中对控件的属性进行操作可能会出现代码异常,可以使用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 

 

 Modbus从站模拟器下载链接:

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

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

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

 

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

 

 

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

 

 实现的最终效果:

 

 

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

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

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

 

posted @ 2020-11-02 15:55  熊来闯一闯  阅读(17159)  评论(5编辑  收藏  举报