ModbusTcp协议的Java Socket
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
————————————————