1. 程式人生 > >Modbus協議棧開發筆記之二:Modbus訊息幀的生成

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

歡迎關注: