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