modbus?關於modbus RTU的使用說明

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; 
	}

24 thoughts on “modbus?關於modbus RTU的使用說明

  1. 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,如果都正常在動作

    很冒昧請教您,請您莫見怪

  2. 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,均可滿足需求

  3. 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 似乎有符合要求吧.又當心會誤解格式用意.

    最後,就拍一下馬屁,大大的文章,我都很喜歡唷,不論是標題與內容,都很簡單扼要,直接進入核心,最重要的是 敘述的很白話且明白,很喜歡大大的文章唷. 乾八ㄉ 唷

  4. 不知道您是否一定要遵循modbus的規則
    如果沒有必要一定符合的話,
    是可以將0x03與0x10功能碼的address改為2word來做

  5. 大大您好:) 日前接上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封包傳送方式使用嗎?

  6. 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沒有衝突。

  7. 謝謝大大回覆:)
    那麼 如果是使用modbus RTU 格式在TCP/IP上傳送 加上CRC應該是沒問題了,前面也就不必加上6個byte對吧 他的回應確實沒有多出6個byte。
    另外請教 每次傳送同樣的資料 CRC都會一樣嗎? 他傳送了三個同樣的資料 回傳的CRC都是一樣的。

  8. Hi Jacob,

    1.當然使用TCP/IP的協定傳送modbus RTU格式加上CRC是沒問題的,TCP/IP就好像火車,上面要坐著誰都不奇怪,只要他是可以上車的乘客,都可以乘坐。

    2.如果每次傳送的資料內容都不一樣,但是CRC卻是一樣的,那CRC肯定是有問題的,但如果傳送的資料都一樣CRC當然就會一樣囉。

    3.不要叫我大大啦。

  9. 我目前是在ios app替老闆開發這一項modbus功能,但是可能對於這種通信沒有很有天份,搞了蠻久,網路上頂多找到一個有人寫好的lib,但又不大會用,或者是,使用方式感覺沒有我需求的,若從0開始寫一個modbus RTU & TCP的lib,大大有沒有什麼技術支援小弟呢?

    就讓我叫大大吧 呵呵

  10. 坦白說對於APP開發我是一竅不通,我只會寫寫MCU跟簡單的測試程式而已。

    至於說技術支援,如果你能開啟socket port應該直接把modbus rtu的data往後拋到指定設備上應該就可以了吧?不就是單純的socket操作呢?

  11. 沒錯 ! 確實是將資料丟到指定ip上,但在丟到ip之前,不知道如何去設定格式,使用第三方的lib發現function能讓我傳入的值只有兩個...?
    可是00 06 00 0D 00 F3 CRC 除了CRC,前面有這麼多要設定,所以不知道怎麼用呢...

  12. Dear Jacob,

    00 06 000D 00F3 CRC

    第一個byte 00 = slave address ,通常00 = broadcast address
    第二個byte 06 = 寫入一個位址
    第三四個byte 000D = 寫入位址
    第五六個byte 00F3 = 寫入的data

    你搞不清楚怎麼用的原因,可能是不知道設備的操作位址
    通常必須先知到後端設備的操作位址 才能知道要怎麼控制

  13. 最近剛要玩Modbus,這文章可以提供我很多資訊
    文末,大大貼了你所寫的接受處理流程,請問可否
    1. 貼出傳送處理流程
    2. 提供remote.h

  14. Dear jackshowme,

    1.其實整個modbus的0x03、0x06、0x10 Function code處理流程已在程式裡面了,其指示針對modbusRAM的記憶體做讀取或寫入的處理。

    2.其實remote.h裡面只定義了各Function的prototype而已。

    XD

  15. 我也要請教個問題
    在win7 64bit 環境下如何開發 modbus
    目前由於作業系統改成64bit 造成絕大多數的modbus測試軟體都怪怪的,想要自行開發,各位先進是否有想法?

發表迴響

這個網站採用 Akismet 服務減少垃圾留言。進一步瞭解 Akismet 如何處理網站訪客的留言資料