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 采用主从通讯方式,只有一个设备可以发送请求;
更多详细内容参考:
-
Modbus协议解析–小白一看就懂的协议_panda@Code的博客-CSDN博客_modbus协议
-
详解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 本身就具备差错校验的能力,因此不需要校验码。
Java 实现 Modbus 通讯
本文中使用 Modbus4j 开源库实现 Modbus 的通讯,其依赖如下:
<dependency>
<groupId>com.infiniteautomation</groupId>
<artifactId>modbus4j</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>io.github.java-native</groupId>
<artifactId>jssc</artifactId>
<version>2.9.4</version>
</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 Slave 创建从机,File --> new
,在打开的窗口的空白处点击右键,选择Slave Definotion
可以对从机进行配置
选中偏移量对应的格子,右键点击Format
可以设置存储的类型
按照上述流程创建四个从机,分别对应四个功能区
单击工具栏的Connection --> Connect
,配置连接(若已连接需要先断开Connection --> cDisonnect
)
至此模拟从机配置完毕,可以修改代码中的串口,通过 Modbus-RTU 的模式读取不同功能区的不同偏移量上的数值,上述代码亲测可用,此处不再演示
Modbus-TCP 代码验证
在上述从机配置的基础上,首先断开连接,并重新创建连接,连接方式选择Modbus TCP/IP
即可
然后修改代码,采用 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);
}
}
}