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