C#ModBus Tcp的學習及Master的實現

Modbus已經成爲工業領域通訊協議的業界標準(De facto),而且如今是工業電子設備之間經常使用的鏈接方式。數組

因此這也是咱們工控領域軟件開發的所必懂的通信協議,我也是初次學習,先貼上個人學習筆記網絡

 

一 .協議概述異步

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

(2) 若是按照國際 ISO/OSI 的 7 層網絡模型來講,標準 MODBUS 協議定義了通訊物理層、鏈路層及應用層;ide

物理層:定義了基於 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/1mPAhRixLbsDb7h2ePENTRA
提取碼:b5w6

以上都爲我本身學習總結並實現,有錯誤之處,但願你們不吝賜教,感謝(抱拳)!

相關文章
相關標籤/搜索