20141022新增 modbus?關於modbus RTU的使用說明II
先前留下了一些關於modbus的文章卻好像沒有留下一點關於modbus基礎的介紹
modbus是一種工業控制常用的通訊協定,他定義了一個標準的通訊封包格式,
而非一種通訊技術,最早modbus是使用於PLC上,漸漸的許多工控設備也開始
採用modbus作為一種標準的通訊格式。
而modbus也產生出了許多不同的形態,如modbus RTU為最原始的以二進制方式表示
也有使用ASCII的modbus ASCII,以及modbus TCP/IP等等型態,
其不同的差異只有在於部分的格式不同(ASCII採用字元編碼方式傳送)
在低階的硬體控制中,最常使用的莫過於modbus RTU這種以二進制方式傳送的通訊
是最簡單不過的,在RS485(TTL485)中經常可以看到modbus的通訊協定
其原因為modbus本身也具有master與slave的架構,在一個並聯(RS485)的通訊環境
中,有一節點(設備)為master,由該master向其他slave通訊,進行通訊、控制等。
modbus提供許多操作功能碼,其詳細的定義了這些功能的作用以及格式
這邊所提到,定義,指的只是一個規則,我們在寫程式或在使用時就必須遵照
這些規則進行,就等於是在實行標準的modbus協議。
以下是我簡單介紹的modbus RTU的使用方式
1.讀/寫位址表(Mapping table):
在使用modbus協定中,要讀取或寫入設備,通常必須先知道欲控制或讀取設備之
記憶體暫存器位置表,不同的產品、設備,都會有自己的位址表,基本上是不會
相同的,標準中每個地址的長度以1word表示,每個位址所代表的資料量為1word
等於 1 address = 1 word data
但在實際用途上,經常性的資料範圍會有大於1word的時候,所以有些人則會連續
定義兩個2 address做為1個資料的內容存放空間,所以,在使用modbus RTU前,
必須先了解該設備的位址表,才有辦法讀取或控制自己要的內容
2.Slave address/Slave ID 設備端ID
當要讀寫slave時,必須先知道該設備的address/ID,至於這個ID要從何得知?
通常就要問負責該設備的人,或者設計者,或者原廠公司,或者熟悉他的人
3.常用的modbus功能碼(function code) - 0x03
0x03功能碼(function code)所定義的功能為讀取多個暫存器,用來讀取一連續
位址的資料。
master讀取格式:
設備ID(slave address/ID) + 0x03 + 讀取起始位置(word) + 讀取的數量(word) + CRC16
slave回復格式:
設備ID(slave address/ID + 0x03 + 回復資料的byte數 + 資料1(word) + ... + 資料n(word) + CRC16
例如:(以下所出現之命令數字均已16進制表示)
master送出: 05 03 0100 0003 + CRC16
slave回應: 05 03 06 00A1 00B2 00C3 + CRC16
這樣就或許到暫存器位址中0100,0101,0102的資料分別為
0100 = 00A1
0101 = 00B2
0102 = 00C3
4.常用的modbus功能碼(function code) - 0x06
0x06的作用,在於寫入單一個暫存器資料,因一次只能寫入一筆資料,
依照modbus標準的定義來看,一次只能寫入1word的資料量。
master寫入格式:
設備ID(slave address/ID) + 0x06 + 寫入暫存器位置(word) + 寫入資料(word) + CRC16
slave回復格式:
設備ID(slave address/ID) + 0x06 + 寫入暫存器位置(word) + 寫入資料(word) + CRC16
0x06寫入與回應是相同的時候,代表寫入成功,如寫入失敗時,則會有另外一種格式,於後面介紹
寫入範例:
master 送出: 05 06 0205 9999 + CRC16
slave 回應: 05 06 0205 9999 + CRC16
經過上面的命令後,slave端暫存器位置0205資料寫入變更為0x9999
5.常用的modbus功能碼(function code) - 0x10
0x10是在執行連續寫入多個暫存器用,其發送命令類似0x03功能的回覆命令
master寫入格式:
設備ID(slave address/ID) + 0x10 + 寫入暫存器起始位置(word) + 寫入的數量(word) + 資料數量(byte)
+ 第1筆資料(word) + ... +第n筆資料(word) + CRC16
slave回復格式:
設備ID(slave address/ID) + 0x10 + 寫入暫存器起始位置(word) + 寫入的數量(word) + CRC16
這個命令通常剛接觸的人都會霧煞煞,簡單說就是指定一個寫入的起始位置,然後預計寫入多少個暫存器
然後再加上一個資料數量,這個資料數量就是後面所帶的資料量有幾個byte,所以等於寫入的數量乘以2
因為一次需要寫入一個word的資料量,以至於就必須告訴slave端說後面帶了多少資料,很多人會說
已經知道寫入幾個了,為什麼還要脫褲子放屁的加上一個資料byte數呢?其原因應該是增加資料的可靠度
當slave端收到命令時,可以有助於驗證資料的正確性,如同0x03功能碼回覆時,也必須帶一個資料長度的大小
也是一樣的作用。
寫入範例:
master 送出: 05 10 0601 0003 06 000A 000B 000C + CRC16
slave 回應: 05 10 0601 0003 + CRC16
這行命令代表了我要寫入多個暫存器,由0601開始寫,連續寫入三個,所以後面的資料長度是6byte
經過命令之後
0601 = 000A
0602 = 000B
0603 = 000C
6.錯誤例外處理
標準modbus中也提供了錯誤例外的處理,例如當收到不合乎規則的命令時,
該如何回應master端的要求,其格式相當簡單
slave回應的錯誤例外格式:
設備ID(slave address/ID) + (功能碼 | 0x80) + 錯誤識別碼 + CRC
由上面格式中可以看到,當發生錯誤時,功能碼最高位元會 OR 0x80
也就是最高位元為1時代表有錯誤產生,而錯誤識別碼在標準modbus中也有定義
01 - Illegal function
錯誤的功能碼,要求指令中的Function是不正確的。
02 - Illegal data address
錯誤的地址,要求指令中出現了不被辨識的Address。
03 - Illegal data value
錯誤的資料參數,要求指令中出現了錯誤的Data。
04 - Slave device failure
Slave device發生錯誤。
05 - Acknowledge
Slave device正再處理上一個命令。
06 - Slave device busy
Slave device忙碌。
我認為這些錯誤的例外處理代碼,是可以自行定義的,只需在文件中說明清楚即可
7.modbus RTU的Timeout超時
規範中說明第一個命令與上一個命令至少距離3.5字節的時間
熟悉通訊的人可能對於timeout/超時這個時間不是很陌生,但是不熟悉的人可能就會
霧煞煞。設備通訊就跟人與人之間的對談是一樣的,想想在電話中與遠處的人交談時
你如何決定何時該回話?通常都是等待著對方結束上一句話的n個時間內(幾毫秒內),
而這幾毫秒內就相當於modbus中的timeout超時時間,代表這個命令傳送已經結束了
slave端可以進行資料的解析做處理,在modbus ASCII傳送時,因為是ASCII(字元碼)的關係
可以使用特定的字元做為結束的標記,slave端一直接收資料,直到收到特定字元時,
則代表該次傳送命令已結束,而在modbus RTU中,因為沒有特定的字元可以做標示
因為在二進制中,並沒有辦法使用哪個編碼做為特定字元(有存在衝突的可能),
所以取而代之使用時間做為結束的表示,上面所說的3.5字節的時間,
是modbsu標準的定義,以一個baud rate 9600bps的通訊環境來說,1個字節相當於1ms
換句話說,3.5 * 1 = 3.5ms也就是相當於需要4ms時,就可以判斷該命令已結束
但是在實際應用中,因為通訊環境的因素可能不是著麼理想(存在許多干擾的因素)
通常timeout時間我都抓在5~10字節時間,比較不會發生master尚未傳送完命令
但slave已經關閉接收的問題。
以下是我寫的modbus slave接收處理程式,
其程式作用在於操作modbusRAM[]陣列變數,
以modbusRAM[]陣列變數做為modbus address的呼應
所以當master傳送操作、讀取命令時,均是在操作讀取modbusRAM[]陣列內容。
#include "remote.h" //The remote function is standard modbus : 1 addr = 1 word data uint16 modbusRAM[128]; uint8 commTimeCount; static uint8 slaveID; static uint8 txCount; //transfer buffer counter static uint8 cnt; //receive buffer counter uint8 commRxBuffer[200]; //receive remote command buffer uint8 commTxBuffer[200]; //---------------------------------------------------------------------------- //Communication Uart Receive Interrupt //這是UART的中斷程式,採用1byte接收中斷一次 //---------------------------------------------------------------------------- void commUartINT (void) { commRxBuffer[cnt] = commUart_bReadRxData(); if (commRxBuffer[0] == slaveID) { cnt++; } else { cnt = 0; commRxBuffer[0] = 0; } commTimeCount = 0; return; } //---------------------------------------------------------------------------- //Remote communication function in here //Modbus命令接收Timeout檢查 //---------------------------------------------------------------------------- void remoteFun (void) { uint16 crcTmp; if ( cnt != 0 && commTimeCount > RXTIMEOUT) { //communication response status check switch( commRxBuffer[1] ) { case 0x03: modbus03(); break; case 0x06: modbus06(); break; case 0x10: modbus10(); break; default: funError(ErrorFuncCode); } cnt = 0; commTimeCount = 0; return; } if ( cnt == 0 ) { commTimeCount = 0; } } //---------------------------------------------------------------------------- //Modbus function code 0x03 - read many register //Modbus 0x03功能-讀取多個暫存器 //---------------------------------------------------------------------------- void modbus03 (void) { uint8 dptr,count; uint16 crcTmp; // 0 1 2 3 4 5 6 7 //01 03 00 01 00 00 AA BB if (....) { //modbus 03錯誤檢查放置於此 funError(ErrorData); return; } dptr = 0; count = commRxBuffer[5]; while (count) { commTxBuffer[3+dptr+dptr] = modbusRAM[commRxBuffer[3] + dptr] >> 8; commTxBuffer[4+dptr+dptr] = modbusRAM[commRxBuffer[3] + dptr++]; count--; } commTxBuffer[0] = commRxBuffer[0]; commTxBuffer[1] = commRxBuffer[1]; commTxBuffer[2] = dptr << 1; crcTmp = crcCHK( commTxBuffer , ( 3 + dptr + dptr )); commTxBuffer[ 3 + dptr + dptr ] = crcTmp; commTxBuffer[ 4 + dptr + dptr ] = crcTmp >> 8; commTxFunction( 5 + dptr + dptr ); } //---------------------------------------------------------------------------- //Modbus function code 0x06 - write one register //Modbus 0x06功能-寫入單一個暫存器 //---------------------------------------------------------------------------- void modbus06 (void) { // 0 1 2 3 4 5 6 7 //01 06 00 01 00 0F AA BB uint8 x; uint16 data; if (......) { //modbus 06錯誤檢查放置於此 funError(ErrorAddress); return; } data = commRxBuffer[4]; data <<= 8; data |= commRxBuffer[5]; modbusRAM[commRxBuffer[3]] = data; if ( commRxBuffer[0] == 0xFF ) { return; } for ( x = 0 ; x < cnt ; x++ ) { commTxBuffer[x] = commRxBuffer[x]; } commTxFunction(cnt); } //---------------------------------------------------------------------------- //Modbus function code 0x10 - write many register //Modbus 0x10功能-寫入多個暫存器 //---------------------------------------------------------------------------- void modbus10 (void) { // 0 1 2 3 4 5 6 7 8 9 10 11 12 //01 10 00 01 00 02 04 00 0F 00 0F AA BB uint8 count,dptr; uint16 crcTmp; if (......) { //modbus 10錯誤檢查放置於此 funError(ErrorAddress); return; } count = commRxBuffer[5]; dptr = 0; while (count) { modbusRAM[commRxBuffer[3] + dptr] = (commRxBuffer[7+dptr+dptr])<<8; modbusRAM[commRxBuffer[3] + dptr] |= commRxBuffer[8+dptr+dptr++]; count--; } if ( commRxBuffer[0] == 0xFF ) { return; } for ( count=0; count<6; count++ ) { commTxBuffer[count] = commRxBuffer[count]; } crcTmp = crcCHK( commTxBuffer,6); commTxBuffer[6] = crcTmp; commTxBuffer[7] = crcTmp>>8; commTxFunction(8); } //---------------------------------------------------------------------------- //Modbus Error Function response //---------------------------------------------------------------------------- void funError ( uint8 exception ) { uint16 crcTmp; if ( commRxBuffer[0] == 0xFF ) { return; } commTxBuffer[0] = slaveID; commTxBuffer[1] = commRxBuffer[1] | 0x80; commTxBuffer[2] = exception; crcTmp = crcCHK( commTxBuffer , 3 ); commTxBuffer[3] = crcTmp; commTxBuffer[4] = crcTmp >> 8; commTxFunction(5); } //---------------------------------------------------------------------------- //Modbus CRC16 function check //---------------------------------------------------------------------------- uint16 crcCHK ( uint8* data,uint8 length) { int cnt; unsigned int tmpCRC=0xFFFF; while (length--) { tmpCRC ^= *data++; for (cnt=0; cnt<8; cnt++) { if (tmpCRC & 0x01) tmpCRC = (tmpCRC >> 1) ^ 0xA001; else tmpCRC = tmpCRC >> 1; } } return tmpCRC; }
hi 這篇文章幫助我快速瞭解官網Modbus寫的SPEC.
小弟雖然略懂英文,一直在看官網protocal有兩項讓小弟有點混亂
煩請大大幫個忙
1. Read File Record
- 小弟的目的 是透過Modbus TCP/IP接收資料,並且UPDATE IC FW
- 使用Read File Record 0x14方式
- 前提是傳送格式上,讓我很混亂,不是很清楚格式
- 而且最大資料是 253 bytes
2. Diagnostics
- 目的是在Read / Write之前,先確認slave state,如果都正常在動作
很冒昧請教您,請您莫見怪
Hi Smile,
我剛剛查了一下0x14功能碼,因為我也沒使用過這個功能碼
不過他的example讓我也有點混亂,原因為如下網址
http://www.modbus.org/docs/Modbus_Application_Protocol_V1_1b.pdf
Page.33的地方是0x14的說明,但上述的格式
[Sub-Req. x, File Resp. length] [1 Byte] [0x07 to 0xF5]
說明這個byte所定義的格式應該是由0x07~0xF5的值
但是對照下方範例的時候卻是0x05這個小於0x07的數值
建議您,如果沒有原因上的限制一定要用0x14這個格式的話
0x03與0x10是可以滿足IC firmware update功能的。
我再找時間消化一下其他功能碼的作用,因為我工作上只用到
0x03、0x06與0x10,均可滿足需求
Hi Admin:
相當感謝您的回應,雖然小弟題問幾個問題,幾乎是爬滿了整個google大神,才不得已厚臉皮的把問題丟給您.想說還是問有經驗的大大比較穩當點.哈哈.
很高興大大的協助,相當願意分享經驗,也許我們可以成為好朋友的 ^^. 原本是要使用0x13 (program controller),但.....官網翻遍無論是傳統格式&新格式modbus rtu,都沒有說明資料格式.
也許我沒有透徹官方的用意吧.
我這邊是要保留128k bytes(update bin for F/W),光address就需要3bytes.使用0x03 read 雖然可以做得到,但有風險lose,機制上並不是完整.
希望能分好幾個group傳輸(ex.32bytes 一個group,假設64k bytes = 65536 bytes / 32 bytes = 2048個group)
有address & bytes count & group count,可根據不同問題去read group
因此才會想到使用(0x14) Read File Record,但格式上有點混亂,並不適合傳送128k bytes
現在正評估使用(0x18) Read FIFO Queue 似乎有符合要求吧.又當心會誤解格式用意.
最後,就拍一下馬屁,大大的文章,我都很喜歡唷,不論是標題與內容,都很簡單扼要,直接進入核心,最重要的是 敘述的很白話且明白,很喜歡大大的文章唷. 乾八ㄉ 唷
不知道您是否一定要遵循modbus的規則
如果沒有必要一定符合的話,
是可以將0x03與0x10功能碼的address改為2word來做
大大您好:) 日前接上case 在看完一些協定與code後 , 發現還是不大會使用!想在這請教一下!
我老闆使用了個Android APP 利用wifi傳送資料給自制的電路板 已經寫好modbus server
那麼 他傳送之後得到的回應是 00 06 00 0D 00 F3 59 9D
從這邊得知 由左至右00是Address 06是function code 00 0D是Register Address Hi & Lo
00 F3是資料 59 9D是CRC16
我的問題是 modbus的TCP 顧名思義是採用網路傳輸 我在別的文章上看到使用TCP/IP則不用理會CRC16??
modbus RTU能用網路IP封包傳送方式使用嗎?
Hi Jacob,
我不知道我的理解是否正確,提供您參考
1.因為TCP/IP的傳送格式本身就有其自己的檢查機制,故可以將Modbus RTU的CRC省略,透過TCP/IP本身的機制防止收到垃圾資料,這是我認為省略Modbus RTU CRC的原因。
2.但是如果是使用Modbus TCP的話,我記得除了省略CRC以外,在命令前面還會加上6個byte表示後面命令長度,如 00 00 00 00 00 06 01 03 01 02 00 AA
3.Modbus RTU能用網路IP封包傳送方式嗎?
我的答案是肯定的,因為只要是資料,就可以透過TCP/IP拋出來,Modbus只是定義了一種資料格式,所以與TCP/IP沒有衝突。
謝謝大大回覆:)
那麼 如果是使用modbus RTU 格式在TCP/IP上傳送 加上CRC應該是沒問題了,前面也就不必加上6個byte對吧 他的回應確實沒有多出6個byte。
另外請教 每次傳送同樣的資料 CRC都會一樣嗎? 他傳送了三個同樣的資料 回傳的CRC都是一樣的。
Hi Jacob,
1.當然使用TCP/IP的協定傳送modbus RTU格式加上CRC是沒問題的,TCP/IP就好像火車,上面要坐著誰都不奇怪,只要他是可以上車的乘客,都可以乘坐。
2.如果每次傳送的資料內容都不一樣,但是CRC卻是一樣的,那CRC肯定是有問題的,但如果傳送的資料都一樣CRC當然就會一樣囉。
3.不要叫我大大啦。
我目前是在ios app替老闆開發這一項modbus功能,但是可能對於這種通信沒有很有天份,搞了蠻久,網路上頂多找到一個有人寫好的lib,但又不大會用,或者是,使用方式感覺沒有我需求的,若從0開始寫一個modbus RTU & TCP的lib,大大有沒有什麼技術支援小弟呢?
就讓我叫大大吧 呵呵
坦白說對於APP開發我是一竅不通,我只會寫寫MCU跟簡單的測試程式而已。
至於說技術支援,如果你能開啟socket port應該直接把modbus rtu的data往後拋到指定設備上應該就可以了吧?不就是單純的socket操作呢?
沒錯 ! 確實是將資料丟到指定ip上,但在丟到ip之前,不知道如何去設定格式,使用第三方的lib發現function能讓我傳入的值只有兩個...?
可是00 06 00 0D 00 F3 CRC 除了CRC,前面有這麼多要設定,所以不知道怎麼用呢...
Dear Jacob,
00 06 000D 00F3 CRC
第一個byte 00 = slave address ,通常00 = broadcast address
第二個byte 06 = 寫入一個位址
第三四個byte 000D = 寫入位址
第五六個byte 00F3 = 寫入的data
你搞不清楚怎麼用的原因,可能是不知道設備的操作位址
通常必須先知到後端設備的操作位址 才能知道要怎麼控制
這篇文好詳細ㄛ,幫了我很大的忙!
Dear WWH,
感謝您的鼓勵!^^
大大昨天有寄email給您
請問有收到嗎
Dear Johnny,
不好意思!我有收到, 這兩天比較忙>< 我晚一點回覆您!
最近剛要玩Modbus,這文章可以提供我很多資訊
文末,大大貼了你所寫的接受處理流程,請問可否
1. 貼出傳送處理流程
2. 提供remote.h
Dear jackshowme,
1.其實整個modbus的0x03、0x06、0x10 Function code處理流程已在程式裡面了,其指示針對modbusRAM的記憶體做讀取或寫入的處理。
2.其實remote.h裡面只定義了各Function的prototype而已。
XD
路過受教
🙂
Dear
Modus RTU 必須認證CRC ,CRC 是必須經過計算的。
🙂
我也要請教個問題
在win7 64bit 環境下如何開發 modbus
目前由於作業系統改成64bit 造成絕大多數的modbus測試軟體都怪怪的,想要自行開發,各位先進是否有想法?
Dear Jimmy
我建議Visual Studio、Qt可能都是參考選項
Hi Sir
我也想請教一些問題。
關於Modbus的傳輸時間5-10個字節
看完您的程式我還是無法了解該如何去設定。
可以問您相關的知識嗎?
Hi Phil,
我想你說的傳輸時間5-10個字節,應該是指Modbus RTU格式的Timeout時間?
如果是的話可以再參考一下 7.modbus RTU的Timeout超時 說明,以一個baud rate 9600bps的通訊環境來說,1個字節相當於1ms換句話說,3.5字節 * 1 = 3.5ms也就是相當於需要4ms的延遲結束時間。
如果以這篇文章的範例來看,Timeout的設定應是透過commTimeCount來決定延遲時間的。