C#?NModbus?RTU通信實現(xiàn)方法詳解
Modbus協(xié)議時應用于電子控制器上的一種通用語言。通過此協(xié)議,控制器相互之間、控制器經(jīng)由網(wǎng)絡/串口和其它設備之間可以進行通信。它已經(jīng)成為了一種工業(yè)標準。有了這個通信協(xié)議,不同的廠商生成的控制設備就可以連城工業(yè)網(wǎng)絡,進行集中監(jiān)控。
本文實現(xiàn)需要借用一個開源的NModbus庫來完成,通過在菜單欄,工具-----NuGet包管理器-----管理解決方案的NuGet程序包,安裝NModbus的開源庫。

本次實例的基本框架和實現(xiàn)效果如下所示:

可自動識別當前設備的可用串口。

Modbus RTU通信的具體的實現(xiàn)如下:
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 參數(shù)配置
private static IModbusMaster master;
private static SerialPort port;
//寫線圈或?qū)懠拇嫫鲾?shù)組
private bool[] coilsBuffer;
private ushort[] registerBuffer;
//功能碼
private string functionCode;
//功能碼序號
private int functionOder;
//參數(shù)(分別為從站地址,起始地址,長度)
private byte slaveAddress;
private ushort startAddress;
private ushort numberOfPoints;
//串口參數(shù)
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>
/// 串口參數(shù)獲取
/// </summary>
/// <returns></返回串口配置參數(shù)>
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("請選擇串口參數(shù)");
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("參數(shù)配置錯誤");
//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("參數(shù)配置錯誤");
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("參數(shù)配置錯誤");
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("參數(shù)配置錯誤");
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>
/// 設置讀參數(shù)
/// </summary>
private void SetReadParameters()
{
if (txt_startAddr1.Text == "" || txt_slave1.Text == "" || txt_length.Text == "")
{
MessageBox.Show("請?zhí)顚懽x參數(shù)!");
}
else
{
slaveAddress = byte.Parse(txt_slave1.Text);
startAddress = ushort.Parse(txt_startAddr1.Text);
numberOfPoints = ushort.Parse(txt_length.Text);
}
}
/// <summary>
/// 設置寫參數(shù)
/// </summary>
private void SetWriteParametes()
{
if (txt_startAddr2.Text == "" || txt_slave2.Text == "" || txt_data.Text == "")
{
MessageBox.Show("請?zhí)顚憣憛?shù)!");
}
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數(shù)組
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數(shù)組
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
{
//初始化串口參數(shù)
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(() =>
{
//初始化串口參數(shù)
while (AutoFlag)
{
try
{
ExecuteFunction();
}
catch (Exception)
{
MessageBox.Show("初始化異常");
}
Thread.Sleep(500);
}
});
}
/// <summary>
/// 讀取數(shù)據(jù)時,失能寫數(shù)據(jù);寫數(shù)據(jù)時,失能讀數(shù)據(jù)
/// </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)建的控件,在子線程中讀取設置控件的屬性會出現(xiàn)異常,使用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)
{
}
}
}在線程中對控件的屬性進行操作可能會出現(xiàn)代碼異常,可以使用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(() =>
{
//初始化串口參數(shù)
while (AutoFlag)
{
try
{
ExecuteFunction();
}
catch (Exception)
{
MessageBox.Show("初始化異常");
}
Thread.Sleep(500);
}
});
}自動獲取當前設備的可用串口實現(xiàn)如下:
#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對本次實例進行測試需要使用到串口模擬軟件,串口模擬器可以到網(wǎng)上下載

Modbus從站需要完成一下兩步操作:
一、菜單欄Connection-----Connect

二、菜單欄Setup-----Slave Definition

最后需要運行自己創(chuàng)建的Modbus RTU Master上位機,完成相應的配置:

實現(xiàn)的最終效果:

到此這篇關于C# NModbus RTU通信實現(xiàn)方法詳解的文章就介紹到這了,更多相關C# NModbus RTU通信內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
C#使用IronPython庫調(diào)用Python腳本
這篇文章介紹了C#使用IronPython庫調(diào)用Python腳本的方法,文中通過示例代碼介紹的非常詳細。對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-06-06
C#固定大小緩沖區(qū)及使用指針復制數(shù)據(jù)詳解
這篇文章主要為大家介紹了C#固定大小緩沖區(qū)及使用指針復制數(shù)據(jù)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12

