自從開源了我們自己開發的Modbus協定棧之後,有很多朋友建議我針對性的做幾個示例。是以我們就基于平時我們的應用整理了幾個簡單但可以說明基本的應用方法的示例,在這一篇中我們先來使用協定棧實作Modbus RTU主站的示例。
1、何為RTU主站
Modbus協定是一個主從協定,那肯定就有主站和從站之分。所謂主站說的簡單一點就是能夠主動發起通訊的對象,是以主站就是發起通訊的一方。
對于RTU主站來說,自己并不會産生資料,而是要從從站擷取資料。在Modbus RTU協定中從站不會主動向外發送資料,是以需要主站發送資料請求,從站才會向其傳回請求的資料。這一過程如下圖所示:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5iM2ETO5MDM2gDNmFzMhZTNyYzXzAzNxgTMzEzLcRDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
從上圖我們不難看出,首先主站要主動發起資料請求,這也是它為什麼被稱之為主站的緣由。它首先告訴從站我需要哪些資料。然後從站按照主站的請求傳回資料。主站得到響應後解析資料,這樣就完成了主從站之間的一次資料通訊。是以主站就需要主動發起每一次資料通訊的對象。
2、如何實作RTU主站
我們已經簡單的說明了什麼是RTU的主站,那麼如何實作這一主站呢?其實在協定棧中,我們已經實作了主站的資料請求指令的合成以及響應資料的解析,是以我們使用協定棧時就是要控制何時将協定棧合成的主站請求指令發出以及如何解析資料響應進而得到想要的資料的過程。
在我們的協定棧中實作了0x01、0x02、0x03、0x04、0x05、0x06、0x0F以及0x10等功能碼。也就是說主站對象可以生成面向這些功能碼的從站資料請求。也可以解析面向這些功能碼的從站資料響應。可以表示為下圖所示:
從上圖我們很清楚,協定棧已經實作了面向這些功能碼的資料請求指令的生成以及資料響應消息的解析。我們使用協定棧時需要做的就是要告訴協定棧我要生成哪些資料請求指令以及如何解析資料響應消息。
2.1、怎麼生成資料請求
對于資料請求,我們不一定需要面向全部功能碼的請求,我們隻需要根據我們的需求合成我們想要的請求。
在協定棧中,針對資料請求的生成我們定義了一個從站通路指令生成函數。該函數的原型如下:
uint16_t CreateAccessSlaveCommand(ObjAccessInfo objInfo,void *dataList,uint8_t *commandBytes)
該函數有3個參數,其中ObjAccessInfo objInfo為對象通路資訊;void *dataList為資料清單指針,該參數主要用于寫從站功能的指令生成;uint8_t *commandBytes為傳回的從站通路指令。
ObjAccessInfo是一個結構體,向函數傳遞我們想要生成的從站通路指令的相關資訊,包括站位址,功能碼,起始位址和數量。該結構體的定義如下:
1 /*定義用于傳遞要通路從站(伺服器)的資訊*/
2 typedef struct{
3 uint8_t unitID;
4 FunctionCode functionCode;
5 uint16_t startingAddress;
6 uint16_t quantity;
7 }ObjAccessInfo;
2.2、怎麼解析資料響應
對于資料響應,我們同樣不需要考慮全部的操作碼,我們一般需要考慮讀請求的響應,因為他們的資料需要解析。而對于寫請求傳回數響應隻是告訴主站成功或者不成功,即使不成功隻需要在寫一次就可以了,不存在資料更新的問題。
在協定棧中,我們實作了主站解析從站資料響應的解析函數。使用這一函數我們隻需要将收到的資料響應封包傳遞給解析函數就可以完成解析。該函數的原型定義如下:
void ParsingSlaveRespondMessage(RTULocalMasterType *master,uint8_t *recievedMessage,uint8_t *command)
這個函數有3個參數,其中RTULocalMasterType *master為主站對象;uint8_t *recievedMessage為接收到的響應消息;uint8_t *command為發送的指令序列。将這幾個參數傳遞給解析函數就可實作資料響應的解析。
RTULocalMasterType是一個結構體,用以生命一個主站對象,這個對象就是我們要實作各種操作的主站,這一結構體的定義如下:
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;
3、RTU主站編碼
有了前面的說明,我們基于協定棧實作一個主站應用就很容易了。接下來我們就基于協定棧具體實作一個主站應用。
3.1、定義主站對象
首先我們要聲明一個主站對象,這是我們操作的基礎。在接下來的各種操作中我們都是基于這一對象來實作的。具體操作如下:
RTULocalMasterType rtuMaster;
定義了這個主站對象後,我們還需要對這一對象進行初始化。協定棧同樣提供了一個主站對象的初始化函數。函數的原型定義如下:
1 /*初始化RTU主站對象*/
2 void InitializeRTUMasterObject(RTULocalMasterType *master,uint16_t slaveNumber,
3 RTUAccessedSlaveType *pSlave,
4 UpdateCoilStatusType pUpdateCoilStatus,
5 UpdateInputStatusType pUpdateInputStatus,
6 UpdateHoldingRegisterType pUpdateHoldingRegister,
7 UpdateInputResgisterType pUpdateInputResgister
8 )
該函數的參數除了主站對象外,還有從站的數量即從站對象清單,還有四個資料更新函數指針。這幾個函數指針将應用于資料響應的解析過程中,具體在後面描述。使用這一初始化函數實作對主站對象的初始化,使其能夠實作各項操作,具體如下:
/*初始化RTU主站對象*/
InitializeRTUMasterObject(&hgraMaster,2,hgraSlave,NULL,NULL,NULL,NULL);
這裡我們将幾個資料處理函數指針變量傳入NULL,表示初始化為預設的操作函數,當然我們也可以編寫這些函數,在後續的資料解析時将會詳細說明。
3.2、生成資料請求
在前面,我們已經描述了資料請求指令的生成函數,該函數有一個ObjAccessInfo參數,這個參數用于傳遞需要生成指令的資訊。這是一個結構體,我們需要定義一個對象變量。
ObjAccessInfo hgraInfo;
然後使用這個對象來實作資料請求的生成。具體操作如下所示:
1 /* 生成1号從站通路指令 */
2 hgraInfo.unitID=hgraSlave[0].stationAddress;
3 hgraInfo.functionCode=ReadCoilStatus;
4 hgraInfo.startingAddress=0x0000;
5 hgraInfo.quantity=8;
6
7 CreateAccessSlaveCommand(hgraInfo,NULL,slave1ReadCommand[0]);
生成的資料請求什麼時候發送給完全由主程序來實作已經與協定棧沒有關系了。
3.3、解析資料響應
收到資料響應後我們需要對其進行解析。前面我們已經介紹了解析從站資料響應的函數。具體的調用形式如下:
ParsingSlaveRespondMessage(&hgraMaster,hgraRxBuffer,NULL);
我們對hgraMaster主站對象收到的從站響應hgraRxBuffer進行解析。最後傳入的NULL表示我們不指定主站發送的資料請求,而是讓主站從請求清單中去自己查找。
當然我們需要實作資料更新處理回調函數。這幾個函數是在對象初始化的時候以函數指針的形式傳遞的。原型如下:
1 /*更新讀回來的線圈狀态*/
2 __weak void UpdateCoilStatus(uint8_t salveAddress,uint16_t startAddress,uint16_t quantity,bool *stateValue)
3 {
4 //在用戶端(主站)應用中實作
5 }
6
7 /*更新讀回來的輸入狀态值*/
8 __weak void UpdateInputStatus(uint8_t salveAddress,uint16_t startAddress,uint16_t quantity,bool *stateValue)
9 {
10 //在用戶端(主站)應用中實作
11 }
12
13 /*更新讀回來的保持寄存器*/
14 __weak void UpdateHoldingRegister(uint8_t salveAddress,uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)
15 {
16 //在用戶端(主站)應用中實作
17 }
18
19 /*更新讀回來的輸入寄存器*/
20 __weak void UpdateInputResgister(uint8_t salveAddress,uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)
21 {
22 //在用戶端(主站)應用中實作
23 }
我們可根據需要重定義這些函數,當然我們沒有響應的資料可以不必實作,如我們沒有使用輸入寄存器,那麼更新輸入寄存器的回調函數則可以不用重定義。如下在我們的例子中重定義為:
1 /*更新讀回來的保持寄存器*/
2 void UpdateHoldingRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)
3 {
4 uint16_t startRegister=HoldingResterEndAddress+1;
5
6 switch(salveAddress)
7 {
8 case BPQStationAddress: //更新讀取的變頻器參數
9 {
10 startRegister=36;
11 break;
12 }
13 case PUMPStationAddress: //更新蠕動泵
14 {
15 // aPara.phyPara.pumpRotateSpeed=registerValue[1];
16 startRegister=HoldingResterEndAddress+1;
17 break;
18 }
19 case JIG1StationAddress: //更新擺臂小電機
20 {
21 startRegister=48;
22 break;
23 }
24 case JIG2StationAddress: //更新擺臂小電機
25 {
26 startRegister=52;
27 break;
28 }
29 case JIG3StationAddress: //更新擺臂小電機
30 {
31 startRegister=56;
32 break;
33 }
34 case HLPStationAddress: //更新紅外溫度
35 {
36 aPara.phyPara.hlpObjectTemperature=registerValue[0]/100.0;
37 startRegister=HoldingResterEndAddress+1;
38 break;
39 }
40 case ROL1StationAddress: //更新擺臂控制
41 {
42 startRegister=quantity<3?60:62;
43 break;
44 }
45 case ROL2StationAddress: //更新擺臂控制
46 {
47 startRegister=quantity<3?70:72;
48 break;
49 }
50 case ROL3StationAddress: //更新擺臂控制
51 {
52 startRegister=quantity<3?80:82;
53 break;
54 }
55 case DRUMStationAddress: //更新滾筒電機
56 {
57 startRegister=quantity<3?90:92;
58 break;
59 }
60 default: //故障态
61 {
62 startRegister=HoldingResterEndAddress+1;
63 break;
64 }
65 }
66
67 if(startRegister<=HoldingResterEndAddress)
68 {
69 for(int i=0;i<quantity;i++)
70 {
71 aPara.holdingRegister[startRegister+i]=registerValue[i];
72 }
73 }
74 }
75
76 /*更新讀回來的輸入寄存器*/
77 void UpdateInputResgister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)
78 {
79 uint16_t startRegister=HoldingResterEndAddress+1;
80
81 switch(salveAddress)
82 {
83 case BPQStationAddress: //更新讀取的變頻器參數
84 {
85 startRegister=HoldingResterEndAddress+1;
86 break;
87 }
88 case PUMPStationAddress: //更新蠕動泵
89 {
90 //aPara.phyPara.pumpRotateSpeed=registerValue[1]; //第一版背闆
91 aPara.phyPara.pumpRotateSpeed=(uint16_t)((float)registerValue[1]*6.0/128.0+0.5); //第二版背闆
92 startRegister=HoldingResterEndAddress+1;
93 break;
94 }
95 case JIG1StationAddress: //更新擺臂小電機
96 {
97 startRegister=HoldingResterEndAddress+1;
98 break;
99 }
100 case JIG2StationAddress: //更新擺臂小電機
101 {
102 startRegister=HoldingResterEndAddress+1;
103 break;
104 }
105 case JIG3StationAddress: //更新擺臂小電機
106 {
107 startRegister=HoldingResterEndAddress+1;
108 break;
109 }
110 case ROL1StationAddress: //更新擺臂控制
111 {
112 startRegister=HoldingResterEndAddress+1;
113 break;
114 }
115 case ROL2StationAddress: //更新擺臂控制
116 {
117 startRegister=HoldingResterEndAddress+1;
118 break;
119 }
120 case ROL3StationAddress: //更新擺臂控制
121 {
122 startRegister=HoldingResterEndAddress+1;
123 break;
124 }
125 case DRUMStationAddress: //更新滾筒電機
126 {
127 startRegister=HoldingResterEndAddress+1;
128 break;
129 }
130 default: //故障态
131 {
132 startRegister=HoldingResterEndAddress+1;
133 break;
134 }
135 }
136
137 if(startRegister<=HoldingResterEndAddress)
138 {
139 for(int i=0;i<quantity;i++)
140 {
141 aPara.holdingRegister[startRegister+i]=registerValue[i];
142 }
143 }
144 }
4、RTU主站小結
我們實作了這個RTU主站執行個體,我們可以使用如Modsim這樣的軟體在PC上模拟Modbus RTU從站來測試這個主站應用,操作結果是沒有問題的。
在使用協定棧實作RTU主站時需要注意,協定棧支援在同一裝置上以不同的通訊端口實作不同的主站應用,而且每一台主站都支援多個從站。具體實作隻需要根據協定棧定義就可以了。
我們來總結一下使用協定棧實作主站應用的步驟,以友善大家使用協定棧實作Modbus RTU主站應用。
第一步,使用主站對象類型聲明一個主站對象。然後對這個主站對象進行初始化。初始化主站對象時。需要指定從站數量,從站清單以及更新資料的回調函數指針。
第二步,生成通路從站的資料請求清單。這個資料請求清單是按每一台從站來劃分的,将清單的指針存在對應的從站對象中。然後在需要的時候發送相應的資料請求。
第三步,解析接收的從站資料響應。協定棧已經定義好了解析函數,隻需傳入消息就可自動解析。但是更新資料的回調函數必須根據具體的變量來編寫。可以每台主站獨立編寫也可使用預設的函數。不過建議每台主站獨立編寫,這樣比較清晰。
源碼下載下傳
歡迎關注:
如果您希望更友善且及時的閱讀相關文章,關注我的微信公衆号【木南創智】