Modbus tcp 格式說明 通訊機制 附C#測試工具用於學習,測試


前言:


 之前的博客介紹了如何用C#來讀寫modbus tcp服務器的數據,文章:http://www.cnblogs.com/dathlin/p/7885368.html

當然也有如何創建一個服務器文章:http://www.cnblogs.com/dathlin/p/7782315.html

但是上面的兩篇文章是已經封裝好的API,只要調用就可以實現功能了,對於想了解modbus tcp的原理的人可能就不適合了,最近有不少網友的想要了解這個協議,所以在這里再寫一篇介紹Modbus tcp的文章,不過這篇文章是簡易版本的,未來我再研究深入的話,再開一篇高級版,在簡易版中,就略去了成功標志位及其他數據標志,這些到等到后面再說。

先分享一下,我自己學習的地址來源:http://blog.csdn.net/thebestleo/article/details/52269999  聲明:本文並非轉載,並非照搬原文章,是在我參照原博客的基礎上,理解了基本的modbus通訊,並結合自己的理解,重新寫一篇更好入門的文章,此處貼出原作者的帖子以示尊重知識產權,原文章有些地方有一點錯誤,而且早就停止更新了,也沒有提供方便的測試工具,官方的modbus 測試工具是試驗版本的,需要購買序列號才可以,所以此處提供我自己的測試工具,地址如下,下面的介紹的例子都是基於這個工具來實現的。

https://github.com/dathlin/ModBusTcpTools/raw/master/download/download.zip

關於該測試工具也是開放源代碼的,如果想要查看源代碼:https://github.com/dathlin/ModBusTcpTools

聯系作者及加群方式(激活碼在群里發放):http://www.hslcommunication.cn/Cooperation

 

准備條件:


在上面的測試工具下載之前,需要一些額外的知識補充,此處不管你是學習什么語言的,對於socket通信層來說,其實是一樣的,下面的講解的內容是直接基於底層的,無關語法的操作。

但是需要你對字節概念非常清晰,一般都是byte數組,一個byte有8個位,這個也要非常的清晰,如果連byte是什么都搞不清楚,那么對本文下面的內容理解會非常的吃力,那么還是建議你再看看計算機原理這些書,對於socket通信,每種語言都有不同的寫法,但是所有的語言都有兩個共同點,都能實現把數據發送到socket和從socket接收數據,至於這個如果去做,就參照你自己需要使用的語言了,此處不做這方面的說明了。

關於十六進制文本,在本文的下面的內容上,所有的byte字節數組都表示成十六進制形式,比如 FF 10 代表2個字節,一個是255,另一個是16。

byte[] temp = new byte[2];
temp[0] = 0xFF;
temp[1] = 0x10;

如果將上述的temp看作是讀取到的線圈的數據,那么轉換規則如下:

先將上述數據轉化成二進制 : 1111 1111    (第一個byte,我們從高位寫到地位)             0001 0000   (第二個byte,我們從高位寫到地位)

對應的線圈就是,線圈7-線圈0,,,,第二個byte對應的線圈是,線圈15-線圈8     這里一定要好好理解,從byte上來說,temp[0]是地位,temp[1]是高位,深入到每個byte里面的二進制,高位在前,低位在后。

在C#里等同於下面的代碼,和C語言,java也是非常的相近,還算比較好理解。

如果我說,發送 00 00 00 00 00 06 FF 01 00 00 00 01 到socket上去,那么也就是:

byte[] temp = new temp[12];
temp[0] = 0x00;
temp[1] = 0x00;
temp[2] = 0x00;
temp[3] = 0x00;
temp[4] = 0x00;
temp[5] = 0x06;
temp[6] = 0xFF;
temp[7] = 0x01;
temp[8] = 0x00;
temp[9] = 0x00;
temp[10] = 0x00;
temp[11] = 0x01;

socket.Send(temp);

先不要管上面的數據是什么含義,知道上面的代碼是啥含義就行了。接下來就是下載上面的測試工具,開始真正的學習modbus tcp協議了!

 

測試工具初始化


先運行Server.exe文件,端口里輸入502,然后點擊啟動服務即可,如下:

然后運行Client.exe程序,在Ip地址里輸入127.0.0.1,端口里輸入502,點擊配置即可,我們看到,如果你的服務器程序運行在了別的電腦上,甚至是雲端,只要客戶端的ip修改成服務器的ip,端口號對應上,就可以訪問到服務器的數據了。

特殊測試不用去管,和我們現在學習的東西不一致。

 

功能碼詳細解釋


對於modbus來說,涉及的功能碼也就是0x01,0x02,0x03,0x05,0x06,0x0F,0x10了,其實分類來說,就只有兩種,線圈和寄存器,也就是位讀寫和字讀寫,首先需要清楚的是功能碼不一樣,對應數據的解析規則也不一樣,下面就針對不同的情況來說。

首先說明的是,modbus協議呢,最終目的還是為了實現數據交互,既然是數據交互,那就是包含了數據讀和寫,我們把我們的想法轉化成一串數據,發送給設備(或者叫服務器),它返回一串數據,根據規則解析出來,這樣就得到了我們真正想要的數據。下面就來第一個想法實現吧。

另外,在modbus服務器端,數據是使用地址的方式來公開的,這很好理解,服務器端保存了很多數據,你想要訪問某個數據肯定需要指定唯一的身份標識,從連續的地址來區分數據是最常用的做法,不僅好理解,還便於擴展,比如你還可以讀取連續地址的數據塊。如果采用字符串名字來標識數據,就沒有這個特點。

對於位操作來說(各種線圈和離散量),一個地址代表了一個bool變量,即 0 和 1,要么通要么斷,就好比一些普通的開關。

對於寄存器來說,一個地址代表了2個byte,共有65536種方式,可以滿足大多數日常使用了,比如我們讀取地址0的寄存器,返回 00 00 及代表寄存器0數據為0,如果返回 01 00 ,那么代表寄存器0數據為 256 

 

功能碼0x01:


我不直接上一串數據,這樣看着也累,我們從例子出發,現在我們需要讀取線圈(離散量)操作,我想讀取地址0的線圈是否是通還是斷的。我們有了這個功能需求后,就可以根據需求來寫出特殊的指令了。根據協議指定,需要填寫長度為12的byte數組

byte[0]    byte[1]    byte[2]   byte[3]   byte[4]   byte[5]   byte[6]   byte[7]   byte[8]   byte[9]   byte[10]   byte[11]  

byte[0]    byte[1]             : 消息號---------隨便指定,服務器返回的數據的前兩個字和這個一樣

byte[2]   byte[3]              :modbus標識,強制為0即可

byte[4]   byte[5]               :指示排在byte[5]后面所有字節的個數,也就是總長度-6

byte[6]:                       站號,對於TCP協議來說,不重要,可以隨便指定,對於rtu及ascii來說,就需要選擇設備的站號信息。

byte[7] :                     功能碼,這里就需要填入我們的真正的想法了

byte[8]  byte[9]               :起始地址,比如我們想讀取地址0的數據,就填 00 00 ,如果我們想讀取地址1000的數據,怎么辦,填入 03 E8 ,也就是將1000轉化十六進制填進去。

byte[10]  byte[11]              :指定想讀取的數據長度,比如我們就想讀取地址0的一個數據,這里就寫 00 01,如果我們想讀取地址0-999共計一個數據的長度,就寫 03 E8。和起始地址是一樣的。

 

有了上面的格式之后,接下來我們就按照格式來填寫數據吧,我們需要讀取地址0的數據,那么指定如下

00 00 00 00 00 06 FF 01 00 00 00 01

消息號設為0,站號FF,功能碼01,地址01,長度01:將上面的指令在客戶端程序里進行輸入,點擊發送,這樣就在下面的響應框里接收到服務器反饋的數據,我們最終需要的信息就在反饋的數據里了。

前面是接收到數據的時間,自動忽略,那么返回的數據就是 00 00 00 00 00 04 FF 01 01 00 共計10個字節的數據,ok,這玩意到底是什么意思呢,我們來分別解析下:

byte[0]  byte[1] :             消息號,我們之前寫發送指令的時候,是多少,這里就是多少。

byte[2]  byte[3]:           必須都為0,代表這是modbus 通信

byte[4]  byte[5]:           指示byte[5]后面的所有字節數,你數數看是不是4個?所以這里是00 04,如果后面共有100個,那么這里就是 00 64

byte[6]:                        站號,之前我們寫了FF,那么這里也就是FF

byte[7]:                        功能碼,我們之前寫了01的功能碼,這里也是01,和我們發送的指令是一致的

byte[8]:                        指示byte[8]后面跟隨的字節數量,因為跟在byte[8]后面的就是真實的數據,我們最終想要的結果就在byte[8]后面

byte[9]:                        真實的數據,哈哈,這肯定就是我們真正想要的東西了,我們知道一個byte有8位,但是我們只讀取了一個位數據,所有這里的有效值只是byte[9]的最低位,二進制為 0000 0000 我們看到最低位為0,所以最終我們讀取的地址0的線圈為斷。

 

假設我們讀取地址10,開始的共10個線圈呢,那么會返回什么?所以我們發送 00 00 00 00 00 06 FF 01 00 0A 00 0A

我們接收到了:00 00 00 00 00 05 FF 01 02 79 01      前面的8個字節的信息參照上面的分析,是一致的,我們就針對后面三個字節着重分析。我們讀取了10個位,那么一個字節可以表示8個位,那么我們的結果至少需要2個byte才能表示完,所以最終的數據肯定是2個字節,那么02就是后面的字節數量,也就是真實的數據長度。

要想從 79 01 數據中分析出我們真實想要的數據,還需要經過最后一次數據轉換。先轉為二進制:

0111 1001           0000 0001

第二步:按每八位進行分割,上述其實已經分割好了,中間空格多的是分割,以字為單位,將二進制順序顛倒:

1001 1110         1000000

第三步:最終數據就是    線圈10-線圈19的通斷情況是:通,斷,斷,通,通,通,通,斷,通,斷     再后面的0都是無效的

至此我們獲取到了我們最終的數據!因為此處服務器都是0,所以所有的線圈都是斷,等會可以結合05功能碼寫線圈進行聯合測試。

 

功能碼0x02:


這個功能碼和上面的一致,在本服務器里不支持這個功能碼。發送和解析規則和上面的一致,不再贅述。

 

功能碼0x05:


我們先講解05功能碼,這個功能碼是實現數據寫入,它能實現什么功能呢,我們可以利用這個功能碼來指定某個線圈通或斷,具體怎么操作呢,有了之前01功能碼的經驗,下面的代碼看起來就順利多了。

比如我要指定地址0的寄存器為通: 00 00 00 00 00 06 FF 05 00 00 FF 00    前面的含義都是一致的,我們就分析 05 00 00 FF 00

05 是功能碼, 00 00 是我們指定的地址,如果我們想寫地址1000為通,那么就為 03 E8,至於FF 00是規定的數據,如果你想地址線圈通,就填這個值,想指定線圈為斷,就填 00 00 ,其他任何的值都對結果無效。

然后我們看看寫入的操作服務器返回了什么 ?  我們看到也是  00 00 00 00 00 06 FF 05 00 00 FF 00   因為在你寫入的操作中,是不帶讀取數據的,所以服務器會直接復制一遍你的指令並返回。

 

下面再舉例一些方便理解(我們只需要指定地址及是否通斷的情況即可):

寫入地址100為通: 00 00 00 00 00 06 FF 05 00 64 FF 00   

寫入地址1000為斷:00 00 00 00 00 06 FF 05 03 E8 00 00

 

功能碼0x0F:


我們已經實現了0x05來單個的線圈寫入,我們可以指定線圈100為通,其實就兩個信息需要指定,線圈地址是什么,通還是端,然后我們就可以自然而然的寫出指令碼了,但是現在我們需要實現一個功能時,將地址0-999共計1000個線圈全部為off,這怎么搞?

按照我們之前的經驗,可以發送一千次的0x05功能碼的指令來實現,大不了寫1000次么。。。。。(寫到第100次的時候估計已經吐血了)

所以我們就繼續研究有沒有其他的功能碼來實現,突然發現0x0F這一個神奇的功能碼,這個功能碼是什么意思呢,就是為了批量寫入而存在的,就比如上面的例子0-999都為off,那么指令是什么呢。

00 00 00 00 00 84 FF 0F 00 00 03 E8 7D ...(后面跟125個byte,都是00) 

上面的指令就實現了我們的需求,現在來詳細解釋下,它怎么就實現了我們的需求。分析之前,我們發現不同的功能碼,的前8個字節的規律是一模一樣了,都是標識號+modbus號+長度+站號,后面基本是跟地址和長度,或是直接是地址和數據。

00 00         消息標識號,隨便寫什么,反正你寫什么數據,服務器就復制一遍而已。

00 00         modbus標志號,都是00 就對了。

00 84         我們先轉化為十進制,0x0084轉化十進制就是132,也就是說,00 84(不包含00 84)后面跟了132個字節

FF              站號,其實也是隨便寫,反正服務器返回一樣的

00 00         起始地址,此處就是0,如果起始地址為100,那么就寫00 64,如果起始地址為1000,那么就寫03 E8

03 E8         我們需要寫的數據長度,因為我們需要寫1000個線圈,就是03 E8,如果我們寫999個線圈,那么就是03 E7

7D             這個字節代表后面跟隨的真實寫入的數據的長度,為125個字節。

125個字節  真實的數據,我們寫1000個位,那么一個字節為8位,那么剛好125個字節可以塞完數據,那么問題來了,如果我們想實現000-998共計999個地址都是off。那怎么搞。 

那么指令為 00 00 00 00 00 84 FF 0F 00 00 03 E7 7D ...(后面125個byte,都是00)咦,怎么還是125個,原來無論寫多少個,比如x個,如果是8的倍數,剛好x/8個byte,如果除不盡怎么辦,就是x/8+1個字節,這樣才能裝滿我們需要寫的數據。

既然后面都是125個字節,那么寫1000個還是999個,那么區分的關鍵就在於長度,03 E8還是03 E7。


大致的數據在上面已經說明了,具體怎么寫數據看下面,比如我們寫入地址10-地址19共計10個長度的線圈,要求的結果分別是,On,Off,Off,On,On,On,On,Off,On,Off,也就是 通,斷,斷,通,通,通,通,斷,通,斷,接下來轉換0和1,如下:

1001111010

接下來就是關鍵了,怎么轉化成真實的byte,這樣我們就可以最終寫出來指令了。

第一步:以8個8個為單位進行切割,結果為 10011110    10

第二步:第一步的字單位,每個單位前后順序顛倒,不然不足8位,前面補零,結果為   0111 1001          0000 0001

第三步:這下可以寫成真實的數據了,79 01

 

那么最接下來我們就可以寫最終的指令了,實現寫入地址10-19為:通,斷,斷,通,通,通,通,斷,通,斷  也即 00 00 00 00 00 09 FF 0F 00 0A 00 0A 02 79 01         (地址10-19的線圈分別為 通,斷,斷,通,通,通,通,斷,通,斷)

注意上述第二步為什么要順序顛倒,那是因為在計算機的單個byte存儲中,高位在前,地位在后,而對於多個連續的byte來說,地位在前,高位在后,所以需要顛倒,如果還是不明白,就先死記,終有一天會恍然大悟。

現在應該能實現任何的連續線圈的寫入了吧。


寫入之后,看看了服務器返回了什么:

00 00 00 00 00 06 FF 0F 00 0A 00 0A    :現在再來看這個數據就很簡單了,就是返回了我們寫入數據的前12分字節,然后把00 09長度更改為實際的長度 00 06,因為是寫入操作,所以返回的數據沒什么意義。

 

 

功能碼0x03:


該功能碼實現寄存器的數據讀取,我們需要知道的是,一個寄存器占2個byte,而且是高位在前,地位在后,那么如果寄存器0的數據為1000,那么我們讀取到的數據就是03 E8,這是我們最終想要的東西,03功能碼和01功能碼很接近,就是功能碼替換一下,返回的數據解析不一樣而已,比如我們需要讀取地址0的寄存器數據:

00 00 00 00 00 06 FF 03 00 00 00 01               是不是很熟悉?,當你看到這個的時候,腦力里馬上就是功能碼03,讀取寄存器,地址0,長度1        返回如下:

00 00 00 00 00 05 FF 03 02 03 E8                    主要就是看功能碼后面的數據了,我們想要的真實數據肯定藏在后面,也就是 02 03 E8,不是說一個寄存器返回2個字節嘛,怎么就變成3個了?事實上第一個字節不是代表數據,而是代表后面的字節長度是2個字節,那么03 E8就是真實的數據了,代表了寄存器0存儲了1000這個數據。

 

未完待續...

 

 

上述的說明有點問題,再經過了一段時間的深入理解,包括了modbus-tcp和modbus-rtu,還有modbus-ascii  重新整理

2019年1月22日 20:03:13  重新說明!


 

如果要講解modbus協議,首先要清楚什么是協議,通常來說,協議是用來實現數據交流的,再通常的說,人與人之間的聊天也是基於一個協議的,當一個人和你說某某地點有金礦,叫你去挖金礦的時候,就發生了數據信息的傳播,你就知道對方的意思了,你為什么能理解呢?因為兩者遵循同一種協議,這個協議就是中文,如果另一個人和你說英文,你就聽不懂了,這個是協議不通,如果你懂好幾種語言,那么你就兼容很多種協議。

 

如果要深入的將協議,那么必須要有載體,協議不是憑空存在的。是需要有載體的,載體是什么?比如語言是協議的話,喉嚨是發聲音的載體,是用來發送數據的,而耳朵是用來接收數據的載體。那么計算機協議的載體是什么?就是發送數據流和接收數據流,我們知道計算機的世界都是二進制的,那么載體之上都是Byte數組。

 

對於TCP來說,socket就是載體,可以用來發送和接收byte數組,對於串口來說 serialport就是載體,也可以用來發送和接收數據。上面說到了,協議是用來信息交流的,計算機的Byte數組怎么用來信息交流呢?答案就是協議。

舉個例子,在socket上可以隨意的發送byte數組,接收byte數組。如果我們要對方給我發一個數據,0-100的一個數組,對方可以發 0x64 過來,就是100,那么100這個地址的含義是什么?這個就要雙方的約定啦,我們雙方都規定這是溫度,那么這就是溫度,如果對方要給我們發送一個雙字節的數據,那么100是 00 64  還是 64 00 呢?這個還是要雙方規定的,如果有一方已經制定了規則,我們就根據對方的規則就行了。

 

我們在升級下例子,如果你是設備,你要支持別人來你這讀取數據,如果你的數據只有一個,比如0-100,那么對方想你請求數據時,你直接回復一個字節就可以了。但是通常你的數據有很多,幾百個,幾千個,甚至上萬個,總不能一次性都給對方吧,這個數據是 00  64 64 64 64 64 。。。。然后你得做一張表說明每個數據是啥意思,不然別人不理解呀。別人可能只要其中的一個數據而已,卻給了對方一堆沒用的。所以我們需要引入一個地址的概念,我們給所有的數據排排隊,我們規定一個地址對應一個字節,那么10000個數據,就是0-9999的地址,對方請求數據前至少要告訴我地址信息,如果給我發 00 64 ,我就知道對方是要地址100的數據,我們就給他返回地址100的數據。但是呢,如果想讀2個數據呢?我們在協議中再加入一個長度的概念,對方想讀數據的時候,除了告訴我地址,還應該告訴我長度,這個長度應該是2個字節的,這樣范圍就比較大了,也比較靈活。我們規定 00 64 00 02  這個信息就是讀取地址100開始2個地址的數據。然后我們給他返回2個字節。

上面的例子已經充分說明了一些情況,如果再考慮的復雜點,如果對方請求了不存在的地址怎么辦?請求長度太長怎么辦?我們需要引入返回錯誤碼的機制,我們可以規定返回第一個字節是錯誤碼,為0 ,后面才跟着正確的數據,如果我們想支持對方寫入數據,這時候就要引入功能碼的概念了,如果我們想支持總線,就是多設備的組網,那么就要引入站號的概念了。至此,modbus協議誕生了。

 

首先,這個協議規定了數據地址,數據都是存儲在內存里的,數據地址就好比是內存地址,一個地址對應了2個字節的數據,還規定了高地位,比如1000數據,是 03 E8 ,那么有人就會問了,那么一個4字節的數據,在modbus協議怎么存儲的呢?比如float浮點數,答案是:modbus協議根本沒有規定foat的存儲規則。無非就是占用2個地址而已,服務器愛怎么存就怎么存,所以其他的數據類型就要具體服務器具體分析了。

比如 01 03 00 64 00 01   就是標准的modbus協議,規定了一個字節是站號,用來總線里區分設備的,第二個是功能碼(要不然怎么知道是讀數據還是寫數據呢?),第三和第四是地址,2個字節的范圍是0-65535,第五和第六字節是長度,也是0-65535,到此為止是不是基本涵蓋了你所有想讀的數據?這真是一個很精簡的協議了,簡單明了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM