Modbus协议栈实现Modbus RTU多主站支持
前面我們已經詳細講解過Modbus協議棧的開發過程,并且利用協議棧封裝了Modbus RTU主站和從站,Modbus TCP服務器與客戶端,Modbus ASCII主站與從站應用。但在使用過程中,我們發現一些使用不便和受限的地方,所以我們就想要更新一下協議棧,主要是應用站的封裝。
1、存在的局限性
在原有的協議棧中,我們所封裝的Modbus RTU主站是一個特定的主站,即它只是一個主站。在通常的應用中不會有什么問題,但在有些應用場合就會顯現出它的局限性。
首先,作為一個特定的主站,帶多個從站時,寫從站的處理變的非常復雜,需要分辨不同的從站,不同的變量。當有多個端口時,還需要分辨不同的端口。
其次,作為一個特定的主站,帶多個從站時,讀從站的處理,即使是不同的端口也不能分辨相同站地址的從站。因為同一主站的解析函數是同一個,所以即使在不同端口也很難分辨,除非在解析前傳遞端口信息,這其實是將多余的信息傳遞到協議棧。這樣做不但程序不夠明晰也缺乏一般性。
最后,將所有的Modbus從站通訊都作為唯一的一個特定從站來處理,使得各部分混雜在一起,程序結構很不清晰,對象也不明確。
2、更新設計
考慮到前述的局限性,我們將主站及其帶訪問的從站定義為通用的對象,而當我們在具體應用中使用時,再將其特例化為特定的主站和從站對象。
首先我們來考慮主站,原則上我們規劃的每一個主站對象對應我們設備上的一個端口,那么在同一端口下,也就是在一個特定主站下,我們可以定義多個地址不同的從站,但不同端口下不受影響。如下圖所示:
從上圖中我們可以發現,我們的目的就是讓協議棧支持,多主站和多從站,并且在不同主站下,從站的地址重復不受影響。接下來我們還需要考慮從站對象。主站對從站的操作無非兩類:讀從站信息和寫從站信息。
對于讀從站信息來說,主站需要發送請求命令,等待從站返回響應信息,然后主站解析收到的信息并更新對應的參數值。有兩點需要我們考慮,第一返回的響應消息是沒有對應的寄存器地址的,所以要想在解析的時候定位寄存器就必須知道發送的命令,為了便于分辨我們將命令存放在從站對象中。第二在解析響應時,如果兩條命令的響應類似是沒法分辨的,所以我們還需要記住上一條命令是什么。也存儲于從站對象中。
而對于寫從站操作,無論寫的要求來自于哪里,對于協議棧來說肯定是其它的數據處理進程發過來的,所接到要求后我們需要記錄是哪一個主站管理的哪一個從站的哪些參數。對于主站我們不需要分辨,因為每個主站都是獨立的處理進程,但是對于從站和參數我們就需要分辨。每一個主站可以帶的站地址為0到255,但0和255已有定義,所以實際是1到254個。所以我們使用一個256位的變量,每位對應站號來標志其是否有需要寫的請求。記錄于主站,具體如下:
而每個從站的寫參數請求標志則存儲于各個從站對象,因為不同的從站可能有很大區別,存儲于各個從站更加靈活。
3、如何實現
我們已經設計了我們的更新,但具體如何實現它呢?我們主要從以下幾個方面來實現它。第一,實現主站對象類型和從站對象類型。第二,主站對象的實例化及從站對象的實例化。第三,讀從站的主站操作過程。第四,寫從站的主站操作過程。接下來我們將一一描述之。
3.1、定義對象類型
在Modbus RTU協議棧的封裝中,我們需要定義主站對象和從站對象,自然也需要定義這兩種類型。至于其功能前述已經描述過。
首先我們來定義本地主站的類型,其成員包括:一個uint32_t的寫從站標志數組;從站數量字段;從站順序字段;本主站所管理的從站列表;4個數據更新函數指針。具體定義如下:
/* 定義本地RTU主站對象類型 */ typedef struct LocalRTUMasterType{uint32_t flagWriteSlave[8];?? //寫一個站控制標志位,最多256個站,與站地址對應。uint16_t slaveNumber;???????? //從站列表中從站的數量uint16_t readOrder;?????????? //當前從站在從站列表中的位置RTUAccessedSlaveType *pSlave;???????? //從站列表UpdateCoilStatusType pUpdateCoilStatus;?????? //更新線圈量函數UpdateInputStatusType pUpdateInputStatus;???? //更新輸入狀態量函數UpdateHoldingRegisterType pUpdateHoldingRegister;???? //更新保持寄存器量函數UpdateInputResgisterType pUpdateInputResgister;?????? //更新輸入寄存器量函數 }RTULocalMasterType;關于主站對象類型,在前面的更新設計中已經講的很清楚了,只有兩個需要說明一下。第一,從站列表是用來記錄本主站所管理的從站對象。第二,readOrder字段表示為當前訪問從站在列表中的位置,而slaveNumber是從站對象的數量,即列表的長度。具體如下圖所示:
還需要定義從站對象,此從站對象只是便于主站而用于表示真實的從站。主站的從站列表中就是此對象。具體結構如下:
/* 定義被訪問RTU從站對象類型 */ typedef struct AccessedRTUSlaveType{uint8_t stationAddress;?????? //站地址uint8_t cmdOrder;???????????? //當前命令在命令列表中的位置uint16_t commandNumber;?????? //命令列表中命令的總數uint8_t (*pReadCommand)[8];?? //讀命令列表uint8_t *pLastCommand;??????? //上一次發送的命令uint32_t flagPresetCoil;????? //預置線圈控制標志位uint32_t flagPresetReg;?????? //預置寄存器控制標志位 }RTUAccessedSlaveType;關于從站對象有兩個字段需要說一下,就是flagPresetCoil和flagPresetReg字段。這兩個字段用來表示對線圈和保持寄存器的寫請求。
3.2、實例化對象
我們定義了主站即從站對象類型,我們在使用時就需要實例化這些對象。一般來說一個硬件端口我們將其實例化為一個主站對象。
RTULocalMasterType hgraMaster;
/*初始化RTU主站對象*/
InitializeRTUMasterObject(&hgraMaster,2,hgraSlave,NULL,NULL,NULL,NULL);
而一個主站對象會管理1到254個從站對象,所以我們可以將多個從站對象實例組成數組,并將其賦予主站管理。
RTUAccessedSlaveType hgraSlave[]={{1,0,2,slave1ReadCommand,NULL,0x00,0x00},{2,0,2,slave2ReadCommand,NULL,0x00,0x00}};
所以,根據主站和從站實例化的條件,我們需要先實例化從站對象才能完整實例化主站對象。在主站的初始化中,我們這里將4的數據處理函數指針初始化為NULL,有一個默認的處理函數會復制給它,該函數是上一版本的延續,在簡單應用時簡化操作。從站的上一個發送的命令指針也被賦值為NULL,因為初始時還沒有命令發送。
3.3、讀從站操作
讀從站操作原理上與以前的版本是一樣的。按照一定的順序給從站發送命令再對收到的消息進行解析。我們對主站及其所管理的從站進行了定義,將發送命令保存于從站對象,將從站列表保存于主站對象,所以我們需要對解析函數進行修改。
/*解析收到的服務器相應信息*/ /*uint8_t *recievedMessage,接收到的消息列表*/ /*uint8_t *command,發送的讀操作命令,若為NULL則在命令列表中查找*/ void ParsingSlaveRespondMessage(RTULocalMasterType *master,uint8_t *recievedMessage,uint8_t *command) {int i=0;int j=0;uint16_t startAddress;uint16_t quantity;uint8_t *cmd=NULL;/*如果不是讀操作的反回信息不需要處理*/if(recievedMessage[1]>0x04){return;}/*判斷功能碼是否有誤*/FunctionCode fuctionCode=(FunctionCode)recievedMessage[1];if (CheckFunctionCode(fuctionCode) != MB_OK){return;}/*校驗接收到的信息是否有錯*/uint16_t byteCount=recievedMessage[2];bool chechMessageNoError=CheckRTUMessageIntegrity(recievedMessage,byteCount+5);if(!chechMessageNoError){return;}if((command==NULL)||(!CheckMessageAgreeWithCommand(recievedMessage,command))){while(i<master->slaveNumber){if(master->pSlave[i].stationAddress==recievedMessage[0]){break;}i++;}if(i>=master->slaveNumber){return;}if((master->pSlave[i].pLastCommand==NULL)||(!CheckMessageAgreeWithCommand(recievedMessage,master->pSlave[i].pLastCommand))){j=FindCommandForRecievedMessage(recievedMessage,master->pSlave[i].pReadCommand,master->pSlave[i].commandNumber);if(j<0){return;}cmd=master->pSlave[i].pReadCommand[j];}else{cmd=master->pSlave[i].pLastCommand;}}else{cmd=command;}startAddress=(uint16_t)cmd[2];startAddress=(startAddress<<8)+(uint16_t)cmd[3];quantity=(uint16_t)cmd[4];quantity=(quantity<<8)+(uint16_t)cmd[5];if((fuctionCode>=ReadCoilStatus)&&(fuctionCode<=ReadInputRegister)){HandleSlaveRespond[fuctionCode-1](master,recievedMessage,startAddress,quantity);} }解析函數的主要部分是在檢查接收到的消息是否是合法的Modbus RTU消息。檢查沒問題則調用協議站解析。而最后調用的數據處理函數則是我們需要在具體應用中編寫。在前面主站初始化時,回調函數我們初始化為NULL,實際在協議棧中有弱化的函數定義,需要針對具體的寄存器和變量地址實現操作。
3.4、寫從站操作
寫從站操作則是在其它進程請求后,我們標識需要寫的對象再統一處理。對具體哪個從站的寫標識存于主站實例。而該從站的哪些變量需要寫則記錄在從站實例中。
所以在進程檢測到需要寫一個從站時則置位對應的位,即改變flagWriteSlave中的對應位。而需要寫該站的哪些變量則標記flagPresetCoil和flagPresetReg的對應位。修改這些標識都在其它請求更改的進程中實現,而具體的寫操作則在本主站進程中,檢測到標志位的變化統一執行。
這部分不修改協議棧的代碼,因為各站及各變量都至于具體對象相關聯,所以在具體的應用中修改。
4、回歸驗證
為了驗證我們前面的更新設計是符合要求的,我們設計一個難度較高的實驗系統。這一實驗系統包括Modbus網關,上位Modbus主站以及下位的Modbus從站。我們所要實現的是Modbus網關部分,其具體結構圖設計如下:
從上圖我們知道,該Modbus網關需要實現一個Modbus從站用于和上位的通訊;需要實現兩個Modbus主站用于和下位的通訊。
在這個實驗中,讀操作沒有什么需要說的,只需要發送命令,解析返回消息即可。所以我們重點描述一下寫操作,為了方便操作,在需要寫的連續段,我們只要找到第一個請求寫的位置后,就將后續連續可寫數據一次性寫入。修改寫標志位的代碼如下:
/* 寫從站寄存器控制 */ static void WriteSlaveRegisterControll(uint16_t startAddress,uint16_t endAddress) {if((12<=startAddress)&&(startAddress<=71)&&(12<=endAddress)&&(endAddress<=71)){ModifyWriteRTUSlaveEnableFlag(&hgraMaster,hgraMaster.pSlave[0].stationAddress,true);if((startAddress<=12)&&(13<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x01;}if((startAddress<=14)&&(15<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x02;}if((startAddress<=16)&&(17<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x04;}if((startAddress<=18)&&(19<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x08;}if((startAddress<=20)&&(21<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x10;}if((startAddress<=22)&&(23<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x20;}if((startAddress<=24)&&(25<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x40;}if((startAddress<=26)&&(27<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x80;}if((startAddress<=32)&&(32<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x100;}if((startAddress<=33)&&(33<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x200;}if((startAddress<=34)&&(34<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x400;}if((startAddress<=35)&&(35<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x800;}if((startAddress<=36)&&(36<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x1000;}if((startAddress<=37)&&(37<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x2000;}if((startAddress<=38)&&(38<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x4000;}if((startAddress<=39)&&(39<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x8000;}if((startAddress<=40)&&(40<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x10000;}if((startAddress<=41)&&(41<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x20000;}if((startAddress<=42)&&(42<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x40000;}if((startAddress<=43)&&(43<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x80000;}if((startAddress<=44)&&(44<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x100000;}if((startAddress<=45)&&(45<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x200000;}if((startAddress<=46)&&(46<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x400000;}if((startAddress<=47)&&(47<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x800000;}if((startAddress<=52)&&(55<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x1000000;}if((startAddress<=56)&&(59<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x2000000;}if((startAddress<=60)&&(63<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x4000000;}if((startAddress<=64)&&(67<=endAddress)){hgraMaster.pSlave[0].flagPresetReg|=0x8000000;}}if((72<=startAddress)&&(startAddress<=131)&&(72<=endAddress)&&(endAddress<=131)){ModifyWriteRTUSlaveEnableFlag(&hgraMaster,hgraMaster.pSlave[1].stationAddress,true);if((startAddress<=72)&&(73<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x01;}if((startAddress<=74)&&(75<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x02;}if((startAddress<=76)&&(77<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x04;}if((startAddress<=78)&&(79<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x08;}if((startAddress<=80)&&(81<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x10;}if((startAddress<=82)&&(83<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x20;}if((startAddress<=84)&&(85<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x40;}if((startAddress<=86)&&(87<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x80;}if((startAddress<=92)&&(92<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x100;}if((startAddress<=93)&&(93<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x200;}if((startAddress<=94)&&(94<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x400;}if((startAddress<=95)&&(95<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x800;}if((startAddress<=96)&&(96<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x1000;}if((startAddress<=97)&&(97<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x2000;}if((startAddress<=98)&&(98<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x4000;}if((startAddress<=99)&&(99<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x8000;}if((startAddress<=100)&&(100<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x10000;}if((startAddress<=101)&&(101<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x20000;}if((startAddress<=102)&&(102<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x40000;}if((startAddress<=103)&&(103<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x80000;}if((startAddress<=104)&&(104<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x100000;}if((startAddress<=105)&&(105<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x200000;}if((startAddress<=106)&&(106<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x400000;}if((startAddress<=107)&&(107<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x800000;}if((startAddress<=112)&&(115<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x1000000;}if((startAddress<=116)&&(119<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x2000000;}if((startAddress<=120)&&(123<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x4000000;}if((startAddress<=124)&&(127<=endAddress)){hgraMaster.pSlave[1].flagPresetReg|=0x8000000;}}if((132<=startAddress)&&(startAddress<=191)&&(131<=endAddress)&&(endAddress<=191)){ModifyWriteRTUSlaveEnableFlag(&hgpjMaster,hgpjMaster.pSlave[0].stationAddress,true);}if((192<=startAddress)&&(startAddress<=251)&&(192<=endAddress)&&(endAddress<=251)){ModifyWriteRTUSlaveEnableFlag(&hgpjMaster,hgpjMaster.pSlave[1].stationAddress,true);} }然后在主站對象的進程中檢測標志位,根據標志位的狀態來實現操作,具體的操作代碼很簡單,且不具普遍性,在此不貼出。
5、幾點注意
雖然我們對主站對象和從站對象進行了封裝,但我們在使用時人需要注意一些問題。
(1)、4個回調函數的定義,這4個回調函數用于處理從粘返回的信息,對應Modbus定義的四種數據,需要根據主站對象管理的從站情況來實現。
(2)、對于寫操作標識符,一般都是在請求進程置位,在主站對象所在的進程檢測并操作,然后復位。
告之:源代碼可上Github下載:https://github.com/foxclever/Modbus
歡迎關注:
總結
以上是生活随笔為你收集整理的Modbus协议栈实现Modbus RTU多主站支持的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PC软件开发技术之一:在WinCC中通过
- 下一篇: axios vue 回调函数_前端Vue