實現Modbus ASCII多主站應用

  前面咱們已經分析了Modbus RTU的更新設計和具體實現(若是不清楚可查看前一篇文章)。其實Modbus ASCII與Modbus RTU都是基於串行鏈路實現的,因此有不少的共同點,基於此,這篇文章咱們只討論與Modbus RTU所不一樣的部分。git

1、更新設計github

  關於原來的協議棧在Modbus ASCII主站應用時所存在的侷限性與Modbus RTU也是同樣的,因此咱們不分析它的不足,只討論更新設計。咱們將主站及其所訪問的從站定義爲通用的對象,而當咱們在具體應用中使用時,再將其特例化爲特定的主站和從站對象。數組

  首先咱們來考慮主站,原則上咱們規劃的每個主站對象對應咱們設備上的一個端口,這裏所說端口就是指串口。那麼在同一端口下,也就是在一個特定主站下,咱們能夠定義多個地址不一樣的從站。而在不一樣的端口上能夠具備地址相同的從站。以下圖所示:服務器

 

  從上圖中咱們能夠發現,咱們的目的就是讓協議棧支持,多主站和多從站,而且在不一樣主站下,從站的地址重複不受影響。從上圖看視乎一個主站對象能夠同時管理254個從站對象,事實上還要受到帶載能力的影響。網絡

  接下來咱們還須要考慮從站對象。主站對從站的操做無非兩類:讀從站信息和寫從站信息。對於讀從站信息來講,主站須要發送請求命令,等待從站返回響應信息,而後主站解析收到的信息並更新對應的參數值。有兩點須要咱們考慮,第一返回的響應消息是沒有對應的寄存器地址的,因此要想在解析的時候定位寄存器就必須知道發送的命令,爲了便於分辨咱們將命令存放在從站對象中。第二在解析響應時,若是兩條命令的響應相似是無法分辨的,因此咱們還須要記住上一條命令是什麼。也存儲於從站對象中。函數

  而對於寫從站操做,不管寫的要求來自於哪裏,對於協議棧來講確定是其它的數據處理進程發過來的,所接到要求後咱們須要記錄是哪個主站管理的哪個從站的哪些參數。對於主站咱們不須要分辨,由於每一個主站都是獨立的處理進程,可是對於從站和參數咱們就須要分辨。每個主站能夠帶的站地址爲0到255,但0和255已有定義,因此實際是1到254個。因此咱們使用一個256位的變量,每位對應站號來標誌其是否有須要寫的請求。記錄於主站,具體以下:ui

 

  事實上,咱們不可能會用到256個標誌位,由於Modbus ASCII自己就是爲簡單應用而設定的。咱們使用256個標誌位,主要是考慮到站地址的取值範圍,方便軟件操做而定的。還有每一個從站的寫參數請求標誌,咱們將其存儲於各個從站對象,由於不一樣的從站可能有很大區別,存儲於各個從站更加靈活方便。編碼

2、編碼實現spa

  咱們已經設計了咱們的更新,接下來咱們就根據這一設計來實現它。咱們主要從如下幾個方面來操做:第一,實現主站對象類型和從站對象類型;第二,主站對象的實例化及從站對象的實例化;第三,讀從站的主站操做過程;第四,寫從站的主站操做過程。接下來咱們將一一描述之。設計

2.1、定義對象類型

  與在Modbus RTU同樣,在Modbus ASCII協議棧的封裝中,咱們也須要定義主站對象和從站對象,天然也免不了要定義這兩種類型。

首先咱們來定義本地主站的類型,其成員包括:一個uint32_t的寫從站標誌數組;從站數量字段;從站順序字段;本主站過管理的從站列表;4個數據更新函數指針。具體定義以下:

 1 /* 定義本地ASCII主站對象類型 */
 2 typedef struct LocalASCIIMasterType{
 3   uint32_t flagWriteSlave[8];   //寫一個站控制標誌位,最多256個站,與站地址對應。
 4   uint16_t slaveNumber;         //從站列表中從站的數量
 5   uint16_t readOrder;           //當前從站在從站列表中的位置
 6   ASCIIAccessedSlaveType *pSlave;         //從站列表
 7   UpdateCoilStatusType pUpdateCoilStatus;       //更新線圈量函數
 8   UpdateInputStatusType pUpdateInputStatus;     //更新輸入狀態量函數
 9   UpdateHoldingRegisterType pUpdateHoldingRegister;     //更新保持寄存器量函數
10   UpdateInputResgisterType pUpdateInputResgister;       //更新輸入寄存器量函數
11 }ASCIILocalMasterType;

  關於主站對象類型,在前面的更新設計中已經講的很清楚了,只有兩個字段須要說明一下。第一,從站列表是用來記錄本主站所管理的從站對象。第二,readOrder字段表示爲當前訪問從站在列表中的位置,而slaveNumber是從站對象的數量,即列表的長度。具體以下圖所示:

 

  還須要定義從站對象,此從站對象只是便於主站而用於表示真是的從站。主站的從站列表中就是此對象。具體結構以下:

 1 /* 定義被訪問ASCII從站對象類型 */
 2 typedef struct AccessedASCIISlaveType{
 3   uint8_t stationAddress;       //站地址
 4   uint8_t cmdOrder;             //當前命令在命令列表中的位置
 5   uint16_t commandNumber;       //命令列表中命令的總數
 6   uint8_t (*pReadCommand)[17];   //讀命令列表
 7   uint8_t *pLastCommand;        //上一次發送的命令
 8   uint32_t flagPresetCoil;      //預置線圈控制標誌位
 9   uint32_t flagPresetReg;       //預置寄存器控制標誌位
10 }ASCIIAccessedSlaveType;

  關於從站對象有三個字段須要說明一下。首先咱們來看一看「讀命令列表(uint8_t (*pReadCommand)[17])」字段,與Modbus RTU不一樣,它是17個字節,這是由Modbus ASCII消息格式決定的。以下:

 

  還有就是flagPresetCoil和flagPresetReg字段。這兩個字段用來表示對線圈和保持寄存器的寫請求。

2.2、實例化對象

  咱們定義了主站即從站對象類型,咱們在使用時就須要實例化這些對象。通常來講一個硬件端口咱們將其實例化爲一個主站對象。

ASCIILocalMasterType hgraMaster;

/*初始化ASCII主站對象*/

InitializeASCIIMasterObject(&hgraMaster,2,hgraSlave,NULL,NULL,NULL,NULL);

  而一個主站對象會管理1到254個從站對象,因此從站對象咱們能夠將多個從站對象實例組成數組,並將其賦予主站管理。

ASCIIAccessedSlaveType hgraSlave[]={{1,0,2,slave1ReadCommand,NULL,0x00,0x00},{2,0,2,slave2ReadCommand,NULL,0x00,0x00}};

  因此,根據主站和從站實例化的條件,咱們須要先實例化從站對象才能完整實例化主站對象。在主站的初始化中,咱們這裏將4的數據處理函數指針初始化爲NULL,有一個默認的處理函數會複製給它,該函數是上一版本的延續,在簡單應用時簡化操做。從站的上一個發送的命令指針也被賦值爲NULL,由於初始時尚未命令發送。

2.3、讀從站操做

  讀從站操做原理上與之前的版本是同樣的。按照必定的順序給從站發送命令再對收到的消息進行解析。咱們對主站及其所管理的從站進行了定義,將發送命令保存於從站對象,將從站列表保存於主站對象,因此咱們須要對解析函數進行修改。

 1 /*解析收到的服務器相應信息*/
 2 void ParsingAsciiSlaveRespondMessage(AsciiLocalMasterType *master,uint8_t *recievedMessage, uint8_t *command,uint16_t rxLength)
 3 {
 4     int i=0;
 5     int j=0;
 6     uint8_t *cmd=NULL;
 7    
 8     /*判斷是否爲Modbus ASCII消息*/
 9     if (0x3A != recievedMessage[0])
10     {
11         return ;
12     }
13  
14     /*判斷消息是否接收完整*/
15     if ((rxLength < 17) || (recievedMessage[rxLength - 2] != 0x0D) || (recievedMessage[rxLength - 1] != 0x0A))
16     {
17         return ;
18     }
19  
20     uint16_t length = rxLength - 3;
21     uint8_t hexMessage[256];
22  
23     if (!CovertAsciiMessageToHex(recievedMessage + 1, hexMessage, length))
24     {
25         return ;
26     }
27    
28     /*校驗接收到的數據是否正確*/
29     if (!CheckASCIIMessageIntegrity(hexMessage, length/2))
30     {
31         return ;
32     }
33    
34     /*判斷功能碼是否有誤*/
35     FunctionCode fuctionCode = (FunctionCode)hexMessage[1];
36     if (CheckFunctionCode(fuctionCode) != MB_OK)
37     {
38         return;
39     }
40  
41     if ((command == NULL)||(!CheckMessageAgreeWithCommand(recievedMessage, command)))
42     {
43         while(i<master->slaveNumber)
44         {
45             if(master->pSlave[i].stationAddress==hexMessage[0])
46             {
47                 break;
48             }
49             i++;
50         }
51        
52         if(i>=master->slaveNumber)
53         {
54             return;
55         }
56    
57         if((master->pSlave[i].pLastCommand==NULL)||(!CheckMessageAgreeWithCommand(recievedMessage,master->pSlave[i].pLastCommand)))
58         {
59             j=FindAsciiCommandForRecievedMessage(recievedMessage,master->pSlave[i].pReadCommand,master->pSlave[i].commandNumber);
60      
61             if(j<0)
62             {
63                 return;
64             }
65      
66             cmd=master->pSlave[i].pReadCommand[j];
67         }
68         else
69         {
70             cmd=master->pSlave[i].pLastCommand;
71         }
72     }
73     else
74     {
75         cmd=command;
76     }
77  
78     uint8_t hexCommand[256];
79     CovertAsciiMessageToHex(cmd + 1, hexCommand, 14);
80  
81     uint16_t startAddress = (uint16_t)hexCommand[2];
82     startAddress = (startAddress << 8) + (uint16_t)hexCommand[3];
83     uint16_t quantity = (uint16_t)hexCommand[4];
84     quantity = (quantity << 8) + (uint16_t)hexCommand[5];
85  
86     if ((fuctionCode >= ReadCoilStatus) && (fuctionCode <= ReadInputRegister))
87     {
88         HandleAsciiSlaveRespond[fuctionCode - 1](master,hexMessage,startAddress,quantity);
89     }
90 }

  解析函數的主要部分是在檢查接收到的消息是不是合法的Modbus ASCII消息。檢查沒問題則調用協議站解析。而最後調用的數據處理函數則是咱們須要在具體應用中編寫。在前面主站初始化時,回調函數咱們初始化爲NULL,實際在協議佔中有弱化的函數定義,須要針對具體的寄存器和變量地址實現操做。特別要說明的是,解析Modbus ASCII消息時,在去除開始字符和結束字符後,須要將ASCII碼轉化爲二進制數才能完成解析。

2.4、寫從站操做

  寫從站操做則是在其它進程請求後,咱們標識須要寫的對象再統一處理。對具體哪一個從站的寫標識存於主站實例。而該從站的哪些變量須要寫則記錄在從站實例中。

  因此在進程檢測到須要寫一個從站時則置位對應的位,即改變flagWriteSlave中的對應位。而須要寫該站的哪些變量則標記flagPresetCoil和flagPresetReg的對應位。修改這些標識都在其它請求更改的進程中實現,而具體的寫操做則在本主站進程中,檢測到標誌位的變化統一執行。

  這部分不修改協議棧的代碼,由於各站及各變量都至於具體對象相關聯,因此在具體的應用中修改。

3、迴歸驗證

  考慮到Modbus ASCII和Modbus RTU的類似性,咱們設計一樣的的網絡結構。但考慮到Modbus ASCII通常用於小數據量通信,因此咱們設計相對簡單的從站。因此咱們設計的網絡爲:協議棧創建2個主機,每一個主機管理2個從站,每一個從站有8個線圈及2個保持寄存器。具體結構如圖:

 

  從上圖咱們知道,該Modbus網關須要實現一個Modbus從站用於和上位的通信;須要實現兩個Modbus主站用於和下位的通信。

  在這個實驗中,讀操做沒有什麼須要說的,只須要發送命令解析返回消息便可。因此咱們中點描述一下爲了方便操做,在須要寫的連續段,咱們只要找到第一個請求寫的位置後,就將後續連續可寫數據一次性寫入。修改寫標誌位的代碼以下:

  1 /* 修改從站線圈量使能控制 */
  2 static void PresetSlaveCoilControll(uint16_t startAddress,uint16_t endAddress)
  3 {
  4   if((8<=startAddress)&&(startAddress<=15)&&(8<=endAddress)&&(endAddress<=15))
  5   {
  6     ModifyWriteRTUSlaveEnableFlag(&hgraMaster,hgraMaster.pSlave[0].stationAddress,true);
  7    
  8     if((startAddress<=8)&&(8<=endAddress))
  9     {
 10       hgraMaster.pSlave[0].flagPresetCoil|=0x01;
 11     }
 12     if((startAddress<=9)&&(9<=endAddress))
 13     {
 14       hgraMaster.pSlave[0].flagPresetCoil|=0x02;
 15     }
 16     if((startAddress<=10)&&(10<=endAddress))
 17     {
 18       hgraMaster.pSlave[0].flagPresetCoil|=0x04;
 19     }
 20     if((startAddress<=11)&&(11<=endAddress))
 21     {
 22       hgraMaster.pSlave[0].flagPresetCoil|=0x08;
 23     }
 24     if((startAddress<=12)&&(12<=endAddress))
 25     {
 26       hgraMaster.pSlave[0].flagPresetCoil|=0x10;
 27     }
 28     if((startAddress<=13)&&(13<=endAddress))
 29     {
 30       hgraMaster.pSlave[0].flagPresetCoil|=0x20;
 31     }
 32     if((startAddress<=14)&&(14<=endAddress))
 33     {
 34       hgraMaster.pSlave[0].flagPresetCoil|=0x40;
 35     }
 36     if((startAddress<=15)&&(15<=endAddress))
 37     {
 38       hgraMaster.pSlave[0].flagPresetCoil|=0x80;
 39     }
 40   }
 41  
 42   if((16<=startAddress)&&(startAddress<=23)&&(16<=endAddress)&&(endAddress<=23))
 43   {
 44     ModifyWriteRTUSlaveEnableFlag(&hgraMaster,hgraMaster.pSlave[1].stationAddress,true);
 45     if((startAddress<=16)&&(16<=endAddress))
 46     {
 47       hgraMaster.pSlave[1].flagPresetCoil|=0x01;
 48     }
 49     if((startAddress<=17)&&(17<=endAddress))
 50     {
 51       hgraMaster.pSlave[1].flagPresetCoil|=0x02;
 52     }
 53     if((startAddress<=18)&&(18<=endAddress))
 54     {
 55       hgraMaster.pSlave[1].flagPresetCoil|=0x04;
 56     }
 57     if((startAddress<=19)&&(19<=endAddress))
 58     {
 59       hgraMaster.pSlave[1].flagPresetCoil|=0x08;
 60     }
 61     if((startAddress<=20)&&(20<=endAddress))
 62     {
 63       hgraMaster.pSlave[1].flagPresetCoil|=0x10;
 64     }
 65     if((startAddress<=21)&&(21<=endAddress))
 66     {
 67       hgraMaster.pSlave[1].flagPresetCoil|=0x20;
 68     }
 69     if((startAddress<=22)&&(22<=endAddress))
 70     {
 71       hgraMaster.pSlave[1].flagPresetCoil|=0x40;
 72     }
 73     if((startAddress<=23)&&(23<=endAddress))
 74     {
 75       hgraMaster.pSlave[1].flagPresetCoil|=0x80;
 76     }
 77   }
 78  
 79   if((24<=startAddress)&&(startAddress<=31)&&(24<=endAddress)&&(endAddress<=31))
 80   {
 81     ModifyWriteRTUSlaveEnableFlag(&hgpjMaster,hgpjMaster.pSlave[0].stationAddress,true);
 82     if((startAddress<=24)&&(24<=endAddress))
 83     {
 84       hgpjMaster.pSlave[0].flagPresetCoil|=0x01;
 85     }
 86     if((startAddress<=25)&&(25<=endAddress))
 87     {
 88       hgpjMaster.pSlave[0].flagPresetCoil|=0x02;
 89     }
 90     if((startAddress<=26)&&(26<=endAddress))
 91     {
 92       hgpjMaster.pSlave[0].flagPresetCoil|=0x04;
 93     }
 94     if((startAddress<=27)&&(27<=endAddress))
 95     {
 96       hgpjMaster.pSlave[0].flagPresetCoil|=0x08;
 97     }
 98     if((startAddress<=28)&&(28<=endAddress))
 99     {
100       hgpjMaster.pSlave[0].flagPresetCoil|=0x10;
101     }
102     if((startAddress<=29)&&(29<=endAddress))
103     {
104       hgpjMaster.pSlave[0].flagPresetCoil|=0x20;
105     }
106     if((startAddress<=30)&&(30<=endAddress))
107     {
108       hgpjMaster.pSlave[0].flagPresetCoil|=0x40;
109     }
110     if((startAddress<=31)&&(31<=endAddress))
111     {
112       hgpjMaster.pSlave[0].flagPresetCoil|=0x80;
113     }
114   }
115  
116   if((32<=startAddress)&&(startAddress<=39)&&(32<=endAddress)&&(endAddress<=39))
117   {
118     ModifyWriteRTUSlaveEnableFlag(&hgpjMaster,hgpjMaster.pSlave[1].stationAddress,true);
119     if((startAddress<=32)&&(32<=endAddress))
120     {
121       hgpjMaster.pSlave[1].flagPresetCoil|=0x01;
122     }
123     if((startAddress<=33)&&(33<=endAddress))
124     {
125       hgpjMaster.pSlave[1].flagPresetCoil|=0x02;
126     }
127     if((startAddress<=34)&&(34<=endAddress))
128     {
129       hgpjMaster.pSlave[1].flagPresetCoil|=0x04;
130     }
131     if((startAddress<=35)&&(35<=endAddress))
132     {
133       hgpjMaster.pSlave[1].flagPresetCoil|=0x08;
134     }
135     if((startAddress<=36)&&(36<=endAddress))
136     {
137       hgpjMaster.pSlave[1].flagPresetCoil|=0x10;
138     }
139     if((startAddress<=37)&&(37<=endAddress))
140     {
141       hgpjMaster.pSlave[1].flagPresetCoil|=0x20;
142     }
143     if((startAddress<=38)&&(38<=endAddress))
144     {
145       hgpjMaster.pSlave[1].flagPresetCoil|=0x40;
146     }
147     if((startAddress<=39)&&(39<=endAddress))
148     {
149       hgpjMaster.pSlave[1].flagPresetCoil|=0x80;
150     }
151   }
152  
153 }

  與Modbus RTU同樣也是在請求修改進程中置位索要寫的從站的寫請求標誌位和對應參數的寫請求標誌位。而後在主站對象的進程中檢測標誌位,根據標誌位的狀態來實現操做。

告之:源代碼可上Github下載:https://github.com/foxclever/Modbus

歡迎關注:

 

相關文章
相關標籤/搜索