Modbus 协议快速入门

@

Modbus 协议快速入门

1.什么是 Modbus 协议

什么是协议:是一种约定或规则或规则,它在计算机网络和通信领域起着至关重要的作用。具体来说,协议是网络中(或一般业务中)进行数据交换和解释信息时所要遵守的一套规则和约定,或者说是通信双方共同遵守的一组规则或标准。这些规则或标准详细定义了信息的格式、传输的顺序、控制信息以及同步机制等各个方面。

什么是modbus协议:Modbus协议是一种串行通信协议,由Modicon公司(现为施耐德电气Schneider Electric)于1979年发表,旨在实现可编辑逻辑控制器(PLC)之间的通信。如今,它已经成为工业领域通信协议的业界标准,并且是工业电子设备之间常用的连接方式。Modbus是主从方式通信,也就是说,不能同步进行通信,总线上每次只有一个数据进行传输,既主机发送,从机应答,主机不发送,总线上就没有数据通信。一个主线上只能有一个主句,可以有若干个从机。

什么是串行通信协议:串行通信是一种计算机通信方式,它在主机与外设以及主机之间的数据传输中起着重要作用。串行通信是指数据按位依次传输的通信方式,每位数据占据固定的时间长度,并使用少数几条通信线路完成系统间的信息交换。在串行通信中,数据被分解为一系列单独的比特(位),并按顺序通过传输线进行传输。每个比特在传输线上占用固定的时间长度,这样接收端就可以按照相同的时序接收并重组这些数据位,从而回复出原始数据

2.Modbus有什么用

Modbus协议广泛应用于工业控制和自动化领域,可以连接各种设备和控制器,用于实现数据交换、监控和控制。具体包括:

  1. 工业自动化控制:Modbus被广泛应用于工业自动化控制系统中,用于连接PLC、传感器、执行器等设备,实现监控和控制
  2. 智能家居:Modbus协议可以应用于智能家居系统中,用于连接各种传感器和执行器,实现远程控制和检测
  3. 能源监控:Modbus协议可以用于能源监控系统,连接电表、燃气表、水表等设备,实现能源数据的采集和分析。
  4. 环境检测:Modbus协议可以应用于环境检监测系统中,连接各种传感器和仪器,监测环境参数如温度、湿度、气压等。
  5. 智能交通: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效率虽然低,但方便调试显示。

从上图可以看出:

  1. 比TRU多了起始段 : ,多个结束符 CR,LF
  2. 地址和功能都变成了2个字节
  3. 数据部分更加繁琐,但更符合人们的查看

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)

posted @ 2024-10-15 14:35  Windows_XP  阅读(13)  评论(0编辑  收藏  举报