想搞清楚modbus rtu嗎?帶你瞭解後並可以實踐

隨著工業網際網路的推進,智慧工廠離不開對底層裝置的掌控,從業人員對工業協議理解的也要慢慢加深。

最近在用c#寫modbus的從站,算是比較系統的整理一下modbus協議的內容,網上總結的內容總感覺不是特別的齊全,在此和大家分享一下我理解的modbus協議。有說的不對的地方還請大家指正。

我將從以下幾方面進行闡述:

modbus協議簡介

modbus的請求與響應過程

modbus rtu的master程式碼實現

modbus協議簡介

Modbus

是一種序列通訊協議,是Modicon公司(現在的施耐德電氣Schneider Electric)於1979年為使用可程式設計邏輯控制器(PLC)通訊而發表。Modbus已經成為工業領域通訊協議的業界標準(De facto),並且現在是工業電子裝置之間常用的連線方式。 Modbus比其他通訊協議使用的更廣泛的主要原因有:

公開發表並且無版權要求

易於部署和維護

對供應商來說,修改移動本地的位元或位元組沒有很多限制

Modbus允許多個 (大約240個) 裝置連線在同一個網路上進行通訊,舉個例子,一個由測量溫度和溼度的裝置,並且將結果傳送給計算機。在資料採集與監視控制系統(SCADA)中,Modbus通常用來連線監控計算機和遠端終端控制系統(RTU)。

Modbus協議目前存在用於串列埠、乙太網以及其他支援網際網路協議的網路的版本。

Modbus協議是一個master/slave架構的協議。有一個節點是master節點,其他使用Modbus協議參與通訊的節點是slave節點。每一個slave裝置都有一個唯一的地址。

一個ModBus命令包含了打算執行的裝置的Modbus地址。所有裝置都會收到命令,但只有指定位置的裝置會執行及迴應指令(地址0例外,指定地址0的指令是廣播指令,所有收到指令的裝置都會執行,不過不迴應指令)。所有的Modbus命令包含了檢查碼,以確定到達的命令沒有被破壞。基本的ModBus命令能指令一個RTU改變它的暫存器的某個值,控制或者讀取一個I/O埠,以及指揮裝置回送一個或者多個其暫存器中的資料。

用大白話總結一下:modbus由施耐德電氣的前身Modicon公司發表,是免費使用的。它是主從結構,有主站(master)和從站(slave)之分。主站發出請求,從站響應。每個從站都有唯一的站地址,透過站地址識別它們。每一個從站都有預定義的暫存器地址。

modbus的請求與響應過程

想搞清楚modbus rtu嗎?帶你瞭解後並可以實踐

響應過程

上圖是一個modbus請求響應過程的示意圖,主站發出一個查詢報文,從站響應一個結果報文。modbus協議定義了報文的內容,報文內容包括裝置地址、功能程式碼、資料和校驗碼。

裝置地址:設定範圍為1-255,因為它站一個byte。

功能碼:modbus的主要功能碼見下圖

想搞清楚modbus rtu嗎?帶你瞭解後並可以實踐

modbus功能碼

資料:報文核心內容,不同的功能碼對應的資料格式不同,下面會具體說明。

校驗碼:用於驗證報文的正確性,經過特定演算法後的值,RTU模式的校驗演算法叫CRC,ASCII模式的演算法叫LRC。從上面的響應過程圖我們就可以發現,主站傳送請求報文的時候把校驗碼也一起傳送給從站;從站接受報文後經過特定演算法計算得出校驗碼,然後與收到的校驗碼進行比較,如果相等說明報文傳輸正確,沒有收到外借干擾;確定接收到報文正確後,從站作為響應,傳送一段報文給主站;同樣的主站也需要對從站發過來的報文進行校驗碼的比較,相等則解析資料(這個資料就是我們想要的資料),不相等則說明收到的資料有問題,不能使用這次的資料。到這裡整個請求過程就結束了。CRC和LRC的具體演算法,在“

modbus rtu和ascii的客戶端程式碼實現

”中會具體說明。

有了上面的簡單認識,我們開始再進一步。我們來了解一下modbus的報文格式。

前面我們也說到了,功能碼不同對應的報文的格式也不同。理解報文的格式非常重要,因為它和後面的程式設計程式碼緊密相關,不然你不能理解程式碼為什麼要那麼寫。下面我們來列舉不同功能碼對應的報文格式。

讀線圈的報文

想搞清楚modbus rtu嗎?帶你瞭解後並可以實踐

讀線圈的報文

如圖所示,讀線圈的請求報文為:站號 、功能碼 、起始地址高8位 、起始地址低8位 、讀取數量高8位、 讀取數量低8位、 CRC校驗低8位 、CRC校驗高8位,佔8byte,這是固定的。這裡注意一下兩個byte的形成一個word的值就是你要讀取布林量的數量。

讀線圈的響應報文為:站號 、功能碼 、位元組數量 、第1個byte狀態 、……。。。。。 、第n個byte狀態 、CRC校驗低8位、 CRC校驗高8位。這是一個長度不固定的報文,長度根據請求的數量而定。響應的內容以byte為基礎單位,所以如果你讀取8以內的布林數量主站響應一個byte,讀取9的話就響應2byte。

線圈單個寫入

想搞清楚modbus rtu嗎?帶你瞭解後並可以實踐

線圈單個寫入

如圖所示,寫單個線圈的報文為:站號 、功能碼 、寫地址高8位 、寫地址低8位 、資料內容高8位 、資料內容低8位 、CRC校驗低8位 、CRC校驗高8位。資料內容為0時寫入為0,資料內容大於0時寫入為1(我是用modsim32來測試的,不保證其它從站也是如此,畢竟程式碼都是不同人敲出來的)。

寫單個線圈的響應報文:站號 、功能碼 、寫地址高8位 、寫地址低8位 、資料內容高8位 、資料內容低8位 、CRC校驗低8位 、CRC校驗高8位。

多個線圈寫入

想搞清楚modbus rtu嗎?帶你瞭解後並可以實踐

多個線圈寫入

如圖所示,寫多個線圈的請求報文為:站號 、功能碼 、寫起始地址高8位 、寫起始地址低8位 、寫入的數量高8位 、寫入的數量低8位 、位元組個數 、寫入值高8位 、寫入值低8位 、CRC校驗低8位 、CRC校驗高8位。請求報文長度不固定,與寫入的數量對應。

寫多個線圈的響應的報文為:站號 、功能碼 、寫起始地址高8位 、寫起始地址低8位 、寫入的數量高8位 、寫入的數量低8位 、CRC校驗低8位 、CRC校驗高8位。

讀離散輸入

想搞清楚modbus rtu嗎?帶你瞭解後並可以實踐

讀離散輸入

如圖所示,讀離散輸入的請求報文為:站號、 功能碼 、起始地址高8位 、起始地址低8位、 讀取數量高8位 、讀取數量低8位、 CRC校驗低8位 、CRC校驗高8位。

讀線圈的響應報文為:站號 、功能碼 、位元組數量 、第1個byte狀態 、……。。。。。 、第n個byte狀態 、CRC校驗低8位、 CRC校驗高8位。這是一個長度不固定的報文,長度根據請求的數量而定。響應的內容以byte為基礎單位,所以如果你讀取8以內的布林數量主站響應一個byte,讀取9的話就響應2byte。

報文格式和讀取線圈一模一樣。

讀取保持暫存器

想搞清楚modbus rtu嗎?帶你瞭解後並可以實踐

讀取保持暫存器

如圖所示,圖中已經說明的很清楚了,我就不展開說了。

寫單個保持暫存器

想搞清楚modbus rtu嗎?帶你瞭解後並可以實踐

寫單個保持暫存器

寫多個保持暫存器

想搞清楚modbus rtu嗎?帶你瞭解後並可以實踐

寫多個保持暫存器

讀輸入暫存器

想搞清楚modbus rtu嗎?帶你瞭解後並可以實踐

讀輸入暫存器

看了上面的請求響應報文後,下面我們就要根據報文格式來編寫modbus主站程式了。

modbus rtu的客戶端程式碼實現

想搞清楚modbus rtu嗎?帶你瞭解後並可以實踐

請求響應過程

讀線圈的程式碼:

過程說明:

1,主站發出請求(請求報文)

2,從站接到報文後做一個crc校驗,校驗後響應一個報文

3,主站讀取從站響應的報文,做crc校驗,校驗後沒問題,獲取的資料就是從站響應的資料。

byte address = Convert。ToByte(txtSlaveID。Text); ushort start = Convert。ToUInt16(txtStartAddr。Text); //ushort count = Convert。ToUInt16(txtRegisterQty。Text); ushort count = 0; int yushu = Convert。ToUInt16(txtRegisterQty。Text) % 8; int shang = Convert。ToUInt16(txtRegisterQty。Text) / 8; if (yushu == 0) { count = (ushort)shang; } else { count = (ushort)(shang + 1); } byte[] response = new byte[count]; try { mb。Open(lstPorts。SelectedItem。ToString(), Convert。ToInt32(lstBaudrate。SelectedItem。ToString()), 8, Parity。None, StopBits。One); mb。sendFc1(Convert。ToUInt16(txtRegisterQty。Text) , address, start, count,ref response); } catch (Exception ex) { LogHelper。WriteLog(ex。Message, ex); }

//開啟串列埠public bool Open(string portName, int baudRate, int databits, Parity parity, StopBits stopBits) { //Ensure port isn‘t already opened: if (!sp。IsOpen) { //Assign desired settings to the serial port: sp。PortName = portName; sp。BaudRate = baudRate; sp。DataBits = databits; sp。Parity = parity; sp。StopBits = stopBits; //These timeouts are default and cannot be editted through the class at this point: sp。ReadTimeout = 1000; sp。WriteTimeout = 1000; try { sp。Open(); } catch (Exception err) { modbusStatus = “Error opening ” + portName + “: ” + err。Message; return false; } modbusStatus = portName + “ opened successfully”; return true; } else { modbusStatus = portName + “ already opened”; return false; } }

//傳送和接收報文public bool sendFc1(ushort requests, byte address,ushort start,ushort count,ref byte[] values) { //Ensure port is open: if (sp。IsOpen) { //Clear in/out buffers: sp。DiscardOutBuffer(); sp。DiscardInBuffer(); //Function 1 request is always 8 bytes:站號1位元組,功能碼1位元組,起始位2位元組,數量2位元組,crc校驗2位元組 byte[] message = new byte[8]; //Function 1 response buffer:報文格式:站號1位元組,功能嗎1位元組,個數1位元組,crc校驗2位元組,資料佔conut個的位元組 byte[] response = new byte[5 + count]; //Build outgoing modbus message: BuildMessage(address, (byte)1, start, requests, ref message); //Send modbus message to Serial Port: try { sp。Write(message, 0, message。Length);//輸出位元組 GetResponse(ref response);//根據response的長度讀位元組 } catch (Exception err) { modbusStatus = “Error in read event: ” + err。Message; Console。WriteLine(err); LogHelper。WriteLog(err。Message,err); return false; } //Evaluate message: if (CheckResponse(response))//兩個CRC校驗碼的對比 { //Return requested register values: for (int i = 0; i < count; i++)//response。Length - 5因為除了資料所佔位元組還有站號、功能碼、位元組數、功能碼所佔的5個位元組 { values[i] = response[i + 3];//第四個開始是資料 Console。WriteLine(values[i]); } modbusStatus = “Read successful”; return true; } else { modbusStatus = “CRC error”; return false; } } else { modbusStatus = “Serial port not open”; return false; } }

//組織報文格式private void BuildMessage(byte address, byte type, ushort start, ushort registers, ref byte[] message) { //Array to receive CRC bytes: byte[] CRC = new byte[2]; message[0] = address;//地址 message[1] = type;//功能碼 message[2] = (byte)(start >> 8);//高位,讀取地址開始 message[3] = (byte)start;//低位,讀取地址開始 message[4] = (byte)(registers >> 8);//讀取的個數 message[5] = (byte)registers; GetCRC(message, ref CRC); message[message。Length - 2] = CRC[0]; message[message。Length - 1] = CRC[1]; }

//計算crc碼private void GetCRC(byte[] message, ref byte[] CRC) { //Function expects a modbus message of any length as well as a 2 byte CRC array in which to //return the CRC values: ushort CRCFull = 0xFFFF; byte CRCHigh = 0xFF, CRCLow = 0xFF; char CRCLSB; for (int i = 0; i < (message。Length) - 2; i++) { CRCFull = (ushort)(CRCFull ^ message[i]); for (int j = 0; j < 8; j++) { CRCLSB = (char)(CRCFull & 0x0001); CRCFull = (ushort)((CRCFull >> 1) & 0x7FFF); if (CRCLSB == 1) CRCFull = (ushort)(CRCFull ^ 0xA001); } } CRC[1] = CRCHigh = (byte)((CRCFull >> 8) & 0xFF); CRC[0] = CRCLow = (byte)(CRCFull & 0xFF); }

//讀取響應報文private void GetResponse(ref byte[] response) { //There is a bug in 。Net 2。0 DataReceived Event that prevents people from using this //event as an interrupt to handle data (it doesn’t fire all of the time)。 Therefore //we have to use the ReadByte command for a fixed length as it‘s been shown to be reliable。 for (int i = 0; i < response。Length; i++) //for (int i = 0; i < sp。BytesToRead; i++) { //LogHelper。WriteLog(DateTime。Now。ToString()); //int a = sp。ReadByte(); response[i] = (byte)(sp。ReadByte()); Console。WriteLine(response[i]); } }

//校驗crc碼——算出來的crc碼和讀取的crc碼作比較private bool CheckResponse(byte[] response) { //Perform a basic CRC check: byte[] CRC = new byte[2]; GetCRC(response, ref CRC); if (CRC[0] == response[response。Length - 2] && CRC[1] == response[response。Length - 1]) return true; else return false; }

讀保持暫存器的程式碼

//點選事件,讀取保持暫存器private void Button5_Click(object sender, EventArgs e) { try { //開啟串列埠 if (!sp。IsOpen) { mb。Open(lstPorts。SelectedItem。ToString(), Convert。ToInt32(lstBaudrate。SelectedItem。ToString()), 8, Parity。None, StopBits。One); } //傳送命令 byte address = Convert。ToByte(txtSlaveID。Text); ushort start = Convert。ToUInt16(txtStartAddr。Text); ushort requestCount = Convert。ToUInt16(txtRegisterQty。Text); byte[] result = new byte[2*requestCount]; mb。sendFc03(address, start, requestCount, ref result); //顯示 string itemString; for (int i = 0; i < result。Length; i++) { itemString = “[” + Convert。ToString(start + i + 10001) + “] , MB[” + Convert。ToString(start + i) + “] = ” + result[i]。ToString(“X”); DoGUIUpdate(itemString); } } catch (Exception ex) { LogHelper。WriteLog(ex。Message, ex); } }

public bool sendFc03(byte address,ushort start,ushort requestCount, ref byte[] results) { if (sp。IsOpen) { //Clear in/out buffers: sp。DiscardOutBuffer(); sp。DiscardInBuffer(); //Function 1 request is always 8 bytes: byte[] message = new byte[8]; //Function 1 response buffer:報文格式:站號1位元組,功能嗎1位元組,個數1位元組,crc校驗2位元組,資料佔conut個的位元組 byte[] response = new byte[5 + 2*requestCount]; //Build outgoing modbus message: byte[] CRC = new byte[2]; message[0] = address;//地址 message[1] = (byte)3;//功能碼 message[2] = (byte)(start >> 8);//高位,讀取地址開始 message[3] = (byte)start;//低位,讀取地址開始 message[4] = (byte)(requestCount >> 8);//寫的個數 message[5] = (byte)requestCount; GetCRC(message, ref CRC); message[message。Length - 2] = CRC[0]; message[message。Length - 1] = CRC[1]; //Send modbus message to Serial Port: try { sp。Write(message, 0, message。Length);//輸出位元組 GetResponse(ref response);//讀位元組 } catch (Exception err) { modbusStatus = “Error in read event: ” + err。Message; Console。WriteLine(err); LogHelper。WriteLog(err。Message, err); return false; } //Evaluate message: if (CheckResponse(response))//兩個CRC校驗碼的對比 { //把資料放到result中 for (int i = 0; i < 2*requestCount; i++)//response。Length - 5因為除了資料所佔位元組還有站號、功能碼、位元組數、功能碼所佔的5個位元組 { results[i] = response[i + 3];//第四個開始是資料 Console。WriteLine(results[i]); } modbusStatus = “read successful”; return true; } else { modbusStatus = “CRC error”; return false; } } else { modbusStatus = “Serial port not open”; return false; } }

上面用到的方法的程式碼在上面已經有了,這裡就不在重複寫了。最主要的區別還是報文格式的不同,程式碼需要根據報文格式編寫。只要理解了報文格式,程式碼不是問題。

總結:

上面的程式碼是用。net寫的主站,用modsim32做從站。

其實最重要的還是理解這個請求響應過程和報文的格式,然後一步一步往下走就可以了。

其他的modbus功能碼的程式碼這裡就不一一說明了,相信只要理解了上面的程式碼就能舉一反三了。

有什麼問題,歡迎留言交流