C#?NModbus?RTU通信實現方法詳解
Modbus協議時應用于電子控制器上的一種通用語言。通過此協議,控制器相互之間、控制器經由網絡/串口和其它設備之間可以進行通信。它已經成為了一種工業(yè)標準。有了這個通信協議,不同的廠商生成的控制設備就可以連城工業(yè)網絡,進行集中監(jiān)控。
本文實現需要借用一個開源的NModbus庫來完成,通過在菜單欄,工具-----NuGet包管理器-----管理解決方案的NuGet程序包,安裝NModbus的開源庫。
本次實例的基本框架和實現效果如下所示:
可自動識別當前設備的可用串口。
Modbus RTU通信的具體的實現如下:
using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using Modbus.Device; using System.Net.Sockets; using System.Threading; using System.IO.Ports; using System.Drawing.Text; using System.Windows.Forms.VisualStyles; using System.Timers; using System.CodeDom.Compiler; namespace ModbusRtuMaster { public partial class Form1 : Form { #region 參數配置 private static IModbusMaster master; private static SerialPort port; //寫線圈或寫寄存器數組 private bool[] coilsBuffer; private ushort[] registerBuffer; //功能碼 private string functionCode; //功能碼序號 private int functionOder; //參數(分別為從站地址,起始地址,長度) private byte slaveAddress; private ushort startAddress; private ushort numberOfPoints; //串口參數 private string portName; private int baudRate; private Parity parity; private int dataBits; private StopBits stopBits; //自動測試標志位 private bool AutoFlag = false; //獲取當前時間 private System.DateTime Current_time; //定時器初始化 private System.Timers.Timer t = new System.Timers.Timer(1000); private const int WM_DEVICE_CHANGE = 0x219; //設備改變 private const int DBT_DEVICEARRIVAL = 0x8000; //設備插入 private const int DBT_DEVICE_REMOVE_COMPLETE = 0x8004; //設備移除 #endregion public Form1() { InitializeComponent(); GetSerialLstTb1(); } private void Form1_Load(object sender, EventArgs e) { //界面初始化 cmb_portname.SelectedIndex = 0; cmb_baud.SelectedIndex = 5; cmb_parity.SelectedIndex = 2; cmb_databBits.SelectedIndex = 1; cmb_stopBits.SelectedIndex = 0; } #region 定時器 //定時器初始化,失能狀態(tài) private void init_Timer() { t.Elapsed += new System.Timers.ElapsedEventHandler(Execute); t.AutoReset = true;//設置false定時器執(zhí)行一次,設置true定時器一直執(zhí)行 t.Enabled = false;//定時器使能true,失能false //t.Start(); } private void Execute(object source,System.Timers.ElapsedEventArgs e) { //停止定時器后再打開定時器,避免重復打開 t.Stop(); //ExecuteFunction();可添加執(zhí)行操作 t.Start(); } #endregion #region 串口配置 /// <summary> /// 串口參數獲取 /// </summary> /// <returns></返回串口配置參數> private SerialPort InitSerialPortParameter() { if (cmb_portname.SelectedIndex < 0 || cmb_baud.SelectedIndex < 0 || cmb_parity.SelectedIndex < 0 || cmb_databBits.SelectedIndex < 0 || cmb_stopBits.SelectedIndex < 0) { MessageBox.Show("請選擇串口參數"); return null; } else { portName = cmb_portname.SelectedItem.ToString(); baudRate = int.Parse(cmb_baud.SelectedItem.ToString()); switch (cmb_parity.SelectedItem.ToString()) { case "奇": parity = Parity.Odd; break; case "偶": parity = Parity.Even; break; case "無": parity = Parity.None; break; default: break; } dataBits = int.Parse(cmb_databBits.SelectedItem.ToString()); switch (cmb_stopBits.SelectedItem.ToString()) { case "1": stopBits = StopBits.One; break; case "2": stopBits = StopBits.Two; break; default: break; } port = new SerialPort(portName, baudRate, parity, dataBits, stopBits); return port; } } #endregion #region 串口收/發(fā) private async void ExecuteFunction() { Current_time = System.DateTime.Now; try { if (port.IsOpen == false) { port.Open(); } if (functionCode != null) { switch (functionCode) { case "01 Read Coils"://讀取單個線圈 SetReadParameters(); try { coilsBuffer = master.ReadCoils(slaveAddress, startAddress, numberOfPoints); } catch(Exception) { MessageBox.Show("參數配置錯誤"); //MessageBox.Show(e.Message); AutoFlag = false; break; } SetMsg("[" + Current_time.ToString("yyyy-MM-dd HH:mm:ss" + "]" + " ")); for (int i = 0; i < coilsBuffer.Length; i++) { SetMsg(coilsBuffer[i] + " "); } SetMsg("\r\n"); break; case "02 Read DisCrete Inputs"://讀取輸入線圈/離散量線圈 SetReadParameters(); try { coilsBuffer = master.ReadInputs(slaveAddress, startAddress, numberOfPoints); } catch(Exception) { MessageBox.Show("參數配置錯誤"); AutoFlag = false; break; } SetMsg("[" + Current_time.ToString("yyyy-MM-dd HH:mm:ss" + "]" + " ")); for (int i = 0; i < coilsBuffer.Length; i++) { SetMsg(coilsBuffer[i] + " "); } SetMsg("\r\n"); break; case "03 Read Holding Registers"://讀取保持寄存器 SetReadParameters(); try { registerBuffer = master.ReadHoldingRegisters(slaveAddress, startAddress, numberOfPoints); } catch (Exception) { MessageBox.Show("參數配置錯誤"); AutoFlag = false; break; } SetMsg("[" + Current_time.ToString("yyyy-MM-dd HH:mm:ss" + "]" + " ")); for (int i = 0; i < registerBuffer.Length; i++) { SetMsg(registerBuffer[i] + " "); } SetMsg("\r\n"); break; case "04 Read Input Registers"://讀取輸入寄存器 SetReadParameters(); try { registerBuffer = master.ReadInputRegisters(slaveAddress, startAddress, numberOfPoints); } catch (Exception) { MessageBox.Show("參數配置錯誤"); AutoFlag = false; break; } SetMsg("[" + Current_time.ToString("yyyy-MM-dd HH:mm:ss" + "]" + " ")); for (int i = 0; i < registerBuffer.Length; i++) { SetMsg(registerBuffer[i] + " "); } SetMsg("\r\n"); break; case "05 Write Single Coil"://寫單個線圈 SetWriteParametes(); await master.WriteSingleCoilAsync(slaveAddress, startAddress, coilsBuffer[0]); break; case "06 Write Single Registers"://寫單個輸入線圈/離散量線圈 SetWriteParametes(); await master.WriteSingleRegisterAsync(slaveAddress, startAddress, registerBuffer[0]); break; case "0F Write Multiple Coils"://寫一組線圈 SetWriteParametes(); await master.WriteMultipleCoilsAsync(slaveAddress, startAddress, coilsBuffer); break; case "10 Write Multiple Registers"://寫一組保持寄存器 SetWriteParametes(); await master.WriteMultipleRegistersAsync(slaveAddress, startAddress, registerBuffer); break; default: break; } } else { MessageBox.Show("請選擇功能碼!"); } port.Close(); } catch (Exception ex) { port.Close(); MessageBox.Show(ex.Message); } } #endregion /// <summary> /// 設置讀參數 /// </summary> private void SetReadParameters() { if (txt_startAddr1.Text == "" || txt_slave1.Text == "" || txt_length.Text == "") { MessageBox.Show("請?zhí)顚懽x參數!"); } else { slaveAddress = byte.Parse(txt_slave1.Text); startAddress = ushort.Parse(txt_startAddr1.Text); numberOfPoints = ushort.Parse(txt_length.Text); } } /// <summary> /// 設置寫參數 /// </summary> private void SetWriteParametes() { if (txt_startAddr2.Text == "" || txt_slave2.Text == "" || txt_data.Text == "") { MessageBox.Show("請?zhí)顚憣憛?"); } else { slaveAddress = byte.Parse(txt_slave2.Text); startAddress = ushort.Parse(txt_startAddr2.Text); //判斷是否寫線圈 if (functionOder == 4 || functionOder == 6) { string[] strarr = txt_data.Text.Split(' '); coilsBuffer = new bool[strarr.Length]; //轉化為bool數組 for (int i = 0; i < strarr.Length; i++) { // strarr[i] == "0" ? coilsBuffer[i] = false : coilsBuffer[i] = true; if (strarr[i] == "0") { coilsBuffer[i] = false; } else { coilsBuffer[i] = true; } } } else { //轉化ushort數組 string[] strarr = txt_data.Text.Split(' '); registerBuffer = new ushort[strarr.Length]; for (int i = 0; i < strarr.Length; i++) { registerBuffer[i] = ushort.Parse(strarr[i]); } } } } /// <summary> /// 創(chuàng)建委托,打印日志 /// </summary> /// <param name="msg"></param> public void SetMsg(string msg) { richTextBox1.Invoke(new Action(() => { richTextBox1.AppendText(msg); })); } /// <summary> /// 清空日志 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void button2_Click(object sender, EventArgs e) { richTextBox1.Clear(); } /// <summary> /// 單擊button1事件,串口完成一次讀/寫操作 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void button1_Click(object sender, EventArgs e) { //AutoFlag = false; //button_AutomaticTest.Enabled = true; try { //初始化串口參數 InitSerialPortParameter(); master = ModbusSerialMaster.CreateRtu(port); ExecuteFunction(); } catch (Exception) { MessageBox.Show("初始化異常"); } } /// <summary> /// 自動測試初始化 /// </summary> 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); } }); } /// <summary> /// 讀取數據時,失能寫數據;寫數據時,失能讀數據 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void comboBox1_SelectedIndexChanged(object sender, EventArgs e) { if (comboBox1.SelectedIndex >= 4) { groupBox2.Enabled = true; groupBox1.Enabled = false; } else { groupBox1.Enabled = true; groupBox2.Enabled = false; } //委托事件,在主線程中創(chuàng)建的控件,在子線程中讀取設置控件的屬性會出現異常,使用Invoke方法可以解決 comboBox1.Invoke(new Action(() => { functionCode = comboBox1.SelectedItem.ToString(); functionOder = comboBox1.SelectedIndex; })); } /// <summary> /// 將打印日志顯示到最新接收到的符號位置 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void richTextBox1_TextChanged(object sender, EventArgs e) { this.richTextBox1.SelectionStart = int.MaxValue; this.richTextBox1.ScrollToCaret(); } /// <summary> /// 自動化測試 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void button_AutomaticTest_Click(object sender, EventArgs e) { AutoFlag = false; button_AutomaticTest.Enabled = false; //自動收發(fā)按鈕失能,避免從復開啟線程 if (AutoFlag == false) { AutomaticTest(); } } /// <summary> /// 串口關閉,停止讀/寫 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void button_ClosePort_Click(object sender, EventArgs e) { AutoFlag = false; button1.Enabled = true; button_AutomaticTest.Enabled = true; t.Enabled = false;//失能定時器 if (port.IsOpen) { port.Close(); } } #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 private void label11_Click(object sender, EventArgs e) { } private void txt_slave1_TextChanged(object sender, EventArgs e) { } private void label7_Click(object sender, EventArgs e) { } private void txt_startAddr1_TextChanged(object sender, EventArgs e) { } private void label8_Click(object sender, EventArgs e) { } private void txt_length_TextChanged(object sender, EventArgs e) { } } }
在線程中對控件的屬性進行操作可能會出現代碼異常,可以使用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
對本次實例進行測試需要使用到串口模擬軟件,串口模擬器可以到網上下載
Modbus從站需要完成一下兩步操作:
一、菜單欄Connection-----Connect
二、菜單欄Setup-----Slave Definition
最后需要運行自己創(chuàng)建的Modbus RTU Master上位機,完成相應的配置:
實現的最終效果:
到此這篇關于C# NModbus RTU通信實現方法詳解的文章就介紹到這了,更多相關C# NModbus RTU通信內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!