天天看點

Modbus協定棧應用執行個體之二:Modbus RTU從站應用

  自從開源了我們自己開發的Modbus協定棧之後,有很多朋友建議我針對性的做幾個示例。是以我們就基于平時我們的應用整理了幾個簡單但可以說明基本的應用方法的示例,這一篇中我們将使用協定棧實作一個Modbus RTU從站應用。

1、何為RTU從站

  Modbus協定是一個主從協定,那肯定就有主站和從站之分。所謂從站就是被動動響應通訊的對象,是以從站總是響應通訊的一方。

  對于RTU從站來說,它是資料的資料的生産者,從站通過響應主站資料請求的方式将資料發送給主站。這一過程如下圖所示:

Modbus協定棧應用執行個體之二:Modbus RTU從站應用

  從上圖我們不難看出,首先主站要主動發起資料請求,這也是它為什麼被稱之為主站的緣由。它首先告訴從站我需要哪些資料。然後從站按照主站的請求傳回資料。主站得到響應後解析資料,這樣就完成了主從站之間的一次資料通訊。是以主站就需要主動發起每一次資料通訊的對象。

2、如何實作RTU從站

  我們已經了解的從站總是響應主站的資料請求來實作資料的傳送。下面我們來看看使用協定棧如何實作一個從站。

  我們知道從站是資料的生産者,對于Modbus協定來說有四類資料:線圈、狀态、輸入寄存器和保持寄存器。是以在從站中我們要為這四種資料定義相應的位址,以便主站能夠對應的通路。是以設計一個從站我們先來設計它的資料位址,在我們的例子中我們規定如下:

Modbus協定棧應用執行個體之二:Modbus RTU從站應用

  我們規定了每類資料類型的數量為8,對于從站來說除了生成這些資料外,還需要根據主站的資料請求來傳回相應的資料響應。在我們的協定棧中實作了0x01、0x02、0x03、0x04、0x05、0x06、0x0F以及0x10等功能碼。也就是說主站對象會生成面向這些功能碼的從站資料請求。從站收到請求後,解析請求并根據請求生成響應的資料響應。可以表示為下圖所示:

Modbus協定棧應用執行個體之二:Modbus RTU從站應用

  從上圖我們明白協定棧中已經實作了對收到的主站資料請求進行解析以及根據解析生成對應的響應的函數。我們使用協定棧時,主要需要做兩個方面的事情:解析資料請求和生成資料響應。

  在協定棧中定義了一個解析函數,該函數将收到的資料請求消息解析,并根據解析的結果生成傳回的資料響應。該函數的原型如下:

  uint16_t ParsingMasterAccessCommand(uint8_t *receivedMessage, uint8_t *respondBytes, uint16_t rxLength, uint8_t StationAddress)

  這個函數有四個參數:uint8_t *receivedMessage是收到的資料請求消息; uint8_t *respondBytes是傳回的資料響應消息,也是函數需要生成的;uint16_t rxLength是接收到的資料請求消息的長度;uint8_t StationAddress本站的位址。而函數的傳回值則是生成的資料響應詳細的長度。

在解析的過程中,該函數判斷消息的完整性,并根據不同的功能碼調用不同的回調函數來實作,包括設定本地資料和擷取本地資料的相關回調函數,在後續将讨論它們的實作。

3、RTU從站編碼

  我們已經詳述了使用協定棧實作RTU從站的方法,接下來我們就來利用協定棧具體開發一個RTU從站的執行個體。

  我們調用解析函數對接收到的資料請求進行解析,具體調用方式如下所示:

  respondLength=ParsingMasterAccessCommand(hgudRxBuffer,respondBytes,hgudRxLength,StationAddress);

  傳回值會有3種情況,傳回值為0則表示接收到的資料請求消息是錯誤的。傳回值為65535則表示傳回的消息尚未接收完整。傳回的是一個合适的數值則表示解析成功,傳回了資料響應的長度。

  當然我們需要實作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 }      

  我們需要做的工作就是根據我們具體執行個體中4類資料量的位址配置設定來實作這8個回調函數。當然,如果從站沒有某一類資料量操作,回調函數則不需要編寫。在我們的執行個體中我們将這幾個函數實作如下:

1 /*擷取想要讀取的Coil量的值*/
 2 void GetCoilStatus(uint16_t startAddress,uint16_t quantity,bool *statusList)
 3 {
 4   uint16_t start;
 5   uint16_t count;
 6   /*先判斷位址是否處于合法範圍*/
 7   start=(startAddress>CoilStartAddress)?((startAddress<=CoilEndAddress)?startAddress:CoilEndAddress):CoilStartAddress;
 8   count=((start+quantity-1)<=CoilEndAddress)?quantity:(CoilEndAddress-start);
 9  
10   for(int i=0;i<count;i++)
11   {
12     statusList[i]=dPara.coil[start+i];
13   }
14 }
15  
16 /*擷取想要讀取的保持寄存器的值*/
17 void GetHoldingRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)
18 {
19   uint16_t start;
20   uint16_t count;
21   /*先判斷位址是否處于合法範圍*/
22   start=(startAddress>HoldingResterStartAddress)?((startAddress<=HoldingResterEndAddress)?startAddress:HoldingResterEndAddress):HoldingResterStartAddress;
23   count=((start+quantity-1)<=HoldingResterEndAddress)?quantity:(HoldingResterEndAddress-start);
24  
25   for(int i=0;i<count;i++)
26   {
27     registerValue[i]=aPara.holdingRegister[start+i];
28   }
29 }
30  
31 /*設定單個線圈的值*/
32 void SetSingleCoil(uint16_t coilAddress,bool coilValue)
33 {
34   /*先判斷位址是否處于合法範圍*/
35   if((4<=coilAddress)&&(coilAddress<=CoilEndAddress))
36   {
37     dPara.coil[coilAddress]=coilValue;
38   }
39  
40   PresetSlaveCoilControll(coilAddress,coilAddress);
41 }
42  
43 /*設定多個線圈的值*/
44 void SetMultipleCoil(uint16_t startAddress,uint16_t quantity,bool *statusValue)
45 {
46   uint16_t endAddress=startAddress+quantity-1;
47   if((4<=startAddress)&&(startAddress<=CoilEndAddress)&&(4<=endAddress)&&(endAddress<=CoilEndAddress))
48   {
49     for(int i=0;i<quantity;i++)
50     {
51       dPara.coil[i+startAddress]=statusValue[i];
52     }
53   }
54  
55   PresetSlaveCoilControll(startAddress,endAddress);
56 }
57  
58 /*設定單個寄存器的值*/
59 void SetSingleRegister(uint16_t registerAddress,uint16_t registerValue)
60 {
61   bool noError=(bool)(((41<=registerAddress)&&(registerAddress<=42))
62                       ||((44<=registerAddress)&&(registerAddress<=45))
63                       ||((50<=registerAddress)&&(registerAddress<=51))
64                       ||((54<=registerAddress)&&(registerAddress<=55))
65                       ||((58<=registerAddress)&&(registerAddress<=59)));
66   if(noError)
67   {
68     aPara.holdingRegister[registerAddress]=registerValue;
69   }
70  
71   WriteSlaveRegisterControll(registerAddress,registerAddress);
72 }
73  
74 /*設定多個寄存器的值*/
75 void SetMultipleRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)
76 {
77   uint16_t endAddress=startAddress+quantity-1;
78  
79   bool noError=(bool)(((8<=startAddress)&&(startAddress<=15)&&(8<=endAddress)&&(endAddress<=15))
80                       ||((41<=startAddress)&&(startAddress<=42)&&(41<=endAddress)&&(endAddress<=42))
81                       ||((44<=startAddress)&&(startAddress<=47)&&(44<=endAddress)&&(endAddress<=47))
82                       ||((50<=startAddress)&&(startAddress<=51)&&(50<=endAddress)&&(endAddress<=51))
83                       ||((54<=startAddress)&&(startAddress<=55)&&(54<=endAddress)&&(endAddress<=55))
84                       ||((58<=startAddress)&&(startAddress<=59)&&(58<=endAddress)&&(endAddress<=59))
85                       ||((62<=startAddress)&&(startAddress<=67)&&(62<=endAddress)&&(endAddress<=67))
86                       ||((72<=startAddress)&&(startAddress<=77)&&(72<=endAddress)&&(endAddress<=77))
87                       ||((82<=startAddress)&&(startAddress<=87)&&(82<=endAddress)&&(endAddress<=87))
88                       ||((92<=startAddress)&&(startAddress<=97)&&(92<=endAddress)&&(endAddress<=97))
89                       ||((100<=startAddress)&&(startAddress<=115)&&(100<=endAddress)&&(endAddress<=115)));
90   if(noError)
91   {
92     for(int i=0;i<quantity;i++)
93     {
94       aPara.holdingRegister[startAddress+i]=registerValue[i];
95     }
96   }
97 
98   WriteSlaveRegisterControll(startAddress,endAddress);
99 }      

  到這裡對從站的開發實際已經完成。對于這些回調函數并不是全部需要編寫,而是要根據我們自己定義的從站各類參數的位址配置設定來實作。

4、RTU從站小結

  我們實作了一個簡單的RTU從站執行個體,我們可以通過一些RTU主站軟體來測試它。這樣的軟體有很多,常見的如Modscan、Modbus Poll等。這裡我們使用Modbus Poll來測試一下,如下圖所示:

Modbus協定棧應用執行個體之二:Modbus RTU從站應用

  RTU從站的實作相對較簡單,因為在同一台裝置上隻需實作一個從站,哪怕是通過不同的端口來通路。這一點與主站是不一樣的,原因是從站的資料是自己産生,而且隻需被動響應主站請求,而且理論上同一條總線隻會有一個主站。

  接下來我們來總結一下使用協定棧實作RTU從站的工作流程,或者說實作的步驟。首先從站要解析從主站送來的資料請求。在協定棧中已經封裝了資料請求的解析函數、是以我們實作從站時首先就是調用這一函數來解析接收到的資料請求消息。

  然後将解析函數傳回的資料響應消息發送到主站就可以了。也就是說使用協定棧,隻需要調用一下這個函數從站功能就實作了。這是因為這個函數實作了整個從站的響應過程,大緻分三個步驟:第一步,解析收到的主站資料請求消息;第二步,根據解析的結果預置資料或者擷取資料,預置和擷取資料由8個回調函數實作;第三步,生成從站資料響應消息。說到這裡我們已經清楚,RTU從站必須實作這些回調函數,其它工作則全由協定棧完成。

  源碼下載下傳:​

歡迎關注:

如果您希望更友善且及時的閱讀相關文章,關注我的微信公衆号【木南創智】