天天看點

Modbus協定棧實作Modbus RTU多主站支援

  前面我們已經詳細講解過Modbus協定棧的開發過程,并且利用協定棧封裝了Modbus RTU主站和從站,Modbus TCP伺服器與用戶端,Modbus ASCII主站與從站應用。但在使用過程中,我們發現一些使用不便和受限的地方,是以我們就想要更新一下協定棧,主要是應用站的封裝。

1、存在的局限性

  在原有的協定棧中,我們所封裝的Modbus RTU主站是一個特定的主站,即它隻是一個主站。在通常的應用中不會有什麼問題,但在有些應用場合就會顯現出它的局限性。

  首先,作為一個特定的主站,帶多個從站時,寫從站的處理變的非常複雜,需要分辨不同的從站,不同的變量。當有多個端口時,還需要分辨不同的端口。

  其次,作為一個特定的主站,帶多個從站時,讀從站的處理,即使是不同的端口也不能分辨相同站位址的從站。因為同一主站的解析函數是同一個,是以即使在不同端口也很難分辨,除非在解析前傳遞端口資訊,這其實是将多餘的資訊傳遞到協定棧。這樣做不但程式不夠明晰也缺乏一般性。

  最後,将所有的Modbus從站通訊都作為唯一的一個特定從站來處理,使得各部分混雜在一起,程式結構很不清晰,對象也不明确。

2、更新設計

  考慮到前述的局限性,我們将主站及其帶通路的從站定義為通用的對象,而當我們在具體應用中使用時,再将其特例化為特定的主站和從站對象。

  首先我們來考慮主站,原則上我們規劃的每一個主站對象對應我們裝置上的一個端口,那麼在同一端口下,也就是在一個特定主站下,我們可以定義多個位址不同的從站,但不同端口下不受影響。如下圖所示:

Modbus協定棧實作Modbus RTU多主站支援

  從上圖中我們可以發現,我們的目的就是讓協定棧支援,多主站和多從站,并且在不同主站下,從站的位址重複不受影響。接下來我們還需要考慮從站對象。主站對從站的操作無非兩類:讀從站資訊和寫從站資訊。

  對于讀從站資訊來說,主站需要發送請求指令,等待從站傳回響應資訊,然後主站解析收到的資訊并更新對應的參數值。有兩點需要我們考慮,第一傳回的響應消息是沒有對應的寄存器位址的,是以要想在解析的時候定位寄存器就必須知道發送的指令,為了便于分辨我們将指令存放在從站對象中。第二在解析響應時,如果兩條指令的響應類似是沒法分辨的,是以我們還需要記住上一條指令是什麼。也存儲于從站對象中。

  而對于寫從站操作,無論寫的要求來自于哪裡,對于協定棧來說肯定是其它的資料處理程序發過來的,所接到要求後我們需要記錄是哪一個主站管理的哪一個從站的哪些參數。對于主站我們不需要分辨,因為每個主站都是獨立的處理程序,但是對于從站和參數我們就需要分辨。每一個主站可以帶的站位址為0到255,但0和255已有定義,是以實際是1到254個。是以我們使用一個256位的變量,每位對應站号來标志其是否有需要寫的請求。記錄于主站,具體如下:

Modbus協定棧實作Modbus RTU多主站支援

  而每個從站的寫參數請求标志則存儲于各個從站對象,因為不同的從站可能有很大差別,存儲于各個從站更加靈活。

3、如何實作

  我們已經設計了我們的更新,但具體如何實作它呢?我們主要從以下幾個方面來實作它。第一,實作主站對象類型和從站對象類型。第二,主站對象的執行個體化及從站對象的執行個體化。第三,讀從站的主站操作過程。第四,寫從站的主站操作過程。接下來我們将一一描述之。

3.1、定義對象類型

  在Modbus RTU協定棧的封裝中,我們需要定義主站對象和從站對象,自然也需要定義這兩種類型。至于其功能前述已經描述過。

  首先我們來定義本地主站的類型,其成員包括:一個uint32_t的寫從站标志數組;從站數量字段;從站順序字段;本主站所管理的從站清單;4個資料更新函數指針。具體定義如下:

1 /* 定義本地RTU主站對象類型 */
 2 typedef struct LocalRTUMasterType{
 3   uint32_t flagWriteSlave[8];   //寫一個站控制标志位,最多256個站,與站位址對應。
 4   uint16_t slaveNumber;         //從站清單中從站的數量
 5   uint16_t readOrder;           //目前從站在從站清單中的位置
 6   RTUAccessedSlaveType *pSlave;         //從站清單
 7   UpdateCoilStatusType pUpdateCoilStatus;       //更新線圈量函數
 8   UpdateInputStatusType pUpdateInputStatus;     //更新輸入狀态量函數
 9   UpdateHoldingRegisterType pUpdateHoldingRegister;     //更新保持寄存器量函數
10   UpdateInputResgisterType pUpdateInputResgister;       //更新輸入寄存器量函數
11 }RTULocalMasterType;      

  關于主站對象類型,在前面的更新設計中已經講的很清楚了,隻有兩個需要說明一下。第一,從站清單是用來記錄本主站所管理的從站對象。第二,readOrder字段表示為目前通路從站在清單中的位置,而slaveNumber是從站對象的數量,即清單的長度。具體如下圖所示:

Modbus協定棧實作Modbus RTU多主站支援

  還需要定義從站對象,此從站對象隻是便于主站而用于表示真實的從站。主站的從站清單中就是此對象。具體結構如下:

1 /* 定義被通路RTU從站對象類型 */
 2 typedef struct AccessedRTUSlaveType{
 3   uint8_t stationAddress;       //站位址
 4   uint8_t cmdOrder;             //目前指令在指令清單中的位置
 5   uint16_t commandNumber;       //指令清單中指令的總數
 6   uint8_t (*pReadCommand)[8];   //讀指令清單
 7   uint8_t *pLastCommand;        //上一次發送的指令
 8   uint32_t flagPresetCoil;      //預置線圈控制标志位
 9   uint32_t flagPresetReg;       //預置寄存器控制标志位
10 }RTUAccessedSlaveType;      

  關于從站對象有兩個字段需要說一下,就是flagPresetCoil和flagPresetReg字段。這兩個字段用來表示對線圈和保持寄存器的寫請求。

3.2、執行個體化對象

  我們定義了主站即從站對象類型,我們在使用時就需要執行個體化這些對象。一般來說一個硬體端口我們将其執行個體化為一個主站對象。

  RTULocalMasterType hgraMaster;

  /*初始化RTU主站對象*/

  InitializeRTUMasterObject(&hgraMaster,2,hgraSlave,NULL,NULL,NULL,NULL);

  而一個主站對象會管理1到254個從站對象,是以我們可以将多個從站對象執行個體組成數組,并将其賦予主站管理。

  RTUAccessedSlaveType hgraSlave[]={{1,0,2,slave1ReadCommand,NULL,0x00,0x00},{2,0,2,slave2ReadCommand,NULL,0x00,0x00}};

  是以,根據主站和從站執行個體化的條件,我們需要先執行個體化從站對象才能完整執行個體化主站對象。在主站的初始化中,我們這裡将4的資料處理函數指針初始化為NULL,有一個預設的處理函數會複制給它,該函數是上一版本的延續,在簡單應用時簡化操作。從站的上一個發送的指令指針也被指派為NULL,因為初始時還沒有指令發送。

3.3、讀從站操作

  讀從站操作原理上與以前的版本是一樣的。按照一定的順序給從站發送指令再對收到的消息進行解析。我們對主站及其所管理的從站進行了定義,将發送指令儲存于從站對象,将從站清單儲存于主站對象,是以我們需要對解析函數進行修改。

1 /*解析收到的伺服器相應資訊*/
 2 /*uint8_t *recievedMessage,接收到的消息清單*/
 3 /*uint8_t *command,發送的讀操作指令,若為NULL則在指令清單中查找*/
 4 void ParsingSlaveRespondMessage(RTULocalMasterType *master,uint8_t *recievedMessage,uint8_t *command)
 5 {
 6   int i=0;
 7   int j=0;
 8   uint16_t startAddress;
 9   uint16_t quantity;
10   uint8_t *cmd=NULL;
11  
12   /*如果不是讀操作的反回資訊不需要處理*/
13   if(recievedMessage[1]>0x04)
14   {
15     return;
16   }
17  
18   /*判斷功能碼是否有誤*/
19   FunctionCode fuctionCode=(FunctionCode)recievedMessage[1];
20   if (CheckFunctionCode(fuctionCode) != MB_OK)
21   {
22     return;
23   }
24  
25   /*校驗接收到的資訊是否有錯*/
26   uint16_t byteCount=recievedMessage[2];
27   bool chechMessageNoError=CheckRTUMessageIntegrity(recievedMessage,byteCount+5);
28   if(!chechMessageNoError)
29   {
30     return;
31   }
32  
33   if((command==NULL)||(!CheckMessageAgreeWithCommand(recievedMessage,command)))
34   {
35     while(i<master->slaveNumber)
36     {
37       if(master->pSlave[i].stationAddress==recievedMessage[0])
38       {
39         break;
40       }
41       i++;
42     }
43    
44     if(i>=master->slaveNumber)
45     {
46       return;
47     }
48    
49     if((master->pSlave[i].pLastCommand==NULL)||(!CheckMessageAgreeWithCommand(recievedMessage,master->pSlave[i].pLastCommand)))
50     {
51       j=FindCommandForRecievedMessage(recievedMessage,master->pSlave[i].pReadCommand,master->pSlave[i].commandNumber);
52      
53       if(j<0)
54       {
55         return;
56       }
57      
58       cmd=master->pSlave[i].pReadCommand[j];
59     }
60     else
61     {
62       cmd=master->pSlave[i].pLastCommand;
63     }
64   }
65   else
66   {
67     cmd=command;
68   }
69  
70   startAddress=(uint16_t)cmd[2];
71   startAddress=(startAddress<<8)+(uint16_t)cmd[3];
72   quantity=(uint16_t)cmd[4];
73   quantity=(quantity<<8)+(uint16_t)cmd[5];
74  
75   if((fuctionCode>=ReadCoilStatus)&&(fuctionCode<=ReadInputRegister))
76   {
77     HandleSlaveRespond[fuctionCode-1](master,recievedMessage,startAddress,quantity);
78   }
79 }      

  解析函數的主要部分是在檢查接收到的消息是否是合法的Modbus RTU消息。檢查沒問題則調用協定站解析。而最後調用的資料處理函數則是我們需要在具體應用中編寫。在前面主站初始化時,回調函數我們初始化為NULL,實際在協定棧中有弱化的函數定義,需要針對具體的寄存器和變量位址實作操作。

3.4、寫從站操作

  寫從站操作則是在其它程序請求後,我們辨別需要寫的對象再統一處理。對具體哪個從站的寫辨別存于主站執行個體。而該從站的哪些變量需要寫則記錄在從站執行個體中。

  是以在程序檢測到需要寫一個從站時則置位對應的位,即改變flagWriteSlave中的對應位。而需要寫該站的哪些變量則标記flagPresetCoil和flagPresetReg的對應位。修改這些辨別都在其它請求更改的程序中實作,而具體的寫操作則在本主站程序中,檢測到标志位的變化統一執行。

  這部分不修改協定棧的代碼,因為各站及各變量都至于具體對象相關聯,是以在具體的應用中修改。

4、回歸驗證

  為了驗證我們前面的更新設計是符合要求的,我們設計一個難度較高的實驗系統。這一實驗系統包括Modbus網關,上位Modbus主站以及下位的Modbus從站。我們所要實作的是Modbus網關部分,其具體結構圖設計如下:

Modbus協定棧實作Modbus RTU多主站支援

  從上圖我們知道,該Modbus網關需要實作一個Modbus從站用于和上位的通訊;需要實作兩個Modbus主站用于和下位的通訊。

  在這個實驗中,讀操作沒有什麼需要說的,隻需要發送指令,解析傳回消息即可。是以我們重點描述一下寫操作,為了友善操作,在需要寫的連續段,我們隻要找到第一個請求寫的位置後,就将後續連續可寫資料一次性寫入。修改寫标志位的代碼如下:

1 /* 寫從站寄存器控制 */
  2 static void WriteSlaveRegisterControll(uint16_t startAddress,uint16_t endAddress)
  3 {
  4   if((12<=startAddress)&&(startAddress<=71)&&(12<=endAddress)&&(endAddress<=71))
  5   {
  6     ModifyWriteRTUSlaveEnableFlag(&hgraMaster,hgraMaster.pSlave[0].stationAddress,true);
  7    
  8     if((startAddress<=12)&&(13<=endAddress))
  9     {
 10       hgraMaster.pSlave[0].flagPresetReg|=0x01;
 11     }
 12     if((startAddress<=14)&&(15<=endAddress))
 13     {
 14       hgraMaster.pSlave[0].flagPresetReg|=0x02;
 15     }
 16     if((startAddress<=16)&&(17<=endAddress))
 17     {
 18       hgraMaster.pSlave[0].flagPresetReg|=0x04;
 19     }
 20     if((startAddress<=18)&&(19<=endAddress))
 21     {
 22       hgraMaster.pSlave[0].flagPresetReg|=0x08;
 23     }
 24     if((startAddress<=20)&&(21<=endAddress))
 25     {
 26       hgraMaster.pSlave[0].flagPresetReg|=0x10;
 27     }
 28     if((startAddress<=22)&&(23<=endAddress))
 29     {
 30       hgraMaster.pSlave[0].flagPresetReg|=0x20;
 31     }
 32     if((startAddress<=24)&&(25<=endAddress))
 33     {
 34       hgraMaster.pSlave[0].flagPresetReg|=0x40;
 35     }
 36     if((startAddress<=26)&&(27<=endAddress))
 37     {
 38       hgraMaster.pSlave[0].flagPresetReg|=0x80;
 39     }
 40    
 41     if((startAddress<=32)&&(32<=endAddress))
 42     {
 43       hgraMaster.pSlave[0].flagPresetReg|=0x100;
 44     }
 45     if((startAddress<=33)&&(33<=endAddress))
 46     {
 47       hgraMaster.pSlave[0].flagPresetReg|=0x200;
 48     }
 49     if((startAddress<=34)&&(34<=endAddress))
 50     {
 51       hgraMaster.pSlave[0].flagPresetReg|=0x400;
 52     }
 53     if((startAddress<=35)&&(35<=endAddress))
 54     {
 55       hgraMaster.pSlave[0].flagPresetReg|=0x800;
 56     }
 57     if((startAddress<=36)&&(36<=endAddress))
 58     {
 59       hgraMaster.pSlave[0].flagPresetReg|=0x1000;
 60     }
 61     if((startAddress<=37)&&(37<=endAddress))
 62     {
 63       hgraMaster.pSlave[0].flagPresetReg|=0x2000;
 64     }
 65     if((startAddress<=38)&&(38<=endAddress))
 66     {
 67       hgraMaster.pSlave[0].flagPresetReg|=0x4000;
 68     }
 69     if((startAddress<=39)&&(39<=endAddress))
 70     {
 71       hgraMaster.pSlave[0].flagPresetReg|=0x8000;
 72     }
 73     if((startAddress<=40)&&(40<=endAddress))
 74     {
 75       hgraMaster.pSlave[0].flagPresetReg|=0x10000;
 76     }
 77     if((startAddress<=41)&&(41<=endAddress))
 78     {
 79       hgraMaster.pSlave[0].flagPresetReg|=0x20000;
 80     }
 81     if((startAddress<=42)&&(42<=endAddress))
 82     {
 83       hgraMaster.pSlave[0].flagPresetReg|=0x40000;
 84     }
 85     if((startAddress<=43)&&(43<=endAddress))
 86     {
 87       hgraMaster.pSlave[0].flagPresetReg|=0x80000;
 88     }
 89     if((startAddress<=44)&&(44<=endAddress))
 90     {
 91       hgraMaster.pSlave[0].flagPresetReg|=0x100000;
 92     }
 93     if((startAddress<=45)&&(45<=endAddress))
 94     {
 95       hgraMaster.pSlave[0].flagPresetReg|=0x200000;
 96     }
 97     if((startAddress<=46)&&(46<=endAddress))
 98     {
 99       hgraMaster.pSlave[0].flagPresetReg|=0x400000;
100     }
101     if((startAddress<=47)&&(47<=endAddress))
102     {
103       hgraMaster.pSlave[0].flagPresetReg|=0x800000;
104     }
105    
106     if((startAddress<=52)&&(55<=endAddress))
107     {
108       hgraMaster.pSlave[0].flagPresetReg|=0x1000000;
109     }
110     if((startAddress<=56)&&(59<=endAddress))
111     {
112       hgraMaster.pSlave[0].flagPresetReg|=0x2000000;
113     }
114     if((startAddress<=60)&&(63<=endAddress))
115     {
116       hgraMaster.pSlave[0].flagPresetReg|=0x4000000;
117     }
118     if((startAddress<=64)&&(67<=endAddress))
119     {
120       hgraMaster.pSlave[0].flagPresetReg|=0x8000000;
121     }
122   }
123  
124   if((72<=startAddress)&&(startAddress<=131)&&(72<=endAddress)&&(endAddress<=131))
125   {
126     ModifyWriteRTUSlaveEnableFlag(&hgraMaster,hgraMaster.pSlave[1].stationAddress,true);
127    
128     if((startAddress<=72)&&(73<=endAddress))
129     {
130       hgraMaster.pSlave[1].flagPresetReg|=0x01;
131     }
132     if((startAddress<=74)&&(75<=endAddress))
133     {
134       hgraMaster.pSlave[1].flagPresetReg|=0x02;
135     }
136     if((startAddress<=76)&&(77<=endAddress))
137     {
138       hgraMaster.pSlave[1].flagPresetReg|=0x04;
139     }
140     if((startAddress<=78)&&(79<=endAddress))
141     {
142       hgraMaster.pSlave[1].flagPresetReg|=0x08;
143     }
144     if((startAddress<=80)&&(81<=endAddress))
145     {
146       hgraMaster.pSlave[1].flagPresetReg|=0x10;
147     }
148     if((startAddress<=82)&&(83<=endAddress))
149     {
150       hgraMaster.pSlave[1].flagPresetReg|=0x20;
151     }
152     if((startAddress<=84)&&(85<=endAddress))
153     {
154       hgraMaster.pSlave[1].flagPresetReg|=0x40;
155     }
156     if((startAddress<=86)&&(87<=endAddress))
157     {
158       hgraMaster.pSlave[1].flagPresetReg|=0x80;
159     }
160    
161     if((startAddress<=92)&&(92<=endAddress))
162     {
163       hgraMaster.pSlave[1].flagPresetReg|=0x100;
164     }
165     if((startAddress<=93)&&(93<=endAddress))
166     {
167       hgraMaster.pSlave[1].flagPresetReg|=0x200;
168     }
169     if((startAddress<=94)&&(94<=endAddress))
170     {
171       hgraMaster.pSlave[1].flagPresetReg|=0x400;
172     }
173     if((startAddress<=95)&&(95<=endAddress))
174     {
175       hgraMaster.pSlave[1].flagPresetReg|=0x800;
176     }
177     if((startAddress<=96)&&(96<=endAddress))
178     {
179       hgraMaster.pSlave[1].flagPresetReg|=0x1000;
180     }
181     if((startAddress<=97)&&(97<=endAddress))
182     {
183       hgraMaster.pSlave[1].flagPresetReg|=0x2000;
184     }
185     if((startAddress<=98)&&(98<=endAddress))
186     {
187       hgraMaster.pSlave[1].flagPresetReg|=0x4000;
188     }
189     if((startAddress<=99)&&(99<=endAddress))
190     {
191       hgraMaster.pSlave[1].flagPresetReg|=0x8000;
192     }
193     if((startAddress<=100)&&(100<=endAddress))
194     {
195       hgraMaster.pSlave[1].flagPresetReg|=0x10000;
196     }
197     if((startAddress<=101)&&(101<=endAddress))
198     {
199       hgraMaster.pSlave[1].flagPresetReg|=0x20000;
200     }
201     if((startAddress<=102)&&(102<=endAddress))
202     {
203       hgraMaster.pSlave[1].flagPresetReg|=0x40000;
204     }
205     if((startAddress<=103)&&(103<=endAddress))
206     {
207       hgraMaster.pSlave[1].flagPresetReg|=0x80000;
208     }
209     if((startAddress<=104)&&(104<=endAddress))
210     {
211       hgraMaster.pSlave[1].flagPresetReg|=0x100000;
212     }
213     if((startAddress<=105)&&(105<=endAddress))
214     {
215       hgraMaster.pSlave[1].flagPresetReg|=0x200000;
216     }
217     if((startAddress<=106)&&(106<=endAddress))
218     {
219       hgraMaster.pSlave[1].flagPresetReg|=0x400000;
220     }
221     if((startAddress<=107)&&(107<=endAddress))
222     {
223       hgraMaster.pSlave[1].flagPresetReg|=0x800000;
224     }
225    
226     if((startAddress<=112)&&(115<=endAddress))
227     {
228       hgraMaster.pSlave[1].flagPresetReg|=0x1000000;
229     }
230     if((startAddress<=116)&&(119<=endAddress))
231     {
232       hgraMaster.pSlave[1].flagPresetReg|=0x2000000;
233     }
234     if((startAddress<=120)&&(123<=endAddress))
235     {
236       hgraMaster.pSlave[1].flagPresetReg|=0x4000000;
237     }
238     if((startAddress<=124)&&(127<=endAddress))
239     {
240       hgraMaster.pSlave[1].flagPresetReg|=0x8000000;
241     }
242   }
243  
244   if((132<=startAddress)&&(startAddress<=191)&&(131<=endAddress)&&(endAddress<=191))
245   {
246     ModifyWriteRTUSlaveEnableFlag(&hgpjMaster,hgpjMaster.pSlave[0].stationAddress,true);
247   }
248  
249   if((192<=startAddress)&&(startAddress<=251)&&(192<=endAddress)&&(endAddress<=251))
250   {
251     ModifyWriteRTUSlaveEnableFlag(&hgpjMaster,hgpjMaster.pSlave[1].stationAddress,true);
252   }
253 }      

  然後在主站對象的程序中檢測标志位,根據标志位的狀态來實作操作,具體的操作代碼很簡單,且不具普遍性,在此不貼出。

5、幾點注意

  雖然我們對主站對象和從站對象進行了封裝,但我們在使用時人需要注意一些問題。

  (1)、4個回調函數的定義,這4個回調函數用于處理從粘傳回的資訊,對應Modbus定義的四種資料,需要根據主站對象管理的從站情況來實作。

  (2)、對于寫操作辨別符,一般都是在請求程序置位,在主站對象所在的程序檢測并操作,然後複位。

告之:源代碼可上Github下載下傳:https://github.com/foxclever/Modbus

歡迎關注:

繼續閱讀