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 }
線上程中對控制元件的屬性進行操作可能會出現程式碼異常,可以使用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通訊,相關方面的解析可能還不夠到位,如存在相關問題,歡迎一塊討論完成,一起學習一起進步!