C#ModBus Tcp Master的實現(1)

来源:https://www.cnblogs.com/pandefu/archive/2019/05/07/10824331.html
-Advertisement-
Play Games

Modbus已經成為工業領域通信協議的業界標準(De facto),並且現在是工業電子設備之間常用的連接方式。 所以這也是我們工控領域軟體開發的所必懂的通訊協議,我也是初次學習,先貼上我的學習筆記 一 .協議概述 (1)Modbus協議是應用於控制器上的一種通用語言,實現控制器之間,控制器通過網路和 ...


Modbus已經成為工業領域通信協議的業界標準(De facto),並且現在是工業電子設備之間常用的連接方式。

所以這也是我們工控領域軟體開發的所必懂的通訊協議,我也是初次學習,先貼上我的學習筆記

 

一 .協議概述

(1)Modbus協議是應用於控制器上的一種通用語言,實現控制器之間,控制器通過網路和其他設備之間的通信,支持傳統RS232/RS422/RS485和乙太網設備,它已經成為一種通用的工業標準,有了它不同廠商生產的控制設備可以連成工業網路,進行集中控制,此協議定義了一個控制器能認識使用的消息結構

(2) 如果按照國際 ISO/OSI 的 7 層網路模型來說,標準 MODBUS 協議定義了通信物理層、鏈路層及應用層;

物理層:定義了基於 RS232 和 RS485 的非同步串列通信規範;

鏈路層:規定了基於站號識別、主 / 從方式的介質訪問控制;

應用層:規定了信息規範(或報文格式)及通信服務功能;

 

二. 協議要點

(1) MODBUS 是主 / 從通信協議。主站主動發送報文 , 只有與主站發送報文中呼叫地址相同的從站才向主站發送回答報文。

(2) 報文以 0 地址發送時為廣播模式,無需從站應答,可作為廣播報文發送,包括:

  ①修改線圈狀態;

  ②修改寄存器內容;

  ③強置多線圈;

  ④預置多寄存器;

  ⑤詢問診斷;

(3) MODBUS 規定了 2 種字元傳輸模式: ASCII 模式、 RTU (二進位)模式;兩種傳輸模式不能混用;

(4) 傳輸錯誤校驗

  傳輸錯誤校驗有奇偶校驗、冗餘校驗檢驗。

  當校驗出錯時,報文處理停止,從機不再繼續通信,不對此報文產生應答;

  通信錯誤一旦發生,報文便被視為不可靠; MODBUS 主機在一定時間過後仍未收到從站應答,即作出“通信錯誤已發生”的判斷。

(5) 報文級(字元級)採用 CRC-16 (迴圈冗餘錯誤校驗)

(6) MODBUS 報文 RTU 格式

 

三. 異常應答

(1) 從機接收到的主機報文,沒有傳輸錯誤,但從機無法正確執行主機命令或無法作出正確應答,從機將以“異常應答”回答之。

(2) 異常應答報文格式

例:主機發請求報文,功能碼 01 :讀 1 個 04A1 線圈值

  由於從機最高線圈地址為 0400 ,則 04A1 超地址上限,從機作出異常應答如下(註意:功能碼最高位置 1 ):

(3)異常應答碼

四. 寄存器和功能碼

modbus的功能碼很多,且不同功能碼對應的報文也不一致,後續博客我會借用開源庫實現一個modbus master 測試功能碼 解析報文

下邊我用表格總結一下寄存器,功能碼,報文格式

註:

(1)報文中的所有位元組均為16進位

(2)由上圖我們總結出不同的功能碼的報文(無論詢問報文還是響應報文)前8個位元組都是一致的 都是2位元組消息號+2位元組ModBus標識+2位元組長度+1位元組站號+1位元組功能碼 後邊根據功能碼不同而不同

(3)報文中,指定線圈通斷標誌  FF00 置線圈為ON  0000置線圈為OFF

 

五.具體實現

接下來我們使用開源庫NModbus庫,來實現一個Modbus master

創建工程,從NuGet管理器安裝NModbusu

 

先簡單介紹一下NModbus中的幾個重要方法

 

接下來做具體實現

 

  1 using System;
  2 using System.Collections.Generic;
  3 using System.ComponentModel;
  4 using System.Data;
  5 using System.Drawing;
  6 using System.Linq;
  7 using System.Text;
  8 using System.Threading.Tasks;
  9 using System.Windows.Forms;
 10 using NModbus;
 11 using System.Net.Sockets;
 12 using System.Threading;
 13 
 14 namespace ModbusTcp
 15 {
 16     public partial class Form1 : Form
 17     {
 18 
 19         private static ModbusFactory modbusFactory;
 20         private static IModbusMaster master;
 21         //寫線圈或寫寄存器數組
 22         bool[] coilsBuffer;
 23         ushort[] registerBuffer;
 24         //功能碼
 25         string functionCode;
 26         //參數(分別為站號,起始地址,長度)
 27         byte slaveAddress;
 28         ushort startAddress;
 29         ushort numberOfPoints;
 30 
 31         public Form1()
 32         {
 33             InitializeComponent();
 34 
 35         }
 36         private void Form1_Load(object sender, EventArgs e)
 37         {
 38             //初始化modbusmaster
 39             modbusFactory = new ModbusFactory();
 40             //在本地測試 所以使用迴環地址,modbus協議規定埠號 502
 41             master = modbusFactory.CreateMaster(new TcpClient("127.0.0.1", 502));
 42             //設置讀取超時時間
 43             master.Transport.ReadTimeout = 2000;
 44             master.Transport.Retries = 2000;
 45             groupBox1.Enabled = false;
 46             groupBox2.Enabled = false;
 47         }
 48         /// <summary>
 49         /// 讀/寫
 50         /// </summary>
 51         /// <param name="sender"></param>
 52         /// <param name="e"></param>
 53         private void button1_Click(object sender, EventArgs e)
 54         {
 55             ExecuteFunction();
 56         }
 57 
 58         private async void ExecuteFunction()
 59         {
 60             try
 61             {
 62                 //重新實例化是為了 modbus slave更換連接時不報錯 
 63                 master = modbusFactory.CreateMaster(new TcpClient("127.0.0.1", 502));
 64                 if (functionCode != null)
 65                 {
 66                     switch (functionCode)
 67                     {
 68                         case "01 Read Coils"://讀取單個線圈
 69                             SetReadParameters();
 70                             coilsBuffer = master.ReadCoils(slaveAddress, startAddress, numberOfPoints);
 71 
 72                             for (int i = 0; i < coilsBuffer.Length; i++)
 73                             {
 74                                 SetMsg(coilsBuffer[i] + "");
 75                             }
 76                             break;
 77                         case "02 Read DisCrete Inputs"://讀取輸入線圈/離散量線圈
 78                             SetReadParameters();
 79 
 80                             coilsBuffer = master.ReadInputs(slaveAddress, startAddress, numberOfPoints);
 81                             for (int i = 0; i < coilsBuffer.Length; i++)
 82                             {
 83                                 SetMsg(coilsBuffer[i] + "");
 84                             }
 85                             break;
 86                         case "03 Read Holding Registers"://讀取保持寄存器
 87                             SetReadParameters();
 88                             registerBuffer = master.ReadHoldingRegisters(slaveAddress, startAddress, numberOfPoints);
 89                             for (int i = 0; i < registerBuffer.Length; i++)
 90                             {
 91                                 SetMsg(registerBuffer[i] + "");
 92                             }
 93                             break;
 94                         case "04 Read Input Registers"://讀取輸入寄存器
 95                             SetReadParameters();
 96                             registerBuffer = master.ReadInputRegisters(slaveAddress, startAddress, numberOfPoints);
 97                             for (int i = 0; i < registerBuffer.Length; i++)
 98                             {
 99                                 SetMsg(registerBuffer[i] + "");
100                             }
101                             break;
102                         case "05 Write Single Coil"://寫單個線圈
103                             SetWriteParametes();
104                             await master.WriteSingleCoilAsync(slaveAddress, startAddress, coilsBuffer[0]);
105                             break;
106                         case "06 Write Single Registers"://寫單個輸入線圈/離散量線圈
107                             SetWriteParametes();
108                             await master.WriteSingleRegisterAsync(slaveAddress, startAddress, registerBuffer[0]);
109                             break;
110                         case "0F Write Multiple Coils"://寫一組線圈
111                             SetWriteParametes();
112                             await master.WriteMultipleCoilsAsync(slaveAddress, startAddress, coilsBuffer);
113                             break;
114                         case "10 Write Multiple Registers"://寫一組保持寄存器
115                             SetWriteParametes();
116                             await master.WriteMultipleRegistersAsync(slaveAddress, startAddress, registerBuffer);
117                             break;
118                         default:
119                             break;
120                     }
121 
122                 }
123                 else
124                 {
125                     MessageBox.Show("請選擇功能碼!");
126                 }
127                master.Dispose();
128             }
129             catch (Exception ex)
130             {
131 
132                 MessageBox.Show(ex.Message);
133             }
134         }
135         private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
136         {
137             if (comboBox1.SelectedIndex >= 4)
138             {
139                 groupBox2.Enabled = true;
140                 groupBox1.Enabled = false;
141             }
142             else
143             {
144                 groupBox1.Enabled = true;
145                 groupBox2.Enabled = false;
146             }
147             comboBox1.Invoke(new Action(() => { functionCode = comboBox1.SelectedItem.ToString(); }));
148         }
149 
150         /// <summary>
151         /// 初始化讀參數
152         /// </summary>
153         private void SetReadParameters()
154         {
155             if (txt_startAddr1.Text == "" || txt_slave1.Text == "" || txt_length.Text == "")
156             {
157                 MessageBox.Show("請填寫讀參數!");
158             }
159             else
160             {
161                 slaveAddress = byte.Parse(txt_slave1.Text);
162                 startAddress = ushort.Parse(txt_startAddr1.Text);
163                 numberOfPoints = ushort.Parse(txt_length.Text);
164             }
165         }
166         /// <summary>
167         /// 初始化寫參數
168         /// </summary>
169         private void SetWriteParametes()
170         {
171             if (txt_startAddr2.Text == "" || txt_slave2.Text == "" || txt_data.Text == "")
172             {
173                 MessageBox.Show("請填寫寫參數!");
174             }
175             else
176             {
177                 slaveAddress = byte.Parse(txt_slave2.Text);
178                 startAddress = ushort.Parse(txt_startAddr2.Text);
179                 //判斷是否寫線圈
180                 if (comboBox1.SelectedIndex == 4 || comboBox1.SelectedIndex == 6)
181                 {
182                     string[] strarr = txt_data.Text.Split(' ');
183                     coilsBuffer = new bool[strarr.Length];
184                     //轉化為bool數組
185                     for (int i = 0; i < strarr.Length; i++)
186                     {
187                         // strarr[i] == "0" ? coilsBuffer[i] = true : coilsBuffer[i] = false;
188                         if (strarr[i] == "0")
189                         {
190                             coilsBuffer[i] = false;
191                         }
192                         else
193                         {
194                             coilsBuffer[i] = true;
195                         }
196                     }
197                 }
198                 else
199                 {
200                     //轉化ushort數組
201                     string[] strarr = txt_data.Text.Split(' ');
202                     registerBuffer = new ushort[strarr.Length];
203                     for (int i = 0; i < strarr.Length; i++)
204                     {
205                         registerBuffer[i] = ushort.Parse(strarr[i]);
206                     }
207                 }
208             }
209         }
210         /// <summary>
211         /// 清除文本
212         /// </summary>
213         /// <param name="sender"></param>
214         /// <param name="e"></param>
215         private void button2_Click(object sender, EventArgs e)
216         {
217             richTextBox1.Clear();
218         }
219         /// <summary>
220         /// SetMessage
221         /// </summary>
222         /// <param name="msg"></param>
223         public void SetMsg(string msg)
224         {
225             richTextBox1.Invoke(new Action(() => { richTextBox1.AppendText(msg + "\r\n"); }));
226         }
227 
228     }
229 }
View Code

 

界面佈局

 

六 功能測試及報文解析

這裡功能測試我們需要藉助測試工具  Modbus Slave(Modbus從站客戶端)

鏈接:https://pan.baidu.com/s/1Z3bET3l_2a4e6cu_p250tg
提取碼:hq1r

簡單說明一下,這裡我實現了常用的幾個功能碼

0x01 讀一組線圈

0x02 讀一組輸入線圈/離散量線圈

0x03 讀一組保持寄存器

0x04 讀一組輸入寄存器

0x05 寫單個線圈

0x06 寫單個保持寄存器

0x0F 寫多個線圈

0x10 寫多個保持寄存器

簡單說一下Modbus Slave 的操作

打開連接,建立連接,選擇連接方式為Tcp/Ip 設置 Ip和埠號

選擇線圈或寄存器

點擊Setup->Slave Definition,這裡的Function我們需要讀/寫什麼線圈或寄存器就對應選擇

 

 

測試1 功能碼0x01

這裡我們所有的測試從站都使用站號1 起始地址0 長度10

功能碼0x01 讀取線圈 Modbus Slave的Function選擇01 Coil Status(0x)

測試結果:

點擊Display->Communication 可以截取報文,我也不知道為什麼他報文字體那麼小(絕望ing)

000000-Rx:00 01 00 00 00 06 01 01 00 00 00 05
000001-Tx:00 01 00 00 00 04 01 01 01 06

測試2  功能碼0x10

功能碼0x10 寫入一組數據到保持寄存器  Modbus Slave的Function選擇03 Holding Register(4x) (說明一下 線圈和保持寄存器才有寫操作)

測試結果

 報文

000070-Rx:00 01 00 00 00 11 01 10 00 00 00 05 0A 00 0C 00 22 00 38 00 4E 00 5A
000071-Tx:00 01 00 00 00 06 01 10 00 00 00 05

上文測試了一個讀操作和一個寫操作,其他功能碼的測試與上文一致,有興趣的可以自行測試,

下一篇博客我要針對不同的功能碼做對應的報文解析

程式源碼:

鏈接:https://pan.baidu.com/s/1549Fu65wLtNvsxM0Bj71dA
提取碼:1j96

以上都為我自己學習總結並實現,有錯誤之處,希望大家不吝賜教,感謝(抱拳)!

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 五一假期回來,練習一下C#的一些知識,瞭解一下排序。 練習數據: 寫一個類: using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Ta ...
  • 直接切入主題 有時候同一個項目下我們可能會使用多個窗體,窗體間方法互相調用也不可避免,好了,使用無參無返回值的方法,開始上圖 1、新建一個winform項目Form1,並再添加一個窗體Form2;拖入button和textbox,如下 2、先編輯Form2,定義屬性存放無參無返回值的方法,重載For ...
  • 今天在使用 C# 操作 Excel 時,一直在報錯誤: 檢索 COM 類工廠中 CLSID 為 {00024500-0000-0000-C000-000000000046} 的組件失敗,原因是出現以下錯誤: 8007065e 這個類型的數據不受支持。 (異常來自 HRESULT:0x8007065E ...
  • 一個字元串就是一個string類型數據,此類型變數我們可以把它看作一個只讀數組,其元素是char變數,在這裡我們來說下string類型的常用命令。 1、TocharArray():將此實例中的字元複製到 Unicode 字元數組。其示例是“char[] ch = <string類型變數>.ToCha ...
  • 因項目原因,需要使用SQLite的全文索引,用到了最新的fts5模塊 但在咱們.net framwork中卻會提示“SQL logic error no such module: fts5”:找不到fts5模塊…… 百度了很久都沒有找到項目解決方案,可能是C#用SQLite比較少的原因,更何況用的更 ...
  • https://3gstudent.github.io/3gstudent.github.io/Exchange-Web-Service(EWS)%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/ EWS是郵箱的一個開放的介面服務,可以取到郵箱的各種信息,郵件收發、會議、日期 ...
  • 在之前的文章中我們是使用Rester來測試我們的WebAPI的。接下來,我們來創建一個實際的頁面來測試之前我們寫的WebAPI。我們創建一個HTML頁面,併在頁面使用 jQuery 來調用 Web API 。通過jQuery來調用增刪除改查WebAPI介面,並用 API 介面返回的響應中的詳細信息更... ...
  • 概述 在嵌入式開發的過程中,由於經常需要下位機與上位機通信,通信之間就需要協議,有協議就需要進行解碼,而產品開發得過程中,協議可能不斷更新,協議更新就需要解碼軟體更新,不斷更新解碼軟體就很麻煩,如果所有人都願意麻煩,那麼我不願意。在這裡就產生了一個通用的解碼類庫,使用者就可以簡單的改一下協議文件,通 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...