首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Java串口通信实战(六):从JNI底层与串口库访问到Modbus标准化

Java串口通信实战(六):从JNI底层与串口库访问到Modbus标准化

原创
作者头像
Yeats_Liao
发布2025-09-09 09:58:54
发布2025-09-09 09:58:54
4000
代码可运行
举报
运行总次数:0
代码可运行

1. 从汇编到Java:工业通信的技术变迁

十几年前,工厂里的设备控制基本都是用汇编或者C语言直接操作硬件寄存器。那时候要读个温度传感器,得先查手册找到设备的内存映射地址,然后用指针直接访问。虽然效率高,但写起来真的头疼,一个不小心就能让整个系统崩溃。

后来Java在企业级应用中越来越流行,但硬件通信这块一直是个痛点。Java的"一次编写,到处运行"理念和直接操作硬件天然冲突。于是就有了各种解决方案:JNI让我们能调用C代码,串口库让设备通信变得简单,Modbus协议更是成了工业通信的"普通话"。

这些年下来,我见证了工业通信从"手工作坊"到"标准化生产"的转变。今天就和大家分享一下这三种主流方案的实战经验,希望能帮你少踩些坑。

技术演进的三个阶段

第一阶段(1990s-2000s):直接硬件访问时代

那时候主要靠汇编和C语言,程序员需要深入了解硬件细节,效率高但开发难度大。

第二阶段(2000s-2010s):串口通信普及

RS232/RS485成为主流,设备厂商开始提供标准化的通信接口,降低了开发门槛。

第三阶段(2010s至今):协议标准化

Modbus、OPC等标准协议大规模应用,工业4.0推动了通信协议的进一步统一。

2. JNI

JNI诞生于1997年,当时Sun公司意识到Java要在企业级应用中站稳脚跟,就必须能够调用现有的C/C++代码库。特别是在工业控制领域,大量的设备驱动都是用C写的,如果Java不能复用这些代码,那在工业应用中就没有竞争力。

我记得早期用JNI的时候,经常因为内存管理问题导致JVM崩溃。那时候调试工具也不完善,出了问题只能靠经验和运气。不过随着工具链的完善,现在用JNI已经相对安全多了。

有时候你会遇到这样的情况:设备厂商只提供了C/C++的驱动库,或者需要直接操作硬件寄存器。这时候JNI(Java Native Interface)就派上用场了,它就像是Java和底层系统之间的"翻译官"。

2.1 工作流程

使用JNI和硬件通信的过程就像搭积木,需要按步骤来:

  1. 在Java中声明native方法
  2. 编译Java代码生成.class文件
  3. 用javah生成C/C++头文件
  4. 编写C/C++代码实现具体功能
  5. 编译成动态库(.so或.dll)
  6. Java程序加载库并调用

2.2 JNI 读取硬件寄存器

假设我们要读取一个工业设备的寄存器数据,设备厂商提供了C语言的驱动接口。

第一步:Java代码

代码语言:java
复制
public class HardwareRegisterReader {
    // 声明native方法,告诉Java这个方法在C代码里实现
    public native int readRegister(int address);
    
    static {
        // 加载我们编译好的动态库
        System.loadLibrary("hardware_reader");
    }
    
    public static void main(String[] args) {
        HardwareRegisterReader reader = new HardwareRegisterReader();
        
        // 读取地址0x1000的寄存器
        int registerAddr = 0x1000;
        int value = reader.readRegister(registerAddr);
        
        System.out.println("寄存器 0x" + Integer.toHexString(registerAddr) + " 的值: " + value);
    }
}

第二步:生成头文件

编译Java文件后,用javah生成C头文件:

代码语言:bash
复制
javac HardwareRegisterReader.java
javah -jni HardwareRegisterReader

这会生成一个HardwareRegisterReader.h文件,里面定义了C函数的接口。

第三步:C代码实现

代码语言:c
代码运行次数:0
运行
复制
#include "HardwareRegisterReader.h"
#include <stdio.h>
#include <stdlib.h>

// 假设这是设备的基地址
#define DEVICE_BASE_ADDR 0x1000

// 实现Java中声明的native方法
JNIEXPORT jint JNICALL Java_HardwareRegisterReader_readRegister
  (JNIEnv *env, jobject obj, jint address) {
    
    // 这里应该是真实的硬件访问代码
    // 为了演示,我们模拟一个读取过程
    printf("正在读取寄存器地址: 0x%x\n", address);
    
    // 模拟从硬件读取的数值
    int registerValue = (address - DEVICE_BASE_ADDR) + 100;
    
    printf("读取到的值: %d\n", registerValue);
    return registerValue;
}

第四步:编译动态库

Linux/macOS系统:

代码语言:bash
复制
gcc -shared -fPIC -o libhardware_reader.so \
    -I$JAVA_HOME/include -I$JAVA_HOME/include/linux \
    HardwareRegisterReader.c

Windows系统:

代码语言:bash
复制
gcc -shared -o hardware_reader.dll \
    -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" \
    HardwareRegisterReader.c

2.3 JNI 使用要点

优势

  • 能直接访问硬件,不受JVM限制
  • 性能高,适合实时性要求严格的场景
  • 可以复用现有的C/C++驱动库

注意事项

  • 跨平台需要编译多个版本的动态库
  • 调试相对复杂,出错可能导致JVM崩溃
  • 需要熟悉C/C++编程
  • 要注意内存管理,避免内存泄漏

3. 串口通信

串口通信可以说是工业通信的"老祖宗"了。RS232标准早在1962年就发布了,那时候计算机还是大型机的天下。后来RS485的出现解决了传输距离和多设备通信的问题,一下子就在工业现场火了起来。

我刚工作那会儿,工厂里到处都是串口线,密密麻麻的像蜘蛛网一样。那时候调试设备经常要拿着示波器去测信号,现在想想真是"刀耕火种"的年代。不过串口通信的简单可靠让它至今还在工业现场占有一席之地。

在工业现场,串口通信就像是设备之间的"电话线"。虽然现在网络通信很发达,但很多传感器、仪表、老设备还是习惯用串口"聊天"。

3.1 串口通信库选择

Java有几个常用的串口通信库:

JSerialComm:新一代串口库,维护活跃,兼容性好,推荐使用

RXTX:老牌串口库,功能稳定但维护较少

我们主要用JSerialComm来演示,因为它更现代化,bug也少。

3.2 JSerialComm

假设我们要从一个温度传感器读取数据,传感器通过RS485接口连接到电脑。

第一步:添加依赖

Maven项目在pom.xml中添加:

代码语言:xml
复制
<dependency>
    <groupId>com.fazecast</groupId>
    <artifactId>jSerialComm</artifactId>
    <version>2.9.2</version>
</dependency>

第二步:编写通信代码

代码语言:java
复制
import com.fazecast.jSerialComm.SerialPort;

public class TemperatureSensorReader {
    public static void main(String[] args) {
        // 先看看系统有哪些串口
        SerialPort[] availablePorts = SerialPort.getCommPorts();
        System.out.println("发现串口设备:");
        for (int i = 0; i < availablePorts.length; i++) {
            System.out.println((i + 1) + ". " + availablePorts[i].getSystemPortName());
        }
        
        if (availablePorts.length == 0) {
            System.out.println("没有找到串口设备");
            return;
        }
        
        // 选择第一个串口(实际使用时根据具体情况选择)
        SerialPort sensorPort = availablePorts[0];
        
        // 配置串口参数(这些参数要和传感器手册一致)
        sensorPort.setBaudRate(9600);    // 波特率
        sensorPort.setNumDataBits(8);    // 数据位
        sensorPort.setNumStopBits(SerialPort.ONE_STOP_BIT);  // 停止位
        sensorPort.setParity(SerialPort.NO_PARITY);          // 校验位
        
        // 打开串口
        if (sensorPort.openPort()) {
            System.out.println("串口打开成功: " + sensorPort.getSystemPortName());
        } else {
            System.out.println("串口打开失败");
            return;
        }
        
        try {
            // 等待设备准备好
            Thread.sleep(1000);
            
            // 发送读取温度的命令(具体命令格式看传感器手册)
            String readCommand = "READ_TEMP\r\n";
            byte[] commandBytes = readCommand.getBytes();
            int bytesWritten = sensorPort.writeBytes(commandBytes, commandBytes.length);
            System.out.println("发送了 " + bytesWritten + " 字节的命令");
            
            // 等待响应
            Thread.sleep(500);
            
            // 读取传感器响应
            byte[] responseBuffer = new byte[256];
            int bytesRead = sensorPort.readBytes(responseBuffer, responseBuffer.length);
            
            if (bytesRead > 0) {
                String response = new String(responseBuffer, 0, bytesRead).trim();
                System.out.println("传感器响应: " + response);
                
                // 解析温度数据(假设返回格式是 "TEMP:25.6")
                if (response.startsWith("TEMP:")) {
                    String tempStr = response.substring(5);
                    double temperature = Double.parseDouble(tempStr);
                    System.out.println("当前温度: " + temperature + "°C");
                }
            } else {
                System.out.println("没有收到传感器响应");
            }
            
        } catch (Exception e) {
            System.out.println("通信出错: " + e.getMessage());
        } finally {
            // 关闭串口
            sensorPort.closePort();
            System.out.println("串口已关闭");
        }
    }
}

代码解析

串口发现SerialPort.getCommPorts()会列出系统所有可用的串口,在Windows上通常是COM1、COM2这样,Linux上是/dev/ttyUSB0这样。

参数配置:波特率、数据位这些参数必须和设备手册完全一致,否则就像两个人说不同的语言,无法正常通信。

命令发送:不同设备的命令格式不一样,有些用ASCII文本,有些用二进制数据,要仔细看设备文档。

数据接收:串口通信经常需要等待,因为设备处理命令需要时间。

3.4 RXTX

RXTX是老牌的串口库,虽然维护不够积极,但在一些老项目中还在使用:

代码语言:java
复制
import gnu.io.CommPortIdentifier;
import gnu.io.SerialPort;
import java.io.InputStream;
import java.io.OutputStream;

public class RXTXExample {
    public static void main(String[] args) throws Exception {
        // 获取指定串口
        CommPortIdentifier portId = CommPortIdentifier.getPortIdentifier("COM3");
        SerialPort serialPort = (SerialPort) portId.open("MyApp", 2000);
        
        // 设置参数
        serialPort.setSerialPortParams(9600, 
            SerialPort.DATABITS_8, 
            SerialPort.STOPBITS_1, 
            SerialPort.PARITY_NONE);
        
        // 获取输入输出流
        InputStream input = serialPort.getInputStream();
        OutputStream output = serialPort.getOutputStream();
        
        // 发送数据
        output.write("READ_DATA\r\n".getBytes());
        
        // 读取响应
        byte[] buffer = new byte[1024];
        int length = input.read(buffer);
        System.out.println("收到: " + new String(buffer, 0, length));
        
        // 关闭连接
        serialPort.close();
    }
}

3.5 串口通信要点

优势

  • 简单可靠,工业现场使用广泛
  • 传输距离远(RS485可达1200米)
  • 抗干扰能力强

注意事项

  • 参数配置要和设备完全匹配
  • 注意超时处理,避免程序卡死
  • 大数据传输速度较慢
  • 需要了解设备的通信协议

4. Modbus协议

4.0 Modbus协议的诞生与发展

Modbus协议有个有趣的历史。1979年,Modicon公司(后来被施耐德收购)为了让自家的PLC能和其他设备通信,开发了这个协议。当时谁也没想到,这个"内部标准"后来会成为工业通信的事实标准。

我记得2000年左右,工厂里的设备通信还是各家有各家的协议,互相不兼容。那时候做个项目,光是协议转换就要花大量时间。后来Modbus开源了,各个厂商纷纷跟进,才有了今天"万物皆可Modbus"的局面。

2004年Modbus TCP的出现更是革命性的,把传统的串口通信搬到了以太网上。我见过很多老工程师刚开始都不相信网络能用于实时控制,现在看来真是时代的眼泪。

如果说串口是设备间的"电话线",那么Modbus就是它们说话用的"标准语言"。在工业现场,你会发现大部分设备都支持Modbus协议,从变频器到PLC,从温控器到电力仪表。

Modbus有两种常见的传输方式:

  • Modbus RTU:通过串口传输,数据用二进制格式
  • Modbus TCP:通过网络传输,把Modbus数据包装在TCP里

4.1 Modbus TCP 读取变频器数据

假设我们要读取一台变频器的运行状态,变频器支持Modbus TCP协议。

第一步:添加依赖

Maven项目中添加jLibModbus库:

代码语言:xml
复制
<dependency>
    <groupId>com.intelligt.modbus</groupId>
    <artifactId>jlibmodbus</artifactId>
    <version>1.2.8.1</version>
</dependency>

第二步:编写读取代码

代码语言:java
复制
import com.intelligt.modbus.jlibmodbus.Modbus;
import com.intelligt.modbus.jlibmodbus.ModbusMaster;
import com.intelligt.modbus.jlibmodbus.ModbusMasterFactory;
import com.intelligt.modbus.jlibmodbus.exception.ModbusIOException;
import com.intelligt.modbus.jlibmodbus.exception.ModbusNumberException;
import com.intelligt.modbus.jlibmodbus.exception.ModbusProtocolException;
import com.intelligt.modbus.jlibmodbus.tcp.TcpParameters;
import java.net.InetAddress;

public class FrequencyConverterReader {
    public static void main(String[] args) {
        try {
            // 配置变频器的网络参数
            TcpParameters tcpParams = new TcpParameters();
            InetAddress deviceIP = InetAddress.getByName("192.168.1.100"); // 变频器IP
            tcpParams.setHost(deviceIP);
            tcpParams.setPort(Modbus.TCP_PORT); // 默认502端口

            // 创建Modbus主站
            ModbusMaster master = ModbusMasterFactory.createModbusMasterTCP(tcpParams);
            master.connect();
            System.out.println("已连接到变频器: " + deviceIP.getHostAddress());

            // 变频器的从站地址(通常在设备参数中设置)
            int slaveId = 1;
            
            try {
                // 读取变频器状态寄存器(假设地址从40001开始,读取10个寄存器)
                int startAddr = 0;    // Modbus地址40001对应内部地址0
                int quantity = 10;    // 读取10个寄存器
                
                int[] registers = master.readHoldingRegisters(slaveId, startAddr, quantity);
                
                System.out.println("变频器运行数据:");
                System.out.println("运行频率: " + (registers[0] / 100.0) + " Hz");  // 假设频率数据在第一个寄存器,需要除以100
                System.out.println("输出电流: " + (registers[1] / 100.0) + " A");   // 电流数据在第二个寄存器
                System.out.println("输出电压: " + registers[2] + " V");              // 电压数据在第三个寄存器
                System.out.println("运行状态: " + (registers[3] == 1 ? "运行" : "停止")); // 状态位
                
                // 显示所有寄存器的原始数据
                for (int i = 0; i < registers.length; i++) {
                    System.out.println("寄存器[4000" + (i + 1) + "] = " + registers[i]);
                }
                
            } catch (ModbusProtocolException | ModbusNumberException | ModbusIOException e) {
                System.err.println("读取变频器数据失败: " + e.getMessage());
            }

            // 断开连接
            master.disconnect();
            System.out.println("已断开连接");
            
        } catch (Exception e) {
            System.err.println("连接变频器失败: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

代码详解

网络配置:Modbus TCP就像是把传统的Modbus数据"打包"通过网络发送,默认使用502端口。

寄存器地址:这里有个容易搞混的地方。Modbus协议中,40001号寄存器在程序里对应地址0,40002对应地址1,以此类推。

数据解析:不同设备的数据格式不一样,有些需要除以100来得到实际值,有些直接就是真实数据。这些信息通常在设备手册里有说明。

异常处理:网络通信可能出现各种问题,所以要做好异常处理,避免程序崩溃。

4.3 Modbus RTU 串口通信

如果设备只支持串口通信,可以用Modbus RTU:

代码语言:java
复制
import com.intelligt.modbus.jlibmodbus.ModbusMaster;
import com.intelligt.modbus.jlibmodbus.ModbusMasterFactory;
import com.intelligt.modbus.jlibmodbus.serial.SerialParameters;

public class ModbusRTUExample {
    public static void main(String[] args) {
        try {
            // 配置串口参数
            SerialParameters serialParams = new SerialParameters();
            serialParams.setDevice("COM3");        // 串口名称
            serialParams.setBaudRate(9600);       // 波特率
            serialParams.setDataBits(8);          // 数据位
            serialParams.setParity(SerialParameters.PARITY_NONE); // 校验位
            serialParams.setStopBits(1);          // 停止位
            
            // 创建Modbus RTU主站
            ModbusMaster master = ModbusMasterFactory.createModbusMasterRTU(serialParams);
            master.connect();
            
            // 读取从站1的保持寄存器
            int[] data = master.readHoldingRegisters(1, 0, 5);
            
            for (int i = 0; i < data.length; i++) {
                System.out.println("寄存器 " + i + ": " + data[i]);
            }
            
            master.disconnect();
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4.4 Modbus使用要点

优势

  • 工业标准协议,设备支持广泛
  • 协议简单,容易理解和实现
  • 既支持串口也支持网络传输
  • 有大量现成的库可以使用

注意事项

  • 地址映射要搞清楚(40001对应地址0)
  • 不同厂商的数据格式可能不同
  • 网络版本要注意防火墙设置
  • 串口版本要确保参数匹配

5. 三种方式的选择指南

现在我们已经了解了三种主要的Java硬件通信方式,那么在实际项目中该如何选择呢?

5.1 应用场景对比

JNI适合的场景:

  • 设备厂商只提供C/C++驱动,没有其他选择
  • 需要极高的实时性能,比如高速数据采集
  • 要直接操作硬件寄存器或内存映射
  • 复用现有的C/C++代码库

串口通信适合的场景:

  • 传感器、仪表等简单设备的数据读取
  • 老设备改造,原来就是串口接口
  • 距离较远的设备通信(RS485)
  • 对实时性要求不是特别高的应用

Modbus适合的场景:

  • 工业自动化项目,设备普遍支持Modbus
  • 需要和PLC、变频器、电力仪表等设备通信
  • 要求标准化的通信协议
  • 既有串口设备又有网络设备的混合环境

5.2 技术选型建议

考虑因素

JNI

串口通信

Modbus

开发难度

性能表现

最高

中等

中等

跨平台性

维护成本

设备兼容性

看厂商

广泛

工业设备广泛

学习成本

5.3 实际项目经验

小型项目:如果只是读取几个传感器的数据,直接用JSerialComm就够了,简单快捷。

工业项目:优先考虑Modbus,因为大部分工业设备都支持,而且协议标准化程度高,后期维护方便。

高性能项目:如果对实时性要求极高,比如高速运动控制,可能需要用JNI直接调用厂商的驱动库。

混合项目:实际项目中经常需要组合使用,比如用Modbus和PLC通信,用串口读取传感器,用JNI控制特殊设备。

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 从汇编到Java:工业通信的技术变迁
  • 2. JNI
    • 2.1 工作流程
    • 2.2 JNI 读取硬件寄存器
    • 2.3 JNI 使用要点
  • 3. 串口通信
    • 3.1 串口通信库选择
    • 3.2 JSerialComm
    • 3.4 RXTX
    • 3.5 串口通信要点
  • 4. Modbus协议
    • 4.0 Modbus协议的诞生与发展
    • 4.1 Modbus TCP 读取变频器数据
    • 4.3 Modbus RTU 串口通信
    • 4.4 Modbus使用要点
  • 5. 三种方式的选择指南
    • 5.1 应用场景对比
    • 5.2 技术选型建议
    • 5.3 实际项目经验
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档