首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Java Modbus通信实战(三):Modbus RTU串口通信实现

Java Modbus通信实战(三):Modbus RTU串口通信实现

原创
作者头像
Yeats_Liao
修改2025-09-07 22:57:48
修改2025-09-07 22:57:48
720
举报

想象一下,你要和一台工业设备"对话",比如询问温度传感器"现在多少度?"或者告诉电机"转快一点"。

Modbus RTU就是这种"对话"的标准语言,就像人与人之间说普通话一样。

它采用主从结构,就像老师和学生的关系:

  • 主站(老师):你的Java程序,负责提问和下达指令
  • 从站(学生):各种工业设备,只能回答问题和执行指令

本文将通过XYIoT项目的实际代码,手把手教你如何用Java实现这种"对话"。

1. 核心依赖与配置

1.1 Maven依赖配置

就像做菜需要准备食材一样,我们先准备好必要的依赖包:

代码语言:xml
复制
<!-- Modbus串口通信相关依赖 -->
<dependency>
    <groupId>com.intelligt.modbus</groupId>
    <artifactId>jlibmodbus</artifactId>
    <version>1.2.9.7</version>
</dependency>

<!-- 串口通信依赖 -->
<dependency>
    <groupId>org.scream3r</groupId>
    <artifactId>jssc</artifactId>
    <version>2.8.0</version>
</dependency>

1.2 串口参数配置

串口通信就像打电话,需要设置正确的"电话号码"和"通话规则":

代码语言:yaml
复制
modbus:
  serial:
    # 串口名称
    port-name: COM1
    # 波特率
    baud-rate: 9600
    # 数据位(8位)
    data-bits: 8
    # 停止位(1位)
    stop-bits: 1
    # 校验位(0-NONE, 1-ODD, 2-EVEN)
    parity: 0
    # 超时时间(毫秒)
    timeout: 2000
    # 设备地址
    device-address: 1

2. 核心工具类实现

2.1 配置类

这个配置类就像是"通讯录",记录了如何连接设备:

代码语言:java
复制
@Data
@Configuration
@ConfigurationProperties(prefix = "modbus.serial")
public class ModbusSerialConfig {
    private String portName = "COM3";
    private int baudRate = 9600;
    private int dataBits = 8;
    private int stopBits = 1;
    private int parity = 0;
    private int timeout = 1000;
    private int deviceAddress = 1;
}

2.2 串口工具类

这个工具类就像是"翻译官",负责把你的Java指令翻译成设备能懂的Modbus语言:

代码语言:java
复制
@Slf4j
@Component
public class ModbusSerialUtil {
    private static final Map<String, ModbusMaster> CONNECTION_CACHE = new HashMap<>();
    
    private static ModbusSerialConfig getConfig() {
        return SpringUtils.getBean(ModbusSerialConfig.class);
    }
    
    /**
     * 获取ModbusMaster实例 - 就像获取一个专门的"对讲机"
     * 支持连接缓存(避免重复拨号)和自动重连(断线自动重拨)
     */
    public static ModbusMaster getMaster(String portName) {
        ModbusSerialConfig config = getConfig();
        String port = StringUtils.isEmpty(portName) ? config.getPortName() : portName;
        
        log.info("正在连接Modbus串口: {}", port);

        // 优先使用缓存连接 - 就像手机的通话记录,直接重拨上次的号码
        if (CONNECTION_CACHE.containsKey(port) && CONNECTION_CACHE.get(port) != null) {
            ModbusMaster cachedMaster = CONNECTION_CACHE.get(port);
            try {
                if (!cachedMaster.isConnected()) {
                    cachedMaster.connect();
                }
                return cachedMaster;
            } catch (Exception e) {
                log.warn("缓存连接失效: {},重新建立连接", e.getMessage());
            }
        }

        // 建立新的串口连接 - 就像第一次拨打一个新号码
        try {
            // 初始化配置
            SerialParameters serialParameters = new SerialParameters();
            serialParameters.setDevice(port);
            serialParameters.setBaudRate(BaudRate.getBaudRate(config.getBaudRate()));
            serialParameters.setDataBits(config.getDataBits());
            serialParameters.setStopBits(config.getStopBits());
            
            // 设置校验位
            switch (config.getParity()) {
                case 1: serialParameters.setParity(Parity.ODD); break;
                case 2: serialParameters.setParity(Parity.EVEN); break;
                default: serialParameters.setParity(Parity.NONE); break;
            }
            
            // 设置串口工厂(关键步骤)- 就像选择用哪家电信运营商打电话
            SerialUtils.setSerialPortFactory(new SerialPortFactoryJSSC());
            
            // 创建ModbusMaster实例
            ModbusMaster master = ModbusMasterFactory.createModbusMasterRTU(serialParameters);
            master.setResponseTimeout(config.getTimeout());
            
            // 连接串口
            master.connect();
            
            // 缓存连接实例 - 把这个"电话号码"存到通讯录里
            CONNECTION_CACHE.put(port, master);
            return master;
        } catch (Exception e) {
            log.error("创建Modbus串口连接失败", e);
            throw new ServiceException("串口连接建立失败: " + e.getMessage());
        }
    }
}

3. 读写操作实现

3.1 读取保持寄存器

读取寄存器就像问设备"你现在的状态是什么?"比如问温度传感器"现在多少度?":

代码语言:java
复制
public static int[] readHoldingRegisters(int slaveId, int offset, int quantity) {
    ModbusMaster master = getMaster();
    try {
        if (!master.isConnected()) {
            log.info("检测到连接断开,正在重新连接...");
            master.connect();
            // 连接建立后等待设备就绪
            Thread.sleep(500);
        }
        
        // 实现重试机制 - 就像打电话没接通时会自动重拨几次
        int maxRetries = 3;
        
        for (int retry = 0; retry < maxRetries; retry++) {
            try {
                log.info("读取保持寄存器 (第{}/{}次尝试)", retry + 1, maxRetries);
                int[] result = master.readHoldingRegisters(slaveId, offset, quantity);
                log.info("数据读取成功: {}", Arrays.toString(result));
                return result;
            } catch (ModbusIOException e) {
                log.warn("IO异常,准备重试: {}", e.getMessage());
                Thread.sleep(1000);
            } catch (ModbusProtocolException e) {
                log.warn("协议异常,准备重试: {}", e.getMessage());
                Thread.sleep(1000);
            }
        }
        
        throw new ModbusIOException("多次重试后读取仍然失败");
    } catch (Exception e) {
        log.error("保持寄存器读取失败: {}", e.getMessage());
        throw new ServiceException("保持寄存器读取失败: " + e.getMessage());
    }
}

3.2 写入单个寄存器

写入寄存器就像给设备下指令,比如告诉空调"设定温度26度":

代码语言:java
复制
public static void writeSingleRegister(int slaveId, int offset, int value) {
    ModbusMaster master = getMaster();
    try {
        if (!master.isConnected()) {
            master.connect();
        }
        master.writeSingleRegister(slaveId, offset, value);
        
        // 写入后等待设备处理 - 就像发短信后等对方回复"收到"
        Thread.sleep(500);
    } catch (Exception e) {
        log.error("单个寄存器写入失败: {}", e.getMessage());
        throw new ServiceException("单个寄存器写入失败: " + e.getMessage());
    }
}

4. 检测可用串口

4.1 系统串口检测

就像查看电脑上有哪些USB接口可以用一样,我们需要找到可用的串口:

代码语言:java
复制
public static List<String> getSystemPortNames() {
    List<String> portList = new ArrayList<>();
    String osName = System.getProperty("os.name").toLowerCase();
    
    // Windows系统串口检测 - 就像在Windows设备管理器里查看COM口
    if (osName.contains("win")) {
        try {
            Process process = Runtime.getRuntime().exec(new String[] {
                "powershell.exe", "-Command", 
                "[System.IO.Ports.SerialPort]::getportnames()"
            });
            
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(process.getInputStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    line = line.trim();
                    if (!line.isEmpty() && !portList.contains(line)) {
                        portList.add(line);
                    }
                }
            }
        } catch (Exception e) {
            log.warn("Windows串口检测失败: {}", e.getMessage());
        }
    } 
    // Linux系统串口检测 - 就像用ls命令查看/dev目录下的设备文件
    else if (osName.contains("nix") || osName.contains("nux")) {
        try {
            Process process = Runtime.getRuntime().exec("ls -la /dev/tty*");
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(process.getInputStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    if (line.contains("ttyS") || line.contains("ttyUSB")) {
                        String[] parts = line.split("\\s+");
                        String portName = "/dev/" + parts[parts.length - 1];
                        portList.add(portName);
                    }
                }
            }
        } catch (Exception e) {
            log.warn("Linux串口检测失败: {}", e.getMessage());
        }
    }
    
    return portList;
}

5. 测试连接

5.1 连接测试方法

就像打电话前先测试信号是否正常一样,我们需要测试串口连接:

代码语言:java
复制
public static boolean testConnection(String portName) {
    ModbusMaster master = null;
    try {
        log.info("正在测试串口连接: {}", portName);
        
        ModbusSerialConfig config = getConfig();
        SerialParameters serialParameters = new SerialParameters();
        serialParameters.setDevice(portName);
        serialParameters.setBaudRate(BaudRate.getBaudRate(config.getBaudRate()));
        serialParameters.setDataBits(config.getDataBits());
        serialParameters.setStopBits(config.getStopBits());
        
        // 设置校验位
        switch (config.getParity()) {
            case 1: serialParameters.setParity(Parity.ODD); break;
            case 2: serialParameters.setParity(Parity.EVEN); break;
            default: serialParameters.setParity(Parity.NONE); break;
        }
        
        // 设置串口工厂(必需步骤)
        SerialUtils.setSerialPortFactory(new SerialPortFactoryJSSC());
        
        master = ModbusMasterFactory.createModbusMasterRTU(serialParameters);
        master.setResponseTimeout(config.getTimeout());
        
        master.connect();
        boolean connected = master.isConnected();
        
        return connected;
    } catch (Exception e) {
        log.error("串口连接测试失败: {}", e.getMessage());
        return false;
    } finally {
        if (master != null) {
            try {
                master.disconnect();
            } catch (Exception e) {
                log.error("测试连接关闭失败: {}", e.getMessage());
            }
        }
    }
}

6. 实战经验与注意事项

6.1 关键配置要点

这些是实际项目中踩过的坑,就像老司机的驾驶经验:

设置SerialPortFactory

这就像选择正确的"电话线路",不设置就像拿着手机但没插SIM卡:

代码语言:java
复制
   SerialUtils.setSerialPortFactory(new SerialPortFactoryJSSC());

添加操作间延迟

设备需要"思考时间",就像你问朋友问题后要等他回答,不能连珠炮似的问个不停:

代码语言:java
复制
   // 写入操作后延迟
   Thread.sleep(2000);

增加超时设置

就像打电话设置等待时间,超过这个时间没人接就自动挂断:

代码语言:java
复制
   master.setResponseTimeout(2000); // 设置2秒超时

实现重试机制

工业现场就像信号不好的地方,有时需要多打几次电话才能接通:

代码语言:java
复制
   // 最多重试3次
   for (int retry = 0; retry < 3; retry++) {
       try {
           return master.readHoldingRegisters(slaveId, offset, quantity);
       } catch (Exception e) {
           if (retry == 2) throw e;
           Thread.sleep(1000);
       }
   }

使用连接缓存

就像手机的通话记录,避免每次都重新拨号:

代码语言:java
复制
   private static final Map<String, ModbusMaster> CONNECTION_CACHE = new HashMap<>();

7. 帧数据解析示例

7.1 通信日志分析

就像查看手机的通话记录一样,我们可以看到每次"对话"的详细内容。

比如你在控制台会看到这样的日志:

以下面Modbus RTU通信日志为例:

代码语言:bash
复制
Frame sent: 0106000000648821
Frame recv: 0106000000648821

7.2 数据帧结构解析

这串看似乱码的数字,其实就像一封标准格式的信件:

  • 01: 从站地址(设备地址),这里是1
  • 06: 功能码,表示写入单个寄存器
  • 0000: 寄存器地址,这里是0
  • 0064: 写入的值,十六进制0x64即十进制100
  • 8821: CRC校验和

想象Modbus通信就像邮递员送信:

  • 主站:就像邮局,负责发送各种信件(指令)
  • 从站:就像收信人,收到信后按要求办事并回信
  • 地址:就像门牌号,每个设备都有唯一编号
  • 功能码:就像信件类型,告诉收信人要做什么事

7.3 数据帧详细分析

让我们把这个"信件"拆开来看:

数据帧0106000000648821就像一个包裹的标签:

代码语言:bash
复制
01      - 从站地址(设备编号,这里是1号设备)
06      - 功能码(06表示"写单个寄存器")
0000    - 寄存器地址(这里写入第0号寄存器)
0064    - 要写入的数据(0x64十六进制 = 100十进制)
8821    - CRC校验码(确保数据完整性,类似防伪标记)

7.4 常见功能码说明

功能码就像不同类型的业务单,每种单子办不同的事:

  • 01 - 读取线圈状态(开关量输出)
  • 02 - 读取输入状态(开关量输入)
  • 03 - 读取保持寄存器(可读写的数据)
  • 04 - 读取输入寄存器(只读数据)
  • 05 - 写单个线圈
  • 06 - 写单个寄存器
  • 15 - 写多个线圈
  • 16 - 写多个寄存器

7.5 通信实例分析

让我们看几个实际的"对话"例子:

写入命令示例(就像发指令)

代码语言:bash
复制
0106000000648821  (写入请求)
0106000000648821  (设备回复确认)

这就像:

  • 你对1号设备说:"把你的第0个参数设置为100"
  • 设备回复:"好的,已经设置为100了"(重复一遍确认收到)

读取命令示例(就像询问状态)

代码语言:bash
复制
010300000001840A  (读取请求)
0103020064XX      (设备回复,XX是校验码)

这就像:

  • 你问1号设备:"告诉我你第0个参数的值是多少?"
  • 设备回答:"我是1号设备,你问的参数值是100"

7.6 常见通信问题分析

有时候"电话"会出现问题,比如:

代码语言:bash
复制
Frame sent: 010300000001840A
Frame recv: 0103000000          (应该还有后续数据)

就像打电话时信号不好,可能的原因:

  1. 对方正忙,需要时间处理(设备处理指令需要时间)
  2. 信号干扰,话说了一半就断了(数据传输被干扰)
  3. 对方有特殊的接电话习惯(设备有特定时序要求)

所以我们要像打电话一样,说完一句话等对方回应,不要急着说下一句。

7.7 数据类型说明

Modbus就像一个只会说数字的"外国人",它能表达的内容有限:

  • 线圈/离散输入:只会说"是"或"不是"(开关状态)
  • 寄存器:会说0到65535的整数(像计数器)
  • 连续寄存器:把两个数字拼起来说更大的数或小数

8. 总结

通过本文的学习,你已经掌握了用Java与工业设备"对话"的技能:

就像学会打电话

  • 知道怎么拨号(配置串口参数)
  • 会存通讯录(连接缓存)
  • 信号不好时会重拨(重试机制)
  • 知道什么时候该等对方说话(延迟控制)
  • 会测试电话是否通畅(连接测试)

实际应用场景

  • 工厂自动化:让你的程序控制生产线设备
  • 环境监测:实时读取温湿度、压力等传感器数据
  • 能源管理:监控和控制电力设备的运行状态=

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 核心依赖与配置
    • 1.1 Maven依赖配置
    • 1.2 串口参数配置
  • 2. 核心工具类实现
    • 2.1 配置类
    • 2.2 串口工具类
  • 3. 读写操作实现
    • 3.1 读取保持寄存器
    • 3.2 写入单个寄存器
  • 4. 检测可用串口
    • 4.1 系统串口检测
  • 5. 测试连接
    • 5.1 连接测试方法
  • 6. 实战经验与注意事项
    • 6.1 关键配置要点
  • 7. 帧数据解析示例
    • 7.1 通信日志分析
    • 7.2 数据帧结构解析
    • 7.3 数据帧详细分析
    • 7.4 常见功能码说明
    • 7.5 通信实例分析
    • 7.6 常见通信问题分析
    • 7.7 数据类型说明
  • 8. 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档