Modbus協議棧開發筆記之二:Modbus訊息幀的生成
前面我們已經對Modbus的基本事務作了說明,也據此設計了我們將要實現的主從站的操作流程。這其中與Modbus直接相關的就是Modbus訊息幀的生成。Modbus訊息幀也是實現Modbus通訊協議的根本。
1、Modbus訊息幀分析
MODBUS協議在不同的物理鏈路上的訊息幀有一些差異,但我們分析一下就會發現,在這些不同的訊息幀中具有一下相同的部分,這對我們實現統一的資料操作非常重要,具體描述如下:
(1)、簡單協議資料單元
MODBUS協議定義了一個與基礎通訊層無關的簡單協議資料單元(PDU)。簡單協議資料單元的結構如下:
PDU是一個與具體的傳輸網路無關的部分,包含功能碼和資料。對於特定匯流排或網路上的 MODBUS 協議只是在PDU的基礎上在應用資料單元(ADU)上引入一些附加域。
資料單元部分的開發是最基本的部分,主要是2各方面的類容:一是生成客戶端(主站)訪問伺服器(從站)的命令部分;二是生成伺服器(從站)響應客戶端(主站)回覆部分。
(2)、RTU的應用資料單元
對於在序列鏈路上執行的Modbus協議,其應用資料單元(ADU)是在PDU的基礎上,在前面加上地址域,後面加上資料校驗。格式如下圖所示:
地址域就是所訪問從站的地址,為一個8位無符號數,取值0-255,但0和255有固定含義不能使用。CRC校驗採用的是CRC16校驗方式。
(3)、TCP的應用資料單元
在乙太網鏈路上執行的Modbus協議,其應用資料單元(ADU)是在PDU的基礎上新增上MBAP報文頭形成的,具體格式如下圖:
對於MBAP 報文頭,包括下列域:
域 |
長度 |
描述 |
客戶機 |
伺服器 |
事務元識別符號 |
2 個位元組 |
MODBUS 請求/響應事務處理的識別碼 |
客戶機啟動 |
伺服器從接收的請求中重新複製 |
協議識別符號 |
2 個位元組 |
0=MODBUS 協議 |
客戶機啟動 |
伺服器從接收的請求中重新複製 |
長度 |
2 個位元組 |
以下位元組的數量 |
客戶機啟動(請求) |
伺服器(響應)啟動 |
單元識別符號 |
1 個位元組 |
序列鏈路或其它總線上連線的遠端從站的識別碼 |
客戶機啟動 |
伺服器從接收的請求中重新複製 |
從上表中可知報文頭為 7 個位元組長:
事務處理識別符號:用於事務處理配對。在響應中,MODBUS 伺服器複製請求的事務處理識別符號。
協議識別符號:用於系統內的多路複用。通過值 0 識別 MODBUS 協議。
長度:長度域是下一個域的位元組數,包括單元識別符號和資料域。
單元識別符號:為了系統內路由,使用這個域。專門用於通過乙太網TCP-IP 網路和MODBUS序列鏈路之間的閘道器對MODBUS或MODBUS+序列鏈路從站的通訊。說的簡單點就是序列鏈路中的地址域。MODBUS客戶機在請求中設定這個域,在響應中伺服器必須利用相同的值返回這個域。
2、資料幀的具體組成分析
從以上對簡單協議基本資料元、RTU應用資料單元和TCP應用資料單元報文格式的分析,我們發現對於基本資料單元部分已一致的,所以我們可以考慮來分層封裝協議操作部分:
最開始實現Modbus基本資料單元,這是資料公用部分與具體的應用無關,只需要封裝一次,對於這部分的開發只需要按照Modbus的標準協議來開發就好,本次我們計劃實現的功能有8個:
功能碼 |
名稱 |
實現 |
描述 |
0x01 |
讀線圈 |
是 |
對可讀寫型的狀態量進行讀取 |
0x02 |
讀離散輸入 |
是 |
對只讀型的狀態量進行讀取 |
0x03 |
讀保持暫存器 |
是 |
對可讀寫型的暫存器量進行讀取 |
0x04 |
讀輸入暫存器 |
是 |
對只讀型的暫存器量進行讀取 |
0x05 |
寫單個線圈 |
是 |
對單個的讀寫型的狀態量進行寫入 |
0x06 |
寫單個暫存器 |
是 |
對單個的讀寫型的暫存器量進行寫入 |
0x0F |
寫多個線圈 |
是 |
對多個的讀寫型的狀態量進行寫入 |
0x10 |
寫多個暫存器 |
是 |
對多個的讀寫型的暫存器量進行寫入 |
這8個也是Modbus協議所定義的最主要的功能,現在對這幾種功能碼的報文格式描述如下:
(1)讀線圈0x01
讀線圈就是都一種可以寫的開關量,因為Modbus協議起源於PLC應用,而線圈是對PLC的DO輸出的稱呼,一般適用於主站對從站下達操作命令。讀這種具有讀寫功能的狀態量的資料格式如下:
其下發的命令格式為:域名+功能碼+起始地址+數量。
(2)讀離散輸入0x02
讀狀態輸入是讀取一種只讀開關量訊號,對應於PLC中的數字輸入量。讀取這種只讀型開關輸入量的格式如下:
其下發的命令格式為:域名+功能碼+起始地址+數量。
(3)讀保持暫存器0x03
保持暫存器就是指可以讀寫的16位資料,通過單個或多個保持暫存器可以用來表示各種資料,如8位整數、16為整數、32位整數、64位整數以及單雙精度浮點數等。讀取保持暫存器的報文格式如下:
其下發的命令格式為:域名+功能碼+起始地址+數量。
(4)讀輸入暫存器0x04
輸入暫存器是一種只讀形式的16位資料。通過單個或多個輸入暫存器可以表示8位整數、16為整數、32位整數、64位整數以及單雙精度浮點數等。讀取輸入暫存器的報文格式如下:
其下發的命令格式為:域名+功能碼+起始地址+數量。
(5)寫單個線圈0x05
寫單個線圈量就是對單個的可讀寫的開關量進行操作,但是其並非是直接寫“0”或者“1”,而是在需要寫“1”時傳送0xFF00;而在需要寫“0”時傳送0x0000,其具體的報文格式如下:
其下發的命令格式為:域名+功能碼+輸出地址+輸出值。命令的具體內容與讀操作有區別但,格式卻是完全一樣,在程式設計時實際讀和寫可以封裝在一起。
(6)寫單個暫存器0x06
寫單個暫存器就是對單個的保持暫存器進行操作,資料的格式依然是一樣的,實際應用中只適用於對16位整型資料的操作,對於浮點數等則不可以。
其下發的命令格式為:域名+功能碼+輸出地址+輸出值。命令的具體內容與讀操作有區別但,格式卻是完全一樣,在程式設計時實際讀和寫可以封裝在一起。
(7)寫多個線圈0x0F
寫多個線圈的操作物件與寫單個線圈是完全一樣的,不同的是數量和操作值,特別是值,寫“1”就是“1”,寫“0”就是 “0”,這是與寫單個線圈的區別。
其下發的命令格式為:域名+功能碼+起始地址+輸出數量+位元組數+輸出值。命令報文與前面的幾種讀寫操作有較大的區別,必須要單獨處理。
(8)寫多個暫存器0x10
寫多個暫存器的就是對多個可讀寫暫存器同時進行操作,資料報文的格式與寫多個線圈是一致的。
其下發的命令格式為:域名+功能碼+起始地址+輸出數量+位元組數+輸出值。
3、基本資料單元的程式設計
經過上面的分析,我們發現不論是在什麼樣的物理鏈路上實現的應用資料,器基本資料段都是相同的。其實加上域名段的格式也是相同的,所以我們就將
域名+PDU一起作為最基本的資料單元來實現。
對於基本資料單元的實現由分為2種情況:一是作為主站(客戶端)時,對從站(伺服器)的下發命令;二是作為從站(伺服器)時,對主站(客戶端)命令的響應。所以我們將這兩種情況分別封裝為2個基礎函式:
(1)、作為RTU主站(TCP客戶端)時,生成讀寫RTU從站(TCP伺服器)物件的命令:
uint16_t GenerateReadWriteCommand(ObjAccessInfo objInfo,bool *statusList,uint16_t *registerList,uint8_t *commandBytes)
引數分別是PDU單元的基本資訊,寫物件的對應資料,以及生成的命令位元組。而返回值則是生成的命令的長度。
(2)、作為從站(伺服器)時,生成主站讀訪問的響應。對於響應因為寫操作的響應實際上就是複製主站(客戶端)的命令的一部分,所以我們實際需要生成的響應是包括0x01、0x02、0x03、0x04功能碼的情形。
uint16_t GenerateMasterAccessRespond(uint8_t *receivedMessage,bool *statusList,uint16_t *registerList,uint8_t *respondBytes)
引數分別是接收到的資訊,讀取的物件的資料,以及返回的響應訊息。而返回值則是返回的響應訊息的長度。
4、RTU應用資料單元的程式設計
對於RTU應用資料單元來說,其報文格式就是:“域名+PDU+CRC”,而域名+PDU我們在上一節中已經實現了,所以要實現RTU的資料單元實際上我們只需要加上CRC校驗就已經完成了。
對於RTU資料單元的實現由分為2種情況:一是作為主站時,對從站的下發命令;二是作為從站時,對主站命令的響應。所以我們將這兩種情況分別封裝為2個基礎函式:
(1)、作為RTU主站時,生成讀寫RTU從站物件的命令:
/*生成讀寫從站資料物件的命令,命令長度包括2個校驗位元組*/
uint16_t SyntheticReadWriteSlaveCommand(ObjAccessInfo slaveInfo,bool *statusList,uint16_t *registerList,uint8_t *commandBytes)
引數分別是從站基本資訊,下發的資料列表,以及最終生成的命令陣列。返回值是是命令的長度。
(2)、作為從站時,生成主站讀訪問的響應:
/*生成從站應答主站的響應*/
uint16_t SyntheticSlaveAccessRespond(uint8_t *receivedMessage,bool *statusList,uint16_t *registerList,uint8_t *respondBytes)
引數分別是接收到的資訊,返回的資料列表,生成的響應資訊列表。返回值是響應資訊列表的長度。
5、TCP應用資料單元的程式設計
而對於TCP應用資料單元來說,與RTU類式,起報文格式是:“MBAP頭+PDU”,而PDU單元就是前面定義的,所以只需要加上MBAP頭部就可以了,事實上MBAP頭部的實現格式是固定的。
對於TCP應用資料單元的實現同樣分為2中情況:一是作為客戶端時,對伺服器的下發命令;二是作為伺服器時,對客戶端命令的響應。所以我們將這兩種情況分別封裝為2個基礎函式:
(1)、作為TCP客戶端時,生成讀寫TCP伺服器物件的命令:
/*生成讀寫伺服器物件的命令*/
uint16_t SyntheticReadWriteTCPServerCommand(ObjAccessInfo objInfo,bool *statusList,uint16_t *registerList,uint8_t *commandBytes)
(2)、作為(伺服器時,生成客戶端讀寫訪問的響應:
/*合成對伺服器訪問的響應,返回值為命令長度*/
uint16_t SyntheticServerAccessRespond(uint8_t *receivedMessage,bool *statusList,uint16_t *registerList,uint8_t *respondBytes)
6、結束語
其實到這裡我們對Modbus基本協議已經基本實現,甚至使用這些基本操作也能實現Modbus的通訊。事實上很多人在應用寫的Modbus通訊協議比這還要簡單,也能實現部分的Modbus通訊功能。當然這不是我們的目標,否則就不需要專門開發庫了,我們要進一步封裝,讓其更通用也更易用才是我們需要的。
原始碼網址是:https://github.com/foxclever/Modbus
歡迎關注: