協議簡介
Modbus協議,首先從字面理解它包括Mod和Bus兩部分,首先它是一種bus,即總線協議,總線就意味著有主機,有從機,這些設備在同一條總線上。
Modbus支持單主機,多個從機,最多支持247個從機設備。關于Mod,因為這種協議最早被用在PLC控制器中,準確的說是Modicon公司的PLC控制器,這也是Modbus名稱的由來。后來Modicon被施耐德電器收購,Modbus協議廣泛應用在工業控制器、HMI和傳感器上,逐漸被其他廠商所接受,成為工業領域通信協議的業界標準,并且現在是工業電子設備之間常用的連接方式。
每種設備(PLC、HMI、控制面板、驅動程序、動作控制、輸入/輸出設備)都能使用 Modbus協議來啟動遠程操作。
在基于串行鏈路和以太 TCP/IP 網絡的 Modbus 上可以進行相互通信。
一些網關允許在幾種使用 Modbus 協議的總線或網絡之間進行通信
術語說明
HDLC 高級數據鏈路控制
HMI 人機界面
IETF 因特網工程工作組
I/O 輸入/輸出設備
IP 互連網協議
MAC 介質訪問控制
MB Modbus 協議
MBAP Modbus 協議
關于總線補充說明
總線(Bus)是計算機各種功能部件之間傳送信息的公共通信干線,它是由導線組成的傳輸線束, 按照計算機所傳輸的信息種類,計算機的總線可以劃分為數據總線、地址總線和控制總線,分別用來傳輸數據、數據地址和控制信號。總線是一種內部結構,它是cpu、內存、輸入、輸出設備傳遞信息的公用通道,主機的各個部件通過總線相連接,外部設備通過相應的接口電路再與總線相連接,從而形成了計算機硬件系統。在計算機系統中,各個部件之間傳送信息的公共通路叫總線,微型計算機是以總線結構來連接各個功能部件的。
Modbus被廣泛使用主要原因
- 標準、開放,用戶可以免費、放心地使用Modbus協議,不需要交納許可證費,也不會侵犯知識產權
- Modbus可以支持多種電氣接口,如RS-232、RS-485等,還可以在各種介質上傳送,如雙絞線、光纖、無線等。
- Modbus的幀格式簡單、緊湊,通俗易懂。用戶使用容易,廠商開發簡單。
協議版本
Modbus 協議目前分別定義了基于串口()和以太網傳輸數據的協議,其中串口(RS232,RS485,RS422,光纖,無線等)數據傳輸協議分為 Modbus RTU 和 Modbus ASCII串行鏈路協議,以太網數據傳輸協議一種 Modbus TCP/IP協議。
其中,Modbus RTU是一種緊湊的,采用二進制表示數據的方式,Modbus ASCII是一種人類可讀的,冗長的表示方式。
工作模式
Modbus 協議采用主從(Master/Salve)通信方式,TCP/IP傳輸模式下,主節點也叫客戶機,從節點也叫服務器。連接到總線上的所有主機節點中有且只有一個 Master 節點(主節點),其余為 Slave 節點(從節點,可選地址為1~247,每個Slave節點的地址必須唯一)。
主從通信以 請求/應答 為主,每次通訊都是主節點先發送請求(采用廣播模式、單播模式),從節點響應指令,并按要求應答,或者報告異常。當主節不發送請求時,從節不會自己發出數據,從節點之間不能相互通訊(也就是說從節點之間不能相互發送請求)。
無論主節點發送的是廣播指令還是單播指令,實際上所有從節點都會完整接收指令。但發送單播指令時,只有地址和指令中中指定地址相同的從節點才會執行及回應指令,其它從節點將忽略收到的指令,而廣播請求所有收到指令的設備都會執行指令,但不會給主機回應指令。
半雙工通信
Modbus 由于請求/應答機制所以不能同步通信(同步通信需要收發雙方以相同的節奏發送和接收數據),總線上每次只有一幀數據進行傳輸,屬于半雙工通信。
Modbus 沒有支持繁忙機制處理,例如主機給從機發送命令, 如果從機正在處理其他任務,此時從機將無法響應主機,所以需要通過軟件的方式來判斷是否正常接收。
Modbus消息幀
Modbus 協議定義了一個與基礎通信層無關的簡單協議數據單元(PDU -> 功能碼 + 數據 部分)。特定總線或網絡上
的 MODBUS 協議映射能夠在應用數據單元(ADU -> 地址域 + 功能碼 + 數據 + 差錯校驗)上引入一些附加域
通用數據幀
地址域 | 功能碼 | 數據 | 差錯校驗碼 |
---|
1字節 | 1字節 | N字節 | CRC: 16字節 LRC: 1字節 |
說明:每個劃分字段都用16進制表示
地址域
從機設備地址,通常1-247為有效地址,0為廣播地址(用于接收主機的廣播數據),每個從機在總線上地址必須唯一,只有與主機發送的地址碼相符的從機才能響應返回數據。
主節點通過將要聯絡的從節點的地址放入消息中的地址域來選取需要通信的從設備。當從節點發送回應消息時,需要把自己的地址放入回應的地址域中,以便主節點知道是哪一個設備作出的回應。
功能碼
表明主節點請求數據的類型。
當主節點向從設備發送消息時,功能碼將告訴從設備需要執行哪些行為。例如去讀取輸入的開關狀態,讀一組寄存器的數據內容等。
數據
包含寄存器地址和寄存器數據等
差錯校驗
對數據進行冗余校驗的結果,CRC、LRC
其中事務處理正常時,客戶機向服務器發送請求,在功能碼中填充功能碼代號,說明服務器需要執行的動作,在數據碼區填充具體的要求,比如讀寄存器的地址和數量,通信正常時服務器會在返回的通信幀的功能碼區中填充一個操作碼,該操作碼=功能碼,在通訊幀的數據區填充返回的采樣數據。
當出現事務處理異常時,服務器會在返回的通訊幀的功能碼中填充一個差錯碼,該差錯碼 = 功能碼 + 0x80,即將功能碼的最高位置1代表出現錯誤。并在后面的數據段中填充錯誤碼,用來指示本次通信的錯誤具體內容。
Modbus ASCII消息幀
起始位(?? + ADU + 結束符
起始位 | 地址域 | 功能碼 | 數據 | LRC | 結束符 |
---|
: | 1字節 | 1字節 | N字節 | 1字節 | 2個字符 |
說明:消息以 :
冒號字符(ASCII 碼16進制表示 3A
)開始,以回車換行符(CR LF
, ASCII 碼16進制表示 0D
,0A
)結束。
一個典型 ASCII 消息幀如下
起始位 | 地址域 | 功能碼 | 數據 | LRC | 結束符 |
---|
: | 2個字符 | 2個字符 | 0 到 2x252 字符 | 2個字符 | 2個字符 |
Modbus RTU 消息幀
地址域 | 功能碼 | 數據 | CRC低字節 | CRC高字節 |
---|
1字節 | 1字節 | 0 到 252 字節 | 1字節 | 1字節 |
說明:RTU 通信模式下,其發送的字節數據即為原始字節數據,接收端接收后無需再次轉換。
注意:
- RTU 模式,數據幀之間必須至少間隔 3.5 個字符時間,通過時間區間來區分報文,如下:
- RTU 模式下,整個報文幀必須以連續的字符流發送,如果兩個字符之間的空閑間隔大于 1.5 個字符時間,則報文幀被認為不完整應該被接收節點丟棄
字符時間
所謂字符時間指的是傳輸一個 ASCII 字符需要花費的時間,一個 ASCII 字符包含 1 個字節(8 bits),所以傳輸一個字符需要花費傳輸 8 個數據位的時間(所以這里字符傳輸時間指代傳輸1字節數據消耗的時間)。
然而實際上傳輸 1 個字節數據需要花費的時間并不只 8 個位時間,因為除了傳輸固有的 1 字節數據,還需要傳輸一些輔助功能位。例如發送 1 個字節需要固定起始位 1 位,數據位 8 位,校驗位 1 位(可選的),停止位 1 位,其中 8 位數據位才是真正的有效數據,所以有如下公式來計算字符時間。
字符時間=1s / 波特率 × 字符的字節總位數。
例如:固定起始位 1 位,數據位 8 位,奇/偶校驗位 1 位,停止位 1 位,波特率為9600 bps,計算單個字符傳輸時間為:
字符時間 = 1000 ms / 9600 × ( 1 + 8 + 1 + 1 ) = 1.145833 ms
Modbus TCP/IP 消息幀
MBAP 報文頭 + PDU(此處PDU來自數據鏈路層的PDU)
事務元標識符 | 協議標識符 | 長度 | 單元標識符 | 功能碼 | 數據 |
---|
2字節 | 2字節 | 2字節 | 1字節 | 1字節 | N字節 |
MBAP 報文頭包括下列域:
數據編碼
MODBUS 使用一個Big-Endian
(低地址位存放最高有效字節) 表示地址和數據項。這意味著當發射多個字節時,首先發送最高有效位。
例如:
寄存器大小 值
16 0x1234 發送的第一字節為 0x12, 然后 0x34
參考閱讀:Big Endian和Little Endiand的區別
數據模型
為了抽象 PLC 中可訪問的數據,Modbus 協議定義了 數據模型 概念,數據模型定義了四種可訪問的數據類型:
類型 | 大小 | 訪問權限 | 元素地址前綴編碼 | 元素地址范圍(0~65535) | 元素地址范圍(1~9999) |
---|
輸出線圈(Coils) | 1 Bit | 可讀可寫 | 0 | 000000~065535 | 00000~09999 |
輸入離散量(Discrete Input) | 1 Bit | 只讀 | 1 | 100000~165535 | 10000~19999 |
輸入寄存器(Input Registers) | 16 Bit | 只讀 | 3 | 300000~365535 | 30000~39999 |
保持寄存器(Holding Registers) | 16 Bit | 可讀可寫 | 4 | 400000~465535 | 40000~49999 |
實際上以上的數據類型都屬于可編程邏輯控制器(PLC)中的術語,可以簡單理解為用來存放數據的容器,線圈通常用于表示開關狀態(如繼電器的通或斷),而寄存器通常用于存儲線性或非線性的數值數據。
數據模型中的每一種數據類型都最多允許有 65536 個元素,元素的地址編號從 0 開始,因此地址的范圍為:0-65535。
需要說明的是:65536 是 Modbus 協議允許的最大元素范圍,實際應用中一般不需要這么大的存儲區,因此 PLC 廠家普遍采用的是 10000 以內的地址范圍。
引入元素地址前綴編碼,是為了簡化數據模型與設備存儲區的對應關系。
參考鏈接:https://blog.csdn.net/jf_52001760/article/details/130192127
數據模型是一種抽象,在實際使用時必須將其映射到真實的物理存儲區才能被訪問。
Modbus 協議允許設備將四種數據類型分別映射到不同的存儲區塊中,各個區塊之間相互獨立,使用不同的功能碼可讀取到不同的數值,如下圖所示
帶有多個獨立塊的設備
僅有1個塊的設備
功能碼
功能碼整體可以分成三類:
- 公共功能碼
- 用戶自定義功能碼([65, 72], [100, 110])
- 保留功能碼
常用功能碼
功能碼 | 名稱 | 操作類型 | 功能描述 | |
---|
01 | 讀線圈狀態 | 位操作 | 讀位(讀 N 個 bit)讀從機線圈寄存器 |
|
02 | 讀輸入離散量 | 位操作 | 讀位(讀 N 個 bit)讀離散輸入寄存器 |
|
03 | 讀保持寄存器 | 字節操作 | 讀整型,字符型,狀態字,浮點型(讀 N 個 word)讀保持寄存器 |
|
04 | 讀輸入寄存器 | 字節操作 | 讀整型,狀態字,浮點型(讀 N 個word)讀輸入寄存器 |
|
05 | 寫單個線圈 | 位操作 | 寫位(寫 1 個 bit)寫線圈寄存器 |
|
06 | 寫單個保持寄存器 | 字節操作 | 寫整型,字符型,狀態字,浮點型(寫一個 word)寫保持寄存器, |
|
0F | 寫多個線圈 | 位操作 | 寫位(寫 N 個 bit)強置一串連續邏輯線圈的通斷 |
|
10 | 寫多個保持寄存器 | 位操作 | 寫整形,字符型,狀態字,浮點型(寫 N 個 word)把具體的二進制值裝入一串連續的保持寄存器 |
|
請求和應答報文示例
Modbus RTU通信
示例1:寫單個寄存器。向01地址設備0x0105保持寄存器寫入1個數據:0x0190
主機發送: 01 06 01 05 01 90 99 CB
從機回復: 01 06 01 05 01 90 99 CB
說明:01表示從機地址,06功能碼表示寫單個保持寄存器,01 05表示寄存器地址,01 90 表示寫入寄存器的數值,99 CB為CRC校驗值。
可以看出,當寫1個寄存器數據時,從機響應的數據幀和主機發送的數據幀完成一致。
附:CRC(循環冗余校驗)在線計算地址:http://www.ip33.com/crc.html
CRC-16代碼實現
'''生成 CRC 的過程為:
1.將一個 16 位寄存器裝入十六進制 FFFF,將之稱作 CRC 寄存器.
2.將報文的第一個8位字節與上述 CRC 寄存器的低字節異或,結果置于 CRC 寄存器.
3.將 CRC 寄存器右移 1 位 (向 LSB(Least Significant Bit,最低有效位) 方向), MSB(Most Significant Bit,最高有效位) 充零。提取并檢測 LSB。
4.如果 LSB 為 0,則重復步驟 3 (另一次移位).
如果 LSB 為 1: 對 CRC 寄存器異或多項式值 0xA001 (對應16位二進制:1010 0000 0000 0001)
5.重復步驟 3 和 4,直到完成 8 次移位。當做完此操作后,將完成對 8 位字節的完整操作。
6. 對報文中的下一個字節重復步驟 2 到 5,繼續此操作直至所有報文被處理完畢。
7. CRC 寄存器中的最終內容為 CRC 值.
8. 當放置 CRC 值于報文時,需要交換CRC高低字節。
'''
def hex_char_to_int(hex_char):
'''
:param hex_char: 16進制表示的字符
:return: 字節
'''
return "0123456789ABCDEF".find(hex_char)
def hex_string_to_bytes(hex_string):
'''
16進制字符串轉為字節數組
:param hex_string: 16進制表示的字符串
:return: 字節數組
'''
hex_string = hex_string.strip()
hex_string_len = len(hex_string)
if not hex_string_len:
return
byte_array = []
hex_string = hex_string.upper()
for i in range(int(hex_string_len/2)):
high_digit = hex_char_to_int(hex_string[2*i])
low_digit = hex_char_to_int(hex_string[2*i+1])
byte_array.append(high_digit << 4 | low_digit)
if hex_string_len % 2 == 1:
byte_array.append(0x00 | hex_char_to_int(hex_string[hex_string_len-1]))
return byte_array
def bytes_to_hex_string(byte_list):
'''
字節轉為16進制字符串
:param byte_list: 字節數組
:return: 字節數組對應的大寫16進制字符串表示
'''
hex_string = ""
for i in range(len(byte_list)):
hex_string += hex(byte_list[i] & 0xFF)[2:].zfill(2)
return hex_string.upper()
def calculate_crc(data):
reg_crc = 0xFFFF
for byte in data:
reg_crc ^= byte
for _ in range(8):
if reg_crc & 0x0001:
reg_crc = (reg_crc >> 1) ^ 0xA001
else:
reg_crc >>= 1
return reg_crc.to_bytes(2, 'big')
string_data = '01 06 01 05 01 90'
string_data = string_data.replace(' ', '')
byte_list = bytes(string_data, encoding='utf-8')
byte_list = hex_string_to_bytes(string_data)
crc_value = calculate_crc(byte_list)
print(crc_value, bytes_to_hex_string(crc_value))
final_crc = ''
for byte in crc_value:
final_crc = str(hex(byte)).lstrip('0x') + ' ' + final_crc
print(final_crc.upper())
示例2:寫多個寄存器。向01地址設備0x0105、0x0106、0x0107地址保持寄存器,寫入3個寄存器數據:0x1102, 0x0304, 0x0566
主機發送:01 10 01 05 00 03 06 11 02 03 04 05 66 4A 12
從機回復:01 10 01 05 00 03 91 F5
說明:01從機地址,10功能碼表示寫多個保持寄存器,01 05表示起始地址,00 03表示寫3個寄存器,06表示數據量為6個字節,11 02/03 04/05 66分別表示寫入3個寄存器的數值,4A 12 表示CRC校驗數值。
示例3:讀單個寄存器。讀01地址設備0x0105保持寄存器數據。
主機發送:01 03 01 05 00 01 95 F7
從機回復:01 03 02 56 78 87 C6
說明:
03表示讀多個寄存器,0105表示起始地址,00 01表示讀1個寄存器
02表示2個字節,56 78表示寄存器的數據。
示例4:讀多個寄存器。讀01地址設備0x0105、0x0106、0x0107地址保持寄存器,共3個寄存器數據。
主機發送:01 03 01 05 00 03 14 36
從機回復:01 03 06 11 22 33 44 55 66 2A 18
說明:
03表示讀多個寄存器,01 05表示起始地址,00 03 表示讀3個寄存器
06表示6個字節,11 22 33 44 55 66表示寄存器的數據。
Modbus ASCII
例子:向地址為0x01的從設備的0x0405地址,寫入數值0x1234,報文如下:
主機發送請求: :01 06 04 05 12 34 AA <CR><LF>
說明:01表示設備地址,06表示寫單個保持寄存器。04 05 表示寄存器地址,12 34 表示數據,AA 表示LRC校驗值。實際進行校驗的數據不包含起始符(:)和結束符(<CR><LF>
)。
附:LRC校驗(縱向冗余校驗)在線計算地址:http://www.ip33.com/lrc.html
LRC代碼實現
string = '01 06 04 05 12 34'
total = 0
for item in string.split(' '):
total += int(item, 16)
result = total % 256
hex_lrc_vale = hex(256 - result)
錯誤碼
常用錯誤碼如下表所示。
異常碼 | 名稱 | 描述 |
---|
01 (01H) | 非法功能 | 在請求中接收的功能代碼不是從設備的一個授權操作。從設備可能處于錯誤狀態,無法處理特定請求。 |
02 (02H) | 非法數據地址 | 從設備接收的數據地址不是從設備的一個授權地址 |
03 (03H) | 非法數據值 | 指定的數據超過范圍或者不允許使用。 |
04 (04H) | 從站設備故障 | 從設備未能執行一個請求的操作,因為出現了一個無法修復的錯誤 |
05 (05H) | 確認 | 確認 從站設備已經接受請求,并且正在處理這個請求,但是需要長持續時間進行這些操作,返回這個響應防止在客戶機(或主站)中發生超時錯誤,客戶機(或主機)可以繼續發送輪詢程序完成報文來確認是否完成處理 |
06 (06H) | 從站設備忙 | 從設備忙于處理另一個命令。主設備必須在從設備空閑后發送請求 |
07 (07H) | 否定確認 | 從站設備無法執行主站設備發送的請求 |
08 (08H) | 存儲奇偶性差錯 | 從設備在嘗試讀取擴展存儲器的時候從存儲器中檢測到一個奇偶校驗錯誤 |
10 (0AH) | 不可用的網關路徑 | 與網關一起使用,指示網關不能為處理請求分配輸入端口值輸出端口的內部通信路徑。通常意味著網關是錯誤配置的或過載的 |
11 (0BH) | 網關目標設備響應失敗 | 與網關一起使用,指示沒有從目標設備中獲得響應,通常意味著設備不在網絡中 |
?轉自https://www.cnblogs.com/shouke/p/18315015
該文章在 2024/10/14 10:02:26 編輯過