1 簡介
modbus由MODICON公司於1979年開發,是一種工業現場總線協議標准。1996年施耐德公司推出基於以太網TCP/IP的modbus協議:modbusTCP。
Modbus協議是一項應用層報文傳輸協議,包括ASCII、RTU、TCP三種報文類型。
標准的Modbus協議物理層接口有RS232、RS422、RS485和以太網接口,采用master/slave方式通信。
2 ModbusTCP數據幀
ModbusTCP的數據幀可分為兩部分:MBAP+PDU。
2.1 報文頭MBAP
MBAP為報文頭,長度為7字節,組成如下:
事務處理標識 協議標識 長度 單元標識符
2字節 2字節 2字節 1字節
事務處理標識 :可以理解為報文的序列號,一般每次通信之后就要加1以區別不同的通信數據報文。
協議標識符 :00 00表示ModbusTCP協議。
長度 :表示接下來的數據長度,單位為字節。
單元標識符 :可以理解為設備地址。
2.2 幀結構PDU
PDU由功能碼+數據組成。功能碼為1字節,數據長度不定,由具體功能決定。
2.2.1 功能碼
modbus的操作對象有四種:線圈、離散輸入、輸入寄存器、保持寄存器。
線圈:PLC的輸出位,開關量,在MODBUS中可讀可寫
離散量:PLC的輸入位,開關量,在MODBUS中只讀
輸入寄存器:PLC中只能從模擬量輸入端改變的寄存器,在MODBUS中只讀
保持寄存器:PLC中用於輸出模擬量信號的寄存器,在MODBUS中可讀可寫
根據對象的不同,modbus的功能碼有:
- 0x01:讀線圈
- 0x05:寫單個線圈
- 0x0F:寫多個線圈
- 0x02:讀離散量輸入
- 0x04:讀輸入寄存器
- 0x03:讀保持寄存器
- 0x06:寫單個保持寄存器
- 0x10:寫多個保持寄存器
二,模擬了直接發送Socket套接字(上位機)跟Modbus Slave軟件(下位機)進行通信,代碼如下:
Socket socket = new Socket("192.1.1.4",9600);
InputStream is=socket.getInputStream();
OutputStream os=socket.getOutputStream();
byte[] sendInfo = new byte[] { 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03,0x00, 0x00, 0x00, 0x08 }; //發送給Modbus Slave軟件的消息
os.write(sendInfo);
os.flush();
byte[] bs = new byte[32];
is.read(bs);
printHexString(bs); //輸出十六進制
軟件當中Alias從0到9的值如下:
程序取到的結果如下:000700000013010310 0457 08AE 0D05 115C 15B3 1A0A 1E61 22B8 00000000000000
在解釋結果之前,先給出ModbusTcp的報文格式。
ModbusTcp報文主要是由三部分組成,如下圖所示:
其中,MBAP報文頭一般為7個字節,我在這里以上封郵件的例子來做解釋:
byte[] sendInfo = new byte[] { 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03,0x00, 0x00, 0x00, 0x08 };
從byte數組的第1個元素開始到第七個元素,分別是:0007|0000|0006|01
1)0007:事務處理標識符,用於將請求與未來響應之間建立聯系(服務器端應答時候復制該值);
2)0000:協議標識,其中 0 代表Modbus協議,1代表UNI-TE協議(服務器端應答時候復制該值);
3)0006:長度,計算其后續所有字節數(服務器端應答時候該字節重新生成,並取返回指令的相同邏輯值);
4)01:單元標識符,在MODBUS或MODBUS+串行鏈路子網中對設備進行尋址時,這個域是用於路由的目的(服務器端應答時候復制該值)。
因此,我們可以得知服務器端應答時候,其返回的MBAP報文頭為:
0007 | 0000 | 0013 | 01
接下來是功能碼(占用一個字節)。Modbus有一大堆的功能碼,但常用的命令一般有01,02,03,04等幾個
1)01:讀取線圈狀態,Modbus對應的地址為00001開始;
2)02:讀取輸入狀態,Modbus對應的地址為10001開始;
3)03:讀保持寄存器,Modbus對應的地址為40001開始;
4)04:輸入寄存器,Modbus對應的地址為30001開始。
最后便是數據了。數據段的長度是根據功能碼類型來對應的,因此這里我們仍然用sendInfo這個例子:
該例子的功能碼為03,說明讀取的是保持寄存器。在功能碼后面的 0000 為讀取的起始地址,0008 為讀取的數量,因此該指令可以理解為:
讀取保持寄存器當中,從0000地址開始(也就是Modbus的40001地址)依次讀取八個數據(即16個字節)
根據模擬器的截圖,從保持寄存器的0起始地址開始,依次有以上的數據,因此服務器端應答的內容如下:
0007|0000|0013|01|03|10 0457 08AE 0D05 115C 15B3 1A0A 1E61 22B8
其中MBAP報文頭的內容在上面已有說明,這里就說下從單元標識符往后的內容:
1)03:功能碼,跟請求的功能碼一致;
2)10:字節計數,就是后續的字節數(同時也是請求指令當中讀取數量的double);
3)之后的便是存放的數據了,以十六進制給出,一個數據占兩個字節。
使用jlibmodbus
maven依賴
<dependency>
<groupId>com.intelligt.modbus</groupId>
<artifactId>jlibmodbus</artifactId>
<version>1.2.9.7</version>
</dependency>
package com.tcb.jlibmodbus; import java.net.InetAddress; import com.intelligt.modbus.jlibmodbus.Modbus; import com.intelligt.modbus.jlibmodbus.exception.ModbusIOException; import com.intelligt.modbus.jlibmodbus.exception.ModbusNumberException; import com.intelligt.modbus.jlibmodbus.exception.ModbusProtocolException; import com.intelligt.modbus.jlibmodbus.master.ModbusMaster; import com.intelligt.modbus.jlibmodbus.master.ModbusMasterFactory; import com.intelligt.modbus.jlibmodbus.tcp.TcpParameters; /** * Hello world! * */ public class App { public static void main(String[] args) { try { // 設置主機TCP參數 TcpParameters tcpParameters = new TcpParameters(); // 設置TCP的ip地址 InetAddress adress = InetAddress.getByName("127.0.0.1"); // TCP參數設置ip地址 // tcpParameters.setHost(InetAddress.getLocalHost()); tcpParameters.setHost(adress); // TCP設置長連接 tcpParameters.setKeepAlive(true); // TCP設置端口,這里設置是默認端口502 tcpParameters.setPort(Modbus.TCP_PORT); // 創建一個主機 ModbusMaster master = ModbusMasterFactory.createModbusMasterTCP(tcpParameters); Modbus.setAutoIncrementTransactionId(true); int slaveId = 1;//從機地址 int offset = 0;//寄存器讀取開始地址 int quantity = 10;//讀取的寄存器數量 try { if (!master.isConnected()) { master.connect();// 開啟連接 } // 讀取對應從機的數據,readInputRegisters讀取的寫寄存器,功能碼04 int[] registerValues = master.readInputRegisters(slaveId, offset, quantity); // 控制台輸出 for (int value : registerValues) { System.out.println("Address: " + offset++ + ", Value: " + value); } } catch (ModbusProtocolException e) { e.printStackTrace(); } catch (ModbusNumberException e) { e.printStackTrace(); } catch (ModbusIOException e) { e.printStackTrace(); } finally { try { master.disconnect(); } catch (ModbusIOException e) { e.printStackTrace(); } } } catch (RuntimeException e) { throw e; } catch (Exception e) { e.printStackTrace(); } } }
打印到控制台的信息
Address: 0, Value: 88
Address: 1, Value: 66
Address: 2, Value: 8
Address: 3, Value: 6
Address: 4, Value: 32727
Address: 5, Value: 32808
Address: 6, Value: 0
Address: 7, Value: 3
Address: 8, Value: 2
Address: 9, Value: 1
————————————————