Modbus協議棧應用例項之二:Modbus RTU從站應用
自從開源了我們自己開發的Modbus協議棧之後,有很多朋友建議我針對性的做幾個示例。所以我們就基於平時我們的應用整理了幾個簡單但可以說明基本的應用方法的示例,這一篇中我們將使用協議棧實現一個Modbus RTU從站應用。
1、何為RTU從站
Modbus協議是一個主從協議,那肯定就有主站和從站之分。所謂從站就是被動動響應通訊的物件,所以從站總是響應通訊的一方。
對於RTU從站來說,它是資料的資料的生產者,從站通過響應主站資料請求的方式將資料傳送給主站。這一過程如下圖所示:
從上圖我們不難看出,首先主站要主動發起資料請求,這也是它為什麼被稱之為主站的緣由。它首先告訴從站我需要哪些資料。然後從站按照主站的請求返回資料。主站得到響應後解析資料,這樣就完成了主從站之間的一次資料通訊。所以主站就需要主動發起每一次資料通訊的物件。
2、如何實現RTU從站
我們已經瞭解的從站總是響應主站的資料請求來實現資料的傳送。下面我們來看看使用協議棧如何實現一個從站。
我們知道從站是資料的生產者,對於Modbus協議來說有四類資料:線圈、狀態、輸入暫存器和保持暫存器。所以在從站中我們要為這四種資料定義相應的地址,以便主站能夠對應的訪問。所以設計一個從站我們先來設計它的資料地址,在我們的例子中我們規定如下:
我們規定了每類資料型別的數量為8,對於從站來說除了生成這些資料外,還需要根據主站的資料請求來返回相應的資料響應。在我們的協議棧中實現了0x01、0x02、0x03、0x04、0x05、0x06、0x0F以及0x10等功能碼。也就是說主站物件會生成面向這些功能碼的從站資料請求。從站收到請求後,解析請求並根據請求生成響應的資料響應。可以表示為下圖所示:
從上圖我們明白協議棧中已經實現了對收到的主站資料請求進行解析以及根據解析生成對應的響應的函式。我們使用協議棧時,主要需要做兩個方面的事情:解析資料請求和生成資料響應。
在協議棧中定義了一個解析函式,該函式將收到的資料請求訊息解析,並根據解析的結果生成返回的資料響應。該函式的原型如下:
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來測試一下,如下圖所示:
RTU從站的實現相對較簡單,因為在同一臺裝置上只需實現一個從站,哪怕是通過不同的埠來訪問。這一點與主站是不一樣的,原因是從站的資料是自己產生,而且只需被動響應主站請求,而且理論上同一條匯流排只會有一個主站。
接下來我們來總結一下使用協議棧實現RTU從站的工作流程,或者說實現的步驟。首先從站要解析從主站送來的資料請求。在協議棧中已經封裝了資料請求的解析函式、所以我們實現從站時首先就是呼叫這一函式來解析接收到的資料請求訊息。
然後將解析函式返回的資料響應訊息傳送到主站就可以了。也就是說使用協議棧,只需要呼叫一下這個函式從站功能就實現了。這是因為這個函式實現了整個從站的響應過程,大致分三個步驟:第一步,解析收到的主站資料請求訊息;第二步,根據解析的結果預置資料或者獲取資料,預置和獲取資料由8個回撥函式實現;第三步,生成從站資料響應訊息。說到這裡我們已經清楚,RTU從站必須實現這些回撥函式,其它工作則全由協議棧完成。
歡迎關注: