自從開源了我們自己開發的Modbus協議棧之后,有很多朋友建議我針對性的做幾個示例。所以我們就基于平時我們的應用整理了幾個簡單但可以說明基本的應用方法的示例,這一篇中我們來使用協議棧實現Modbus ASCII從站應用。
1 、何為ASCII從站
我們知道Modbus協議是一個主從協議,所以就存在主站和從站之分。所謂ASCII從站,簡單來說就是被動響應主站請求的站點,所以我們可以說ASCII從站就是響應通訊的一方。
對于ASCII從站來說,它會生成數據,但他不會主動向外發送數據,只有當收到主站的數據請求后,從站才會根據主站的請求發送數據。這一過程如下圖所示:
從上圖我們不難看出,首先主站要主動發起數據請求,這也是它為什么被稱之為主站的緣由。它首先告訴從站我需要哪些數據。然后從站按照主站的請求返回數據。主站得到響應后解析數據,這樣就完成了主從站之間的一次數據通訊。所以主站就需要主動發起每一次數據通訊的對象。
雖然Modbus ASCII與Modbus RTU都是基于串行鏈路來實現的,但在數據傳輸的報文格式上存在較大區別。相比于Modbus RTU,Modbus ASCII采用ASCII碼的形式來發送報文,并且有確定的起始字符和結束字符。具體結構如下:
在ASCII模式下,每個8位的字節被拆分成兩個ASCII字符進行發送。對于數據部分,根據具體發送的數據量來確定長度。校驗方式則采用的是LRC校驗方式。LRC校驗較為簡單,把每一個需要傳輸的數據字節迭加后取反加1即可。
2 、如何實現ASCII從站
我們已經了解的從站總是響應主站的數據請求來實現數據的傳送。下面我們來看看使用協議棧如何實現一個從站。
我們知道從站是數據的生產者,對于Modbus協議來說有四類數據:線圈、狀態、輸入寄存器和保持寄存器。所以在從站中我們要為這四種數據定義相應的地址,以便主站能夠對應的訪問。所以設計一個從站我們先來設計它的數據地址,在我們的例子中我們規定如下:
我們規定了每類數據類型的數量為8,對于從站來說除了生成這些數據外,還需要根據主站的數據請求來返回相應的數據響應。在我們的協議棧中實現了0x01、0x02、0x03、0x04、0x05、0x06、0x0F以及0x10等功能碼。也就是說主站對象會生成面向這些功能碼的從站數據請求。從站收到請求后,解析請求并根據請求生成響應的數據響應。可以表示為下圖所示:
從上圖我們明白協議棧中已經實現了對收到的主站數據請求進行解析以及根據解析生成對應的響應的函數。我們使用協議棧時,主要需要做兩個方面的事情:解析數據請求和生成數據響應。
在協議棧中定義了一個解析函數,該函數將收到的數據請求消息解析,并根據解析的結果生成返回的數據響應。該函數的原型如下:
uint16_t ParsingAsciiMasterAccessCommand(uint8_t *receivedMessage,uint8_t *respondBytes, uint16_t rxLength, uint8_t StationAddress)
這個函數有四個參數:uint8_t receivedMessage是收到的數據請求消息; uint8_trespondBytes是返回的數據響應消息,也是函數需要生成的;uint16_t rxLength是接收到的數據請求消息的長度;uint8_t StationAddress本站的地址。而函數的返回值則是生成的數據響應詳細的長度。
在解析的過程中,該函數判斷消息的完整性,并根據不同的功能碼調用不同的回調函數來實現,包括設置本地數據和獲取本地數據的相關回調函數,在后續將討論它們的實現。
3 、 ASCII****從站編碼
我們已經描述了使用協議棧實現Modbus ASCII從站的方法和流程,接下來我們就來利用協議棧具體實現一個Modbus ASCII從站的實例。
我們調用解析函數對接收到的數據請求進行解析,具體調用方式如下所示:
respondLength=ParsingAsciiMasterAccessCommand(asciiSlaveRxBuffer,respondBytes, asciiSlaveRxLength,StationAddress);
返回值會有3種情況,返回值為0則表示接收到的數據請求消息是錯誤的。返回值為65535則表示返回的消息尚未接收完整。返回的是一個合適的數值則表示解析成功,返回了數據響應的長度。
當然我們需要實現8個回調函數,分別是獲取線圈量、獲取狀態量、獲取輸入寄存器和獲取保持寄存器,以及預置單個線圈量、預置多個線圈量、預置單個保持寄存器和預置多個保持寄存器。函數原型定義如下:
/*獲取想要讀取的Coil量的值*/
__weak void GetCoilStatus(uint16_t startAddress,uint16_t quantity,bool*statusList)
{
//如果需要Modbus TCP Server/RTU Slave應用中實現具體內容
}
/*獲取想要讀取的InputStatus量的值*/
__weak void GetInputStatus(uint16_t startAddress,uint16_t quantity,bool*statusValue)
{
//如果需要Modbus TCP Server/RTU Slave應用中實現具體內容
}
/*獲取想要讀取的保持寄存器的值*/
__weak void GetHoldingRegister(uint16_t startAddress,uint16_tquantity,uint16_t *registerValue)
{
//如果需要Modbus TCP Server/RTU Slave應用中實現具體內容
}
/*獲取想要讀取的輸入寄存器的值*/
__weak void GetInputRegister(uint16_t startAddress,uint16_tquantity,uint16_t *registerValue)
{
//如果需要Modbus TCP Server/RTU Slave應用中實現具體內容
}
/*設置單個線圈的值*/
__weak void SetSingleCoil(uint16_t coilAddress,bool coilValue)
{
//如果需要Modbus TCP Server/RTU Slave應用中實現具體內容
}
/*設置單個寄存器的值*/
__weak void SetSingleRegister(uint16_t registerAddress,uint16_tregisterValue)
{
//如果需要Modbus TCP Server/RTU Slave應用中實現具體內容
}
/*設置多個線圈的值*/
__weak void SetMultipleCoil(uint16_t startAddress,uint16_t quantity,bool*statusValue)
{
//如果需要Modbus TCP Server/RTU Slave應用中實現具體內容
}
/*設置多個寄存器的值*/
__weak void SetMultipleRegister(uint16_t startAddress,uint16_tquantity,uint16_t *registerValue)
{
//如果需要Modbus TCP Server/RTU Slave應用中實現具體內容
}
我們需要做的工作就是根據我們具體實例中4類數據量的地址分配來實現這8個回調函數。當然,如果從站沒有某一類數據量操作,回調函數則不需要編寫。在我們的實例中我們將這幾個函數實現如下:
/*獲取想要讀取的Coil量的值*/
void GetCoilStatus(uint16_t startAddress,uint16_tquantity,bool *statusList)
{
uint16_tstart;
uint16_tcount;
/*先判斷地址是否處于合法范圍*/
start=(startAddress>CoilStartAddress)?((startAddress<=CoilEndAddress)?startAddress:CoilEndAddress):CoilStartAddress;
count=((start+quantity-1)<=CoilEndAddress)?quantity:(CoilEndAddress-start);
for(inti=0;i/*獲取想要讀取的保持寄存器的值*/
void GetHoldingRegister(uint16_tstartAddress,uint16_t quantity,uint16_t *registerValue)
{
uint16_tstart;
uint16_tcount;
/*先判斷地址是否處于合法范圍*/
start=(startAddress>HoldingResterStartAddress)?((startAddress<=HoldingResterEndAddress)?startAddress:HoldingResterEndAddress):HoldingResterStartAddress;
count=((start+quantity-1)<=HoldingResterEndAddress)?quantity:(HoldingResterEndAddress-start);
for(inti=0;i/*設置單個線圈的值*/
void SetSingleCoil(uint16_t coilAddress,boolcoilValue)
{
/*先判斷地址是否處于合法范圍*/
if((4<=coilAddress)&&(coilAddress<=CoilEndAddress))
{
dPara.coil[coilAddress]=coilValue;
}
PresetSlaveCoilControll(coilAddress,coilAddress);
}
/*設置多個線圈的值*/
void SetMultipleCoil(uint16_tstartAddress,uint16_t quantity,bool *statusValue)
{
uint16_tendAddress=startAddress+quantity-1;
if((4<=startAddress)&&(startAddress<=CoilEndAddress)&&(4<=endAddress)&&(endAddress<=CoilEndAddress))
{
for(inti=0;iPresetSlaveCoilControll(startAddress,endAddress);
}
/*設置單個寄存器的值*/
void SetSingleRegister(uint16_tregisterAddress,uint16_t registerValue)
{
boolnoError=(bool)(((41<=registerAddress)&&(registerAddress<=42))
||((44<=registerAddress)&&(registerAddress<=45))
||((50<=registerAddress)&&(registerAddress<=51))
||((54<=registerAddress)&&(registerAddress<=55))
||((58<=registerAddress)&&(registerAddress<=59)));
if(noError)
{
aPara.holdingRegister[registerAddress]=registerValue;
}
WriteSlaveRegisterControll(registerAddress,registerAddress);
}
/*設置多個寄存器的值*/
void SetMultipleRegister(uint16_tstartAddress,uint16_t quantity,uint16_t *registerValue)
{
uint16_tendAddress=startAddress+quantity-1;
boolnoError=(bool)(((8<=startAddress)&&(startAddress<=15)&&(8<=endAddress)&&(endAddress<=15))
||((41<=startAddress)&&(startAddress<=42)&&(41<=endAddress)&&(endAddress<=42))
||((44<=startAddress)&&(startAddress<=47)&&(44<=endAddress)&&(endAddress<=47))
||((50<=startAddress)&&(startAddress<=51)&&(50<=endAddress)&&(endAddress<=51))
||((54<=startAddress)&&(startAddress<=55)&&(54<=endAddress)&&(endAddress<=55))
||((58<=startAddress)&&(startAddress<=59)&&(58<=endAddress)&&(endAddress<=59))
||((62<=startAddress)&&(startAddress<=67)&&(62<=endAddress)&&(endAddress<=67))
||((72<=startAddress)&&(startAddress<=77)&&(72<=endAddress)&&(endAddress<=77))
||((82<=startAddress)&&(startAddress<=87)&&(82<=endAddress)&&(endAddress<=87))
||((92<=startAddress)&&(startAddress<=97)&&(92<=endAddress)&&(endAddress<=97))
||((100<=startAddress)&&(startAddress<=115)&&(100<=endAddress)&&(endAddress<=115)));
if(noError)
{
for(inti=0;iWriteSlaveRegisterControll(startAddress,endAddress);
}
到這里對從站的開發實際已經完成。對于這些回調函數并不是全部需要編寫,而是要根據我們自己定義的從站各類參數的地址分配來實現。
4 、 ASCII****從站小結
我們使用協議棧實現了一個簡單的Modbus ASCII從站應用。我們可以使用Modscan、Modbus poll以及各類串口通訊工具對其進行測試。在使用Modbus poll時將其數據格式設為ASCII即可。測試結果如下:
與Modbus RTU從站類似,Modbus ASCII從站的實現也較為簡單,因為在同一臺設備上只需實現一個從站,哪怕是通過不同的端口來訪問。這一點與主站是不一樣的,原因是從站的數據是自己產生,而且只需被動響應主站請求,而且理論上同一條總線只會有一個主站。
接下來我們來總結一下使用協議棧實現RTU從站的工作流程,或者說實現的步驟。首先從站要解析從主站送來的數據請求。在協議棧中已經封裝了數據請求的解析函數、所以我們實現從站時首先就是調用這一函數來解析接收到的數據請求消息。
然后將解析函數返回的數據響應消息發送到主站就可以了。也就是說使用協議棧,只需要調用一下這個函數從站功能就實現了。這是因為這個函數實現了整個從站的響應過程,大致分三個步驟:第一步,解析收到的主站數據請求消息;第二步,根據解析的結果預置數據或者獲取數據,預置和獲取數據由8個回調函數實現;第三步,生成從站數據響應消息。說到這里我們已經清楚,RTU從站必須實現這些回調函數,其它工作則全由協議棧完成。
協議棧下載: https://github.com/foxclever/Modbus
示例下載: https://download.csdn.net/download/foxclever/12882021
-
MODBUS
+關注
關注
28文章
1805瀏覽量
76997 -
ASCII
+關注
關注
5文章
172瀏覽量
35103 -
協議棧
+關注
關注
2文章
141瀏覽量
33632
發布評論請先 登錄
相關推薦
評論