基于ModbusRtu协议的Java Socket通信 报文编码格式与数据采集过程详解


Modbus协议使用串口传输时可以选择RTU或者ASCII模式,并规定了消息、数据结构、命令和应答方式,且需要对数据进行校验。ASCII模式采用LRC校验,RTU模式采用16位CRC校验。通过以太网传输时使用TCP,这种模式下不使用校验,因为TCP协议是一个面向连接的可靠协议。

ModbusRTU如何判断开始与结束
ModbusRTU协议中,需要用时间间隔来判断一帧报文的开始和结束,协议规定的时间为3.5个字符周期。在一帧报文开始前,必须有大于3.5个字符周期的空闲时间,一帧报文结束后,也必须要有3.5个字符周期的空闲时间,否则就会出现粘包的请况。3.5个字符周期是一个具体时间,与波特率有关。
整个报文帧必须以连续的字符流发送。如果两个字符之间的空闲间隔大于 1.5 个字符时间,则报文帧被认为不完整应该被接收节点丢弃。
在这里插入图片描述


CRC循环冗余校验
在 RTU 模式包含一个对全部报文内容执行的,基于循环冗余校验 (CRC - Cyclical Redundancy Checking) 算法的错误检验域。CRC 域检验整个报文的内容。不管报文有无奇偶校验,均执行此检验。

1)CRC有16位,由两个8字节组成
2)CRC附加在报文最后面,先附加低字节,再附加高字节。
3)附加在报文后面的 CRC 的值由发送设备计算。接收设备在接收报文时重新计算 CRC 的值,并将计算结果于实际接收到的CRC 值相比较。如果两个值不相等,则为错误。

ModbusRTU在串行链路中的报文格式
从站地址(1 byte)+功能码(1 byte)+数据区(N bytes)+校验码(2 bytes)

从站地址:一个字节,作用是索引
功能码:一个字节,表明读写功能
数据:通信所传输的数据,可以是多字节
校验:判断接收的数据在传输过程中是否有损失,两个字节

读取输出/保持寄存器:
在这里插入图片描述

CRC工具类:

public class CRC {

    /**
     * 获取源数据和验证码的组合byte数组
     */
    public static byte[] getData(byte[] aa) {
        byte[] bb = getCrc16(aa);
        byte[] cc = new byte[aa.length+bb.length];
        System.arraycopy(aa,0,cc,0,aa.length);
        System.arraycopy(bb,0,cc,aa.length,bb.length);
        return cc;
    }
    /**
     * 获取验证码byte数组,基于Modbus CRC16的校验算法
     */
    private static byte[] getCrc16(byte[] arr_buff) {
        int len = arr_buff.length;

        // 预置 1 个 16 位的寄存器为十六进制FFFF, 称此寄存器为 CRC寄存器。
        int crc = 0xFFFF;
        int i, j;
        for (i = 0; i < len; i++) {
            // 把第一个 8 位二进制数据 与 16 位的 CRC寄存器的低 8 位相异或, 把结果放于 CRC寄存器
            crc = ((crc & 0xFF00) | (crc & 0x00FF) ^ (arr_buff[i] & 0xFF));
            for (j = 0; j < 8; j++) {
                // 把 CRC 寄存器的内容右移一位( 朝低位)用 0 填补最高位, 并检查右移后的移出位
                if ((crc & 0x0001) > 0) {
                    // 如果移出位为 1, CRC寄存器与多项式A001进行异或
                    crc = crc >> 1;
                    crc = crc ^ 0xA001;
                } else
                    // 如果移出位为 0,再次右移一位
                    crc = crc >> 1;
            }
        }
        return intToBytes(crc);
    }
    /**
     * 将int转换成byte数组,低位在前,高位在后
     * 改变高低位顺序只需调换数组序号
     */
    private static byte[] intToBytes(int value)  {
        byte[] src = new byte[2];
        src[1] =  (byte) ((value>>8) & 0xFF);
        src[0] =  (byte) (value & 0xFF);
        return src;
    }
    /**
     * 将字节数组转换成十六进制字符串
     */
    public static String byteTo16String(byte[] data) {
        StringBuffer buffer = new StringBuffer();
        for (byte b : data) {
            buffer.append(byteTo16String(b));
        }
        return buffer.toString();
    }
    /**
     * 将字节转换成十六进制字符串
     * int转byte对照表
     * [128,255],0,[1,128)
     * [-128,-1],0,[1,128)
     */
    public static String byteTo16String(byte b) {
        StringBuffer buffer = new StringBuffer();
        int aa = (int)b;
        if (aa<0) {
            buffer.append(Integer.toString(aa+256, 16)+" ");
        }else if (aa==0) {
            buffer.append("00 ");
        }else if (aa>0 && aa<=15) {
            buffer.append("0"+Integer.toString(aa, 16)+" ");
        }else if (aa>15) {
            buffer.append(Integer.toString(aa, 16)+" ");
        }
        return buffer.toString();
    }

    public static byte[] hexStringToByteArray(String hexString) {
        hexString = hexString.replaceAll(" ", "");
        int len = hexString.length();
        if (len%2 == 1){
            len = len + 1;
            hexString = "0"+ hexString;
        }
        byte[] bytes = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            // 两位一组,表示一个字节,把这样表示的16进制字符串,还原成一个字节
            bytes[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character
                    .digit(hexString.charAt(i + 1), 16));
        }
        return bytes;
    }
}

ModbusRtuEncoder类:

getReadCommand函数

	private short address = 0X01;
    public byte[] getReadCommand(byte stationNumber, short dataAddress, PLCAddressType addressType, int dataAddressLength) {
        //byte[] headerBuffer = Converter.getBytes((short)id++);
        byte[] dataAddressBuffer = Converter.getBytes(dataAddress);
        byte addressByte = (byte)(addressType.ordinal() + 1);

        //int[] data = new int[]{0x01, 0x03, 0x00, 0x65, 0x00, 0x02};
        byte[] data = new byte[]{(byte)address, addressByte, dataAddressBuffer[1], dataAddressBuffer[0], 0x00, (byte)dataAddressLength,}; //小端模式
        byte[] crcByte = CRC.getData(data);
        //String str = CRC.byteTo16String(crcByte).toUpperCase();

        return crcByte;
    }

getWriteCommand函数(主要处理寄存器)

public <T> byte[] getWriteCommand(short stationNumber, short dataAddress, PLCAddressType addressType, T value) {
        Converter aa = new Converter();
        String hex = aa.getHex(value);
        int dataLength = hex.length();
        byte[] hexdata = CRC.hexStringToByteArray(hex);
        byte[] dataAddressBuffer = Converter.getBytes(dataAddress);
        byte[] hexBytes = new byte[]{};
        byte[] data = new byte[]{};
        int functionCode = 0;
        if (dataLength == 8){
            switch (addressType){
                case CoilStatus: {
                    functionCode = 0x0F;
                } case HoldingRegister: {
                    functionCode = 0x10;
                    hexBytes = new byte[]{ hexdata[1], hexdata[0],hexdata[3], hexdata[2]};
                }
                default:break;
            }
            data = new byte[]{(byte)address, (byte)functionCode, dataAddressBuffer[1], dataAddressBuffer[0], 0x00, (byte)(dataLength/4),(byte)(dataLength/2),}; //小端模式
        }
        else if (0 < dataLength && dataLength <= 4){
            switch (addressType){
                case CoilStatus: {
                    functionCode = 0x05;
                } case HoldingRegister: {
                    functionCode = 0x06;
                    hexBytes = new byte[]{ hexdata[1], hexdata[0]};
                }
                default:break;
            }
            data = new byte[]{(byte)address, (byte)functionCode, dataAddressBuffer[1], dataAddressBuffer[0]};
        }

        //int[] data = new int[]{01 10 00 65 00 02 04 a6 42 99 9a 5d 1f};

        byte[] tmp = new byte[data.length+hexBytes.length];
        System.arraycopy(data,0,tmp,0,data.length);
        System.arraycopy(hexBytes,0,tmp,data.length,hexBytes.length);
        byte[] crcByte = CRC.getData(tmp);

        return crcByte;
    }

ModbusRtuDecoder类:

(不完全展示,其他还有检查id合法性之类的)

/**
 * 获取内容长度-通过调试工具事先发现响应报文长度
 * @param recvHeadByte 接收到的头指令
 * @return
 */
public <T> short getContentLength(byte[] recvHeadByte,short dataAddressLength, Class<T> clazz,int WriteOrRead) {
        if(WriteOrRead == 1){
            return 8; //写入rtu
        }
        else if( WriteOrRead == 0 ){
            short dataLen = (short)(5+dataAddressLength*2);
            return dataLen; //读取rtu的两个寄存器
        }
        return 0;
}
/**
     * 获取编码的值
     * @param recvByte 接收到的指令
     * @return
     */
    public <T> String getValue(byte[] recvByte, short dataAddressLength, Class<T> clazz){
        String result = null;
        try {
            switch (dataAddressLength){
                case 2: {
                    byte[] dataBytes = new byte[]{ recvByte[5], recvByte[6], recvByte[3], recvByte[4]}; //小端模式
                    float value = Converter.getFloat(dataBytes);
                    result = String.valueOf(value);
                    break;
                } case 1:{
                    byte[] dataBytes = new byte[]{recvByte[3], recvByte[4]}; //小端模式
                    short value = Converter.getShort(dataBytes);
                    result = String.valueOf(value);
                    break;
                } case 3:{
                    byte[] dataBytes1 = new byte[]{recvByte[3], recvByte[4]}; //小端模式
                    byte[] dataBytes2 = new byte[]{recvByte[7], recvByte[8], recvByte[5], recvByte[6]};
                    short value1 = Converter.getShort(dataBytes1);
                    float value2 = Converter.getFloat(dataBytes2);
                    result = String.valueOf(value1)+" "+String.valueOf(value2);
                    break;
                }

                default:break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

ModbusRtuDeviceTest类:

运行函数

public static void main(String[] args) {
        ModbusRtuDevice modbusRtu = new ModbusRtuDevice("127.0.0.1",502);
        short dataAddress = 100;         // 数据地址
        short dataAddressLength = 3;    // 寄存器长度
        Class clazz = float.class;    // 数据类型
        Class clazzAll =long.class;   // 代表一个混合数据类型
        Object value=83.3f; //(short)950


        while (true){
            try {
                /**
                 * 读取数据
                 */
                OperateResult<String> result1 = modbusRtu.read(dataAddress, PLCAddressType.HoldingRegister, dataAddressLength, clazzAll);
                if(result1.isSuccess){
                    System.out.println("读取设备数据成功:"+result1.content);
                }else{
                    System.out.println("读取设备数据失败:"+result1.message);
                }
                /**
                 * 写入数据
                 */
//                OperateResult<String> result2 = modbusRtu.write(dataAddress, PLCAddressType.HoldingRegister, value);
//                if(result2.isSuccess){
//                    System.out.println("写入设备数据成功:"+result2.content);
//                }else{
//                    System.out.println("写入设备数据失败:"+result2.message);
//                }

            } catch (Exception e) {
                e.printStackTrace();
            }
            finally {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

整体代码框架如下:

在这里插入图片描述

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值