Modbus 协议快速入门
@
Modbus 协议快速入门
1.什么是 Modbus 协议
什么是协议:是一种约定或规则或规则,它在计算机网络和通信领域起着至关重要的作用。具体来说,协议是网络中(或一般业务中)进行数据交换和解释信息时所要遵守的一套规则和约定,或者说是通信双方共同遵守的一组规则或标准。这些规则或标准详细定义了信息的格式、传输的顺序、控制信息以及同步机制等各个方面。
什么是modbus协议:Modbus协议是一种串行通信协议,由Modicon公司(现为施耐德电气Schneider Electric)于1979年发表,旨在实现可编辑逻辑控制器(PLC)之间的通信。如今,它已经成为工业领域通信协议的业界标准,并且是工业电子设备之间常用的连接方式。Modbus是主从方式通信,也就是说,不能同步进行通信,总线上每次只有一个数据进行传输,既主机发送,从机应答,主机不发送,总线上就没有数据通信。一个主线上只能有一个主句,可以有若干个从机。
什么是串行通信协议:串行通信是一种计算机通信方式,它在主机与外设以及主机之间的数据传输中起着重要作用。串行通信是指数据按位依次传输的通信方式,每位数据占据固定的时间长度,并使用少数几条通信线路完成系统间的信息交换。在串行通信中,数据被分解为一系列单独的比特(位),并按顺序通过传输线进行传输。每个比特在传输线上占用固定的时间长度,这样接收端就可以按照相同的时序接收并重组这些数据位,从而回复出原始数据。
2.Modbus有什么用
Modbus协议广泛应用于工业控制和自动化领域,可以连接各种设备和控制器,用于实现数据交换、监控和控制。具体包括:
- 工业自动化控制:Modbus被广泛应用于工业自动化控制系统中,用于连接PLC、传感器、执行器等设备,实现监控和控制
- 智能家居:Modbus协议可以应用于智能家居系统中,用于连接各种传感器和执行器,实现远程控制和检测
- 能源监控:Modbus协议可以用于能源监控系统,连接电表、燃气表、水表等设备,实现能源数据的采集和分析。
- 环境检测:Modbus协议可以应用于环境检监测系统中,连接各种传感器和仪器,监测环境参数如温度、湿度、气压等。
- 智能交通:Modbus可以应用于智能交通系统中,用于连接交通控制设备、车辆检测器等,实现交通信号的控制和管理
总的来说,就是约定了一套设备之间数据交互的规则,用于各种设备之间进行通信的。
3.Modbus内容
3.1 Modbus概述
modbus分为以下三种协议:
- Modbus-RTU
- Modbus-TCP
- Modbus-ASCII
以上三种协议,比较常用的 Modbus-RTU 协议,其次是 Modbus-TCP 协议,一个设备只会有一种协议。
Modbus是主从方式通信
什么是帧:Modbus每发送一次数据就是一个数据帧,每个数据帧都必须符合modbus的帧结构。
3.2 Modbus-RTU
设备必须要有RTU协议!这是Modbus协议上规定的,且默认模式必须是 RTU,ASCLL作为选项。
也就是说,大部分时候我们都是使用 Modbus-RTU协议进行通信
3.2.1 帧格式
帧结构 = 地址 + 功能码 + 数据 + 校验
- 地址(设备编号):占用一个字节,范围是 0-255,其中有效范围是 1-247,其他有特殊用途,比如 255 是广播地址(广播就是应答所有地址,正常的需要两个设备的地址一样才能进行查询和回复)
- 功能码:占一个字节,功能码的意义就是,知道这个指令是干啥的,就是告诉从机你要执行什么操作(常用 0x03,0x06和0x10功能码)
- 数据:根据功能码的不同,数据会有不同的结构,具体详见下面的分析
- 校验:为了保证数据不错误,需传输校验码进行校验,如果校验无误则代表传输的数据并没有丢失。校验的算法为CRC冗余校验
3.2.2 0x03查询寄存器功能码
如果有硬件条件,使用支持modbus协议的设备,通过串口连接主机以及设备,同时打开串口调试工具按下面的进行操作
主机发送:02 03 80 00 00 02 ED F8
从机返回:02 03 04 00 00 20 09 10 F5
如果通过发送上面的数据,从机返回对应数据,则代表我们成功通过串口实现了受用 modbus 协议进行通信
来分析上面的报文
发送 02 03 80 00 00 02 ED F8
02(从机地址) + 03(功能码) + 8000(起始寄存器地址) + 0002(查询的寄存器个数) + ED F8(CRC校验)
1 bit + 1 bit + 2 bit + 2 bit + 2 bit (总长度固定为8bit)
02:从机地址,这里是发给设备编号为02的从机,占1bit
03:功能码,0x03代表查询寄存器功能,占1bit
8000:要查询的起始寄存器地址,这里寄存器地址为 0x8000,占2bit
0002:要查询几个寄存器,这里查询2(0x0002)个寄存器,占2bit
ED F8:CRC校验码,通过CRC算法算的,占2bit
这串报文简单化来讲就是,我向02设备发送报文(02),告诉02设备我要查询寄存器(03功能码),寄存器起始地址是0x8000(根据实际需要修改寄存器地址),我要查询 2(0002)个寄存器,为了保证我发的这个报文是没有被人动过的,我告诉02设备,我这个报文的密码是 ED F8(CRC校验码),02设备你收到我发给你的报文后,看下这个密码能不能打开报文(从机设备会将除校验码外的其他报文用CRC算法进行计算,并比对报文中的CRC校验码,一致则代表报文是完整的),打不开的话那就是我这边给错密码了,或者是中间丢失了一些东西(报文不完整)
回复 02 03 04 00 00 20 09 10 F5
02(从机地址) + 03(功能码)+ 04(数据长度) + 0000(第一个数据) + 2009(第二个数据) + 10 F5(CRC校验)
1 bit + 1 bit +1bit + 2 bit + 2 bit + 2 bit (总长度固定为9bit)
02:从机地址,这里是返回的设备编号为02的从机,占1bit
03:功能码,0x03代表查询寄存器功能,占1bit
04:返回的数据长度,数值为 2x查询的寄存器个数,占1bit
0000:查询0x8000寄存器数据,占2bit
2009:查询0x8001寄存器数据,占2bit
10 F5:CRC校验码,通过CRC算法算的,占2bit
注:功能码和CRC校验中间的为查询后返回的数据,长度为 2bit x 查询的寄存器个数,每2bit作为一个返回的值
这串报文简单化来讲就是,02设备说我是02设备(02),刚刚执行的是查询寄存器的操作(03),总共返回的数据长度为为4bit(04),返回的数据为 0x0000 和 0x2009(0000 2009),主机你收到我的报文后用 10 F5 作为密码进行打开(CRC校验码)
也就是说,
主机发送的报文就是: 找谁(从机地址) + 要干嘛(功能码) + 具体要干的事情细节(数据:寄存器起始地址 + 查询的寄存器个数)+ 验证
从机发送的报文就是: 我是谁(从机地址) + 刚刚干了什么事情(功能码) + 干了几件事情(查询的寄存器个数) + 每件事情的结果(数据:寄存器里面的值)+ 验证
3.2.3 0x06修改寄存器功能码
报文如下
主机发送:02 06 A8 0A 00 01 48 5B
从机返回:02 06 A8 0A 00 01 48 5B
对上面报文进行分析
发送 02 06 A8 0A 00 01 48 5B
02(从机地址) + 06(功能码) + A80A(修改的寄存器地址) + 0001(修改后的数据) + 48 5B(CRC校验)
1 bit + 1 bit + 2 bit + 2 bit + 2 bit (总长度固定为8bit)
02:从机地址,这里是发给设备编号为02的从机,占1bit
06:功能码,0x06代表修改寄存器功能,占1bit
A80A:要修改的寄存器地址,这里寄存器地址为 0xA80A,占2bit
0001:修改后的值,这里为 0x0001,占2bit
48 5B:CRC校验码,通过CRC算法算的,占2bit
0x06发送的报文和0x03发送的报文很像,区别就是0x03修改寄存器的个数咋 0x06 就是修改后的数据,可以进行联想记忆。
这串报文简单化来讲就是,主机向02设备发送报文(02),告诉02设备我要修改寄存器(06),修改的寄存器地址为 0xA80A (A80A ),修改后的数据为 0x0001(0001),打开这个报文的密码是 48 5B(CRC校验码)
回复 02 06 A8 0A 00 01 48 5B
02(从机地址) + 06(功能码) + A80A(修改的寄存器地址) + 0001(修改后的数据) + 48 5B(CRC校验)
1 bit + 1 bit +2bit +2bit + 2 bit (总长度固定为8bit)
这里可以看到,0x06修改命令返回的报文和发送的报文是一致的,但他们代表的意义是不同的,当然,除了一些特殊的从机地址,可以按照返回的报文和发送的报文一致来判断返回报文是否正确
02:从机地址,这里是返回的设备编号为02的从机,占1bit
06:功能码,0x06代表修改寄存器功能,占1bit
8000:已经修改的寄存器地址,这里寄存器地址为 0xA80A,占2bit
0001:修改完成后该寄存器的值,这里为 0x0001,占2bit
48 5B:CRC校验码,通过CRC算法算的,占2bit
这串报文简单化来讲就是,02设备向主机发送报文,告诉主机我是02设备(02),刚刚执行了修改寄存器(0x06),修改后的寄存器地址为 0xA80A (A80A ),修改后的数据为 0x0001(0001),打开这个报文的密码是 48 5B(CRC校验码)
3.2.3 0x10批量修改寄存器功能码
在批量修改或者修改32位寄存器(32位寄存器在存储时占了两个16位的寄存器)时使用这个命令
报文如下
主机发送:02 10 A8 06 00 02 04 00 0F 00 03 93 04
从机返回:02 10 A8 06 00 02 81 9A
对上面报文进行分析
发送:02 10 A8 06 00 02 04 00 0F 00 03 93 04
02(从机地址)+ 10(功能码)+ A806(寄存器起始地址)+ 0002(寄存器个数)+ 04(数据长度)+ 000F(数据1)+ 0003(数据2)+ 9304(CRC校验码)
1bit + 1bit + 2bit + 2bit + 2bit + (2bit * 查询的寄存器个数) + 2bit
02:从机地址,这里是发给设备编号为02的从机,占1bit
10:功能码,0x10代表批量修改寄存器功能,占1bit
A806:要修改的寄存器起始地址,这里寄存器起始地址为 0xA806,占2bit
0002:修改的寄存器个数,这里为 0x0002,占2bit
04:修改的数据长度,数值为 2x修改的寄存器个数,这里为 0x04,占1bit
000F:修改的第一个寄存器的值,这里为 0x000F,占2bit
0003:修改的第二个寄存器的值,这里为000F,占2bit
93 04:CRC校验码,通过CRC算法算的,占2bit
这个报文和发送0x03查询的报文有点相似,就是在查询寄存器个数后面增加了要发送的数据的长度和具体数据,可以联想来记忆
这串报文简单化来讲就是,主机向02设备发送报文(02),告诉02设备我要批量修改寄存器(10功能码),被修改的寄存器起始地址是0xA806(A806),我要修改 2(0002)个寄存器,我接下来要修改的值长度位 0x04(04),第一个寄存器的值要改为 0x000F(000F),第二个寄存器的值要改为 0x0003(0003),为了保证我发的这个报文是没有被人动过的,我告诉02设备,我这个报文的密码是 93 04(CRC校验码),
返回:02 10 A8 06 00 02 81 9A
02(从机地址)+ 10(功能码)+ A806(寄存器起始地址)+ 0002(寄存器个数)+ 819A(CRC校验码)
1bit + 1bit + 2bit +2bit + 2bit (总长度固定为8bit)
02:从机地址,这里是发给设备编号为02的从机,占1bit
10:功能码,0x10代表批量修改寄存器功能,占1bit
A806:已经修改的寄存器起始地址,这里寄存器起始地址为 0xA806,占2bit
0002:修改好了的寄存器个数,这里为 0x0002,占2bit
81 9A:CRC校验码,通过CRC算法算的,占2bit
这串报文简单化来讲就是,02设备向主机发送报文,告诉主机我是02设备(02),刚刚执行了批量修改寄存器(0x10),修改后的寄存器地址为 0xA806 (A806 ),修改好的寄存器个数为 0x0002(0002),打开这个报文的密码是 81 9A(CRC校验码)
注:32位的寄存器需要用 0x10功能码进行批量修改,不能用0x6修改单个寄存器的功能码修改
3.3 Modbus-TCP
Modbus-TCP 报文帧的格式和 Modbus-RTU是差不多的,区别就在于 Modbus-TCP 采用的是TCP进行连接,而非串口,报文帧的头部比Modbus-RTU要多了6bit的数据, Modbus-TCP不需要做CRC冗余校验。
Modbus-TCP 的报文头部起始为 :
-
事物处理标识符:长度2bit,可以理解为报文的序列号,一般每次通信之后就要加1以区别不同的通信数据报文
-
协议标识符:长度2bit, 0x0000 代表 Modbus-TCP协议
-
长度:长度2bit,表示接下来的字节长度,单位字节
报文头部,Nodbus-TCP的报文帧在Modbus-RTU 的基础上,在增加了上面 6 bit 的数据
报文尾部,Nodbus-TCP不需要做CRC冗余校验
以 0x03 查询功能码为例
Modbus-RTU发送:02 10 A8 06 00 02 04 00 0F 00 03 93 04
Modbus-TCP发送:00 01 00 00 00 0B 02 10 A8 06 00 02 04 00 0F 00 03
00 01 00 00 00 0B
00 01:事物处理标识符
00 00:Modbus-TCP协议
00 0B:接下来的数据长度为 11(0x000B) 个
02 10 A8 06 00 02 04 00 0F 00 03:与前面RTU的报文格式差不多,只是少了CRC冗余校验码
3.4 Modbus-ACSSII
一般只需要了解RTU协议,因为Modbus协议的设备都必须有 Modbus-RTU协议,至于ACSLL协议,做个大概了解即可
3.4.1 帧形式
对于RTU协议,比如RTU协议发送一个字节:0x12;ASCLL协议则需要发送2个字节:1个字节代表ASCLL码1,一个代表ASCLL码2,既0x31和0x32才能代表0x12.所以,ASCLL协议的效率比较低。但是,ASCLL更符合串口打印查看,因为串口发送的数据一般都是文本模式(ASCLL)。
但是因为RTU一个字节ASCLL需要两个字节来表示,所以ASCLL发送的数据量是RTU的两倍,ASCLL的效率更低
那么ASCLL码效率更低,数据发送量大为啥还采用这种方式呢?
因为假如你要发送数据0x03,采用RTU方式(16进制发送),计算机终端设备接收到0x03后是不可以显示的,就是不能把0x03打印出来。因为可见字符的ASCLL码是从32-126,不是这个范围以外的显示屏上都看不到,会出现乱码,如果是串口助手的话就会显示口口口口。如果采用ASCLL方式(文本模式发送),就不会出现不可显示和乱码的情况,因为文本模式发送0x03,就是发送ASCLL码0和ASCLL码3.也就是0x30和0x33,是可以正常显示在计算机终端的。所以ASCLL效率虽然低,但方便调试显示。
从上图可以看出:
- 比TRU多了起始段 : ,多个结束符 CR,LF
- 地址和功能都变成了2个字节
- 数据部分更加繁琐,但更符合人们的查看
3.5 CRC冗余校验
主机或子机可用校验码进行判别接收信息是否出错。有时,由于电子噪声或其他一些干扰,信息在传输过程中会发生细微的变化,错误校验码保证了主机或子机对在传送过程中出错的信息不起作用。这样增加了系统的安全和效率。错误校验码采用CRC-16校验方法。
二字节的错误校验码,低字节在前,高字节在后。
/**
* @description: 计算 CRC值
* @author WXP
* @date: 2024/10/14 13:46
*/
public static int calculateCRC(byte[] message, int length) {
int crc = 0xFFFF;
for (int i = 0; i < length; i++) {
crc ^= (message[i] & 0xFF);
for (int j = 0; j < 8; j++) {
if ((crc & 1) == 1) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc = crc >> 1;
}
}
}
return crc;
}
4. 如何通过代码实现 Modbus协议
底层代码实现比较需要注意的就是高低位的转换,下面是高低位的提取代码
int hex = 0x5A6B
byte high = (byte) (hex >> 8);// 高位 高位右移8位得出高位
byte low = (byte) (hex & 0xFF);// 低位 0x00FF,高位和 00做&操作留下低位
4.1 Modbus-RTU
需先导入串口通信的包 jSerialComm
<dependency>
<groupId>com.fazecast</groupId>
<artifactId>jSerialComm</artifactId>
<version>2.9.2</version>
</dependency>
package org.xp;
import com.fazecast.jSerialComm.SerialPort;
import java.util.ArrayList;
import java.util.Arrays;
public class ModbusRTURequestBuilder {
private static final int slaveAddress = 2;
private static final int functionCode = 3;
private static final int updateSingleFunctionCode = 0x0006;
private static final int updateMulFunctionCode = 0x0010;
private static final int startAddress = 0x8000;
private static final int registerCount = 2;
private static final int updateData = 0x0010;
private static final int updateMulData = 0x02;
public static void main(String[] args) {
// 设置通信端口、波特率、数据大小、校验位、停止位
SerialPort serialPort = SerialPort.getCommPort("COM3");
serialPort.setBaudRate(9600);
serialPort.setNumDataBits(8);
serialPort.setParity(SerialPort.NO_PARITY);
serialPort.setNumStopBits(1);
/**
* 03 查询功能
*/
// 打开串口,传输数据
if (serialPort.openPort()) {
byte[] request = ModbusRTURequestBuilder.buildModbusRTURequest(slaveAddress, functionCode, startAddress, registerCount);
System.out.println("发送的报文:" + Arrays.toString(request));
serialPort.writeBytes(request, request.length);
}
// 睡眠 100 毫秒,等待从机响应
try {
Thread.sleep(100); // 如果接受到的报文不完整,则增加睡眠时间
} catch (Exception e) {
e.printStackTrace();
}
// 接收从机响应,处理返回数据
byte[] bytes = new byte[5 + registerCount * 2];
serialPort.readBytes(bytes, bytes.length);
System.out.println("接收的报文:" + Arrays.toString(bytes));
// 校验冗余码
int crc = ModbusRTURequestBuilder.calculateCRC(bytes, bytes.length - 2);
byte high = (byte) (crc & 0xFF);// 高位
byte low = (byte) (crc >> 8);// 低位
System.out.println("冗余码" + high + "," + low);
if (high == bytes[bytes.length - 2] && low == bytes[bytes.length - 1]) {
System.out.println("冗余码校验无误");
// 处理接收的报文
ArrayList<String> strings = formatData(bytes, 3);
System.out.println(Arrays.toString(strings.toArray()));
}
/**
* 06 修改功能
*/
System.out.println("--------------06修改-----------------");
// 打开串口,传输数据
if (serialPort.openPort()) {
byte[] request = ModbusRTURequestBuilder.buildModbusRTURequest(slaveAddress, updateSingleFunctionCode, startAddress, updateData);
System.out.println("发送的报文:" + Arrays.toString(request));
serialPort.writeBytes(request, request.length);
}
// 睡眠 100 毫秒,等待从机响应
try {
Thread.sleep(100); // 如果接受到的报文不完整,则增加睡眠时间
} catch (Exception e) {
e.printStackTrace();
}
// 接收从机响应,处理返回数据
bytes = new byte[8];
serialPort.readBytes(bytes, bytes.length);
System.out.println("接收的报文:" + Arrays.toString(bytes));
// 校验冗余码
crc = ModbusRTURequestBuilder.calculateCRC(bytes, bytes.length - 2);
high = (byte) (crc & 0xFF);// 高位
low = (byte) (crc >> 8);// 低位
System.out.println("冗余码" + high + "," + low);
if (high == bytes[bytes.length - 2] && low == bytes[bytes.length - 1]) {
System.out.println("冗余码校验无误");
// 处理接收的报文
ArrayList<String> strings = formatData(bytes, 2);
System.out.println(Arrays.toString(strings.toArray()));
}
/**
* 10 批量修改
*/
System.out.println("-------------批量修改------------------");
// 打开串口,传输数据
if (serialPort.openPort()) {
// 修改的数据
ArrayList<Integer> dataList = new ArrayList<>();
dataList.add(0x0004);
dataList.add(0x0010);
dataList.add(0x0004);
byte[] request = ModbusRTURequestBuilder.buildModbusRTURequest(slaveAddress, updateMulFunctionCode, startAddress, registerCount, dataList);
System.out.println("发送的报文:" + Arrays.toString(request));
serialPort.writeBytes(request, request.length);
}
// 睡眠 100 毫秒,等待从机响应
try {
Thread.sleep(100); // 如果接受到的报文不完整,则增加睡眠时间
} catch (Exception e) {
e.printStackTrace();
}
// 接收从机响应,处理返回数据
bytes = new byte[8];
serialPort.readBytes(bytes, bytes.length);
System.out.println("接收的报文:" + Arrays.toString(bytes));
// 校验冗余码
crc = ModbusRTURequestBuilder.calculateCRC(bytes, bytes.length - 2);
high = (byte) (crc & 0xFF);// 高位
low = (byte) (crc >> 8);// 低位
System.out.println("冗余码" + high + "," + low);
if (high == bytes[bytes.length - 2] && low == bytes[bytes.length - 1]) {
System.out.println("冗余码校验无误");
// 处理接收的报文
ArrayList<String> strings = formatData(bytes, 2);
System.out.println(Arrays.toString(strings.toArray()));
}
// 关闭连接
serialPort.closePort();
}
/**
* @param from 从第几位开始截取数组
* @description: 讲接收的报文数据进行提取
* @param: bytes 接收到的报文
* @return: 处理后的报文(十六进制字符串)
* @author WXP
* @date: 2024/10/10 14:33
*/
public static ArrayList<String> formatData(byte[] bytes, int from) {
byte[] data = Arrays.copyOfRange(bytes, from, bytes.length - 2);
ArrayList<String> newData = new ArrayList<>();
// 报文中数据
System.out.println("报文中数据" + Arrays.toString(data));
// 判断数组长度是不是偶数
if (data.length % 2 == 0) {
for (int i = 0; i < data.length; i += 2) {
String hex = toHex(data[i]) + toHex(data[i + 1]);
newData.add(hex);
System.out.println("十六进制:" + hex);
System.out.println("十进制:" + Integer.parseInt(hex, 16));
}
}
return newData;
}
/**
* @description: byte 转成 16进制字符串
* @param: b
* @return: 16进制字符串
* @author WXP
* @date: 2024/10/10 14:34
*/
private static String toHex(byte b) {
// Convert byte to unsigned int and then to hex string
return String.format("%02X", b & 0xFF);
}
/**
* @param slaveAddress 从站地址
* @param functionCode 功能码
* @param startAddress 起始寄存器地址
* @param registerCount 寄存器数量
* @param dataList 批量更新的数据,若不是批量更新,则传入 null
* @return Modbus-RTU 协议请求报文
*/
public static byte[] buildModbusRTURequest(int slaveAddress, int functionCode, int startAddress, int registerCount, ArrayList<Integer> dataList) {
// 创建一个字节数组用于存储报文
byte[] request = new byte[dataList != null && !dataList.isEmpty() ? 8 + dataList.size() * 2 - 1 : 8];
// 设置从机地址
request[0] = (byte) slaveAddress;
// 设置状态码
request[1] = (byte) functionCode;
// 设置开始地址
request[2] = (byte) (startAddress >> 8);
request[3] = (byte) (startAddress & 0xff);
// 设置寄存器数量 高位和低位
request[4] = (byte) (registerCount >> 8);
request[5] = (byte) (registerCount & 0xff);
// 存在批量修改时
if (dataList != null && !dataList.isEmpty()) {
// 字节数
request[6] = dataList.get(0).byteValue();
int j = 7;
for (int i = 1; i < dataList.size(); i++) {
request[j] = (byte) (dataList.get(i) >> 8);
j++;
request[j] = (byte) (dataList.get(i) & 0xff);
j++;
}
}
// 设置CRC 校验码
int crc = calculateCRC(request, request.length - 2);
request[request.length - 2] = (byte) (crc & 0xFF); // 低位
request[request.length - 1] = (byte) (crc >> 8); // 高位
return request;
}
/**
* @param slaveAddress 从站地址
* @param functionCode 功能码
* @param startAddress 起始寄存器地址
* @param registerCount 寄存器数量
* @return Modbus-RTU 协议请求报文
*/
public static byte[] buildModbusRTURequest(int slaveAddress, int functionCode, int startAddress, int registerCount) {
return buildModbusRTURequest(slaveAddress, functionCode, startAddress, registerCount, null);
}
/**
* @description: 计算 CRC值
* @param: message
* length
* @return:
* @author WXP
* @date: 2024/10/14 13:46
*/
public static int calculateCRC(byte[] message, int length) {
int crc = 0xFFFF;
for (int i = 0; i < length; i++) {
crc ^= (message[i] & 0xFF);
for (int j = 0; j < 8; j++) {
if ((crc & 1) == 1) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc = crc >> 1;
}
}
}
return crc;
}
}
4.2 Modbus-TCP
通过创建 socket 实现Modbus 的TCP连接,报文解析和Modbus-RTU差不多,多了头部6个字节的解析以及少了后面的CRC冗余码
package org.xp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
/**
* @author sam
* @version 1.0
* @description: ModbusTcp客户端
* @date 2024/10/12 08:28
*/
public class ModbusTcpClient {
private static final int slaveAddress = 2;
private static final int functionCode = 3;
private static final int updateSingleFunctionCode = 0x0006;
private static final int updateMulFunctionCode = 0x0010;
private static final int startAddress = 0x8D00;
private static final int updateSingleStartAddress = 0xA80A;
private static final int updateNulStartAddress = 0xA806;
private static final int registerCount = 2;
private static final int updateData = 0x0001;
private static final int updateMulData = 0x02;
private Socket clientSocket;
private String ipAddress;
private int port;
// 初始化 IP 和端口
public ModbusTcpClient(String ipAddress, int port) {
this.ipAddress = ipAddress;
this.port = port;
}
// 打开 socket 连接
public void connect() throws Exception {
clientSocket = new Socket(ipAddress, port);
// 检查连接状态可以使用clientSocket.isConnected();
}
// 关闭资源
public void disconnect() throws Exception {
if (clientSocket != null) {
clientSocket.close();
}
}
// 发送数据
public void sendRequest(byte[] request) throws Exception {
OutputStream outputStream = clientSocket.getOutputStream();
outputStream.write(request);
}
/**
* @description: 接收数据
* @param:
* @return: 从机返回的数据
* @author WXP
* @date: 2024/10/12 10:53
*/
public byte[] receiveResponse() throws Exception {
InputStream inputStream = clientSocket.getInputStream();
// 假定响应是1024字节,实际使用时可能需要根据实际情况进行调整
byte[] buffer = new byte[1024];
int bytesRead = inputStream.read(buffer);
System.out.println("字节长度:" + bytesRead);
// 截取对应长度的数据放到新的 byte数组中
return bytesRead > 0 ? Arrays.copyOfRange(buffer, 0, bytesRead) : null;
}
/**
* @description: 构建发送消息帧
* @param: slaveAddress 从机地址
* functionCode 功能码
* startAddress 起始地址
* registerCount 寄存器个数
* dataList 数据
* @return: 封装好的消息byte数组
* @author WXP
* @date: 2024/10/12 14:22
*/
public static byte[] buildMessage(int slaveAddress, int functionCode, int startAddress, int registerCount, ArrayList<Integer> dataList) {
// 创建一个字节数组用于存储报文
byte[] request = new byte[dataList != null && !dataList.isEmpty() ? 12 + dataList.size() * 2 - 1 : 12];
// 事物处理标识符
request[0] = (byte) Integer.parseInt("00", 16);
request[1] = (byte) Integer.parseInt("01", 16);
// 协议标识符
request[2] = (byte) Integer.parseInt("00", 16);
request[3] = (byte) Integer.parseInt("00", 16);
// 报文长度
String lengthD = (dataList != null && !dataList.isEmpty()) ? 6 + 2 * dataList.size() - 1 + "" : "6";
String length = Integer.toHexString(Integer.parseInt(lengthD));
request[4] = (byte) (Integer.parseInt(length, 16) >> 8);
request[5] = (byte) (Integer.parseInt(length, 16) & 0xff);
// 设置从机地址
request[6] = (byte) slaveAddress;
// 设置状态码
request[7] = (byte) functionCode;
// 设置开始地址
request[8] = (byte) (startAddress >> 8);
request[9] = (byte) (startAddress & 0xff);
// 设置寄存器数量 高位和低位
request[10] = (byte) (registerCount >> 8);
request[11] = (byte) (registerCount & 0xff);
// 存在批量修改时
if (dataList != null && !dataList.isEmpty()) {
// 字节数
request[12] = dataList.get(0).byteValue();
int j = 13;
for (int i = 1; i < dataList.size(); i++) {
request[j] = (byte) (dataList.get(i) >> 8);
j++;
request[j] = (byte) (dataList.get(i) & 0xff);
j++;
}
}
return request;
}
public static byte[] buildMessage(int slaveAddress, int functionCode, int startAddress, int registerCount) {
return buildMessage(slaveAddress, functionCode, startAddress, registerCount, null);
}
/**
* @param from 从第几位开始截取数组
* @description: 讲接收的报文数据进行提取
* @param: bytes 接收到的报文
* @return: 处理后的报文(十六进制字符串)
* @author WXP
* @date: 2024/10/10 14:33
*/
public static ArrayList<String> formatData(byte[] bytes, int from, int to) {
System.out.println("接受到报文:" + Arrays.toString(bytes));
System.out.println("事物处理标识符:" + toHex(bytes[0]) + toHex(bytes[1]));
System.out.println("TCP协议::" + toHex(bytes[2]) + toHex(bytes[3]));
System.out.println("数据长度::" + toHex(bytes[4]) + toHex(bytes[5]));
System.out.println("从机地址::" + toHex(bytes[6]));
System.out.println("功能码::" + toHex(bytes[7]));
if (toHex(bytes[7]).equals("06")) {
System.out.println("寄存器地址::" + toHex(bytes[8]) + toHex(bytes[9]));
} else if (toHex(bytes[7]).equals("10")) {
System.out.println("寄存器地址::" + toHex(bytes[8]) + toHex(bytes[9]));
System.out.println("寄存器个数::" + toHex(bytes[10]) + toHex(bytes[11]));
} else if (toHex(bytes[7]).equals("03")) {
System.out.println("数据长度::" + toHex(bytes[8]));
}
ArrayList<String> newData = new ArrayList<>();
if (to -from >0){
byte[] data = Arrays.copyOfRange(bytes, from, to);
// 报文中数据
System.out.println("报文中数据" + Arrays.toString(data));
// 判断数组长度是不是偶数
if (data.length % 2 == 0) {
for (int i = 0; i < data.length; i += 2) {
String hex = toHex(data[i]) + toHex(data[i + 1]);
newData.add(hex);
System.out.println("十六进制:" + hex);
// System.out.println("十进制:" + Integer.parseInt(hex, 16));
}
}
}
return newData;
}
/**
* @description: 数据域中没有确切数据时
* @param: bytes
* @return:
* @author WXP
* @date: 2024/10/18 8:07
*/
public static ArrayList<String> formatData(byte[] bytes){
return formatData(bytes,0 ,0);
}
/**
* @description: byte 转成 16进制字符串
* @param: b
* @return: 16进制字符串
* @author WXP
* @date: 2024/10/10 14:34
*/
private static String toHex(byte b) {
// Convert byte to unsigned int and then to hex string
return String.format("%02X", b & 0xFF);
}
public static void main(String[] args) throws IOException {
String hostname = "192.168.21.151"; // 服务器的主机名或IP地址
int port = 502; // 服务器监听的端口
// 构建报文
byte[] bytes = buildMessage(slaveAddress, functionCode, startAddress, registerCount);
byte[] updateSingleBytes = buildMessage(slaveAddress, updateSingleFunctionCode, updateSingleStartAddress, updateData);
byte[] updateMulBytes = null;
ModbusTcpClient client = new ModbusTcpClient(hostname, port);
try {
client.connect();
// 发送报文
client.sendRequest(bytes);
System.out.println("发送请求:" + Arrays.toString(bytes));
// 接收报文
byte[] response = client.receiveResponse();
// 处理响应数据
ArrayList<String> data = formatData(response, 9, response.length);
String hexData = "";
for (String d : data) {
hexData += d;
}
// 相乘,浮点数处理
BigDecimal multiply = new BigDecimal("0.001").multiply(new BigDecimal(Integer.toString(Integer.parseInt(hexData, 16))));
System.out.println("A相电压:" + multiply);
// 修改 06 功能
System.out.println("----------------06---------------");
// 发送报文
client.sendRequest(updateSingleBytes);
System.out.println("发送请求:" + Arrays.toString(updateSingleBytes));
// 接收报文
response = client.receiveResponse();
System.out.println("接受到报文:" + Arrays.toString(response));
// 处理响应数据
formatData(response, 10, response.length);
// 修改 10 功能
System.out.println("----------------10---------------");
// 修改的数据集合
ArrayList<Integer> dataList = new ArrayList<>();
// 字节数
dataList.add(0x0004);
// 修改的数据
dataList.add(0x0010);
dataList.add(0x0004);
// 发送报文
updateMulBytes = buildMessage(slaveAddress, updateMulFunctionCode, updateNulStartAddress, registerCount, dataList);
client.sendRequest(updateMulBytes);
System.out.println("发送请求:" + Arrays.toString(updateMulBytes));
// 接收报文
response = client.receiveResponse();
System.out.println("接受到报文:" + Arrays.toString(response));
// 处理响应数据
formatData(response);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
client.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
5. 其他
还有一些比较深入的内容没有讲到,比如数据模型之类,下面的内容也比较零散,详见这个视频的讲解,感兴趣可以自己研究
这节课带你吃透Modbus通信协议_哔哩哔哩_bilibili
线圈(布尔量,开关)
存储区:
输出线圈: 0
0 0001 - 0 9999
0 00001 - 065536
输入线圈: 1
1 0001 - 1 9999
1 00001 -165536
输出寄存器:4
4 0001 - 4 9999
4 00001 - 465536
输入寄存器:3
3 0001 - 3 9999
3 00001 - 365536
存储区范围:5位(标准地址)和6位(拓展地址)
第一位表示区域,后面几位表示地址
读和写 功能码
读可以读输入和输出的,写只能写输出的,写可以单个写,也可以多个写
读输出线圈 01
读输入线圈 02
读输出寄存器 03
读输入寄存器 04
写单个输出线圈 05
写单个输出寄存器 06
写多个输出线圈 15 (OxF)
写多个输出寄存器 16 (Ox10)