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 协议进行通信

来分析上面的报文

arduino 复制代码
发送  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校验码,一致则代表报文是完整的),打不开的话那就是我这边给错密码了,或者是中间丢失了一些东西(报文不完整)

arduino 复制代码
回复  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

对上面报文进行分析

arduino 复制代码
发送 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校验码)

arduino 复制代码
回复 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位的寄存器)时使用这个命令

报文如下

r 复制代码
主机发送:02 10 A8 06 00 02 04 00 0F 00 03 93 04
从机返回:02 10 A8 06 00 02 81 9A

对上面报文进行分析

yaml 复制代码
发送: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校验码),

yaml 复制代码
返回: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 查询功能码为例

r 复制代码
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校验方法。

二字节的错误校验码,低字节在前,高字节在后。

ini 复制代码
    /**
     * @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协议

底层代码实现比较需要注意的就是高低位的转换,下面是高低位的提取代码

ini 复制代码
    int hex = 0x5A6B    
    byte high = (byte) (hex >> 8);// 高位 高位右移8位得出高位
    byte low = (byte) (hex & 0xFF);// 低位 0x00FF,高位和 00做&操作留下低位

4.1 Modbus-RTU

需先导入串口通信的包 jSerialComm

xml 复制代码
<dependency>
    <groupId>com.fazecast</groupId>
    <artifactId>jSerialComm</artifactId>
    <version>2.9.2</version>
</dependency>
java 复制代码
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冗余码

java 复制代码
package org.xp;
​
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
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 = 0x8000;
    private static final int updateSingleStartAddress = 0xA80A;
    private static final int updateMulStartAddress = 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) {
        byte[] data = Arrays.copyOfRange(bytes, from, to);
        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);
    }
​
    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();
            System.out.println("接受到报文:" + Arrays.toString(response));
​
            // 处理响应数据
            ArrayList<String> data = formatData(response, 9, response.length);
            if (toHex(bytes[7]).equals(data.size() + "")) {
                System.out.println("数据传输位数无误");
            }
​
            // 修改 06 功能
            System.out.println("----------------03---------------");
            // 发送报文
            client.sendRequest(updateSingleBytes);
            System.out.println("发送请求:" + Arrays.toString(updateSingleBytes));
​
            // 接收报文
            response = client.receiveResponse();
            System.out.println("接受到报文:" + Arrays.toString(response));
​
            // 处理响应数据
            data = formatData(response, 10, response.length);
            if (toHex(bytes[7]).equals(data.size() + "")) {
                System.out.println("数据传输位数无误");
            }
​
            // 修改 10 功能
            System.out.println("----------------10---------------");
​
            // 修改的数据集合
            ArrayList<Integer> dataList = new ArrayList<>();
            // 字节数
            dataList.add(0x0004);
            // 修改的数据
            dataList.add(0x0010);
            dataList.add(0x0004);
​
            // 发送报文
            updateMulBytes = buildMessage(slaveAddress, updateMulFunctionCode, updateMulStartAddress, registerCount, dataList);
            client.sendRequest(updateMulBytes);
            System.out.println("发送请求:" + Arrays.toString(updateMulBytes));
​
            // 接收报文
            response = client.receiveResponse();
            System.out.println("接受到报文:" + Arrays.toString(response));
​
            // 处理响应数据
            data = formatData(response, 10, response.length);
            if (toHex(bytes[7]).equals(data.size() + "")) {
                System.out.println("数据传输位数无误");
            }
​
        } 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)

相关推荐
陈哥聊测试10 小时前
软件格局在变,谁能扛起国产替代的大旗?
安全·程序员·产品
Marx82012 天前
英语尬面后,我做了一个生成英语学习视频的小工具
前端·面试·产品
文火冰糖的硅基工坊3 天前
[创业之路-200]:什么是business(业务)?B2B, B2C, B2G业务, 什么是业务设计?
产品经理·需求分析·产品·创业·战略
文火冰糖的硅基工坊3 天前
[创业之路-198]:华为的成立发展与新中国的建立与发展路径的相似性比较
华为·产品经理·需求分析·产品·创业·战略
文火冰糖的硅基工坊4 天前
[创业之路-199]:《华为战略管理法-DSTE实战体系》- 3 - 价值转移理论与利润区理论
华为·产品经理·需求分析·产品·创业·战略
文火冰糖的硅基工坊7 天前
[创业之路-197]:华为的发展路径启示
网络·华为·产品经理·需求分析·产品·创业·战略
文火冰糖的硅基工坊10 天前
[创业之路-189]:《华为战略管理法-DSTE实战体系》-2- 生存与发展的双重旋律:短期与长期、战术与战略的交响乐章
产品经理·需求分析·产品·创业·战略
河北小田11 天前
2.7 用户故事
程序员·产品
文火冰糖的硅基工坊12 天前
[创业之路-182]:《华为战略管理法-DSTE实战体系》-1-华为的发展历程和战略管理演变
华为·产品经理·需求分析·产品·创业·战略
可观测性用观测云12 天前
观测云产品更新 | 场景图表、基础设施、监控告警策略等
产品