C# NModbus RTU通訊實現

熊來闖一闖發表於2020-11-02

Modbus協議時應用於電子控制器上的一種通用語言。通過此協議,控制器相互之間、控制器經由網路/串列埠和其它裝置之間可以進行通訊。它已經成為了一種工業標準。有了這個通訊協議,不同的廠商生成的控制裝置就可以連城工業網路,進行集中監控。

本文實現需要借用一個開源的NModbus庫來完成,通過在選單欄,工具-----NuGet包管理器-----管理解決方案的NuGet程式包,安裝NModbus的開源庫。

本次例項的基本框架和實現效果如下所示:

 

可自動識別當前裝置的可用串列埠。

 

 

 

 

 Modbus 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 }
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通訊,相關方面的解析可能還不夠到位,如存在相關問題,歡迎一塊討論完成,一起學習一起進步!

 

相關文章