實現Modbus TCP多網段客戶端應用

  對於Modbus TCP來講與Modbus RTU和Modbus ASCII有比較大的區別,由於它是運行於以太網鏈路之上,是運行於TCP/IP協議之上的一種應用層協議。在協議棧的前兩個版本中,Modbus TCP做爲客戶端時也存在一些侷限性。咱們將對這些不足做必定更新。git

1、存在的不足github

  在原有的協議棧中,咱們所封裝的Modbus TCP客戶端一個特定的客戶端,即它只是一個客戶端實例。在一般的應用中不會有什麼問題,但在有些應用場合就會顯現出它的侷限性。數組

首先,做爲一個特定的客戶端,如果鏈接多個服務器目標時,修改服務器參數值的處理變的很是複雜,須要分辨是不一樣的服務器,不一樣的變量。當須要從不一樣的網段操做數據時,咱們甚至須要標記不一樣的網段。服務器

  其次,做爲一個特定的客戶端,若是咱們操做的服務器參數類似時,哪怕來自於不一樣的網段,咱們也須要仔細分辨或者傳遞額外的參數。由於同一客戶端的解析函數是同一個。網絡

  最後,將多個Modbus TCP服務器通信都做爲惟一的一個特定的服務器來處理,使得各部分混雜在一塊兒,程序結構很不清晰,對象也不明確。函數

 2、更新設計測試

  考慮到前述的侷限性,咱們將Modbus TCP客戶端及其所訪問的Modbus TCP服務器定義爲通用的對象,而當咱們在具體應用中使用時,再將其特例化爲特定的客戶端和服務器對象。ui

首先咱們來考慮客戶端,原則上咱們規劃的每個客戶端對象管理咱們設備上的一個IP網段的設備。那麼在一個特定客戶端下,咱們能夠定義多達253個不一樣的服務器。以下圖所示:編碼

 

  從上圖中咱們能夠發現,咱們的目的就是讓協議棧支持,多客戶端和多服務器,而且在不一樣客戶端下能夠訪問同網段的多個服務器。接下來咱們還須要考慮服務器對象。客戶端對服務器的操做無非兩類:讀服務器信息和寫服務器信息。spa

  對於讀服務器信息來講,客戶端須要發送請求命令,等待服務器返回響應信息,而後客戶端解析收到的信息並更新對應的參數值。由於返回的響應消息是沒有對應的寄存器地址的,因此要想在解析的時候定位寄存器就必須知道發送的命令,爲了便於分辨咱們將命令存放在服務器對象中。

  而對於寫服務器操做,不管寫的要求來自於哪裏,對於協議棧來講確定是其它的數據處理進程發過來的,所接到要求後咱們須要記錄是哪個客戶端管理的哪個服務器的哪些參數。對於客戶端咱們不須要分辨,由於每一個客戶端都是獨立的處理進程,可是對於服務器和參數咱們就須要分辨。每個客戶端所管理的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

歡迎關注:

 

相關文章
相關標籤/搜索