自從開源了我們自己開發(fā)的Modbus協(xié)議棧之后,有很多朋友建議我針對性的做幾個示例。所以我們就基于平時我們的應(yīng)用整理了幾個簡單但可以說明基本的應(yīng)用方法的示例,這一篇中我們將解說如何使用協(xié)議棧實現(xiàn)一個Modbus TCP客戶端。
1 、何為TCP客戶端
Modbus協(xié)議是一個主從協(xié)議,那肯定就有主站和從站之分,在Modbus TCP中亦稱之為客戶端與服務(wù)器。所謂TCP客戶端其功能基本與RTU主站一樣,RTU主站會向從站發(fā)起數(shù)據(jù)請求,同樣的TCP客戶端也會向服務(wù)器發(fā)起請求。也就是說在Modbus TCP模式下客戶端亦是發(fā)起通訊的一方。
對于TCP客戶端來說,自己并不會產(chǎn)生數(shù)據(jù),它的數(shù)據(jù)均是從服務(wù)器獲取,為了得到數(shù)據(jù)就必須向服務(wù)器發(fā)起數(shù)據(jù)請求。在Modbus TCP協(xié)議中,服務(wù)器一般也不會主動向外發(fā)送數(shù)據(jù),服務(wù)器需要根據(jù)客戶端的數(shù)據(jù)請求來決定是否發(fā)送數(shù)據(jù)、發(fā)送哪些數(shù)據(jù)。這一過程如下圖所示:
從上圖我們不難看出,首先客戶端要主動發(fā)起數(shù)據(jù)請求,客戶端發(fā)起的數(shù)據(jù)請求需要告訴服務(wù)器它請求的數(shù)據(jù)有哪些。服務(wù)器收到這個數(shù)據(jù)請求后,服務(wù)器解析客戶端的請求并按照客戶端的請求返回數(shù)據(jù)??蛻舳耸盏綌?shù)據(jù)響應(yīng)后解析數(shù)據(jù),這樣就完成了客戶端與服務(wù)器之間的一次數(shù)據(jù)通訊。
需要注意的是,Modbus TCP與Modbus RTU不同的是有一個專用的MBAP報文頭來識別Modbus應(yīng)用數(shù)據(jù)單元。這一報文頭由7個字節(jié)組成:
這種MBAP報文頭雖然也是用來識別Modbus數(shù)據(jù)域,但還是與串行鏈路上使用的MODBUS RTU應(yīng)用數(shù)據(jù)單元有一些差別,具體如下:
( 1 ) 、用MBAP報文頭中的單個字節(jié)單元標(biāo)識符取代MODBUS串行鏈路上通常使用的MODBUS從地址域。這個單元標(biāo)識符用于設(shè)備的通信,這些設(shè)備使用單個 IP 地址支持多個獨立MODBUS 終端單元,例如:網(wǎng)橋、路由器和網(wǎng)關(guān)。
( 2 ) 、使用接收者可以驗證的方式來構(gòu)造所有MODBUS請求和響應(yīng)。對于MODBUS PDU有固定長度的功能碼來說,僅功能碼就足夠了。對于在請求或響應(yīng)中攜帶一個可變數(shù)據(jù)的功能碼來說,數(shù)據(jù)域包括字節(jié)數(shù)。
( 3 ) 、使用TCP上傳送MODBUS數(shù)據(jù)域時,即使將報文分成多個信息包來傳輸,可在MBAP報文頭上攜帶附加長度信息,這樣接收者就能夠識別報文的完整性。
2 、如何實現(xiàn)TCP客戶端
我們已經(jīng)簡單的描述了基于TCP/IP的Modbus數(shù)據(jù)通訊,在此基礎(chǔ)上我們將進(jìn)一步描述基于協(xié)議棧的Modbus TCP客戶端的實現(xiàn)。
在協(xié)議棧中,我們已經(jīng)實現(xiàn)了TCP客戶端的數(shù)據(jù)請求命令的合成以及響應(yīng)數(shù)據(jù)的解析,所以我們使用協(xié)議棧時就是要控制何時將協(xié)議棧合成的客戶端請求命令發(fā)出以及如何解析數(shù)據(jù)響應(yīng)進(jìn)而得到想要的數(shù)據(jù)的過程。
在我們的協(xié)議棧中實現(xiàn)了0x01、0x02、0x03、0x04、0x05、0x06、0x0F以及0x10等功能碼。也就是說TCP客戶端對象可以生成面向這些功能碼的服務(wù)器數(shù)據(jù)請求。也可以解析面向這些功能碼的服務(wù)器數(shù)據(jù)響應(yīng)??梢员硎緸橄聢D所示:
從上圖我們很清楚,協(xié)議棧已經(jīng)實現(xiàn)了面向這些功能碼的數(shù)據(jù)請求命令的生成以及數(shù)據(jù)響應(yīng)消息的解析。我們使用協(xié)議棧時需要做的就是要告訴協(xié)議棧我要生成哪些數(shù)據(jù)請求命令以及如何解析數(shù)據(jù)響應(yīng)消息。
2.1 、生成數(shù)據(jù)請求
作為客戶端需要主動向服務(wù)器發(fā)起操作,所以我們要控制客戶端生成可以被服務(wù)器識別的命令序列。在協(xié)議棧中已經(jīng)封裝了生成客戶端訪問服務(wù)器的命令序列的函數(shù),其原型如下:
/ 生成訪問服務(wù)器的命令 /
uint16_t CreateAccessServerCommand(TCPLocalClientType*client,ObjAccessInfo objInfo,void *dataList,uint8_t *commandBytes)
這個函數(shù)有4個參數(shù),分別是:
TCPLocalClientType *client,所發(fā)起訪問的本地客戶端對象。
ObjAccessInfo objInfo,用于生成訪問命令的信息,如站地址、功能碼等。
void *dataList,如果是寫操作,則對應(yīng)需要寫的數(shù)據(jù)列表,線圈為bool量、寄存器為uint16_t型無符號整數(shù)。
uint8_t *commandBytes是生成的命令序列
而返回值則是生成的命令序列的長度。在我們需要生成訪問服務(wù)器的命令時,調(diào)用這個函數(shù)就可實現(xiàn)。不過一定要注意生成的命令序列的長度,定義commandBytes對象時長度一定要足夠。
2.2 、解析數(shù)據(jù)響應(yīng)
當(dāng)客戶端收到服務(wù)器返回的響應(yīng)信息后,客戶端需要對消息進(jìn)行解析,并決定需要進(jìn)行的操作。在協(xié)議棧中封裝了對服務(wù)器響應(yīng)消息的解析函數(shù),該函數(shù)的原型如下:
/ 解析收到的服務(wù)器相應(yīng)信息 /
void ParsingServerRespondMessage(TCPLocalClientType client,uint8_trecievedMessage)
這一解析函數(shù)包含2個參數(shù),TCPLocalClientType client是本地客戶端對象;而uint8_trecievedMessage為接收到的服務(wù)器響應(yīng)消息。本函數(shù)會注意核對任務(wù)號、協(xié)議代碼、功能碼、數(shù)據(jù)完整性等,檢驗正確的消息會被解析,并根據(jù)消息來操作相應(yīng)的數(shù)據(jù)對象,比如讀的是服務(wù)器的保持寄存器,則根據(jù)讀的起始地址和數(shù)量以及數(shù)據(jù)對象的類型來解析之。
3 、 TCP****客戶端編碼
我們講述了客戶端所要進(jìn)行的工作以及協(xié)議棧中封裝好的面向客戶端的操作函數(shù),接下來我們將基于協(xié)議棧來實現(xiàn)一個簡單的Modbus TCP客戶端實例。
3.1 、定義TCP客戶端對象
在開始實現(xiàn)客戶端的相關(guān)操作前,我們需要先聲明并實例化部分用于Modbus TCP客戶端操作的對象。
首先需要定義用于本地操作的本地客戶端,也就是我們要實現(xiàn)的客戶端對象。具體的聲明如下:TCPLocalClientType mbClient;
其次需要聲明一個或者多個服務(wù)器對象,這些服務(wù)器對象是我們所要實現(xiàn)的客戶端所管理的服務(wù)器對象。具體的聲明如下:TCPAccessedServerType mbServer;
同時需要定義一個用于存放讀操作命令的數(shù)組,定義一個寫服務(wù)器操作的線圈量對象數(shù)組和一個寄存器量對象數(shù)組,具體如下:
uint8_t readCommand[10][12];
WritedCoilListNode coilList[3]={{0,0,0,1},{1,0,0,1},{2,0,0,0}};
WritedRegisterListNoderegisterList[3]={{0,0,0,1,1},{1,0,0,1,2},{2,0,0,0,0}};
接下來還需要對客戶端對象和服務(wù)器對象進(jìn)行初始化和實例化。而上述的數(shù)組將作為參數(shù)用于客戶端對象的初始化和服務(wù)器對象的實例化。
此外還要定義4個函數(shù)指針用于對從服務(wù)器讀取回來的數(shù)據(jù)進(jìn)行更新,這幾個函數(shù)的原型如下:
/*更新讀回來的線圈狀態(tài)*/
typedef void (*UpdateCoilStatusType)(uint8_t salveAddress,uint16_tstartAddress,uint16_t quantity,bool *stateValue);
/*更新讀回來的輸入狀態(tài)值*/
typedef void (*UpdateInputStatusType)(uint8_t salveAddress,uint16_tstartAddress,uint16_t quantity,bool *stateValue);
/*更新讀回來的保持寄存器*/
typedef void (*UpdateHoldingRegisterType)(uint8_t salveAddress,uint16_tstartAddress,uint16_t quantity,uint16_t *registerValue);
/*更新讀回來的輸入寄存器*/
typedef void (*UpdateInputResgisterType)(uint8_t salveAddress,uint16_tstartAddress,uint16_t quantity,uint16_t *registerValue);
這幾個函數(shù)的實現(xiàn)要根據(jù)具體的參數(shù)來實現(xiàn)。定義好這些對象后,我們就可以對客戶端進(jìn)行初始化和對服務(wù)器進(jìn)行實例化。
/*初始化TCP客戶端對象*/
InitializeTCPClientObject(&mbClient, NULL, NULL, UpdateHoldingRegister, NULL);
/* 實例化TCP服務(wù)器對象 */
InstantiateTCPServerObject(&mbServer, //要實例化的服務(wù)器對象
&mbClient, //服務(wù)器所屬本地客戶端對象
192, //IP地址第1段
168, //IP地址第2段
183, //IP地址第3段
130, //IP地址第4段
502,
1,
rCmd,
0, //可寫線圈量節(jié)點的數(shù)量
NULL, //寫線圈列表
0, //可寫寄存器量節(jié)點的數(shù)量
NULL); //寫寄存器列表
在這一示例中,我們只定義了一個服務(wù)器所以只需要實例化一個就可以了,實例化函數(shù)會自動將服務(wù)器添加到客戶端管理的服務(wù)器列表中。
3.2 、生成客戶端數(shù)據(jù)請求
作為客戶端需要首先發(fā)起請求。在前一節(jié)我們已經(jīng)講述了生成客戶端請求的函數(shù)。我們只需要調(diào)用該函數(shù)就可以了,但該函數(shù)需要一些參數(shù),我們先來看看這些參數(shù)是否準(zhǔn)備就緒。
第一個參數(shù)是客戶端對象,在前面的描述中我們已經(jīng)生命并初始化完成了這一對象所以直接使用就好。
第二個參數(shù)是要生成請求的信息,其定義為一個結(jié)構(gòu)體變量。
/*定義用于傳遞要訪問從站(服務(wù)器)的信息*/
typedef struct{
uint8_t unitID;
FunctionCode functionCode;
uint16_t startingAddress;
uint16_t quantity;
}ObjAccessInfo;
所以我們需要定義一個該類型變量,并根據(jù)我們的操作要求給其賦值。我們假設(shè)要實現(xiàn)對站地址為1的服務(wù)器對象的保持寄存器從0-9共10個寄存器地址的讀取則可實現(xiàn)為:
ObjAccessInfo tObj={1,ReadHoldingRegister,0x00,10};
第三個參數(shù)為數(shù)據(jù)列表,讀服務(wù)器時無數(shù)據(jù)列表以NULL輸入。第四個參數(shù)則為生成的讀服務(wù)器數(shù)據(jù)的請求命令,我們按要求定義即可使用,于是我們就可以調(diào)用該函數(shù)生成相應(yīng)的命令了。
uint16_t sendLengh;
uint8_t sendCommand[12];
sendLengh=CreateAccessServerCommand(&tClient,tObj,NULL,sendCommand);
這一例子中我們是讀取服務(wù)器保持寄存器的數(shù)據(jù),如果我們寫服務(wù)器對應(yīng)數(shù)據(jù),這只要將dataList組織好,作為參數(shù)傳入就好,不過要注意返回的命令的長度。生成訪問服務(wù)器的命令后,作為客戶端主動發(fā)送相應(yīng)的命令后等待服務(wù)器響應(yīng)。
3.3 、解析服務(wù)器數(shù)據(jù)響應(yīng)
客戶端接收到服務(wù)器的返回信號后,就會調(diào)用解析函數(shù)對消息進(jìn)行解析并根據(jù)具體的消息對數(shù)據(jù)對象進(jìn)行更新。解析函數(shù)非常簡單僅有兩個參數(shù),一個是本地客戶端對象,一個是接收到的響應(yīng)消息。
ParsingServerRespondMessage(&tClient,recievedMessage);
解析函數(shù)會根據(jù)消息內(nèi)容執(zhí)行相應(yīng)的操作。如在這個實例中,我們讀取了服務(wù)器的保持寄存器起始地址為0的10個寄存器,所以解析函數(shù)會調(diào)用保持寄存器數(shù)據(jù)處理函數(shù)來更新數(shù)據(jù),最終其實就是以回調(diào)的方式執(zhí)行。在這里我們將需要實現(xiàn)更新讀回來的保持寄存器的參數(shù)的函數(shù)。
/*更新讀回來的保持寄存器*/
void UpdateHoldingRegisterForClient(uint8_tserverAddress,uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)
{
uint16_tstartRegister=HoldingResterEndAddress+1;
if(serverAddress==130)
{
startRegister=startAddress;
}
for(inti=0;i
至此我們實際已經(jīng)實現(xiàn)了一個簡單的Modbus TCP客戶端。不過我們還要說明一下,使用不同的協(xié)議棧時,解析函數(shù)中的recievedMessage參數(shù)的具體的形式需要調(diào)整。
4 、 TCP****客戶端小結(jié)
我們使用協(xié)議棧完成了一個簡單的Modbus TCP客戶端應(yīng)用實例。同樣的我們可以使用相關(guān)的Modbus測試軟件測試這一示例。我們使用Modbus Slave模擬Modbus TCP服務(wù)器應(yīng)用,然后使用我們實現(xiàn)的客戶端與之通訊,以驗證我們的客戶端。
客戶端接收到的服務(wù)器反饋如下圖:
上圖說明我們基于協(xié)議棧實現(xiàn)的簡單Modbus TCP客戶端是正確的。在使用協(xié)議棧實現(xiàn)Modbus TCP客戶端時,我們需要注意,協(xié)議棧封裝了Modbus TCP客戶端,使得在同一臺設(shè)備上支持在不同的接口實現(xiàn)不同的客戶端,也就是在同一設(shè)備可以實現(xiàn)多個客戶端以管理不同的服務(wù)器。具體的實現(xiàn)可以根據(jù)協(xié)議棧進(jìn)一步發(fā)揮。
最后我們來總結(jié)一下使用協(xié)議棧實現(xiàn)Modbus TCP客戶端應(yīng)用的步驟,以方便大家使用協(xié)議棧實現(xiàn)Modbus TCP客戶端應(yīng)用。
第一步,使用Modbus TCP客戶端對象類型聲明一個Modbus TCP客戶端對象。然后對這個Modbus TCP客戶端對象進(jìn)行初始化。初始化Modbus TCP客戶端對象時。需要指定所管理的服務(wù)器的數(shù)量,服務(wù)器列表以及更新數(shù)據(jù)的回調(diào)函數(shù)指針。
第二步,生成訪問Modbus TCP客戶端的數(shù)據(jù)請求列表。這個數(shù)據(jù)請求列表是按每一臺服務(wù)器來劃分的,將列表的指針存在對應(yīng)的服務(wù)器本地對象中。然后在需要的時候發(fā)送相應(yīng)的數(shù)據(jù)請求。
第三步,解析接收的服務(wù)器數(shù)據(jù)響應(yīng)。協(xié)議棧已經(jīng)定義好了解析函數(shù),只需傳入消息就可自動解析。但是更新數(shù)據(jù)的回調(diào)函數(shù)必須根據(jù)具體的變量來編寫??梢悦颗_Modbus TCP客戶端獨立編寫,也可使用默認(rèn)的函數(shù)。
-
MODBUS
+關(guān)注
關(guān)注
28文章
1821瀏覽量
77334 -
TCP
+關(guān)注
關(guān)注
8文章
1378瀏覽量
79326 -
客戶端
+關(guān)注
關(guān)注
1文章
291瀏覽量
16771 -
協(xié)議棧
+關(guān)注
關(guān)注
2文章
144瀏覽量
33707
發(fā)布評論請先 登錄
相關(guān)推薦
評論