對于Modbus TCP來說與Modbus RTU和Modbus ASCII有比較大的區(qū)別,因?yàn)樗沁\(yùn)行于以太網(wǎng)鏈路之上,是運(yùn)行于TCP/IP協(xié)議之上的一種應(yīng)用層協(xié)議。在協(xié)議棧的前兩個版本中,Modbus TCP作為客戶端時也存在一些局限性。我們將對這些不足作一定更新。
1 、存在的不足
在原有的協(xié)議棧中,我們所封裝的Modbus TCP客戶端一個特定的客戶端,即它只是一個客戶端實(shí)例。在通常的應(yīng)用中不會有什么問題,但在有些應(yīng)用場合就會顯現(xiàn)出它的局限性。
首先,作為一個特定的客戶端,若是連接多個服務(wù)器目標(biāo)時,修改服務(wù)器參數(shù)值的處理變的非常復(fù)雜,需要分辨是不同的服務(wù)器,不同的變量。當(dāng)需要從不同的網(wǎng)段操作數(shù)據(jù)時,我們甚至需要標(biāo)記不同的網(wǎng)段。
其次,作為一個特定的客戶端,如果我們操作的服務(wù)器參數(shù)相似時,哪怕來自于不同的網(wǎng)段,我們也需要仔細(xì)分辨或者傳遞額外的參數(shù)。因?yàn)橥豢蛻舳说慕馕龊瘮?shù)是同一個。
最后,將多個Modbus TCP服務(wù)器通訊都作為唯一的一個特定的服務(wù)器來處理,使得各部分混雜在一起,程序結(jié)構(gòu)很不清晰,對象也不明確。
2 、更新設(shè)計(jì)
考慮到前述的局限性,我們將Modbus TCP客戶端及其所訪問的Modbus TCP服務(wù)器定義為通用的對象,而當(dāng)我們在具體應(yīng)用中使用時,再將其特例化為特定的客戶端和服務(wù)器對象。
首先我們來考慮客戶端,原則上我們規(guī)劃的每一個客戶端對象管理我們設(shè)備上的一個IP網(wǎng)段的設(shè)備。那么在一個特定客戶端下,我們可以定義多達(dá)253個不同的服務(wù)器。如下圖所示:
從上圖中我們可以發(fā)現(xiàn),我們的目的就是讓協(xié)議棧支持,多客戶端和多服務(wù)器,并且在不同客戶端下可以訪問同網(wǎng)段的多個服務(wù)器。接下來我們還需要考慮服務(wù)器對象??蛻舳藢Ψ?wù)器的操作無非兩類:讀服務(wù)器信息和寫服務(wù)器信息。
對于讀服務(wù)器信息來說,客戶端需要發(fā)送請求命令,等待服務(wù)器返回響應(yīng)信息,然后客戶端解析收到的信息并更新對應(yīng)的參數(shù)值。因?yàn)榉祷氐捻憫?yīng)消息是沒有對應(yīng)的寄存器地址的,所以要想在解析的時候定位寄存器就必須知道發(fā)送的命令,為了便于分辨我們將命令存放在服務(wù)器對象中。
而對于寫服務(wù)器操作,無論寫的要求來自于哪里,對于協(xié)議棧來說肯定是其它的數(shù)據(jù)處理進(jìn)程發(fā)過來的,所接到要求后我們需要記錄是哪一個客戶端管理的哪一個服務(wù)器的哪些參數(shù)。對于客戶端我們不需要分辨,因?yàn)槊總€客戶端都是獨(dú)立的處理進(jìn)程,但是對于服務(wù)器和參數(shù)我們就需要分辨。每一個客戶端所管理的IP地址的最后一段為0到255,所以我們可以依據(jù)來分辨服務(wù)器端。而在每一個服務(wù)器節(jié)點(diǎn)中增加狀態(tài)標(biāo)志,用以記錄請求狀態(tài),而所有服務(wù)器端組成鏈表。
3 、編碼實(shí)現(xiàn)
我們已經(jīng)設(shè)計(jì)了我們的更新,接下來我們就根據(jù)這一設(shè)計(jì)來實(shí)現(xiàn)它。我們主要從以下幾個方面來操作:第一,實(shí)現(xiàn)客戶端對象類型和服務(wù)器對象類型;第二,客戶端對象的實(shí)例化及服務(wù)器對象的實(shí)例化;第三,讀服務(wù)器參數(shù)的客戶端操作過程;第四,寫服務(wù)器參的數(shù)客戶端操作過程。接下來我們將一一描述之。
3.1 、定義對象類型
與在Modbus RTU和Modbus ASCII一樣,在Modbus TCP協(xié)議棧的封裝中,我們也需要定義客戶端對象和服務(wù)器對象,自然也免不了要定義這兩種類型。
首先我們來定義本地客戶端的類型,其成員包括:一個uint32_t的寫服務(wù)器標(biāo)志數(shù)組;服務(wù)器數(shù)量字段;服務(wù)器順序字段;本客戶端所管理的服務(wù)器列表;4個數(shù)據(jù)更新函數(shù)指針。具體定義如下:
1 /* 定義本地TCP客戶端對象類型 */
2 typedef struct LocalTCPClientType{
3 uint32_t transaction; //事務(wù)標(biāo)識符
4 uint16_t cmdNumber; //讀服務(wù)器命令的數(shù)量
5 uint16_t cmdOrder; //當(dāng)前從站在從站列表中的位置
6 uint8_t (*pReadCommand)[12]; //讀命令列表
7 ServerListHeadNode ServerHeadNode; //Server對象鏈表的頭節(jié)點(diǎn)
8 UpdateCoilStatusType pUpdateCoilStatus; //更新線圈量函數(shù)
9 UpdateInputStatusType pUpdateInputStatus; //更新輸入狀態(tài)量函數(shù)
10 UpdateHoldingRegisterType pUpdateHoldingRegister; //更新保持寄存器量函數(shù)
11 UpdateInputResgisterType pUpdateInputResgister; //更新輸入寄存器量函數(shù)
12 }TCPLocalClientType;
關(guān)于客戶端對象類型,在前面的更新設(shè)計(jì)中已經(jīng)講的很清楚了,只有Server對象鏈表的頭節(jié)點(diǎn)字段需要說明一下。該字段包括兩個類容:第一,服務(wù)器鏈表的頭節(jié)點(diǎn)指針,用來記錄服務(wù)器對象列表。第二,記錄鏈表的長度,即服務(wù)器節(jié)點(diǎn)的數(shù)量。具體如下圖所示:
還需要定義服務(wù)器對象,此服務(wù)器對象只是便于客戶端而用于表示真是的服務(wù)器??蛻舳说姆?wù)器列表中就是此對象。具體結(jié)構(gòu)如下:
1 /* 定義被訪問TCP服務(wù)器對象類型 */
2 typedef struct AccessedTCPServerType{
3 union {
4 uint32_t ipNumber;
5 uint8_t ipSegment[4];
6 }ipAddress; //服務(wù)器的IP地址
7 uint32_t flagPresetServer; //寫服務(wù)器請求標(biāo)志
8 WritedCoilListHeadNode pWritedCoilHeadNode; //可寫的線圈量列表
9 WritedRegisterListHeadNode pWritedRegisterHeadNode; //可寫的保持寄存器列表
10 struct AccessedTCPServerType *pNextNode; //下一個TCP服務(wù)器節(jié)點(diǎn)
11 }TCPAccessedServerType;
關(guān)于服務(wù)器對象有三個字段需要說明一下。首先我們來看一看“讀命令列表(uint8_t (*pReadCommand)[12])”字段,它是12個字節(jié),這是由Modbus TCP消息格式?jīng)Q定的。如下:
我們看到協(xié)議標(biāo)識符為0,是因?yàn)?就表示Modbus TCP。還有可寫的線圈量列表頭節(jié)點(diǎn)和可寫的保持寄存器列表頭節(jié)點(diǎn)。這兩個字段用來表示對線圈和保持寄存器的列表即數(shù)量。
3.2 、實(shí)例化對象
我們定義了客戶端即服務(wù)器對象類型,我們在使用時就需要實(shí)例化這些對象。一般來說一個IP網(wǎng)段我們將其實(shí)例化為一個客戶端對象。
TCPLocalClientType hgraClient;
/ 初始化TCP客戶端對象 /
InitializeTCPClientObject(&hgraClient,2,hgraServer,NULL,NULL,NULL,NULL);
而一個客戶端對象會管理1到253個服務(wù)器對象,所以我們可以將多個服務(wù)器對象實(shí)例組成數(shù)組,并將其賦予客戶端管理。
TCPAccessedServerType hgraServer[]={{{192,168,0,1},0x00,0x00},{{192,168,1,1},0x00,0x00}};
所以,根據(jù)客戶端和服務(wù)器實(shí)例化的條件,我們需要先實(shí)例化服務(wù)器對象才能完整實(shí)例化客戶端對象。在客戶端的初始化中,我們這里將4的數(shù)據(jù)處理函數(shù)指針初始化為NULL,有一個默認(rèn)的處理函數(shù)會復(fù)制給它,該函數(shù)是上一版本的延續(xù),在簡單應(yīng)用時簡化操作。服務(wù)器的上一個發(fā)送的命令指針也被賦值為NULL,因?yàn)槌跏紩r還沒有命令發(fā)送。
3.3 、讀服務(wù)器操作
讀服務(wù)器操作原理上與以前的版本是一樣的。按照一定的順序給服務(wù)器發(fā)送命令再對收到的消息進(jìn)行解析。我們對客戶端及其所管理的服務(wù)器進(jìn)行了定義,將發(fā)送命令保存于服務(wù)器對象,將服務(wù)器列表保存于客戶端對象,所以我們需要對解析函數(shù)進(jìn)行修改。
1 /*解析收到的服務(wù)器相應(yīng)信息*/
2 void ParsingServerRespondMessage(TCPLocalClientType *client,uint8_t *recievedMessage)
3 {
4 /*判斷接收到的信息是否有相應(yīng)的命令*/
5 int cmdIndex=FindCommandForRecievedMessage(client,recievedMessage);
6
7 if((cmdIndex<0)) //沒有對應(yīng)的請求命令,事務(wù)號不相符
8 {
9 return;
10 }
11
12 if((recievedMessage[2]!=0x00)||(recievedMessage[3]!=0x00)) //不是Modbus TCP協(xié)議
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) //數(shù)據(jù)長度不一致
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) //請求的數(shù)據(jù)長度與返回的數(shù)據(jù)長度不一致
42 {
43 return;
44 }
45
46 if((fuctionCode>=ReadCoilStatus)&&(fuctionCode<=ReadInputRegister))
47 {
48 HandleServerRespond[fuctionCode-1](client,recievedMessage,startAddress,quantity);
49 }
50 }
解析函數(shù)的主要部分是在檢查接收到的消息是否是合法的Modbus TCP消息。檢查沒問題則調(diào)用協(xié)議站解析。而最后調(diào)用的數(shù)據(jù)處理函數(shù)則是我們需要在具體應(yīng)用中編寫。在前面客戶端初始化時,回調(diào)函數(shù)我們初始化為NULL,實(shí)際在協(xié)議占中有弱化的函數(shù)定義,需要針對具體的寄存器和變量地址實(shí)現(xiàn)操作。
3.4 、寫服務(wù)器操作
寫服務(wù)器操作則是在其它進(jìn)程請求后,我們標(biāo)識需要寫的對象再統(tǒng)一處理。對具體哪個服務(wù)器的寫標(biāo)識存于客戶端實(shí)例。而該服務(wù)器的哪些變量需要寫則記錄在服務(wù)器實(shí)例中。
所以在進(jìn)程檢測到需要寫一個服務(wù)器時則置位對應(yīng)的位,即改變flagWriteServer中的對應(yīng)位。而需要寫該服務(wù)器的哪些變量則標(biāo)記flagPresetCoil和flagPresetReg的對應(yīng)位。修改這些標(biāo)識都在其它請求更改的進(jìn)程中實(shí)現(xiàn),而具體的寫操作則在本客戶端進(jìn)程中,檢測到標(biāo)志位的變化統(tǒng)一執(zhí)行。
這部分不修改協(xié)議棧的代碼,因?yàn)楦鞣?wù)器及各變量都只與具體對象相關(guān)聯(lián),所以在具體的應(yīng)用中修改。
4 、回歸驗(yàn)證
借鑒前面Modbus ASCII和Modbus RTU的回歸測試經(jīng)驗(yàn),我們設(shè)計(jì)兩個網(wǎng)段、每網(wǎng)段包括一個客戶端及兩個服務(wù)器的網(wǎng)絡(luò)結(jié)構(gòu)。但考慮到我們只是功能性驗(yàn)證,所以我們設(shè)計(jì)相對簡單的服務(wù)器。所以我們設(shè)計(jì)的網(wǎng)絡(luò)為:協(xié)議棧建立2個客戶端,每個客戶端管理同一網(wǎng)段的2個服務(wù)器,每個服務(wù)器有8個線圈及2個保持寄存器。具體結(jié)構(gòu)如圖:
從上圖我們知道,該Modbus網(wǎng)關(guān)需要實(shí)現(xiàn)一個Modbus服務(wù)器用于和上位的通訊;需要實(shí)現(xiàn)兩個Modbus客戶端用于和下位的通訊。
在這個實(shí)驗(yàn)中,讀操作沒有什么需要說的,只需要發(fā)送命令解析返回消息即可。所以我們中點(diǎn)描述一下為了方便操作,在需要寫的連續(xù)段,我們只要找到第一個請求寫的位置后,就將后續(xù)連續(xù)可寫數(shù)據(jù)一次性寫入。
告之: 源代碼可上Github下載:https://github.com/foxclever/Modbus
-
MODBUS
+關(guān)注
關(guān)注
28文章
1750瀏覽量
76711 -
TCP
+關(guān)注
關(guān)注
8文章
1338瀏覽量
78884 -
RTU
+關(guān)注
關(guān)注
0文章
398瀏覽量
28591 -
協(xié)議棧
+關(guān)注
關(guān)注
2文章
138瀏覽量
33596
發(fā)布評論請先 登錄
相關(guān)推薦
評論