前言:
之前的博客介紹了如何用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,到此為止是不是基本涵蓋了你所有想讀的數據?這真是一個很精簡的協議了,簡單明了。