内容目录
Java作为跨平台编程语言,在工业自动化上位机开发中应用广泛。通过Modbus协议,Java程序可以与各种品牌的PLC(西门子、三菱、台达、欧姆龙等)进行高效可靠的数据交互。本文提供完整的Java与PLC通信示例代码,包括Modbus TCP客户端实现、多PLC管理、数据采集、断线重连、批量读写、浮点数处理以及Spring Boot集成方案。
一、Java Modbus通信方案选型
1.1 常用Java Modbus库对比
| 库名 | 协议支持 | 主从支持 | Maven坐标 | 推荐场景 |
|---|---|---|---|---|
| EasyModbus | TCP/RTU/UDP | 主+从 | com.github.s7connector:easymodbus | 新项目首选,API简洁 |
| Jamod | TCP/RTU/ASCII | 主+从 | net.wimpi:jamod | 遗留项目,稳定 |
| j2mod | TCP/RTU/ASCII | 主+从 | com.ghgande:j2mod | 需要ASCII协议 |
| Modbus4J | TCP/RTU | 主+从 | com.infiniteautomation:modbus4j | SCADA系统 |
| modbus-tcp | TCP | 仅主 | com.digitalpetri.modbus:modbus-tcp | 高性能Netty实现 |
1.2 本文使用EasyModbus
EasyModbus是目前最活跃、API最简洁的Java Modbus库,支持TCP、RTU和UDP协议,既可作为客户端也可作为服务端,文档完善,适合大多数项目。
<!-- Maven依赖 -->
<dependency>
<groupId>com.github.s7connector</groupId>
<artifactId>easymodbus</artifactId>
<version>1.0.0</version>
</dependency>
<!-- 串口支持(RTU模式需要) -->
<dependency>
<groupId>com.fazecast</groupId>
<artifactId>jSerialComm</artifactId>
<version>2.9.2</version>
</dependency>
二、Modbus TCP客户端封装
package com.industrial.modbus;
import de.re.easymodbus.modbusclient.ModbusClient;
import de.re.easymodbus.datatypes.DataType;
import java.io.IOException;
import java.net.SocketException;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Modbus TCP客户端封装
* 支持连接管理、自动重连、批量读写、数据类型转换
*/
public class PlcModbusClient {
private final String ipAddress;
private final int port;
private final int unitId;
private ModbusClient client;
private final AtomicBoolean connected = new AtomicBoolean(false);
private Thread reconnectThread;
private volatile boolean running = true;
// 连接参数
private static final int CONNECT_TIMEOUT = 3000;
private static final int RECONNECT_INTERVAL = 5000;
private static final int MAX_RETRY = 3;
public PlcModbusClient(String ipAddress, int port, int unitId) {
this.ipAddress = ipAddress;
this.port = port;
this.unitId = unitId;
}
/**
* 连接PLC
*/
public synchronized boolean connect() {
try {
if (client != null && client.isConnected()) {
return true;
}
client = new ModbusClient(ipAddress, port);
client.setConnectionTimeout(CONNECT_TIMEOUT);
client.setUnitIdentifier(unitId);
client.Connect();
connected.set(true);
System.out.printf("[%s:%d] PLC连接成功,从站地址:%d%n",
ipAddress, port, unitId);
return true;
} catch (Exception e) {
connected.set(false);
System.err.printf("[%s:%d] 连接失败:%s%n", ipAddress, e.getMessage());
return false;
}
}
/**
* 断开连接
*/
public synchronized void disconnect() {
running = false;
if (reconnectThread != null) {
reconnectThread.interrupt();
}
try {
if (client != null && client.isConnected()) {
client.Disconnect();
}
} catch (IOException e) {
// 忽略
}
connected.set(false);
System.out.printf("[%s:%d] 连接已关闭%n", ipAddress);
}
/**
* 启动自动重连
*/
public void startAutoReconnect() {
reconnectThread = new Thread(() -> {
while (running) {
try {
if (!isConnected()) {
System.out.printf("[%s:%d] 检测到断开,尝试重连...%n", ipAddress);
connect();
}
Thread.sleep(RECONNECT_INTERVAL);
} catch (InterruptedException e) {
break;
}
}
}, "Modbus-Reconnect-" + ipAddress);
reconnectThread.setDaemon(true);
reconnectThread.start();
}
/**
* 检查连接状态
*/
public boolean isConnected() {
if (client == null) return false;
try {
// 发送一个简单的读请求测试连接
client.ReadHoldingRegisters(0, 1);
return true;
} catch (Exception e) {
connected.set(false);
return false;
}
}
// ==================== 读操作 ====================
/**
* 读保持寄存器(功能码03)
*/
public int[] readHoldingRegisters(int startAddr, int count) throws IOException {
for (int retry = 0; retry < MAX_RETRY; retry++) {
try {
ensureConnected();
return client.ReadHoldingRegisters(startAddr, count);
} catch (SocketException e) {
connected.set(false);
if (retry < MAX_RETRY - 1) {
connect();
continue;
}
throw e;
}
}
throw new IOException("读取失败,重试次数超限");
}
/**
* 读输入寄存器(功能码04)
*/
public int[] readInputRegisters(int startAddr, int count) throws IOException {
ensureConnected();
return client.ReadInputRegisters(startAddr, count);
}
/**
* 读线圈(功能码01)
*/
public boolean[] readCoils(int startAddr, int count) throws IOException {
ensureConnected();
return client.ReadCoils(startAddr, count);
}
/**
* 读离散输入(功能码02)
*/
public boolean[] readDiscreteInputs(int startAddr, int count) throws IOException {
ensureConnected();
return client.ReadDiscreteInputs(startAddr, count);
}
// ==================== 写操作 ====================
/**
* 写单个寄存器(功能码06)
*/
public void writeSingleRegister(int addr, int value) throws IOException {
ensureConnected();
client.WriteSingleRegister(addr, value);
}
/**
* 写多个寄存器(功能码16)
*/
public void writeMultipleRegisters(int startAddr, int[] values) throws IOException {
ensureConnected();
client.WriteMultipleRegisters(startAddr, values);
}
/**
* 写单个线圈(功能码05)
*/
public void writeSingleCoil(int addr, boolean value) throws IOException {
ensureConnected();
client.WriteSingleCoil(addr, value);
}
/**
* 写多个线圈(功能码15)
*/
public void writeMultipleCoils(int startAddr, boolean[] values) throws IOException {
ensureConnected();
client.WriteMultipleCoils(startAddr, values);
}
// ==================== 数据类型转换 ====================
/**
* 读取32位有符号整数(占用2个寄存器)
*/
public int readInt32(int startAddr) throws IOException {
int[] regs = readHoldingRegisters(startAddr, 2);
return (regs[0] << 16) | (regs[1] & 0xFFFF);
}
/**
* 读取32位无符号整数
*/
public long readUInt32(int startAddr) throws IOException {
int[] regs = readHoldingRegisters(startAddr, 2);
return ((long)(regs[0] & 0xFFFF) << 16) | (regs[1] & 0xFFFF);
}
/**
* 读取32位浮点数
*/
public float readFloat32(int startAddr) throws IOException {
int[] regs = readHoldingRegisters(startAddr, 2);
int bits = (regs[0] << 16) | (regs[1] & 0xFFFF);
return Float.intBitsToFloat(bits);
}
/**
* 读取64位浮点数(占用4个寄存器)
*/
public double readFloat64(int startAddr) throws IOException {
int[] regs = readHoldingRegisters(startAddr, 4);
long bits = ((long)(regs[0] & 0xFFFF) << 48) |
((long)(regs[1] & 0xFFFF) << 32) |
((long)(regs[2] & 0xFFFF) << 16) |
(regs[3] & 0xFFFF);
return Double.longBitsToDouble(bits);
}
/**
* 读取字符串(占用N个寄存器,每个寄存器存2个字符)
*/
public String readString(int startAddr, int registerCount) throws IOException {
int[] regs = readHoldingRegisters(startAddr, registerCount);
StringBuilder sb = new StringBuilder();
for (int reg : regs) {
char high = (char)((reg >> 8) & 0xFF);
char low = (char)(reg & 0xFF);
if (high != 0) sb.append(high);
if (low != 0) sb.append(low);
}
return sb.toString().trim();
}
/**
* 写入32位浮点数
*/
public void writeFloat32(int startAddr, float value) throws IOException {
int bits = Float.floatToIntBits(value);
int[] regs = new int[2];
regs[0] = (bits >> 16) & 0xFFFF;
regs[1] = bits & 0xFFFF;
writeMultipleRegisters(startAddr, regs);
}
private void ensureConnected() throws IOException {
if (!connected.get() || client == null || !client.isConnected()) {
if (!connect()) {
throw new IOException("PLC未连接:" + ipAddress);
}
}
}
public String getIpAddress() { return ipAddress; }
public int getPort() { return port; }
public int getUnitId() { return unitId; }
}
三、多PLC管理
package com.industrial.modbus;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 多PLC连接管理器
* 支持多台PLC的统一管理、批量数据采集
*/
public class PlcConnectionManager {
private final Map<String, PlcModbusClient> connections = new ConcurrentHashMap<>();
/**
* 添加PLC连接
*/
public void addPlc(String name, String ip, int port, int unitId) {
PlcModbusClient client = new PlcModbusClient(ip, port, unitId);
connections.put(name, client);
if (client.connect()) {
client.startAutoReconnect();
}
}
/**
* 获取PLC客户端
*/
public PlcModbusClient getPlc(String name) {
return connections.get(name);
}
/**
* 批量读取所有PLC的指定寄存器
*/
public Map<String, int[]> batchReadHoldingRegisters(int startAddr, int count) {
Map<String, int[]> results = new ConcurrentHashMap<>();
connections.forEach((name, client) -> {
try {
int[] values = client.readHoldingRegisters(startAddr, count);
results.put(name, values);
} catch (IOException e) {
System.err.printf("读取PLC[%s]失败:%s%n", name, e.getMessage());
}
});
return results;
}
/**
* 断开所有连接
*/
public void disconnectAll() {
connections.values().forEach(PlcModbusClient::disconnect);
connections.clear();
}
/**
* 获取连接状态
*/
public Map<String, Boolean> getConnectionStatus() {
Map<String, Boolean> status = new ConcurrentHashMap<>();
connections.forEach((name, client) -> {
status.put(name, client.isConnected());
});
return status;
}
}
四、数据采集服务
package com.industrial.modbus;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.*;
/**
* PLC数据采集服务
* 定时采集PLC数据,支持数据缓存、变化检测、回调通知
*/
public class PlcDataCollector {
private final PlcModbusClient client;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final Map<String, Object> dataCache = new ConcurrentHashMap<>();
private final List<DataChangeListener> listeners = new CopyOnWriteArrayList<>();
private volatile boolean running = false;
// 采集配置
private final List<CollectItem> collectItems = new ArrayList<>();
public interface DataChangeListener {
void onDataChanged(String key, Object oldValue, Object newValue);
}
static class CollectItem {
String key;
int address;
String type; // INT16, INT32, FLOAT32, COIL
CollectItem(String key, int address, String type) {
this.key = key;
this.address = address;
this.type = type;
}
}
public PlcDataCollector(PlcModbusClient client) {
this.client = client;
}
/**
* 添加采集项
*/
public void addCollectItem(String key, int address, String type) {
collectItems.add(new CollectItem(key, address, type));
}
/**
* 添加数据变化监听器
*/
public void addDataChangeListener(DataChangeListener listener) {
listeners.add(listener);
}
/**
* 启动定时采集
*/
public void start(long intervalMs) {
running = true;
scheduler.scheduleAtFixedRate(this::collectAll, 0, intervalMs, TimeUnit.MILLISECONDS);
System.out.println("数据采集服务已启动,间隔:" + intervalMs + "ms");
}
/**
* 停止采集
*/
public void stop() {
running = false;
scheduler.shutdown();
System.out.println("数据采集服务已停止");
}
/**
* 采集所有数据
*/
private void collectAll() {
if (!running) return;
for (CollectItem item : collectItems) {
try {
Object value = readValue(item);
Object oldValue = dataCache.put(item.key, value);
// 检测变化并通知
if (oldValue == null || !oldValue.equals(value)) {
final Object finalOldValue = oldValue;
final Object finalValue = value;
listeners.forEach(l -> l.onDataChanged(item.key, finalOldValue, finalValue));
}
} catch (Exception e) {
System.err.printf("采集[%s]失败:%s%n", item.key, e.getMessage());
}
}
}
private Object readValue(CollectItem item) throws IOException {
switch (item.type) {
case "INT16":
return client.readHoldingRegisters(item.address, 1)[0];
case "INT32":
return client.readInt32(item.address);
case "FLOAT32":
return client.readFloat32(item.address);
case "COIL":
return client.readCoils(item.address, 1)[0];
default:
throw new IllegalArgumentException("不支持的数据类型:" + item.type);
}
}
/**
* 获取缓存数据
*/
public Map<String, Object> getAllData() {
return new HashMap<>(dataCache);
}
/**
* 获取单个数据
*/
public Object getData(String key) {
return dataCache.get(key);
}
}
五、完整使用示例
package com.industrial.modbus;
import java.util.Arrays;
import java.util.Map;
public class PlcCommunicationDemo {
public static void main(String[] args) {
// 1. 创建PLC客户端
PlcModbusClient plc = new PlcModbusClient("192.168.1.100", 502, 1);
try {
// 2. 连接PLC
if (!plc.connect()) {
System.err.println("无法连接到PLC");
return;
}
plc.startAutoReconnect();
System.out.println("PLC连接成功!\n");
// 3. 读取保持寄存器
System.out.println("=== 读取保持寄存器 ===");
int[] regs = plc.readHoldingRegisters(0, 10);
System.out.println("D0-D9:" + Arrays.toString(regs));
// 4. 读取浮点数
System.out.println("\n=== 读取浮点数 ===");
float temperature = plc.readFloat32(10);
float humidity = plc.readFloat32(12);
System.out.printf("温度:%.2f℃%n", temperature);
System.out.printf("湿度:%.2f%%%n", humidity);
// 5. 读取32位整数
System.out.println("\n=== 读取32位整数 ===");
int counter = plc.readInt32(20);
System.out.println("计数器值:" + counter);
// 6. 读取线圈
System.out.println("\n=== 读取线圈 ===");
boolean[] coils = plc.readCoils(0, 8);
for (int i = 0; i < coils.length; i++) {
System.out.printf("M%d:%s%n", i, coils[i] ? "ON" : "OFF");
}
// 7. 读取离散输入
System.out.println("\n=== 读取离散输入 ===");
boolean[] inputs = plc.readDiscreteInputs(0, 4);
for (int i = 0; i < inputs.length; i++) {
System.out.printf("X%d:%s%n", i, inputs[i] ? "ON" : "OFF");
}
// 8. 写入寄存器
System.out.println("\n=== 写入寄存器 ===");
plc.writeSingleRegister(100, 1234);
System.out.println("D100 写入 1234");
int check = plc.readHoldingRegisters(100, 1)[0];
System.out.println("D100 回读:" + check);
// 9. 批量写入
plc.writeMultipleRegisters(200, new int[]{100, 200, 300, 400});
System.out.println("D200-D203 批量写入完成");
// 10. 写入浮点数
plc.writeFloat32(300, 25.5f);
System.out.println("D300 写入浮点数 25.5");
float readBack = plc.readFloat32(300);
System.out.printf("D300 回读:%.2f%n", readBack);
// 11. 控制线圈
System.out.println("\n=== 控制线圈 ===");
plc.writeSingleCoil(0, true);
System.out.println("M0 置为 ON");
Thread.sleep(500);
plc.writeSingleCoil(0, false);
System.out.println("M0 置为 OFF");
// 12. 批量控制线圈
plc.writeMultipleCoils(10, new boolean[]{true, false, true, false});
System.out.println("M10-M13 批量写入完成");
// 13. 读取字符串
System.out.println("\n=== 读取字符串 ===");
String deviceName = plc.readString(400, 10);
System.out.println("设备名称:" + deviceName);
} catch (Exception e) {
System.err.println("通讯异常:" + e.getMessage());
e.printStackTrace();
} finally {
plc.disconnect();
System.out.println("\nPLC连接已关闭");
}
}
}
六、数据采集服务使用示例
package com.industrial.modbus;
public class DataCollectorDemo {
public static void main(String[] args) throws InterruptedException {
// 创建PLC客户端
PlcModbusClient plc = new PlcModbusClient("192.168.1.100", 502, 1);
plc.connect();
plc.startAutoReconnect();
// 创建数据采集服务
PlcDataCollector collector = new PlcDataCollector(plc);
// 添加采集项
collector.addCollectItem("temperature", 10, "FLOAT32");
collector.addCollectItem("humidity", 12, "FLOAT32");
collector.addCollectItem("pressure", 14, "FLOAT32");
collector.addCollectItem("counter", 20, "INT32");
collector.addCollectItem("status", 0, "COIL");
collector.addCollectItem("mode", 50, "INT16");
// 添加数据变化监听器
collector.addDataChangeListener((key, oldValue, newValue) -> {
System.out.printf("[数据变化] %s: %s -> %s%n", key, oldValue, newValue);
});
// 启动采集,每1秒采集一次
collector.start(1000);
// 运行30秒
Thread.sleep(30000);
// 打印当前所有数据
System.out.println("\n=== 当前数据快照 ===");
collector.getAllData().forEach((k, v) -> System.out.printf("%s = %s%n", k, v));
// 停止采集
collector.stop();
plc.disconnect();
}
}
七、Spring Boot集成
package com.industrial.config;
import com.industrial.modbus.PlcModbusClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ModbusConfig {
@Value("${modbus.plc.ip:192.168.1.100}")
private String plcIp;
@Value("${modbus.plc.port:502}")
private int plcPort;
@Value("${modbus.plc.unit-id:1}")
private int unitId;
@Bean(destroyMethod = "disconnect")
public PlcModbusClient plcModbusClient() {
PlcModbusClient client = new PlcModbusClient(plcIp, plcPort, unitId);
if (client.connect()) {
client.startAutoReconnect();
}
return client;
}
}
// Service层
package com.industrial.service;
import com.industrial.modbus.PlcModbusClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Service
public class PlcDataService {
@Autowired
private PlcModbusClient plc;
/**
* 获取设备运行数据
*/
public Map<String, Object> getDeviceData() throws IOException {
Map<String, Object> data = new HashMap<>();
data.put("temperature", plc.readFloat32(10));
data.put("humidity", plc.readFloat32(12));
data.put("pressure", plc.readFloat32(14));
data.put("runningHours", plc.readInt32(20));
data.put("status", plc.readCoils(0, 1)[0]);
return data;
}
/**
* 设置温度设定值
*/
public void setTemperature(float value) throws IOException {
plc.writeFloat32(100, value);
}
/**
* 启动设备
*/
public void startDevice() throws IOException {
plc.writeSingleCoil(0, true);
}
/**
* 停止设备
*/
public void stopDevice() throws IOException {
plc.writeSingleCoil(0, false);
}
}
// Controller层
package com.industrial.controller;
import com.industrial.service.PlcDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.Map;
@RestController
@RequestMapping("/api/plc")
public class PlcController {
@Autowired
private PlcDataService plcService;
@GetMapping("/data")
public Map<String, Object> getData() throws IOException {
return plcService.getDeviceData();
}
@PostMapping("/temperature")
public String setTemperature(@RequestParam float value) throws IOException {
plcService.setTemperature(value);
return "温度设定值已更新为:" + value;
}
@PostMapping("/start")
public String start() throws IOException {
plcService.startDevice();
return "设备已启动";
}
@PostMapping("/stop")
public String stop() throws IOException {
plcService.stopDevice();
return "设备已停止";
}
}
八、常见问题与最佳实践
8.1 常见问题
- 连接超时:检查PLC IP地址、端口、防火墙,确认PLC在线且Modbus TCP已启用
- 返回异常码02:地址越界,确认读取的地址范围在PLC支持范围内
- 返回异常码03:数据值非法,检查写入的值是否超出范围
- 浮点数解析错误:尝试不同的字节序(ABCD/CDAB/DCBA/BADC)
- 并发冲突:多线程访问同一连接时使用synchronized或独立连接
- 连接断开:启用自动重连,网络恢复后自动恢复通讯
- 中文乱码:字符串读取时注意字符编码,PLC中通常使用GBK或ASCII
8.2 最佳实践
- 批量读取:尽量一次读取多个连续寄存器,减少通讯次数,提高效率
- 合理轮询间隔:建议不小于100ms,避免过快轮询导致PLC通讯过载
- 自动重连:生产环境必须启用自动重连机制
- 超时设置:局域网2-3秒,公网5-10秒
- 异常处理:捕获IOException,区分连接异常和数据异常
- 数据缓存:使用采集服务缓存数据,避免频繁读取PLC
- 日志记录:记录通讯日志,包括请求、响应、异常,便于排查
- 连接池:多PLC场景使用连接管理器统一管理
- 地址规划:提前规划寄存器地址,避免冲突,编写地址映射文档
- 测试验证:先用Modbus Poll等工具确认通讯正常,再集成Java代码
九、各品牌PLC Modbus地址参考
| 品牌 | 保持寄存器 | 线圈 | 输入寄存器 | 离散输入 |
|---|---|---|---|---|
| 西门子S7-200 | VW0开始 | M0.0/Q0.0 | AIW0 | I0.0 |
| 三菱FX | D0 | M0/Y0 | – | X0 |
| 台达DVP | D0 | M0/Y0 | – | X0 |
| 欧姆龙CP | D0 | CIO0 | – | CIO0(输入) |
| 施耐德M221 | %MW0 | %M0/%Q0 | %IW0 | %I0 |
本文提供的Java代码实现了与PLC的完整Modbus TCP通讯功能,包括连接管理、自动重连、批量读写、多种数据类型转换、数据采集服务以及Spring Boot集成方案。代码可直接用于工业自动化项目开发,建议先使用Modbus Poll工具确认PLC通讯参数和地址映射,再将Java代码集成到实际项目中。
发表回复