自從開源了我們自己開發(fā)的Modbus協(xié)議棧之后,有很多朋友建議我針對性的做幾個示例。所以我們就基于平時我們的應用整理了幾個簡單但可以說明基本的應用方法的示例,這一篇中我們來使用協(xié)議棧實現(xiàn)Modbus ASCII主站應用。
1 、何為ASCII主站
我們知道Modbus協(xié)議是一個主從協(xié)議,所以就存在主站和從站之分。所謂主站,簡單來說就是能夠主動發(fā)起通訊的站點,所以我們可以說主站就是發(fā)起通訊的一方。
對于ASCII主站來說,它的數(shù)據需要從從站獲取,所以主站要通過通訊的方式與從站實現(xiàn)數(shù)據交流。在Modbus ASCII協(xié)議中從站不會主動向外發(fā)送數(shù)據,所以只有在ASCII主站發(fā)送數(shù)據請求,從站才會向其返回請求的數(shù)據。這一過程如下圖所示:

從上圖我們不難看出,首先主站要主動發(fā)起數(shù)據請求,這也是它為什么被稱之為主站的緣由。它首先告訴從站我需要哪些數(shù)據。然后從站按照主站的請求返回數(shù)據。主站得到響應后解析數(shù)據,這樣就完成了主從站之間的一次數(shù)據通訊。所以主站就需要主動發(fā)起每一次數(shù)據通訊的對象。
雖然Modbus ASCII與Modbus RTU都是基于串行鏈路來實現(xiàn)的,但在數(shù)據傳輸?shù)膱笪母袷缴洗嬖谳^大區(qū)別。相比于Modbus RTU,Modbus ASCII采用ASCII碼的形式來發(fā)送報文,并且有確定的起始字符和結束字符。具體結構如下:

在ASCII模式下,每個8位的字節(jié)被拆分成兩個ASCII字符進行發(fā)送。對于數(shù)據部分,根據具體發(fā)送的數(shù)據量來確定長度。校驗方式則采用的是LRC校驗方式。LRC校驗較為簡單,把每一個需要傳輸?shù)臄?shù)據字節(jié)迭加后取反加1即可。
2 、如何實現(xiàn)ASCII主站
我們已經簡單的說明了什么是ASCII的主站,那么如何實現(xiàn)這一主站呢?其實在協(xié)議棧中,我們已經實現(xiàn)了主站的數(shù)據請求命令的合成以及響應數(shù)據的解析,所以我們使用協(xié)議棧來實現(xiàn)ASCII主站時,我們需要做的就是控制何時將協(xié)議棧合成的主站請求命令發(fā)出以及如何解析數(shù)據響應進而得到想要的數(shù)據的過程。
在我們的協(xié)議棧中實現(xiàn)了0x01、0x02、0x03、0x04、0x05、0x06、0x0F以及0x10等功能碼。也就是說主站對象可以生成面向這些功能碼的從站數(shù)據請求。也可以解析面向這些功能碼的從站數(shù)據響應。可以表示為下圖所示:

從上圖我們很清楚,協(xié)議棧已經實現(xiàn)了面向這些功能碼的數(shù)據請求命令的生成以及數(shù)據響應消息的解析。我們使用協(xié)議棧時需要做的就是要告訴協(xié)議棧我要生成哪些數(shù)據請求命令以及如何解析數(shù)據響應消息。
2.1 、怎么生成數(shù)據請求
對于數(shù)據請求,我們不一定需要面向全部功能碼的請求,我們只需要根據我們的需求合成我們想要的請求。
在協(xié)議棧中,針對數(shù)據請求的生成我們定義了一個從站訪問命令生成函數(shù)。該函數(shù)的原型如下:
uint16_t CreateAccessAsciiSlaveCommand(ObjAccessInfo objInfo, void*dataList, uint8_t *commandBytes)
該函數(shù)有3個參數(shù),其中ObjAccessInfo objInfo為對象訪問信息;void*dataList為數(shù)據列表指針,該參數(shù)主要用于寫從站功能的命令生成;uint8_t *commandBytes為返回的從站訪問命令。
ObjAccessInfo是一個結構體,向函數(shù)傳遞我們想要生成的從站訪問命令的相關信息,包括站地址,功能碼,起始地址和數(shù)量。該結構體的定義如下:
/*定義用于傳遞要訪問從站(服務器)的信息*/
typedef struct{
uint8_t unitID;
FunctionCode functionCode;
uint16_t startingAddress;
uint16_t quantity;
}ObjAccessInfo;
2.2 、怎么解析數(shù)據響應
對于數(shù)據響應,我們同樣不需要考慮全部的操作碼,我們一般需要考慮讀請求的響應,因為他們的數(shù)據需要解析。而對于寫請求返回數(shù)響應只是告訴主站成功或者不成功,即使不成功只需要在寫一次就可以了,不存在數(shù)據更新的問題。
在協(xié)議棧中,我們實現(xiàn)了主站解析從站數(shù)據響應的解析函數(shù)。使用這一函數(shù)我們只需要將收到的數(shù)據響應報文傳遞給解析函數(shù)就可以完成解析。該函數(shù)的原型定義如下:
void ParsingAsciiSlaveRespondMessage(AsciiLocalMasterType master,uint8_trecievedMessage, uint8_t *command,uint16_t rxLength)
這個函數(shù)有4個參數(shù),其中RTULocalMasterType master為主站對象;uint8_trecievedMessage為接收到的響應消息;uint8_t *command為發(fā)送的命令序列。uint16_t rxLength是接受到的數(shù)據響應消息的長度。將這幾個參數(shù)傳遞給解析函數(shù)就可實現(xiàn)數(shù)據響應的解析。
AsciiLocalMasterType 是一個結構體,用以生命一個主站對象,這個對象就是我們要實現(xiàn)各種操作的主站,這一結構體的定義如下:
/* 定義本地ASCII主站對象類型 */
typedef struct LocalASCIIMasterType{
uint32_t flagWriteSlave[8]; //寫一個站控制標志位,最多256個站,與站地址對應。
uint16_t slaveNumber; //從站列表中從站的數(shù)量
uint16_t readOrder; //當前從站在從站列表中的位置
AsciiAccessedSlaveType *pSlave; //從站列表
UpdateCoilStatusType pUpdateCoilStatus; //更新線圈量函數(shù)
UpdateInputStatusType pUpdateInputStatus; //更新輸入狀態(tài)量函數(shù)
UpdateHoldingRegisterType pUpdateHoldingRegister; //更新保持寄存器量函數(shù)
UpdateInputResgisterType pUpdateInputResgister; //更新輸入寄存器量函數(shù)
}AsciiLocalMasterType;
3 、 ASCII****主站編碼
我們已經設計了一個簡單的ASCII主站示例,接下來我們就來編碼實現(xiàn)并驗證這一示例。
3.1 、定義ASCII主站對象
首先我們要聲明一個主站對象,這是我們操作的基礎。在接下來的各種操作中我們都是基于這一對象來實現(xiàn)的。具體操作如下:
AsciiLocalMasterType asciiMaster;
定義了這個主站對象后,我們還需要對這一對象進行初始化。協(xié)議棧同樣提供了一個主站對象的初始化函數(shù)。函數(shù)的原型定義如下:
/*初始化ASCII主站對象*/
voidInitializeASCIIMasterObject(AsciiLocalMasterType *master,
uint16_t slaveNumber,
AsciiAccessedSlaveType*pSlave,
UpdateCoilStatusTypepUpdateCoilStatus,
UpdateInputStatusTypepUpdateInputStatus,
UpdateHoldingRegisterType pUpdateHoldingRegister,
UpdateInputResgisterType pUpdateInputResgister
)
該函數(shù)的參數(shù)除了主站對象外,還有從站的數(shù)量即從站對象列表,還有四個數(shù)據更新函數(shù)指針。這幾個函數(shù)指針將應用于數(shù)據響應的解析過程中,具體在后面描述。使用這一初始化函數(shù)實現(xiàn)對主站對象的初始化,使其能夠實現(xiàn)各項操作,具體如下:
/ 初始化RTU主站對象 /
InitializeASCIIMasterObject(&asciiMaster,2,asciiSlave,NULL,NULL,NULL,NULL);
這里我們將幾個數(shù)據處理函數(shù)指針變量傳入NULL,表示初始化為默認的操作函數(shù),當然我們也可以編寫這些函數(shù),在后續(xù)的數(shù)據解析時將會詳細說明。
3.2 、生成主站數(shù)據請求
在前面,我們已經描述了數(shù)據請求命令的生成函數(shù),該函數(shù)有一個ObjAccessInfo參數(shù),這個參數(shù)用于傳遞需要生成命令的信息。這是一個結構體,我們需要定義一個對象變量。
ObjAccessInfo asciiInfo;
然后使用這個對象來實現(xiàn)數(shù)據請求的生成。具體操作如下所示:
/* 生成1號從站訪問命令 */
asciiInfo.unitID=asciiSlave[0].stationAddress;
asciiInfo.functionCode=ReadCoilStatus;
asciiInfo.startingAddress=0x0000;
asciiInfo.quantity=8;
CreateAccessAsciiSlaveCommand(asciiInfo,NULL,aSlave1ReadCommand[0]);
生成的數(shù)據請求什么時候發(fā)送給完全由主進程來實現(xiàn)已經與協(xié)議棧沒有關系了。
3.3 、解析從站數(shù)據響應
收到數(shù)據響應后我們需要對其進行解析。前面我們已經介紹了解析從站數(shù)據響應的函數(shù)。具體的調用形式如下:
ParsingAsciiSlaveRespondMessage(&asciiMaster,asciiRxBuffer,NULL,asciiRxLength);
我們對asciiMaster主站對象收到的從站響應asciiRxBuffer進行解析。最后傳入的NULL表示我們不指定主站發(fā)送的數(shù)據請求,而是讓主站從請求列表中去自己查找。
當然我們需要實現(xiàn)數(shù)據更新處理回調函數(shù)。這幾個函數(shù)是在對象初始化的時候以函數(shù)指針的形式傳遞的。原型如下:
/*更新讀回來的線圈狀態(tài)*/
__weak void UpdateCoilStatus(uint8_t salveAddress,uint16_tstartAddress,uint16_t quantity,bool *stateValue)
{
//在客戶端(主站)應用中實現(xiàn)
}
/*更新讀回來的輸入狀態(tài)值*/
__weak void UpdateInputStatus(uint8_t salveAddress,uint16_tstartAddress,uint16_t quantity,bool *stateValue)
{
//在客戶端(主站)應用中實現(xiàn)
}
/*更新讀回來的保持寄存器*/
__weak void UpdateHoldingRegister(uint8_t salveAddress,uint16_tstartAddress,uint16_t quantity,uint16_t *registerValue)
{
//在客戶端(主站)應用中實現(xiàn)
}
/*更新讀回來的輸入寄存器*/
__weak void UpdateInputResgister(uint8_t salveAddress,uint16_t startAddress,uint16_tquantity,uint16_t *registerValue)
{
//在客戶端(主站)應用中實現(xiàn)
}
我們可根據需要重定義這些函數(shù),當然我們沒有響應的數(shù)據可以不必實現(xiàn),如我們沒有使用輸入寄存器,那么更新輸入寄存器的回調函數(shù)則可以不用重定義。如下在我們的例子中重定義為:
/*更新讀回來的保持寄存器*/
voidUpdateHoldingRegister(uint16_t startAddress,uint16_t quantity,uint16_t*registerValue)
{
uint16_tstartRegister=HoldingResterEndAddress+1;
switch(salveAddress)
{
case BPQStationAddress: //更新讀取的變頻器參數(shù)
{
startRegister=36;
break;
}
case PUMPStationAddress: //更新蠕動泵
{
startRegister=HoldingResterEndAddress+1;
break;
}
case JIG1StationAddress: //更新擺臂小電機
{
startRegister=48;
break;
}
case JIG2StationAddress: //更新擺臂小電機
{
startRegister=52;
break;
}
case JIG3StationAddress: //更新擺臂小電機
{
startRegister=56;
break;
}
case HLPStationAddress: //更新紅外溫度
{
aPara.phyPara.hlpObjectTemperature=registerValue[0]/100.0;
startRegister=HoldingResterEndAddress+1;
break;
}
case ROL1StationAddress: //更新擺臂控制
{
startRegister=quantity<3?60:62;
break;
}
case ROL2StationAddress: //更新擺臂控制
{
startRegister=quantity<3?70:72;
break;
}
case ROL3StationAddress: //更新擺臂控制
{
startRegister=quantity<3?80:82;
break;
}
case DRUMStationAddress: //更新滾筒電機
{
startRegister=quantity<3?90:92;
break;
}
default: //故障態(tài)
{
startRegister=HoldingResterEndAddress+1;
break;
}
}
if(startRegister<=HoldingResterEndAddress)
{
for(int i=0;i/*更新讀回來的輸入寄存器*/
void UpdateInputResgister(uint16_t startAddress,uint16_tquantity,uint16_t *registerValue)
{
uint16_tstartRegister=HoldingResterEndAddress+1;
switch(salveAddress)
{
case BPQStationAddress: //更新讀取的變頻器參數(shù)
{
startRegister=HoldingResterEndAddress+1;
break;
}
case PUMPStationAddress: //更新蠕動泵
{
aPara.phyPara.pumpRotateSpeed=(uint16_t)((float)registerValue[1]*6.0/128.0+0.5);//第二版背板
startRegister=HoldingResterEndAddress+1;
break;
}
case JIG1StationAddress: //更新擺臂小電機
{
startRegister=HoldingResterEndAddress+1;
break;
}
case JIG2StationAddress: //更新擺臂小電機
{
startRegister=HoldingResterEndAddress+1;
break;
}
case JIG3StationAddress: //更新擺臂小電機
{
startRegister=HoldingResterEndAddress+1;
break;
}
case ROL1StationAddress: //更新擺臂控制
{
startRegister=HoldingResterEndAddress+1;
break;
}
case ROL2StationAddress: //更新擺臂控制
{
startRegister=HoldingResterEndAddress+1;
break;
}
case ROL3StationAddress: //更新擺臂控制
{
startRegister=HoldingResterEndAddress+1;
break;
}
case DRUMStationAddress: //更新滾筒電機
{
startRegister=HoldingResterEndAddress+1;
break;
}
default: //故障態(tài)
{
startRegister=HoldingResterEndAddress+1;
break;
}
}
if(startRegister<=HoldingResterEndAddress)
{
for(int i=0;i
4 、 ASCII****主站小結
我們實現(xiàn)了這個ASCII主站實例,我們可以使用如Modsim這樣的軟件在PC上模擬Modbus ASCII從站來測試這個主站應用。如果自己編寫報文也可使用如串口助手之類的軟件測試。這里我們使用Modsim模擬從站,以AccessPort監(jiān)視其收發(fā)狀態(tài),測試結果如下圖:


在使用協(xié)議棧實現(xiàn)ASCII主站時需要注意,協(xié)議棧支持在同一設備上以不同的通訊端口實現(xiàn)不同的主站應用,而且每一臺主站都支持多個從站。具體實現(xiàn)只需要根據協(xié)議棧定義就可以了。
我們來總結一下使用協(xié)議棧實現(xiàn)主站應用的步驟,以方便大家使用協(xié)議棧實現(xiàn)Modbus ASCII主站應用。
第一步,使用主站對象類型聲明一個主站對象。然后對這個主站對象進行初始化。初始化主站對象時。需要指定從站數(shù)量,從站列表以及更新數(shù)據的回調函數(shù)指針。
第二步,生成訪問從站的數(shù)據請求列表。這個數(shù)據請求列表是按每一臺從站來劃分的,將列表的指針存在對應的從站對象中。然后在需要的時候發(fā)送相應的數(shù)據請求。
第三步,解析接收的從站數(shù)據響應。協(xié)議棧已經定義好了解析函數(shù),只需傳入消息就可自動解析。但是更新數(shù)據的回調函數(shù)必須根據具體的變量來編寫??梢悦颗_主站獨立編寫也可使用默認的函數(shù)。不過建議每臺主站獨立編寫,這樣比較清晰。
源碼下載: https://download.csdn.net/download/foxclever/12882021
-
MODBUS
+關注
關注
28文章
2459瀏覽量
83292 -
ASCII
+關注
關注
5文章
172瀏覽量
36698 -
協(xié)議棧
+關注
關注
2文章
146瀏覽量
34601
發(fā)布評論請先 登錄
使用協(xié)議棧實現(xiàn)Modbus RTU主站應用
使用協(xié)議棧實現(xiàn)Modbus RTU從站應用
使用協(xié)議棧實現(xiàn)Modbus ASCII從站應用
linux平臺實現(xiàn)modbus主機協(xié)議棧的動態(tài)庫libMbpoll
linux平臺實現(xiàn)modbus主機協(xié)議棧的動態(tài)庫libMbpoll
求 Modbus主站 模擬器 !
Modbus多主站問題
基于RT-Thread實現(xiàn)的Agile Modbus協(xié)議棧
基于Modbus RTU協(xié)議下實現(xiàn)的1主多從自組網無線通信形式
Modbus協(xié)議的理解
MODBUS主/從協(xié)議棧
S7200 Modbus通訊協(xié)議遠程終端設備RTU主站和從站示例
如何實現(xiàn)將Modbus主站與CAN總線連接
Profibus DP主站協(xié)議轉Modbus協(xié)議模塊連接馬達保護器案例
使用協(xié)議棧實現(xiàn)Modbus ASCII主站應用
評論