Modbus協定時應用于電子控制器上的一種通用語言。通過此協定,控制器互相之間、控制器經由網絡/序列槽和其它裝置之間可以進行通信。它已經成為了一種工業标準。有了這個通信協定,不同的廠商生成的控制裝置就可以連城工業網絡,進行集中監控。
本文實作需要借用一個開源的NModbus庫來完成,通過在菜單欄,工具-----NuGet包管理器-----管了解決方案的NuGet程式包,安裝NModbus的開源庫。

本次執行個體的基本架構和實作效果如下所示:
可自動識别目前裝置的可用序列槽。
Modbus RTU通信的具體的實作如下:
1using System;
2using System.Collections;
3using System.Collections.Generic;
4using System.ComponentModel;
5using System.Data;
6using System.Drawing;
7using System.Linq;
8using System.Text;
9using System.Threading.Tasks;
10using System.Windows.Forms;
11using Modbus.Device;
12using System.Net.Sockets;
13using System.Threading;
14using System.IO.Ports;
15using System.Drawing.Text;
16using System.Windows.Forms.VisualStyles;
17using System.Timers;
18using System.CodeDom.Compiler;
19 20namespace ModbusRtuMaster
21{
22publicpartialclass Form1 : Form
23 {
24#region 參數配置
25privatestatic IModbusMaster master;
26privatestatic SerialPort port;
27//寫線圈或寫寄存器數組 28privatebool[] coilsBuffer;
29privateushort[] registerBuffer;
30//功能碼 31privatestring functionCode;
32//功能碼序号 33privateint functionOder;
34//參數(分别為從站位址,起始位址,長度) 35privatebyte slaveAddress;
36privateushort startAddress;
37privateushort numberOfPoints;
38//序列槽參數 39privatestring portName;
40privateint baudRate;
41private Parity parity;
42privateint dataBits;
43private StopBits stopBits;
44//自動測試标志位 45privatebool AutoFlag = false;
46//擷取目前時間 47private System.DateTime Current_time;
48 49//定時器初始化 50private System.Timers.Timer t = new System.Timers.Timer(1000);
51 52privateconstint WM_DEVICE_CHANGE = 0x219; //裝置改變 53privateconstint DBT_DEVICEARRIVAL = 0x8000; //裝置插入 54privateconstint DBT_DEVICE_REMOVE_COMPLETE = 0x8004; //裝置移除 55 56#endregion 57 58 59public Form1()
60 {
61 InitializeComponent();
62 GetSerialLstTb1();
63 }
64 65privatevoid 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//定時器初始化,失能狀态 78privatevoid init_Timer()
79 {
80 t.Elapsed += new System.Timers.ElapsedEventHandler(Execute);
81 t.AutoReset = true;//設定false定時器執行一次,設定true定時器一直執行 82 t.Enabled = false;//定時器使能true,失能false
83//(); 84 }
85 86privatevoid Execute(object source,System.Timers.ElapsedEventArgs e)
87 {
88//停止定時器後再打開定時器,避免重複打開 89 t.Stop();
90//ExecuteFunction();可添加執行操作 91 ();
92 }
93#endregion 94 95#region 序列槽配置
96///<summary> 97/// 序列槽參數擷取
98///</summary> 99///<returns></傳回序列槽配置參數>100private SerialPort InitSerialPortParameter()
101 {
102if (cmb_portname.SelectedIndex < 0 || cmb_baud.SelectedIndex < 0 || cmb_parity.SelectedIndex < 0 || cmb_databBits.SelectedIndex < 0 || cmb_stopBits.SelectedIndex < 0)
103 {
104 ("請選擇序列槽參數");
105returnnull;
106 }
107else108 {
109 portName = ();
110 baudRate = int.Parse(());
111112switch (())
113 {
114case"奇":
115 parity = Parity.Odd;
116break;
117case"偶":
118 parity = Parity.Even;
119break;
120case"無":
121 parity = Parity.None;
122break;
123default:
124break;
125 }
126 dataBits = int.Parse(());
127switch (())
128 {
129case"1":
130 stopBits = StopBits.One;
131break;
132case"2":
133 stopBits = StopBits.Two;
134break;
135default:
136break;
137 }
138139 port = new SerialPort(portName, baudRate, parity, dataBits, stopBits);
140return port;
141142 }
143 }
144#endregion145146#region 序列槽收/發
147privateasyncvoid ExecuteFunction()
148 {
149 Current_time = System.DateTime.Now;
150try151 {
152153if ( == false)
154 {
155 port.Open();
156 }
157if (functionCode != null)
158 {
159switch (functionCode)
160 {
161case"01 Read Coils"://讀取單個線圈162 SetReadParameters();
163try164 {
165 coilsBuffer = (slaveAddress, startAddress, numberOfPoints);
166 }
167catch(Exception)
168 {
169 ("參數配置錯誤");
170//();171 AutoFlag = false;
172break;
173 }
174 SetMsg("[" + ("yyyy-MM-dd HH:mm:ss" + "]" + ""));
175for (int i = 0; i < coilsBuffer.Length; i++)
176 {
177 SetMsg(coilsBuffer[i] + "");
178 }
179 SetMsg("\r\n");
180break;
181case"02 Read DisCrete Inputs"://讀取輸入線圈/離散量線圈182 SetReadParameters();
183try184 {
185 coilsBuffer = (slaveAddress, startAddress, numberOfPoints);
186 }
187catch(Exception)
188 {
189 ("參數配置錯誤");
190 AutoFlag = false;
191break;
192 }
193 SetMsg("[" + ("yyyy-MM-dd HH:mm:ss" + "]" + ""));
194for (int i = 0; i < coilsBuffer.Length; i++)
195 {
196 SetMsg(coilsBuffer[i] + "");
197 }
198 SetMsg("\r\n");
199break;
200case"03 Read Holding Registers"://讀取保持寄存器201 SetReadParameters();
202try203 {
204 registerBuffer = (slaveAddress, startAddress, numberOfPoints);
205 }
206catch (Exception)
207 {
208 ("參數配置錯誤");
209 AutoFlag = false;
210break;
211 }
212 SetMsg("[" + ("yyyy-MM-dd HH:mm:ss" + "]" + ""));
213for (int i = 0; i < registerBuffer.Length; i++)
214 {
215 SetMsg(registerBuffer[i] + "");
216 }
217 SetMsg("\r\n");
218break;
219case"04 Read Input Registers"://讀取輸入寄存器220 SetReadParameters();
221try222 {
223 registerBuffer = (slaveAddress, startAddress, numberOfPoints);
224 }
225catch (Exception)
226 {
227 ("參數配置錯誤");
228 AutoFlag = false;
229break;
230 }
231 SetMsg("[" + ("yyyy-MM-dd HH:mm:ss" + "]" + ""));
232for (int i = 0; i < registerBuffer.Length; i++)
233 {
234 SetMsg(registerBuffer[i] + "");
235 }
236 SetMsg("\r\n");
237break;
238case"05 Write Single Coil"://寫單個線圈239 SetWriteParametes();
240await master.WriteSingleCoilAsync(slaveAddress, startAddress, coilsBuffer[0]);
241break;
242case"06 Write Single Registers"://寫單個輸入線圈/離散量線圈243 SetWriteParametes();
244await master.WriteSingleRegisterAsync(slaveAddress, startAddress, registerBuffer[0]);
245break;
246case"0F Write Multiple Coils"://寫一組線圈247 SetWriteParametes();
248await master.WriteMultipleCoilsAsync(slaveAddress, startAddress, coilsBuffer);
249break;
250case"10 Write Multiple Registers"://寫一組保持寄存器251 SetWriteParametes();
252await master.WriteMultipleRegistersAsync(slaveAddress, startAddress, registerBuffer);
253break;
254default:
255break;
256 }
257258 }
259else260 {
261 ("請選擇功能碼!");
262 }
263 port.Close();
264 }
265catch (Exception ex)
266 {
267 port.Close();
268 ();
269 }
270 }
271#endregion272273///<summary>274/// 設定讀參數
275///</summary>276privatevoid SetReadParameters()
277 {
278if ( == "" || == "" || == "")
279 {
280 ("請填寫讀參數!");
281 }
282else283 {
284 slaveAddress = byte.Parse();
285 startAddress = ushort.Parse();
286 numberOfPoints = ushort.Parse();
287 }
288 }
289290///<summary>291/// 設定寫參數
292///</summary>293privatevoid SetWriteParametes()
294 {
295if ( == "" || == "" || txt_data.Text == "")
296 {
297 ("請填寫寫參數!");
298 }
299else300 {
301 slaveAddress = byte.Parse();
302 startAddress = ushort.Parse();
303//判斷是否寫線圈304if (functionOder == 4 || functionOder == 6)
305 {
306string[] strarr = ('');
307 coilsBuffer = newbool[];
308//轉化為bool數組309for (int i = 0; i < ; i++)
310 {
311// strarr[i] == "0" ? coilsBuffer[i] = false : coilsBuffer[i] = true;312if (strarr[i] == "0")
313 {
314 coilsBuffer[i] = false;
315 }
316else317 {
318 coilsBuffer[i] = true;
319 }
320 }
321 }
322else323 {
324//轉化ushort數組325string[] strarr = ('');
326 registerBuffer = newushort[];
327for (int i = 0; i < ; i++)
328 {
329 registerBuffer[i] = ushort.Parse(strarr[i]);
330 }
331 }
332 }
333 }
334335///<summary>336/// 建立委托,列印日志
337///</summary>338///<param name="msg"></param>339publicvoid SetMsg(string msg)
340 {
341 (new Action(() => { (msg); }));
342 }
343344///<summary>345/// 清空日志
346///</summary>347///<param name="sender"></param>348///<param name="e"></param>349privatevoid button2_Click(object sender, EventArgs e)
350 {
351 richTextBox1.Clear();
352 }
353354///<summary>355/// 單擊button1事件,序列槽完成一次讀/寫操作
356///</summary>357///<param name="sender"></param>358///<param name="e"></param>359privatevoid button1_Click(object sender, EventArgs e)
360 {
361//AutoFlag = false;
362//button_AutomaticTest.Enabled = true;363364try365 {
366//初始化序列槽參數367 InitSerialPortParameter();
368369 master = (port);
370371372 ExecuteFunction();
373374 }
375catch (Exception)
376 {
377 ("初始化異常");
378 }
379 }
380381///<summary>382/// 自動測試初始化
383///</summary>384privatevoid AutomaticTest()
385 {
386 AutoFlag = true;
387 button1.Enabled = false;
388389 InitSerialPortParameter();
390 master = (port);
391392 (() =>
393 {
394//初始化序列槽參數395396while (AutoFlag)
397 {
398399try400 {
401402 ExecuteFunction();
403404 }
405catch (Exception)
406 {
407 ("初始化異常");
408 }
409 (500);
410 }
411 });
412 }
413414///<summary>415/// 讀取資料時,失能寫資料;寫資料時,失能讀資料
416///</summary>417///<param name="sender"></param>418///<param name="e"></param>419privatevoid comboBox1_SelectedIndexChanged(object sender, EventArgs e)
420 {
421if (comboBox1.SelectedIndex >= 4)
422 {
423 groupBox2.Enabled = true;
424 groupBox1.Enabled = false;
425 }
426else427 {
428 groupBox1.Enabled = true;
429 groupBox2.Enabled = false;
430 }
431//委托事件,在主線程中建立的控件,在子線程中讀取設定控件的屬性會出現異常,使用Invoke方法可以解決432 comboBox1.Invoke(new Action(() => { functionCode = (); functionOder = }));
433 }
434435///<summary>436/// 将列印日志顯示到最新接收到的符号位置
437///</summary>438///<param name="sender"></param>439///<param name="e"></param>440privatevoid richTextBox1_TextChanged(object sender, EventArgs e)
441 {
442this.richTextBox1.SelectionStart = int.MaxValue;
443this.richTextBox1.ScrollToCaret();
444 }
445446///<summary>447/// 自動化測試
448///</summary>449///<param name="sender"></param>450///<param name="e"></param>451privatevoid button_AutomaticTest_Click(object sender, EventArgs e)
452 {
453 AutoFlag = false;
454 button_AutomaticTest.Enabled = false; //自動收發按鈕失能,避免從複開啟線程455if (AutoFlag == false)
456 {
457 AutomaticTest();
458459 }
460461 }
462463///<summary>464/// 序列槽關閉,停止讀/寫
465///</summary>466///<param name="sender"></param>467///<param name="e"></param>468privatevoid button_ClosePort_Click(object sender, EventArgs e)
469 {
470 AutoFlag = false;
471 button1.Enabled = true;
472 button_AutomaticTest.Enabled = true;
473 t.Enabled = false;//失能定時器474475if ()
476 {
477 port.Close();
478 }
479480 }
481482#region 序列槽下拉清單重新整理
483///<summary>484/// 重新整理下拉清單顯示
485///</summary>486privatevoid GetSerialLstTb1()
487 {
488//清除cmb_portname顯示489 cmb_portname.SelectedIndex = -1;
490 cmb_portname.Items.Clear();
491//擷取序列槽清單492string[] serialLst = ();
493if (serialLst.Length > 0)
494 {
495//取序列槽進行排序496 (serialLst);
497//将序列槽清單輸出到cmb_portname498 cmb_portname.Items.AddRange(serialLst);
499 cmb_portname.SelectedIndex = 0;
500 }
501 }
502503///<summary>504/// 消息處理
505///</summary>506///<param name="m"></param>507protectedoverridevoid WndProc(ref Message m)
508 {
509switch () //判斷消息類型510 {
511case WM_DEVICE_CHANGE: //裝置改變消息512 {
513 GetSerialLstTb1(); //裝置改變時重新花去序列槽清單514 }
515break;
516 }
517base.WndProc(ref m);
518 }
519#endregion520521privatevoid label11_Click(object sender, EventArgs e)
522 {
523524 }
525526privatevoid txt_slave1_TextChanged(object sender, EventArgs e)
527 {
528529 }
530531privatevoid label7_Click(object sender, EventArgs e)
532 {
533534 }
535536privatevoid txt_startAddr1_TextChanged(object sender, EventArgs e)
537 {
538539 }
540541privatevoid label8_Click(object sender, EventArgs e)
542 {
543544 }
545546privatevoid txt_length_TextChanged(object sender, EventArgs e)
547 {
548549 }
550551 }
552 }
View Code
線上程中對控件的屬性進行操作可能會出現代碼異常,可以使用Invoke委托方法完成相應的操作:
publicvoid SetMsg(string msg)
{
(new Action(() => { (msg); }));
}
在進行自動讀/寫操作時,為避免多次點選按鍵控件,多次重複建立新線程;在進入自動讀寫線程中時,将對應的按鍵控件失能,等待停止讀寫操作時再使能:
privatevoid AutomaticTest()
{
AutoFlag = true;
button1.Enabled = false;
InitSerialPortParameter();
master = (port);
(() =>
{
//初始化序列槽參數while (AutoFlag)
{
try
{
ExecuteFunction();
}
catch (Exception)
{
("初始化異常");
}
(500);
}
});
}
自動擷取目前裝置的可用序列槽實作如下:
#region 序列槽下拉清單重新整理
///<summary>/// 重新整理下拉清單顯示
///</summary>privatevoid GetSerialLstTb1()
{
//清除cmb_portname顯示
cmb_portname.SelectedIndex = -1;
cmb_portname.Items.Clear();
//擷取序列槽清單string[] serialLst = ();
if (serialLst.Length > 0)
{
//取序列槽進行排序 (serialLst);
//将序列槽清單輸出到cmb_portname cmb_portname.Items.AddRange(serialLst);
cmb_portname.SelectedIndex = 0;
}
}
///<summary>/// 消息處理
///</summary>///<param name="m"></param>protectedoverridevoid WndProc(ref Message m)
{
switch () //判斷消息類型 {
case WM_DEVICE_CHANGE: //裝置改變消息 {
GetSerialLstTb1(); //裝置改變時重新花去序列槽清單 }
break;
}
base.WndProc(ref m);
}
#endregion
對本次執行個體進行測試需要使用到序列槽模拟軟體,序列槽模拟器可以到網上下載下傳,也可以通過以下連結進行下載下傳:
連結:
提取碼:xy4m
Modbus從站模拟器下載下傳連結:
連結:
提取碼:06i9
Modbus從站需要完成一下兩步操作:
一、菜單欄Connection-----Connect
二、菜單欄Setup-----Slave Definition
最後需要運作自己建立的Modbus RTU Master上位機,完成相應的配置:
實作的最終效果:
完整的工程可通過以下連結下載下傳:
連結:
提取碼:s2m6
本人初次學習Modbus通信,相關方面的解析可能還不夠到位,如存在相關問題,歡迎一塊讨論完成,一起學習一起進步!