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 }
在线程中对控件的属性进行操作可能会出现代码异常,可以使用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通信,相关方面的解析可能还不够到位,如存在相关问题,欢迎一块讨论完成,一起学习一起进步!