Modbus协议栈应用实例之六:Modbus ASCII从站应用
自從開(kāi)源了我們自己開(kāi)發(fā)的Modbus協(xié)議棧之后,有很多朋友建議我針對(duì)性的做幾個(gè)示例。所以我們就基于平時(shí)我們的應(yīng)用整理了幾個(gè)簡(jiǎn)單但可以說(shuō)明基本的應(yīng)用方法的示例,這一篇中我們來(lái)使用協(xié)議棧實(shí)現(xiàn)Modbus ASCII從站應(yīng)用。
1、何為ASCII從站
我們知道Modbus協(xié)議是一個(gè)主從協(xié)議,所以就存在主站和從站之分。所謂ASCII從站,簡(jiǎn)單來(lái)說(shuō)就是被動(dòng)響應(yīng)主站請(qǐng)求的站點(diǎn),所以我們可以說(shuō)ASCII從站就是響應(yīng)通訊的一方。
對(duì)于ASCII從站來(lái)說(shuō),它會(huì)生成數(shù)據(jù),但他不會(huì)主動(dòng)向外發(fā)送數(shù)據(jù),只有當(dāng)收到主站的數(shù)據(jù)請(qǐng)求后,從站才會(huì)根據(jù)主站的請(qǐng)求發(fā)送數(shù)據(jù)。這一過(guò)程如下圖所示:
從上圖我們不難看出,首先主站要主動(dòng)發(fā)起數(shù)據(jù)請(qǐng)求,這也是它為什么被稱(chēng)之為主站的緣由。它首先告訴從站我需要哪些數(shù)據(jù)。然后從站按照主站的請(qǐng)求返回?cái)?shù)據(jù)。主站得到響應(yīng)后解析數(shù)據(jù),這樣就完成了主從站之間的一次數(shù)據(jù)通訊。所以主站就需要主動(dòng)發(fā)起每一次數(shù)據(jù)通訊的對(duì)象。
雖然Modbus ASCII與Modbus RTU都是基于串行鏈路來(lái)實(shí)現(xiàn)的,但在數(shù)據(jù)傳輸?shù)膱?bào)文格式上存在較大區(qū)別。相比于Modbus RTU,Modbus ASCII采用ASCII碼的形式來(lái)發(fā)送報(bào)文,并且有確定的起始字符和結(jié)束字符。具體結(jié)構(gòu)如下:
在ASCII模式下,每個(gè)8位的字節(jié)被拆分成兩個(gè)ASCII字符進(jìn)行發(fā)送。對(duì)于數(shù)據(jù)部分,根據(jù)具體發(fā)送的數(shù)據(jù)量來(lái)確定長(zhǎng)度。校驗(yàn)方式則采用的是LRC校驗(yàn)方式。LRC校驗(yàn)較為簡(jiǎn)單,把每一個(gè)需要傳輸?shù)臄?shù)據(jù)字節(jié)迭加后取反加1即可。
2、如何實(shí)現(xiàn)ASCII從站
我們已經(jīng)了解的從站總是響應(yīng)主站的數(shù)據(jù)請(qǐng)求來(lái)實(shí)現(xiàn)數(shù)據(jù)的傳送。下面我們來(lái)看看使用協(xié)議棧如何實(shí)現(xiàn)一個(gè)從站。
我們知道從站是數(shù)據(jù)的生產(chǎn)者,對(duì)于Modbus協(xié)議來(lái)說(shuō)有四類(lèi)數(shù)據(jù):線圈、狀態(tài)、輸入寄存器和保持寄存器。所以在從站中我們要為這四種數(shù)據(jù)定義相應(yīng)的地址,以便主站能夠?qū)?yīng)的訪問(wèn)。所以設(shè)計(jì)一個(gè)從站我們先來(lái)設(shè)計(jì)它的數(shù)據(jù)地址,在我們的例子中我們規(guī)定如下:
我們規(guī)定了每類(lèi)數(shù)據(jù)類(lèi)型的數(shù)量為8,對(duì)于從站來(lái)說(shuō)除了生成這些數(shù)據(jù)外,還需要根據(jù)主站的數(shù)據(jù)請(qǐng)求來(lái)返回相應(yīng)的數(shù)據(jù)響應(yīng)。在我們的協(xié)議棧中實(shí)現(xiàn)了0x01、0x02、0x03、0x04、0x05、0x06、0x0F以及0x10等功能碼。也就是說(shuō)主站對(duì)象會(huì)生成面向這些功能碼的從站數(shù)據(jù)請(qǐng)求。從站收到請(qǐng)求后,解析請(qǐng)求并根據(jù)請(qǐng)求生成響應(yīng)的數(shù)據(jù)響應(yīng)。可以表示為下圖所示:
從上圖我們明白協(xié)議棧中已經(jīng)實(shí)現(xiàn)了對(duì)收到的主站數(shù)據(jù)請(qǐng)求進(jìn)行解析以及根據(jù)解析生成對(duì)應(yīng)的響應(yīng)的函數(shù)。我們使用協(xié)議棧時(shí),主要需要做兩個(gè)方面的事情:解析數(shù)據(jù)請(qǐng)求和生成數(shù)據(jù)響應(yīng)。
在協(xié)議棧中定義了一個(gè)解析函數(shù),該函數(shù)將收到的數(shù)據(jù)請(qǐng)求消息解析,并根據(jù)解析的結(jié)果生成返回的數(shù)據(jù)響應(yīng)。該函數(shù)的原型如下:
uint16_t ParsingAsciiMasterAccessCommand(uint8_t *receivedMessage, uint8_t *respondBytes, uint16_t rxLength, uint8_t StationAddress)
這個(gè)函數(shù)有四個(gè)參數(shù):uint8_t *receivedMessage是收到的數(shù)據(jù)請(qǐng)求消息; uint8_t *respondBytes是返回的數(shù)據(jù)響應(yīng)消息,也是函數(shù)需要生成的;uint16_t rxLength是接收到的數(shù)據(jù)請(qǐng)求消息的長(zhǎng)度;uint8_t StationAddress本站的地址。而函數(shù)的返回值則是生成的數(shù)據(jù)響應(yīng)詳細(xì)的長(zhǎng)度。
在解析的過(guò)程中,該函數(shù)判斷消息的完整性,并根據(jù)不同的功能碼調(diào)用不同的回調(diào)函數(shù)來(lái)實(shí)現(xiàn),包括設(shè)置本地?cái)?shù)據(jù)和獲取本地?cái)?shù)據(jù)的相關(guān)回調(diào)函數(shù),在后續(xù)將討論它們的實(shí)現(xiàn)。
3、ASCII從站編碼
我們已經(jīng)描述了使用協(xié)議棧實(shí)現(xiàn)Modbus ASCII從站的方法和流程,接下來(lái)我們就來(lái)利用協(xié)議棧具體實(shí)現(xiàn)一個(gè)Modbus ASCII從站的實(shí)例。
我們調(diào)用解析函數(shù)對(duì)接收到的數(shù)據(jù)請(qǐng)求進(jìn)行解析,具體調(diào)用方式如下所示:
respondLength=ParsingAsciiMasterAccessCommand (asciiSlaveRxBuffer,respondBytes, asciiSlaveRxLength,StationAddress);
返回值會(huì)有3種情況,返回值為0則表示接收到的數(shù)據(jù)請(qǐng)求消息是錯(cuò)誤的。返回值為65535則表示返回的消息尚未接收完整。返回的是一個(gè)合適的數(shù)值則表示解析成功,返回了數(shù)據(jù)響應(yīng)的長(zhǎng)度。
當(dāng)然我們需要實(shí)現(xiàn)8個(gè)回調(diào)函數(shù),分別是獲取線圈量、獲取狀態(tài)量、獲取輸入寄存器和獲取保持寄存器,以及預(yù)置單個(gè)線圈量、預(yù)置多個(gè)線圈量、預(yù)置單個(gè)保持寄存器和預(yù)置多個(gè)保持寄存器。函數(shù)原型定義如下:
/*獲取想要讀取的Coil量的值*/ __weak void GetCoilStatus(uint16_t startAddress,uint16_t quantity,bool *statusList) {//如果需要Modbus TCP Server/RTU Slave應(yīng)用中實(shí)現(xiàn)具體內(nèi)容 }/*獲取想要讀取的InputStatus量的值*/ __weak void GetInputStatus(uint16_t startAddress,uint16_t quantity,bool *statusValue) {//如果需要Modbus TCP Server/RTU Slave應(yīng)用中實(shí)現(xiàn)具體內(nèi)容 }/*獲取想要讀取的保持寄存器的值*/ __weak void GetHoldingRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue) {//如果需要Modbus TCP Server/RTU Slave應(yīng)用中實(shí)現(xiàn)具體內(nèi)容 }/*獲取想要讀取的輸入寄存器的值*/ __weak void GetInputRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue) {//如果需要Modbus TCP Server/RTU Slave應(yīng)用中實(shí)現(xiàn)具體內(nèi)容 }/*設(shè)置單個(gè)線圈的值*/ __weak void SetSingleCoil(uint16_t coilAddress,bool coilValue) {//如果需要Modbus TCP Server/RTU Slave應(yīng)用中實(shí)現(xiàn)具體內(nèi)容 }/*設(shè)置單個(gè)寄存器的值*/ __weak void SetSingleRegister(uint16_t registerAddress,uint16_t registerValue) {//如果需要Modbus TCP Server/RTU Slave應(yīng)用中實(shí)現(xiàn)具體內(nèi)容 }/*設(shè)置多個(gè)線圈的值*/ __weak void SetMultipleCoil(uint16_t startAddress,uint16_t quantity,bool *statusValue) {//如果需要Modbus TCP Server/RTU Slave應(yīng)用中實(shí)現(xiàn)具體內(nèi)容 }/*設(shè)置多個(gè)寄存器的值*/ __weak void SetMultipleRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue) {//如果需要Modbus TCP Server/RTU Slave應(yīng)用中實(shí)現(xiàn)具體內(nèi)容 }我們需要做的工作就是根據(jù)我們具體實(shí)例中4類(lèi)數(shù)據(jù)量的地址分配來(lái)實(shí)現(xiàn)這8個(gè)回調(diào)函數(shù)。當(dāng)然,如果從站沒(méi)有某一類(lèi)數(shù)據(jù)量操作,回調(diào)函數(shù)則不需要編寫(xiě)。在我們的實(shí)例中我們將這幾個(gè)函數(shù)實(shí)現(xiàn)如下:
/*獲取想要讀取的Coil量的值*/ void GetCoilStatus(uint16_t startAddress,uint16_t quantity,bool *statusList) {uint16_t start;uint16_t count;/*先判斷地址是否處于合法范圍*/start=(startAddress>CoilStartAddress)?((startAddress<=CoilEndAddress)?startAddress:CoilEndAddress):CoilStartAddress;count=((start+quantity-1)<=CoilEndAddress)?quantity:(CoilEndAddress-start);for(int i=0;i<count;i++){statusList[i]=dPara.coil[start+i];} }/*獲取想要讀取的保持寄存器的值*/ void GetHoldingRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue) {uint16_t start;uint16_t count;/*先判斷地址是否處于合法范圍*/start=(startAddress>HoldingResterStartAddress)?((startAddress<=HoldingResterEndAddress)?startAddress:HoldingResterEndAddress):HoldingResterStartAddress;count=((start+quantity-1)<=HoldingResterEndAddress)?quantity:(HoldingResterEndAddress-start);for(int i=0;i<count;i++){registerValue[i]=aPara.holdingRegister[start+i];} }/*設(shè)置單個(gè)線圈的值*/ void SetSingleCoil(uint16_t coilAddress,bool coilValue) {/*先判斷地址是否處于合法范圍*/if((4<=coilAddress)&&(coilAddress<=CoilEndAddress)){dPara.coil[coilAddress]=coilValue;}PresetSlaveCoilControll(coilAddress,coilAddress); }/*設(shè)置多個(gè)線圈的值*/ void SetMultipleCoil(uint16_t startAddress,uint16_t quantity,bool *statusValue) {uint16_t endAddress=startAddress+quantity-1;if((4<=startAddress)&&(startAddress<=CoilEndAddress)&&(4<=endAddress)&&(endAddress<=CoilEndAddress)){for(int i=0;i<quantity;i++){dPara.coil[i+startAddress]=statusValue[i];}}PresetSlaveCoilControll(startAddress,endAddress); }/*設(shè)置單個(gè)寄存器的值*/ void SetSingleRegister(uint16_t registerAddress,uint16_t registerValue) {bool noError=(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); }/*設(shè)置多個(gè)寄存器的值*/ void SetMultipleRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue) {uint16_t endAddress=startAddress+quantity-1;bool noError=(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(int i=0;i<quantity;i++){aPara.holdingRegister[startAddress+i]=registerValue[i];}}WriteSlaveRegisterControll(startAddress,endAddress); }到這里對(duì)從站的開(kāi)發(fā)實(shí)際已經(jīng)完成。對(duì)于這些回調(diào)函數(shù)并不是全部需要編寫(xiě),而是要根據(jù)我們自己定義的從站各類(lèi)參數(shù)的地址分配來(lái)實(shí)現(xiàn)。
4、ASCII從站小結(jié)
我們使用協(xié)議棧實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的Modbus ASCII從站應(yīng)用。我們可以使用Modscan、Modbus poll以及各類(lèi)串口通訊工具對(duì)其進(jìn)行測(cè)試。在使用Modbus poll時(shí)將其數(shù)據(jù)格式設(shè)為ASCII即可。測(cè)試結(jié)果如下:
與Modbus RTU從站類(lèi)似,Modbus ASCII從站的實(shí)現(xiàn)也較為簡(jiǎn)單,因?yàn)樵谕慌_(tái)設(shè)備上只需實(shí)現(xiàn)一個(gè)從站,哪怕是通過(guò)不同的端口來(lái)訪問(wèn)。這一點(diǎn)與主站是不一樣的,原因是從站的數(shù)據(jù)是自己產(chǎn)生,而且只需被動(dòng)響應(yīng)主站請(qǐng)求,而且理論上同一條總線只會(huì)有一個(gè)主站。
接下來(lái)我們來(lái)總結(jié)一下使用協(xié)議棧實(shí)現(xiàn)RTU從站的工作流程,或者說(shuō)實(shí)現(xiàn)的步驟。首先從站要解析從主站送來(lái)的數(shù)據(jù)請(qǐng)求。在協(xié)議棧中已經(jīng)封裝了數(shù)據(jù)請(qǐng)求的解析函數(shù)、所以我們實(shí)現(xiàn)從站時(shí)首先就是調(diào)用這一函數(shù)來(lái)解析接收到的數(shù)據(jù)請(qǐng)求消息。
然后將解析函數(shù)返回的數(shù)據(jù)響應(yīng)消息發(fā)送到主站就可以了。也就是說(shuō)使用協(xié)議棧,只需要調(diào)用一下這個(gè)函數(shù)從站功能就實(shí)現(xiàn)了。這是因?yàn)檫@個(gè)函數(shù)實(shí)現(xiàn)了整個(gè)從站的響應(yīng)過(guò)程,大致分三個(gè)步驟:第一步,解析收到的主站數(shù)據(jù)請(qǐng)求消息;第二步,根據(jù)解析的結(jié)果預(yù)置數(shù)據(jù)或者獲取數(shù)據(jù),預(yù)置和獲取數(shù)據(jù)由8個(gè)回調(diào)函數(shù)實(shí)現(xiàn);第三步,生成從站數(shù)據(jù)響應(yīng)消息。說(shuō)到這里我們已經(jīng)清楚,RTU從站必須實(shí)現(xiàn)這些回調(diào)函數(shù),其它工作則全由協(xié)議棧完成。
協(xié)議棧下載:https://github.com/foxclever/Modbus
示例下載:https://download.csdn.net/download/foxclever/12882021
歡迎關(guān)注:
?
總結(jié)
以上是生活随笔為你收集整理的Modbus协议栈应用实例之六:Modbus ASCII从站应用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 分布式基础
- 下一篇: 改进初学者的PID-初始化