Modbus協議棧應用例項之四:ModbusTCP伺服器應用
原始碼下載:https://download.csdn.net/download/foxclever/12838885
自從開源了我們自己開發的Modbus協議棧之後,有很多朋友建議我針對性的做幾個示例。所以我們就基於平時我們的應用整理了幾個簡單但可以說明基本的應用方法的示例,這一篇中我們來簡述如何使用協議棧實現一個Modbus TCP伺服器應用。
1、何為TCP伺服器
Modbus協議是一個主從協議,那肯定就有主站和從站之分,在Modbus TCP中亦稱之為客戶端與伺服器。所謂TCP客戶端其功能基本與RTU主站一樣,RTU主站會向從站發起資料請求,同樣的TCP客戶端也會向伺服器發起請求。也就是說在Modbus TCP模式下客戶端亦是發起通訊的一方。
對於TCP客戶端來說,自己並不會產生資料,它的資料均是從伺服器獲取,為了得到資料就必須向伺服器發起資料請求。在Modbus TCP協議中,伺服器一般也不會主動向外發送資料,伺服器需要根據客戶端的資料請求來決定是否傳送資料、傳送哪些資料。這一過程如下圖所示:
從上圖我們不難看出,首先客戶端要主動發起資料請求,客戶端發起的資料請求需要告訴伺服器它請求的資料有哪些。伺服器收到這個資料請求後,伺服器解析客戶端的請求並按照客戶端的請求返回資料。客戶端收到資料響應後解析資料,這樣就完成了客戶端與伺服器之間的一次資料通訊。
需要注意的是,Modbus TCP與Modbus RTU不同的是有一個專用的MBAP報文頭來識別Modbus應用資料單元。這一報文頭由7個位元組組成:
這種MBAP報文頭雖然也是用來識別Modbus資料域,但還是與序列鏈路上使用的MODBUS RTU應用資料單元有一些差別,具體如下:
(1)、用MBAP報文頭中的單個位元組單元識別符號取代MODBUS序列鏈路上通常使用的MODBUS從地址域。這個單元識別符號用於裝置的通訊,這些裝置使用單個 IP 地址支援多個獨立MODBUS 終端單元,例如:網橋、路由器和閘道器。
(2)、使用接收者可以驗證的方式來構造所有MODBUS請求和響應。對於MODBUS PDU有固定長度的功能碼來說,僅功能碼就足夠了。對於在請求或響應中攜帶一個可變資料的功能碼來說,資料域包括位元組數。
(3)、使用TCP上傳送MODBUS資料域時,即使將報文分成多個資訊包來傳輸,可在MBAP報文頭上攜帶附加長度資訊,這樣接收者就能夠識別報文的完整性。
2、如何實現TCP伺服器
我們已經簡單的描述了基於TCP/IP的Modbus資料通訊,在此基礎上我們將進一步描述基於協議棧的Modbus TCP伺服器的實現。
在協議棧中,我們已經實現了Modbus TCP伺服器的基本功能,如資料的管理及響應客戶端的請求等。Modbus TCP伺服器作為資料的生產者,管理者四類資料:線圈量、狀態量、輸入暫存器和保持暫存器。所以在Modbus TCP伺服器中我們要為這四種資料定義相應的地址,以便客戶端能夠對應的訪問。所以設計一個Modbus TCP伺服器我們先來設計它的資料地址。在我們的例子中,出於操作方便,我們規定了每類資料型別的數量為10,我們以用的最多的保持暫存器為例,定義暫存器地址為40001到40010。
在我們的協議棧中實現了0x01、0x02、0x03、0x04、0x05、0x06、0x0F以及0x10等功能碼。也就是說客戶端物件會生成面向這些功能碼的Modbus TCP伺服器資料請求。Modbus TCP伺服器收到請求後,解析請求並根據請求生成響應的資料響應。可以表示為下圖所示:
從上圖我們明白協議棧中已經實現了對收到的主站資料請求進行解析以及根據解析生成對應的響應的函式。我們使用協議棧時,主要需要做兩個方面的事情:解析資料請求和生成資料響應。
在協議棧中定義了一個解析函式,該函式將收到的資料請求訊息解析,並根據解析的結果生成返回的資料響應。該函式的原型如下:
/*解析接收到的資訊,返回響應命令的長度*/
uint16_t ParsingClientAccessCommand(uint8_t *receivedMessage,uint8_t *respondBytes)
這個函式有2個引數:uint8_t *receivedMessage是收到的資料請求訊息; uint8_t *respondBytes是返回的資料響應訊息,也是函式需要生成的;而函式的返回值則是生成的資料響應詳細的長度。
在解析的過程中,該函式判斷訊息的完整性,並根據不同的功能碼呼叫不同的回撥函式來實現,包括設定本地資料和獲取本地資料的相關回調函式,在後續將討論它們的實現。
3、TCP伺服器編碼
到這裡其實我們已經很清楚,使用協議棧實現Modbus TCP伺服器只需要在TCP/IP收到客戶端請求後呼叫sendLen = ParsingClientAccessCommand(buffer, sendBuf);函式解析收到的請求命令。並根據請求執行相應的操作就可以了。那需要實現哪些操作呢?在協議棧中定義了8個回撥函式,分別是獲取線圈量、獲取狀態量、獲取輸入暫存器和獲取保持暫存器,以及預置單個線圈量、預置多個線圈量、預置單個保持暫存器和預置多個保持暫存器。函式原型定義如下:
1 /*獲取想要讀取的Coil量的值*/ 2 __weak void GetCoilStatus(uint16_t startAddress,uint16_t quantity,bool *statusList) 3 { 4 //如果需要Modbus TCP Server/RTU Slave應用中實現具體內容 5 } 6 7 /*獲取想要讀取的InputStatus量的值*/ 8 __weak void GetInputStatus(uint16_t startAddress,uint16_t quantity,bool *statusValue) 9 { 10 //如果需要Modbus TCP Server/RTU Slave應用中實現具體內容 11 } 12 13 /*獲取想要讀取的保持暫存器的值*/ 14 __weak void GetHoldingRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue) 15 { 16 //如果需要Modbus TCP Server/RTU Slave應用中實現具體內容 17 } 18 19 /*獲取想要讀取的輸入暫存器的值*/ 20 __weak void GetInputRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue) 21 { 22 //如果需要Modbus TCP Server/RTU Slave應用中實現具體內容 23 } 24 25 /*設定單個線圈的值*/ 26 __weak void SetSingleCoil(uint16_t coilAddress,bool coilValue) 27 { 28 //如果需要Modbus TCP Server/RTU Slave應用中實現具體內容 29 } 30 31 /*設定單個暫存器的值*/ 32 __weak void SetSingleRegister(uint16_t registerAddress,uint16_t registerValue) 33 { 34 //如果需要Modbus TCP Server/RTU Slave應用中實現具體內容 35 } 36 37 /*設定多個線圈的值*/ 38 __weak void SetMultipleCoil(uint16_t startAddress,uint16_t quantity,bool *statusValue) 39 { 40 //如果需要Modbus TCP Server/RTU Slave應用中實現具體內容 41 } 42 43 /*設定多個暫存器的值*/ 44 __weak void SetMultipleRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue) 45 { 46 //如果需要Modbus TCP Server/RTU Slave應用中實現具體內容 47 }
這些函式就是我們要根據我們的Modbus TCP伺服器功能設計實現的。對於我們這個測試例子我們只需要實現讀取保持暫存器就可以了。具體實現如下:
1 /*獲取想要讀取的保持暫存器的值*/ 2 void GetHoldingRegister(uint16_t startAddress, uint16_t quantity, uint16_t* registerValue) 3 { 4 uint16_t start; 5 uint16_t count; 6 7 /*先判斷地址是否處於合法範圍*/ 8 start = (startAddress > 0) ? ((startAddress <= 9) ? startAddress : 9) : 0; 9 count = ((start + quantity - 1) <= 9) ? quantity : (9 - start); 10 11 for (int i = 0; i < count; i++) 12 { 13 registerValue[i] = holdingRegister[start + i]; 14 } 15 }
這個例子中我們實現了讀取40001到40010保持暫存器的值。
4、TCP伺服器小結
我們在TCP伺服器的基礎上使用我們的協議棧實現一個Modbus TCP伺服器應用。其實使用協議棧實現Modbus TCP伺服器應用是很簡單的,我們需要使用如ModPoll這樣的軟體來測試一下它。
我們讀取10個保持暫存器,值分別為對應位固定的1到10,如上圖讀出的結果與預期一致。我們還可以採用TCP&UDP測試工具來看一下報文,具體如下:
同樣的,在同一臺裝置上只需實現一個Modbus TCP伺服器,哪怕是通過不同的網路埠來訪問。這一點與客戶端是不一樣的,原因是Modbus TCP伺服器的資料是自己產生,而且只需被動響應客戶端的資料請求。
接下來我們來總結一下使用協議棧實現Modbus TCP伺服器的工作流程,或者說實現的步驟。首先Modbus TCP伺服器要解析從客戶端送來的資料請求。在協議棧中已經封裝了資料請求的解析函式、所以我們實現Modbus TCP伺服器時首先就是呼叫這一函式來解析接收到的資料請求訊息。
然後將解析函式返回的資料響應訊息傳送到客戶端就可以了。也就是說使用協議棧,只需要呼叫一下這個函式Modbus TCP伺服器功能就實現了。這是因為這個函式實現了整個Modbus TCP伺服器的響應過程,大致分三個步驟:第一步,解析收到的客戶端資料請求訊息;第二步,根據解析的結果預置資料或者獲取資料,預置和獲取資料由8個回撥函式實現;第三步,生成Modbus TCP伺服器資料響應訊息。說到這裡我們已經清楚,Modbus TCP伺服器必須實現這些回撥函式,其它工作則全由協議棧完成。