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測試軟體都怪怪的,想要自行開發,各位先進是否有想法?

發表迴響