目录

  • 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位,报文如下。

springboot modbus 寄存器地址解析 modbus寄存器起始地址_寄存器

响应:数据长度为0x01个字节,数据为0x01,第一个线圈为ON,其余为OFF,报文如下。

springboot modbus 寄存器地址解析 modbus寄存器起始地址_16进制_02


报文解释:

  • 响应报文中的数据是【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开始的内存中,大端/小端分别会按如下顺序存储,读取时也要按相应顺序读取。

springboot modbus 寄存器地址解析 modbus寄存器起始地址_数据_03

Java程序作为主机时通过ModbusTCP通信

Modbus Slave模拟器

modbus slave作为从机,java程序访问从机获取数据/写入数据。
仿真软件网址:https://modbustools.com/download.html

使用方式如下。

  1. 选择TCP模式,端口是固定的502
  2. springboot modbus 寄存器地址解析 modbus寄存器起始地址_java_04

    springboot modbus 寄存器地址解析 modbus寄存器起始地址_寄存器_05

  3. 设置功能码等信息
  4. springboot modbus 寄存器地址解析 modbus寄存器起始地址_寄存器_06


  5. springboot modbus 寄存器地址解析 modbus寄存器起始地址_数据_07

  6. 设置值
    双击单元格,设置该地址的值
  7. springboot modbus 寄存器地址解析 modbus寄存器起始地址_java_08

  8. 这里可以选择数据类型和格式
    Signed:有符号
    Unsigned:无符号
    Hex:十六进制
    Binary:二进制
    AB CD/CD AB等等表示字节序
  9. springboot modbus 寄存器地址解析 modbus寄存器起始地址_16进制_09

AB CD/CD AB等字节序解释

如:32位整数1,按不同的字节序会解析得到不用的结果

springboot modbus 寄存器地址解析 modbus寄存器起始地址_寄存器_10

通过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个寄存器,数据设置和读取结果如下。

springboot modbus 寄存器地址解析 modbus寄存器起始地址_寄存器_11


springboot modbus 寄存器地址解析 modbus寄存器起始地址_数据_12

字节流转二进制

/**
 * 字节流转二进制
 * @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();
}