【应用】Modbus 通讯协议

Modbus 通讯协议

  • Modbus 协议基础
    • Modbus 存储区
    • Modbus-RTU 协议
    • Modbus-TCP 协议
  • Java 实现 Modbus 通讯
    • Modbus Read
    • Modbus Write
  • 模拟数据进行代码测试
    • Modbus-RTU 代码验证
    • Modbus-TCP 代码验证
  • SerialPortWrapper 实现类代码

Modbus 协议基础

  • Modbus 是一种总线通讯协议,其支持多种电气接口(RS-232/RS-485);

  • Modbus 是应用层报文传输协议,其定义了控制器能够认识和使用的消息结构;

  • Modbus 采用主从通讯方式,只有一个设备可以发送请求;

更多详细内容参考:

  1. Modbus协议解析–小白一看就懂的协议_panda@Code的博客-CSDN博客_modbus协议

  2. 详解Modbus通信协议—清晰易懂_Z小旋的博客-CSDN博客_modbus

Modbus 存储区

Modbus 在从机中存储数据,其规定了四个功能区:

区号 名称 读写 概述
0区 输出线圈 可读写布尔量 0=Fales,1=True
1区 输入线圈 只读布尔量 0=Fales,1=True
3区 输入寄存器 只读寄存器 需要指定数据类型
4区 保持寄存器 可读写寄存器 需要指定数据类型

Modbus-RTU 协议

RTU 协议的帧结构为机地址 + 功能码 + 数据 + 校验码

其中的功能码描述了要执行的操作,常用的功能码如下:

功能码 功能说明
01H 读取输出线圈
02H 读取输入线圈
03H 读取保持寄存器
04H 读取输入寄存器
05H 写入单线圈
06H 写入单寄存器
0FH 写入多线圈
10H 写入多寄存器

Modbus-TCP 协议

TCP 协议将 RTU 协议拆分,将功能码与数据提取出来,拼接 MBAP 报文头部组成。TCP 本身就具备差错校验的能力,因此不需要校验码。

【应用】Modbus 通讯协议_第1张图片

Java 实现 Modbus 通讯

本文中使用 Modbus4j 开源库实现 Modbus 的通讯,其依赖如下:

        <dependency>
            <groupId>com.infiniteautomationgroupId>
            <artifactId>modbus4jartifactId>
            <version>3.0.3version>
        dependency>
        <dependency>
            <groupId>io.github.java-nativegroupId>
            <artifactId>jsscartifactId>
            <version>2.9.4version>
        dependency>

在该开源库中,对于 RTU 和 TCP 传输来说,其根本不同在于创建不同的 Modbus Master,而读取和写入的方法相同,详见代码。

注意:在实现 RTU 传输时,我们需要实现开源包中的 SerialPortWrapper 方法以创建 Master,对应实现类以及实现类中引用类的代码放至文末。

Modbus Read

public class Modbus_Read {

    // master 工厂
    static ModbusFactory modbusFactory;

    // master 对象
    static ModbusMaster master;

    // 静态方法初始化 master
    static {
        modbusFactory = new ModbusFactory();
        // 使用 Modbus-TCP 进行通信
//        IpParameters param = new IpParameters();
//        param.setHost("localhost");
//        param.setPort(502);
//        master = modbusFactory.createTcpMaster(param, false);
        // 使用 Modbus-RTU 进行通信
        SerialPortWrapper serialParameters = new SerialPortWrapperImpl("COM12", 9600,
                8, 1, 0, 0, 0);
        master = modbusFactory.createRtuMaster(serialParameters);
        try {
            master.init();
        } catch (ModbusInitException e) {
            throw new RuntimeException("master 初始化失败~");
        }
    }

    /**
     * 主方法测试
     */
    public static void main(String[] args) throws ModbusTransportException, ErrorResponseException {
        System.out.println("=====读线圈 CoilStatus=====");
        System.out.println("0>>>" + readCoilStatus(1, 0));
        System.out.println("5>>>" + readCoilStatus(1, 5));
        System.out.println("=====读离散量输入 InputStatus=====");
        System.out.println("7>>>" + readInputStatus(1, 7));
        System.out.println("8>>>" + readInputStatus(1, 8));
        System.out.println("=====读保持寄存器 HoldingRegister=====");
        System.out.println("0>>>" + readHoldingRegister(1, 0, DataType.FOUR_BYTE_FLOAT));
        System.out.println("2>>>" + readHoldingRegister(1, 2, DataType.FOUR_BYTE_FLOAT));
        System.out.println("=====读输入寄存器 InputRegisters=====");
        System.out.println("6>>>" + readInputRegisters(1, 6, DataType.FOUR_BYTE_FLOAT));
        System.out.println("8>>>" + readInputRegisters(1, 8, DataType.FOUR_BYTE_FLOAT));
        System.out.println("=====测试批量读取=====");
        batchRead();
    }

    /**
     * 读线圈 CoilStatus
     * @param slaveId 从机id
     * @param offset 偏移量
     * @return 读取数据值
     */
    public static Boolean readCoilStatus(int slaveId, int offset)
            throws ModbusTransportException, ErrorResponseException {
        BaseLocator<Boolean> locator = BaseLocator.coilStatus(slaveId, offset);
        return master.getValue(locator);
    }


    /**
     * 读离散量输入 InputStatus
     * @param slaveId 从机id
     * @param offset 偏移量
     * @return 读取数据值
     */
    public static Boolean readInputStatus(int slaveId, int offset)
            throws ModbusTransportException, ErrorResponseException {
        BaseLocator<Boolean> locator = BaseLocator.inputStatus(slaveId, offset);
        return master.getValue(locator);
    }

    /**
     * 读保持寄存器 HoldingRegister
     * @param slaveId 从机id
     * @param offset 偏移量
     * @param dataType 数据类型
     * @return 读取数据值
     */
    public static Number readHoldingRegister(int slaveId, int offset, int dataType)
            throws ModbusTransportException, ErrorResponseException {
        BaseLocator<Number> locator = BaseLocator.holdingRegister(slaveId, offset, dataType);
        return master.getValue(locator);
    }


    /**
     * 读输入寄存器 InputRegisters
     * @param slaveId 从机id
     * @param offset 偏移量
     * @param dataType 数据类型
     * @return 读取数据值
     */
    public static Number readInputRegisters(int slaveId, int offset, int dataType)
            throws ModbusTransportException, ErrorResponseException {
        BaseLocator<Number> locator = BaseLocator.inputRegister(slaveId, offset, dataType);
        return master.getValue(locator);
    }


    public static void batchRead() throws ModbusTransportException, ErrorResponseException {

        BatchRead<Integer> batch = new BatchRead<Integer>();

        batch.addLocator(0, BaseLocator.coilStatus(1, 0));
        batch.addLocator(1, BaseLocator.inputStatus(1, 7));
        batch.addLocator(2, BaseLocator.holdingRegister(1, 0, DataType.FOUR_BYTE_FLOAT));
        batch.addLocator(3, BaseLocator.inputRegister(1, 6, DataType.FOUR_BYTE_FLOAT));

        batch.setContiguousRequests(false);
        BatchResults<Integer> results = master.send(batch);
        System.out.println(results.getValue(0));
        System.out.println(results.getValue(1));
        System.out.println(results.getValue(2));
        System.out.println(results.getValue(3));
    }

}

Modbus Write

public class Modbus_Write {

    // master 工厂
    static ModbusFactory modbusFactory;

    // master 对象
    static ModbusMaster master;

    // 静态方法初始化 master
    static {
        modbusFactory = new ModbusFactory();
        // 使用 Modbus-TCP 进行通信
//        IpParameters param = new IpParameters();
//        param.setHost("localhost");
//        param.setPort(502);
//        master = modbusFactory.createTcpMaster(param, false);
        // 使用 Modbus-RTU 进行通信
        SerialPortWrapper serialParameters = new SerialPortWrapperImpl("COM12", 9600,
                8, 1, 0, 0, 0);
        master = modbusFactory.createRtuMaster(serialParameters);
        try {
            master.init();
        } catch (ModbusInitException e) {
            throw new RuntimeException("master 初始化失败~");
        }
    }

    /**
     * 主方法测试
     */
    public static void main(String[] args) throws ModbusTransportException, ErrorResponseException {
        System.out.println("=====写线圈 CoilStatus=====");
        System.out.println(writeCoil(1, 5, true));
        System.out.println("=====批量写线圈 CoilStatus=====");
        System.out.println(writeCoils(1, 6, new boolean[]{true, false, true, false}));
        System.out.println("=====写保持寄存器 HoldingRegister=====");
        writeRegister(1, 8, 11.1234, DataType.FOUR_BYTE_FLOAT);
    }

    /**
     * 写线圈 CoilStatus
     * @param slaveId 从机id
     * @param writeOffset 偏移量
     * @param writeValue 写入值
     * @return 写入结果
     */
    public static boolean writeCoil(int slaveId, int writeOffset, boolean writeValue)
            throws ModbusTransportException {
        // 创建请求
        WriteCoilRequest request = new WriteCoilRequest(slaveId, writeOffset, writeValue);
        // 发送请求并获取响应对象
        WriteCoilResponse response = (WriteCoilResponse) master.send(request);
        return !response.isException();
    }

    /**
     * 批量写线圈 CoilStatus
     * @param slaveId 从机id
     * @param startOffset 写入起始偏移量
     * @param bdata 写入数据集
     * @return 写入结果
     */
    public static boolean writeCoils(int slaveId, int startOffset, boolean[] bdata)
            throws ModbusTransportException {
        // 创建请求
        WriteCoilsRequest request = new WriteCoilsRequest(slaveId, startOffset, bdata);
        // 发送请求并获取响应对象
        WriteCoilsResponse response = (WriteCoilsResponse) master.send(request);
        return !response.isException();
    }

    /**
     * 写保持寄存器 HoldingRegister
     * @param slaveId 从机id
     * @param writeOffset 偏移量
     * @param writeValue 写入值
     * @param dataType 写入值数据类型
     */
    public static void writeRegister(int slaveId, int writeOffset, Number writeValue, int dataType)
            throws ModbusTransportException, ErrorResponseException {
        // 创建寻址对象
        BaseLocator<Number> locator = BaseLocator.holdingRegister(slaveId, writeOffset, dataType);
        // 执行写入操作
        master.setValue(locator, writeValue);
    }

}

模拟数据进行代码测试

本文使用 Modbus slave 模拟从机进行测试,百度网盘下载链接如下:

链接:https://pan.baidu.com/s/1PIGT8Zpi2cYCFRTRZwjiNA?pwd=hku4 
提取码:hku4

Modbus-RTU 代码验证

RTU 协议需要通过总线进行传输,因此需要借助软件在计算机上开机虚拟串口,此处使用 Virtual Serial Port Driver,百度网盘下载链接如下:

链接:https://pan.baidu.com/s/1bQMUIOnK56LyvxcH4Gsdqg?pwd=p5fm 
提取码:p5fm

打开 Virtual Serial Port Driver,在计算机上创建一对虚拟串口

【应用】Modbus 通讯协议_第2张图片

创建成功后在设备管理器中可以查找到对应的虚拟串口

【应用】Modbus 通讯协议_第3张图片

使用 Modbus Slave 创建从机,File --> new,在打开的窗口的空白处点击右键,选择Slave Definotion可以对从机进行配置

【应用】Modbus 通讯协议_第4张图片

选中偏移量对应的格子,右键点击Format可以设置存储的类型

【应用】Modbus 通讯协议_第5张图片

按照上述流程创建四个从机,分别对应四个功能区

【应用】Modbus 通讯协议_第6张图片

单击工具栏的Connection --> Connect,配置连接(若已连接需要先断开Connection --> cDisonnect

【应用】Modbus 通讯协议_第7张图片

至此模拟从机配置完毕,可以修改代码中的串口,通过 Modbus-RTU 的模式读取不同功能区的不同偏移量上的数值,上述代码亲测可用,此处不再演示

Modbus-TCP 代码验证

在上述从机配置的基础上,首先断开连接,并重新创建连接,连接方式选择Modbus TCP/IP即可

【应用】Modbus 通讯协议_第8张图片

然后修改代码,采用 Modbus-Tcp 进行通信的方式创建 Master,随后即可通过配置不同的功能能区和偏移量实现数据的读写,上述代码亲测可用,此处不再演示

SerialPortWrapper 实现类代码

package com.zqf.modbus.modbus_tcp.impl;

import com.serotonin.modbus4j.serial.SerialPortWrapper;
import jssc.SerialPort;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.InputStream;
import java.io.OutputStream;

public class SerialPortWrapperImpl implements SerialPortWrapper {

    private static final Logger LOG = LoggerFactory.getLogger(SerialPortWrapperImpl.class);
    private final SerialPort port;
    private final int baudRate;
    private final int dataBits;
    private final int stopBits;
    private final int parity;
    private final int flowControlIn;
    private final int flowControlOut;

    public SerialPortWrapperImpl(String commPortId, int baudRate, int dataBits, int stopBits,
                                 int parity, int flowControlIn, int flowControlOut) {
        this.baudRate = baudRate;
        this.dataBits = dataBits;
        this.stopBits = stopBits;
        this.parity = parity;
        this.flowControlIn = flowControlIn;
        this.flowControlOut = flowControlOut;

        port = new SerialPort(commPortId);

    }

    @Override
    public void close() throws Exception {
        port.closePort();
        //listeners.forEach(PortConnectionListener::closed);
        LOG.debug("Serial port {} closed", port.getPortName());
    }

    @Override
    public void open() {
        try {
            port.openPort();
            port.setParams(this.getBaudRate(), this.getDataBits(), this.getStopBits(), this.getParity());
            port.setFlowControlMode(this.getFlowControlIn() | this.getFlowControlOut());

            //listeners.forEach(PortConnectionListener::opened);
            LOG.debug("Serial port {} opened", port.getPortName());
        } catch (Exception ex) {
            LOG.error("Error opening port : {} for {} ", port.getPortName(), ex);
        }
    }

    @Override
    public InputStream getInputStream() {
        return new SerialInputStream(port);
    }

    @Override
    public OutputStream getOutputStream() {
        return new SerialOutputStream(port);
    }

    @Override
    public int getBaudRate() {
        return baudRate;
        //return SerialPort.BAUDRATE_9600;
    }

    @Override
    public int getFlowControlIn() {
        return flowControlIn;
        //return SerialPort.FLOWCONTROL_NONE;
    }

    @Override
    public int getFlowControlOut() {
        return flowControlOut;
        //return SerialPort.FLOWCONTROL_NONE;
    }

    @Override
    public int getDataBits() {
        return dataBits;
        //return SerialPort.DATABITS_8;
    }

    @Override
    public int getStopBits() {
        return stopBits;
        //return SerialPort.STOPBITS_1;
    }

    @Override
    public int getParity() {
        return parity;
        //return SerialPort.PARITY_NONE;
    }
}

其中 SerialInputStream 和 SerialOutputStream 类代码如下:

package com.zqf.modbus.modbus_tcp.impl;

import jssc.SerialPort;

import java.io.IOException;
import java.io.InputStream;

public class SerialInputStream extends InputStream {

    private SerialPort serialPort;
    private int defaultTimeout = 0;

    /**
     * Instantiates a SerialInputStream for the given {@link SerialPort} Do not
     * create multiple streams for the same serial port unless you implement
     * your own synchronization.
     *
     * @param sp The serial port to stream.
     */
    public SerialInputStream(SerialPort sp) {
        serialPort = sp;
    }

    /**
     * Set the default timeout (ms) of this SerialInputStream. This affects
     * subsequent calls to {@link #read()}
     * The default timeout can be 'unset'
     * by setting it to 0.
     *
     * @param time The timeout in milliseconds.
     */
    public void setTimeout(int time) {
        defaultTimeout = time;
    }

    /**
     * Reads the next byte from the port. If the timeout of this stream has been
     * set, then this method blocks until data is available or until the timeout
     * has been hit. If the timeout is not set or has been set to 0, then this
     * method blocks indefinitely.
     */
    @Override
    public int read() throws IOException {
        return read(defaultTimeout);
    }

    /**
     * The same contract as {@link #read()}, except overrides this stream's
     * default timeout with the given timeout in milliseconds.
     *
     * @param timeout The timeout in milliseconds.
     * @return The read byte.
     * @throws IOException On serial port error or timeout
     */
    public int read(int timeout) throws IOException {
        byte[] buf = new byte[1];
        try {
            if (timeout > 0) {
                buf = serialPort.readBytes(1, timeout);
            } else {
                buf = serialPort.readBytes(1);
            }
            return buf[0];
        } catch (Exception e) {
            throw new IOException(e);
        }
    }

    /**
     * Non-blocking read of up to buf.length bytes from the stream. This call
     * behaves as read(buf, 0, buf.length) would.
     *
     * @param buf The buffer to fill.
     * @return The number of bytes read, which can be 0.
     * @throws IOException on error.
     */
    @Override
    public int read(byte[] buf) throws IOException {
        return read(buf, 0, buf.length);
    }

    /**
     * Non-blocking read of up to length bytes from the stream. This method
     * returns what is immediately available in the input buffer.
     *
     * @param buf The buffer to fill.
     * @param offset The offset into the buffer to start copying data.
     * @param length The maximum number of bytes to read.
     * @return The actual number of bytes read, which can be 0.
     * @throws IOException on error.
     */
    @Override
    public int read(byte[] buf, int offset, int length) throws IOException {

        if (buf.length < offset + length) {
            length = buf.length - offset;
        }

        int available = this.available();

        if (available > length) {
            available = length;
        }

        try {
            byte[] readBuf = serialPort.readBytes(available);
//            System.arraycopy(readBuf, 0, buf, offset, length);
            System.arraycopy(readBuf, 0, buf, offset, readBuf.length);
            return readBuf.length;
        } catch (Exception e) {
            throw new IOException(e);
        }
    }

    /**
     * Blocks until buf.length bytes are read, an error occurs, or the default
     * timeout is hit (if specified). This behaves as blockingRead(buf, 0,
     * buf.length) would.
     *
     * @param buf The buffer to fill with data.
     * @return The number of bytes read.
     * @throws IOException On error or timeout.
     */
    public int blockingRead(byte[] buf) throws IOException {
        return blockingRead(buf, 0, buf.length, defaultTimeout);
    }

    /**
     * The same contract as {@link #blockingRead(byte[])} except overrides this
     * stream's default timeout with the given one.
     *
     * @param buf The buffer to fill.
     * @param timeout The timeout in milliseconds.
     * @return The number of bytes read.
     * @throws IOException On error or timeout.
     */
    public int blockingRead(byte[] buf, int timeout) throws IOException {
        return blockingRead(buf, 0, buf.length, timeout);
    }

    /**
     * Blocks until length bytes are read, an error occurs, or the default
     * timeout is hit (if specified). Saves the data into the given buffer at
     * the specified offset. If the stream's timeout is not set, behaves as
     * {@link #read(byte[], int, int)} would.
     *
     * @param buf The buffer to fill.
     * @param offset The offset in buffer to save the data.
     * @param length The number of bytes to read.
     * @return the number of bytes read.
     * @throws IOException on error or timeout.
     */
    public int blockingRead(byte[] buf, int offset, int length) throws IOException {
        return blockingRead(buf, offset, length, defaultTimeout);
    }

    /**
     * The same contract as {@link #blockingRead(byte[], int, int)} except
     * overrides this stream's default timeout with the given one.
     *
     * @param buf The buffer to fill.
     * @param offset Offset in the buffer to start saving data.
     * @param length The number of bytes to read.
     * @param timeout The timeout in milliseconds.
     * @return The number of bytes read.
     * @throws IOException On error or timeout.
     */
    public int blockingRead(byte[] buf, int offset, int length, int timeout) throws IOException {
        if (buf.length < offset + length) {
            throw new IOException("Not enough buffer space for serial data");
        }

        if (timeout < 1) {
            return read(buf, offset, length);
        }

        try {
            byte[] readBuf = serialPort.readBytes(length, timeout);
            System.arraycopy(readBuf, 0, buf, offset, length);
            return readBuf.length;
        } catch (Exception e) {
            throw new IOException(e);
        }
    }

    @Override
    public int available() throws IOException {
        int ret;
        try {
            ret = serialPort.getInputBufferBytesCount();
            if (ret >= 0) {
                return ret;
            }
            throw new IOException("Error checking available bytes from the serial port.");
        } catch (Exception e) {
            throw new IOException("Error checking available bytes from the serial port.");
        }
    }

}
package com.zqf.modbus.modbus_tcp.impl;

import jssc.SerialPort;
import jssc.SerialPortException;

import java.io.IOException;
import java.io.OutputStream;

public class SerialOutputStream extends OutputStream {

    SerialPort serialPort;

    /**
     * Instantiates a SerialOutputStream for the given {@link SerialPort} Do not
     * create multiple streams for the same serial port unless you implement
     * your own synchronization.
     *
     * @param sp The serial port to stream.
     */
    public SerialOutputStream(SerialPort sp) {
        serialPort = sp;
    }

    @Override
    public void write(int b) throws IOException {
        try {
            serialPort.writeInt(b);
        } catch (SerialPortException e) {
            throw new IOException(e);
        }
    }

    @Override
    public void write(byte[] b) throws IOException {
        write(b, 0, b.length);

    }

    @Override
    public void write(byte[] b, int off, int len) throws IOException {
        byte[] buffer = new byte[len];
        System.arraycopy(b, off, buffer, 0, len);
        try {
            serialPort.writeBytes(buffer);
        } catch (SerialPortException e) {
            throw new IOException(e);
        }
    }

}

你可能感兴趣的:(Java,开发实战,网络,tcp/ip,网络协议,java)