目录
- Modbus通信协议
- 存储区-线圈和寄存器
- 功能码
- Modbus通信数据帧
- MBAP报文头
- PDU详细结构
- Java程序作为主机时通过ModbusTCP通信
- Modbus Slave模拟器
- 通过modbus-master-tcp实现通信
Modbus通信协议
Modbus通信协议由Modicon公司(现在的施耐德电气Schneider Electric)于1979年为可编程逻辑控制(即PLC)通信而发表,是工业电子设备之间常用的连接方式。
它包括ASCII、RTU、TCP三种报文类型,采用master/slave方式通信。
存储区-线圈和寄存器
为了更好存储不同的数据类型,modbus会将布尔和非布尔的数据分开存储。
布尔类型:0/1 或 false/true。
- 线圈:存储布尔值
从电气角度来看,在电气控制回路中,一般都是靠接触器或中间继电器来实现控制,接触器或中继最终靠的是线圈的得电和失电来控制触点闭合和断开,因此用线圈表示布尔量; - 寄存器:存储非布尔值
用来暂时存放参与运算的数据和运算结果,具有接收数据、存放数据和输出数据的功能。而寄存器在计算机中,就是用来存储数据的,因此非布尔的数据放在寄存器里。
对于不同类型的存储区又分为只读和读写两种情况,故存在以下四种存储区:
存储区名称 | 存储类型 | 读写 | 存储区代号 | 绝对地址范围 | 相对地址范围 |
输出线圈 | 线圈 | 读写 | 0区 | 000001-065536 | 0-65535 |
输入线圈 | 线圈 | 只读 | 1区 | 100001-165536 | 0-65535 |
输入寄存器 | 寄存器 | 只读 | 3区 | 300001-365536 | 0-65535 |
保持寄存器 | 寄存器 | 读写 | 4区 | 400001-465536 | 0-65535 |
存储区范围: 无论是什么存储区,都会有一个范围的限制,Modbus规定每个存储区的最大范围是65536。
在实际使用中,一般用不了这么多地址,一般情况下,10000以内就已经足够使用了;因此,为了方便有一种短的地址模型,如下图所示(反之其它地址范围剩余部分则为:长地址模型)
存储区名称 | 存储区代号 | 绝对地址范围 | 相对地址范围 |
输出线圈 | 0区 | 00001-09999 | 0-9998 |
输入线圈 | 1区 | 10001-19999 | 0-9998 |
输入寄存器 | 3区 | 30001-39999 | 0-9998 |
保持寄存器 | 4区 | 40001-49999 | 0-9998 |
功能码
对于4个存储区的不同操作,modbus分别定义了不同的功能码用来区分不同行为。
主要的8个功能码如下。(Modbus规约中的功能码其实不止这8个,还有一些功能码是用于诊断或异常码,但是一般很少使用)
代码 | 功能码 | 中文名称 | 英文名 | 位操作/字操作 | 操作数量 |
01 | 0x01 | 读输出线圈 | READ COIL STATUS | 位操作 | 单个或多个 |
02 | 0x02 | 读输入线圈 | READ INPUT STATUS | 位操作 | 单个或多个 |
03 | 0x03 | 读保持寄存器 | READ HOLDING REGISTER | 字操作 | 单个或多个 |
04 | 0x04 | 读输入寄存器 | READ INPUT REGISTER | 字操作 | 单个或多个 |
05 | 0x05 | 写单个输出线圈 | WRITE SINGLE COIL | 位操作 | 单个 |
06 | 0x06 | 写单个保持寄存器 | WRITE SINGLE REGISTER | 字操作 | 单个 |
15 | 0x0F | 写多个输出线圈 | WRITE MULTIPLE COIL | 位操作 | 多个 |
16 | 0x10 | 写多个保持寄存器 | WRITE MULTIPLE REGISTER | 字操作 | 多个 |
Modbus通信数据帧
ModbusTCP的数据帧可分为两部分:MBAP+PDU。
MBAP报文头
MBAP为报文头,长度为7字节,组成如下。
内容 | 长度 | 解释 |
事务处理标识 | 2字节 | 可以理解为报文的序列号,一般每次通信之后就要加1以区别不同的通信数据报文。 |
协议标识符 | 2字节 | 00 00表示ModbusTCP协议。 |
长度 | 2字节 | 表示接下来的数据长度,单位为字节。 |
单元标识符 | 1字节 | 可以理解为设备地址。 |
PDU详细结构
PDU由功能码+数据组成。 功能码为1字节,数据长度不定,由具体功能决定。
一个字节(Byte)为8个Bit(位),8Bit用2个16进制直接就能表达出来,不管阅读还是存储都比其他进制要方便,故以下均用16进制表示。
例如读线圈:
请求:MBAP 功能码 起始地址H 起始地址L 数量H 数量L(共12字节,H表示高位,L表示地位)
响应:MBAP 功能码 数据长度 数据
请求:在从站0x01中,读取开始地址为0x0002的线圈数据,读0x0008位,报文如下。
响应:数据长度为0x01个字节,数据为0x01,第一个线圈为ON,其余为OFF,报文如下。
报文解释:
- 响应报文中的数据是【01】,是16进制的,读取的是8个线圈需要将16进制的【01】转为2进制得到【0000 0001】,故只有第一个线圈为ON,其余为OFF。
- 响应报文中的数据长度是【01】,表示1个字节,指的是响应报文中的数据【01】的长度,【01】是2个16进制 = 1个字节。
- 接下来的数据长度,以响应报文为例接下来的数据长度是【00 04】表示4个字节,指的是【00 04】后面【01 01 01 01】这段报文的长度,【01 01 01 01】是8个16进制 = 4个字节。
高位/低位解释
比如一个8位二进制0000 0001, 0000就是高四位,0001就是低四位。
字节序(大端/小端)
指在内存中以字节为单位的排列顺序,与cpu和操作系统有关,操作系统可以选择大小端,java默认读取按大端读取。
小端(Little-Endian) 就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。(低低高高,低地址低字节,高地址高字节)
大端(Big-Endian) 就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。(低高高低,低地址高字节,高地址低字节)
比如将0x123456存入从0x0000开始的内存中,大端/小端分别会按如下顺序存储,读取时也要按相应顺序读取。
Java程序作为主机时通过ModbusTCP通信
Modbus Slave模拟器
modbus slave作为从机,java程序访问从机获取数据/写入数据。
仿真软件网址:https://modbustools.com/download.html
使用方式如下。
- 选择TCP模式,端口是固定的502
- 设置功能码等信息
- 设置值
双击单元格,设置该地址的值 - 这里可以选择数据类型和格式
Signed:有符号
Unsigned:无符号
Hex:十六进制
Binary:二进制
AB CD/CD AB等等表示字节序
AB CD/CD AB等字节序解释
如:32位整数1,按不同的字节序会解析得到不用的结果
通过modbus-master-tcp实现通信
注入依赖
<dependency>
<groupId>com.digitalpetri.modbus</groupId>
<artifactId>modbus-master-tcp</artifactId>
<version>1.2.0</version>
</dependency>
读取数据
public static void main(String[] args) throws ExecutionException, InterruptedException {
//建立连接
ModbusTcpMasterConfig config = new ModbusTcpMasterConfig
//ip地址
.Builder("localhost")
//端口
.setPort(502).build();
ModbusTcpMaster master = new ModbusTcpMaster(config);
master.connect();
//1、读取保持寄存器,开始地址:0,读取寄存器数量:2,slaveId:1
CompletableFuture<ReadHoldingRegistersResponse> future1 = master.sendRequest(
new ReadHoldingRegistersRequest(0, 2), 1);
ReadHoldingRegistersResponse response1 = future1.get();
ByteBuf buf1 = response1.getRegisters();
//读取十六进制数据
String hex = ByteBufUtil.hexDump(buf1);
//读取字节流
byte[] bytes = ByteBufUtil.getBytes(buf1);
//十六进制转成整数
Integer i = Integer.parseInt(hex, 16);
//字节流转成字符串
String s = new String(bytes);
//释放
ReferenceCountUtil.release(response1);
//2、读取输入寄存器,开始地址:10,读取寄存器数量:2,slaveId:1
CompletableFuture<ReadInputRegistersResponse> future2 = master.sendRequest(
new ReadInputRegistersRequest(10, 2), 1);
ReadInputRegistersResponse response2 = future2.get();
ByteBuf buf2 = response2.getRegisters();
ReferenceCountUtil.release(response2);
//3、读取输出线圈,开始地址:20,读取线圈数量:1,slaveId:1
CompletableFuture<ReadCoilsResponse> future3 = master.sendRequest(
new ReadCoilsRequest(20, 1), 1);
ReadCoilsResponse response3 = future3.get();
ByteBuf buf3 = response3.getCoilStatus();
ReferenceCountUtil.release(response3);
//4、读取输入线圈,开始地址:30,读取线圈数量:1,slaveId:1
CompletableFuture<ReadDiscreteInputsResponse> future4 = master.sendRequest(
new ReadDiscreteInputsRequest(20, 1), 1);
ReadDiscreteInputsResponse response4 = future4.get();
ByteBuf buf4 = response4.getInputStatus();
ReferenceCountUtil.release(response4);
//释放连接
master.disconnect();
Modbus.releaseSharedResources();
}
以读取保持寄存器为例,从地址0开始读取2个寄存器,数据设置和读取结果如下。
字节流转二进制
/**
* 字节流转二进制
* @param bytes 字节流
* @return 二进制字符串
*/
public String bytesToBinarySystem(byte[] bytes){
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
String binary = Integer.toBinaryString(b & 0xFF);
result.append(String.format("%8s", binary).replace(' ', '0'));
}
return result.toString();
}