自從開源了我們自己開發的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主站應用。
第一步,使用主站對象類型聲明一個主站對象。然后對這個主站對象進行初始化。初始化主站對象時。需要指定從站數量,從站列表以及更新數據的回調函數指針。
第二步,生成訪問從站的數據請求列表。這個數據請求列表是按每一台從站來划分的,將列表的指針存在對應的從站對象中。然后在需要的時候發送相應的數據請求。
第三步,解析接收的從站數據響應。協議棧已經定義好了解析函數,只需傳入消息就可自動解析。但是更新數據的回調函數必須根據具體的變量來編寫。可以每台主站獨立編寫也可使用默認的函數。不過建議每台主站獨立編寫,這樣比較清晰。
源碼下載:https://download.csdn.net/download/foxclever/12882021