對於Modbus TCP來說與Modbus RTU和Modbus ASCII有比較大的區別,因為它是運行於以太網鏈路之上,是運行於TCP/IP協議之上的一種應用層協議。在協議棧的前兩個版本中,Modbus TCP作為客戶端時也存在一些局限性。我們將對這些不足作一定更新。
1、存在的不足
在原有的協議棧中,我們所封裝的Modbus TCP客戶端一個特定的客戶端,即它只是一個客戶端實例。在通常的應用中不會有什么問題,但在有些應用場合就會顯現出它的局限性。
首先,作為一個特定的客戶端,若是連接多個服務器目標時,修改服務器參數值的處理變的非常復雜,需要分辨是不同的服務器,不同的變量。當需要從不同的網段操作數據時,我們甚至需要標記不同的網段。
其次,作為一個特定的客戶端,如果我們操作的服務器參數相似時,哪怕來自於不同的網段,我們也需要仔細分辨或者傳遞額外的參數。因為同一客戶端的解析函數是同一個。
最后,將多個Modbus TCP服務器通訊都作為唯一的一個特定的服務器來處理,使得各部分混雜在一起,程序結構很不清晰,對象也不明確。
2、更新設計
考慮到前述的局限性,我們將Modbus TCP客戶端及其所訪問的Modbus TCP服務器定義為通用的對象,而當我們在具體應用中使用時,再將其特例化為特定的客戶端和服務器對象。
首先我們來考慮客戶端,原則上我們規划的每一個客戶端對象管理我們設備上的一個IP網段的設備。那么在一個特定客戶端下,我們可以定義多達253個不同的服務器。如下圖所示:
從上圖中我們可以發現,我們的目的就是讓協議棧支持,多客戶端和多服務器,並且在不同客戶端下可以訪問同網段的多個服務器。接下來我們還需要考慮服務器對象。客戶端對服務器的操作無非兩類:讀服務器信息和寫服務器信息。
對於讀服務器信息來說,客戶端需要發送請求命令,等待服務器返回響應信息,然后客戶端解析收到的信息並更新對應的參數值。因為返回的響應消息是沒有對應的寄存器地址的,所以要想在解析的時候定位寄存器就必須知道發送的命令,為了便於分辨我們將命令存放在服務器對象中。
而對於寫服務器操作,無論寫的要求來自於哪里,對於協議棧來說肯定是其它的數據處理進程發過來的,所接到要求后我們需要記錄是哪一個客戶端管理的哪一個服務器的哪些參數。對於客戶端我們不需要分辨,因為每個客戶端都是獨立的處理進程,但是對於服務器和參數我們就需要分辨。每一個客戶端所管理的IP地址的最后一段為0到255,所以我們可以依據來分辨服務器端。而在每一個服務器節點中增加狀態標志,用以記錄請求狀態,而所有服務器端組成鏈表。
3、編碼實現
我們已經設計了我們的更新,接下來我們就根據這一設計來實現它。我們主要從以下幾個方面來操作:第一,實現客戶端對象類型和服務器對象類型;第二,客戶端對象的實例化及服務器對象的實例化;第三,讀服務器參數的客戶端操作過程;第四,寫服務器參的數客戶端操作過程。接下來我們將一一描述之。
3.1、定義對象類型
與在Modbus RTU和Modbus ASCII一樣,在Modbus TCP協議棧的封裝中,我們也需要定義客戶端對象和服務器對象,自然也免不了要定義這兩種類型。
首先我們來定義本地客戶端的類型,其成員包括:一個uint32_t的寫服務器標志數組;服務器數量字段;服務器順序字段;本客戶端所管理的服務器列表;4個數據更新函數指針。具體定義如下:
1 /* 定義本地TCP客戶端對象類型 */ 2 typedef struct LocalTCPClientType{ 3 uint32_t transaction; //事務標識符 4 uint16_t cmdNumber; //讀服務器命令的數量 5 uint16_t cmdOrder; //當前從站在從站列表中的位置 6 uint8_t (*pReadCommand)[12]; //讀命令列表 7 ServerListHeadNode ServerHeadNode; //Server對象鏈表的頭節點 8 UpdateCoilStatusType pUpdateCoilStatus; //更新線圈量函數 9 UpdateInputStatusType pUpdateInputStatus; //更新輸入狀態量函數 10 UpdateHoldingRegisterType pUpdateHoldingRegister; //更新保持寄存器量函數 11 UpdateInputResgisterType pUpdateInputResgister; //更新輸入寄存器量函數 12 }TCPLocalClientType;
關於客戶端對象類型,在前面的更新設計中已經講的很清楚了,只有Server對象鏈表的頭節點字段需要說明一下。該字段包括兩個類容:第一,服務器鏈表的頭節點指針,用來記錄服務器對象列表。第二,記錄鏈表的長度,即服務器節點的數量。具體如下圖所示:
還需要定義服務器對象,此服務器對象只是便於客戶端而用於表示真是的服務器。客戶端的服務器列表中就是此對象。具體結構如下:
1 /* 定義被訪問TCP服務器對象類型 */ 2 typedef struct AccessedTCPServerType{ 3 union { 4 uint32_t ipNumber; 5 uint8_t ipSegment[4]; 6 }ipAddress; //服務器的IP地址 7 uint32_t flagPresetServer; //寫服務器請求標志 8 WritedCoilListHeadNode pWritedCoilHeadNode; //可寫的線圈量列表 9 WritedRegisterListHeadNode pWritedRegisterHeadNode; //可寫的保持寄存器列表 10 struct AccessedTCPServerType *pNextNode; //下一個TCP服務器節點 11 }TCPAccessedServerType;
關於服務器對象有三個字段需要說明一下。首先我們來看一看“讀命令列表(uint8_t (*pReadCommand)[12])”字段,它是12個字節,這是由Modbus TCP消息格式決定的。如下:
我們看到協議標識符為0,是因為0就表示Modbus TCP。還有可寫的線圈量列表頭節點和可寫的保持寄存器列表頭節點。這兩個字段用來表示對線圈和保持寄存器的列表即數量。
3.2、實例化對象
我們定義了客戶端即服務器對象類型,我們在使用時就需要實例化這些對象。一般來說一個IP網段我們將其實例化為一個客戶端對象。
TCPLocalClientType hgraClient;
/*初始化TCP客戶端對象*/
InitializeTCPClientObject(&hgraClient,2,hgraServer,NULL,NULL,NULL,NULL);
而一個客戶端對象會管理1到253個服務器對象,所以我們可以將多個服務器對象實例組成數組,並將其賦予客戶端管理。
TCPAccessedServerType hgraServer[]={{{192,168,0,1},0x00,0x00},{{192,168,1,1},0x00,0x00}};
所以,根據客戶端和服務器實例化的條件,我們需要先實例化服務器對象才能完整實例化客戶端對象。在客戶端的初始化中,我們這里將4的數據處理函數指針初始化為NULL,有一個默認的處理函數會復制給它,該函數是上一版本的延續,在簡單應用時簡化操作。服務器的上一個發送的命令指針也被賦值為NULL,因為初始時還沒有命令發送。
3.3、讀服務器操作
讀服務器操作原理上與以前的版本是一樣的。按照一定的順序給服務器發送命令再對收到的消息進行解析。我們對客戶端及其所管理的服務器進行了定義,將發送命令保存於服務器對象,將服務器列表保存於客戶端對象,所以我們需要對解析函數進行修改。
1 /*解析收到的服務器相應信息*/ 2 void ParsingServerRespondMessage(TCPLocalClientType *client,uint8_t *recievedMessage) 3 { 4 /*判斷接收到的信息是否有相應的命令*/ 5 int cmdIndex=FindCommandForRecievedMessage(client,recievedMessage); 6 7 if((cmdIndex<0)) //沒有對應的請求命令,事務號不相符 8 { 9 return; 10 } 11 12 if((recievedMessage[2]!=0x00)||(recievedMessage[3]!=0x00)) //不是Modbus TCP協議 13 { 14 return; 15 } 16 17 if(recievedMessage[7]>0x04) //功能碼大於0x04則不是讀命令返回 18 { 19 return; 20 } 21 22 uint16_t mLength=(recievedMessage[4]<<8)+recievedMessage[4]; 23 uint16_t dLength=(uint16_t)recievedMessage[8]; 24 if(mLength!=dLength+3) //數據長度不一致 25 { 26 return; 27 } 28 29 FunctionCode fuctionCode=(FunctionCode)recievedMessage[7]; 30 31 if(fuctionCode!=client->pReadCommand[cmdIndex][7]) 32 { 33 return; 34 } 35 36 uint16_t startAddress=(uint16_t)client->pReadCommand[cmdIndex][8]; 37 startAddress=(startAddress<<8)+(uint16_t)client->pReadCommand[cmdIndex][9]; 38 uint16_t quantity=(uint16_t)client->pReadCommand[cmdIndex][10]; 39 quantity=(quantity<<8)+(uint16_t)client->pReadCommand[cmdIndex][11]; 40 41 if(quantity*2!=dLength) //請求的數據長度與返回的數據長度不一致 42 { 43 return; 44 } 45 46 if((fuctionCode>=ReadCoilStatus)&&(fuctionCode<=ReadInputRegister)) 47 { 48 HandleServerRespond[fuctionCode-1](client,recievedMessage,startAddress,quantity); 49 } 50 }
解析函數的主要部分是在檢查接收到的消息是否是合法的Modbus TCP消息。檢查沒問題則調用協議站解析。而最后調用的數據處理函數則是我們需要在具體應用中編寫。在前面客戶端初始化時,回調函數我們初始化為NULL,實際在協議占中有弱化的函數定義,需要針對具體的寄存器和變量地址實現操作。
3.4、寫服務器操作
寫服務器操作則是在其它進程請求后,我們標識需要寫的對象再統一處理。對具體哪個服務器的寫標識存於客戶端實例。而該服務器的哪些變量需要寫則記錄在服務器實例中。
所以在進程檢測到需要寫一個服務器時則置位對應的位,即改變flagWriteServer中的對應位。而需要寫該服務器的哪些變量則標記flagPresetCoil和flagPresetReg的對應位。修改這些標識都在其它請求更改的進程中實現,而具體的寫操作則在本客戶端進程中,檢測到標志位的變化統一執行。
這部分不修改協議棧的代碼,因為各服務器及各變量都只與具體對象相關聯,所以在具體的應用中修改。
4、回歸驗證
借鑒前面Modbus ASCII和Modbus RTU的回歸測試經驗,我們設計兩個網段、每網段包括一個客戶端及兩個服務器的網絡結構。但考慮到我們只是功能性驗證,所以我們設計相對簡單的服務器。所以我們設計的網絡為:協議棧建立2個客戶端,每個客戶端管理同一網段的2個服務器,每個服務器有8個線圈及2個保持寄存器。具體結構如圖:
從上圖我們知道,該Modbus網關需要實現一個Modbus服務器用於和上位的通訊;需要實現兩個Modbus客戶端用於和下位的通訊。
在這個實驗中,讀操作沒有什么需要說的,只需要發送命令解析返回消息即可。所以我們中點描述一下為了方便操作,在需要寫的連續段,我們只要找到第一個請求寫的位置后,就將后續連續可寫數據一次性寫入。
告之:源代碼可上Github下載:https://github.com/foxclever/Modbus
歡迎關注: