三大硬核方式揭秘:Java如何与底层硬件和工业设备轻松通信!

大家好,我是V哥,程序员聊天真是三句不到离不开技术啊,这不前两天跟一个哥们吃饭,他是我好多年前的学员了,一直保持着联系,现在都李总了,在做工业互联网相关的项目,真是只要 Java 学得好,能干一辈子,卷死的是那些半吊子。

感谢李总给我分享了工业互联网项目的事情,收获很多,今天的内容来聊一聊 Java如何与底层硬件和工业设备轻松通信的事情。

Java读取寄存器数据通常涉及与硬件设备的通信。这种操作通常是通过以下几种方式来实现的:

使用 Modbus 协议读取设备寄存器数据(使用 jLibModbus

Modbus 是一种用于工业自动化设备的通信协议。常见的Modbus通信方式包括:Modbus RTU(基于串行通信)和Modbus TCP(基于网络通信)。在此示例中,我们将使用 Java 和 jLibModbus 库通过 Modbus TCP 协议读取设备的寄存器数据。

实现步骤

  1. 添加 jLibModbus 依赖。
  2. 设置 Modbus Master 客户端。
  3. 通过 Modbus Master 读取设备的寄存器数据。

1. 添加 jLibModbus 依赖

使用 Maven 管理项目时,可以在 pom.xml 中添加 jLibModbus 依赖:

<dependency>
    <groupId>com.intelligt.modbus</groupId>
    <artifactId>jlibmodbus</artifactId>
    <version>1.2.8.1</version> <!-- 根据具体需求设置版本 -->
</dependency>

或者直接下载 jar 包并将其添加到项目的 classpath 中。

2. 示例代码:通过 Modbus TCP 读取寄存器

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.modbus.ModbusFunctionCode;
import com.intelligt.modbus.jlibmodbus.tcp.TcpParameters;

import java.net.InetAddress;

public class ModbusTcpClient {
    public static void main(String[] args) {
        try {
            // 配置 Modbus TCP 参数
            TcpParameters tcpParameters = new TcpParameters();
            InetAddress address = InetAddress.getByName("192.168.1.100"); // 设备的IP地址
            tcpParameters.setHost(address);
            tcpParameters.setPort(Modbus.TCP_PORT); // Modbus 默认TCP端口 502

            // 创建 ModbusMaster 实例
            ModbusMaster master = ModbusMasterFactory.createModbusMasterTCP(tcpParameters);
            master.connect();

            // 设备的 Slave ID(通常是从站地址)
            int slaveId = 1;
            // 读取保持寄存器(Holding Registers),从地址 0 开始,读取 10 个寄存器
            int startAddress = 0;
            int quantity = 10;

            try {
                // 读取保持寄存器数据
                int[] registerValues = master.readHoldingRegisters(slaveId, startAddress, quantity);
                System.out.println("寄存器数据:");
                for (int i = 0; i < registerValues.length; i++) {
                    System.out.println("寄存器[" + (startAddress + i) + "] = " + registerValues[i]);
                }
            } catch (ModbusProtocolException | ModbusNumberException | ModbusIOException e) {
                System.err.println("Modbus 读取失败: " + e.getMessage());
            }

            // 断开连接
            master.disconnect();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

来解释一下代码哈

  1. Modbus TCP 参数

    • TcpParameters 用于配置 Modbus TCP 的主机和端口。默认的Modbus TCP端口是 502
    • 使用 InetAddress.getByName("192.168.1.100") 设置设备的IP地址。
  2. Modbus Master 实例

    • ModbusMaster master = ModbusMasterFactory.createModbusMasterTCP(tcpParameters); 创建一个Modbus主机(Master),用于与从设备通信。
    • master.connect(); 连接到Modbus设备。
  3. 读取保持寄存器

    • int[] registerValues = master.readHoldingRegisters(slaveId, startAddress, quantity); 读取从设备的保持寄存器(Holding Registers),从 startAddress 开始,读取 quantity 个寄存器。
    • 输出寄存器值。
  4. 错误处理

    • 捕获 ModbusProtocolExceptionModbusNumberExceptionModbusIOException 以处理通信过程中可能出现的错误。
  5. 断开连接

    • 使用 master.disconnect(); 在完成数据读取后断开与Modbus设备的连接。

3. 如何使用

  1. 设备配置:确保你有一个支持 Modbus TCP 的设备,并且设备的IP地址与端口正确配置。通常情况下,Modbus TCP设备监听502端口。
  2. 运行程序:运行此 Java 程序,程序将连接到设备并读取指定寄存器的数据。
  3. 结果示例
   寄存器数据:
   寄存器[0] = 1234
   寄存器[1] = 5678
   寄存器[2] = 910
   ...

使用时要注意的事项

  1. 设备通信参数:确保设备支持Modbus TCP协议,并正确配置了IP地址、端口和从站地址(Slave ID)。
  2. 读取不同类型的寄存器:在Modbus中,可以读取不同类型的寄存器,比如输入寄存器(Input Registers)或线圈(Coils)。根据需求调用对应的方法:
    • readInputRegisters(...):读取输入寄存器。
    • readCoils(...):读取线圈状态。
  3. Modbus地址偏移:一些Modbus设备使用1-based地址系统,而程序中可能使用0-based地址,注意这点以防读取错误地址。

小结一下

我们通过 jLibModbus 库使用 Java 读取支持 Modbus TCP 协议的设备的寄存器数据。Modbus是工业控制领域中广泛应用的通信协议,利用Java实现设备通信可以用于各种自动化系统中。如果你的设备使用Modbus RTU协议,可以通过配置串口通信来实现类似的操作。

JNI(Java Native Interface)

Java Native Interface (JNI) 允许Java代码与C/C++等本地语言编写的代码交互,可以用于实现高性能、直接的硬件访问,如寄存器读取。

JNI基本流程

  1. 在Java中声明本地方法。
  2. 使用javac编译Java类。
  3. 使用javah生成C/C++头文件。
  4. 编写C/C++代码实现Java方法。
  5. 编译生成动态库。
  6. Java代码加载动态库并调用本地方法。

来看案例:使用JNI读取寄存器数据

1. Java代码

首先,定义一个Java类,该类声明一个本地方法用于读取寄存器数据。

public class RegisterReader {
    // 声明本地方法,该方法将在C/C++代码中实现
    public native int readRegister(int address);

    static {
        // 加载本地库,假设库名为 "register_reader"
        System.loadLibrary("register_reader");
    }

    public static void main(String[] args) {
        RegisterReader reader = new RegisterReader();
        int registerAddress = 0x1000; // 假设寄存器地址
        int value = reader.readRegister(registerAddress);
        System.out.println("Register Value: " + value);
    }
}

编译Java文件:

javac RegisterReader.java

生成C/C++头文件:

javah -jni RegisterReader

此命令将生成一个RegisterReader.h文件,包含C/C++中需要实现的方法声明。

2. 生成的头文件(RegisterReader.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class RegisterReader */

#ifndef _Included_RegisterReader
#define _Included_RegisterReader
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     RegisterReader
 * Method:    readRegister
 * Signature: (I)I
 */
JNIEXPORT jint JNICALL Java_RegisterReader_readRegister
  (JNIEnv *, jobject, jint);

#ifdef __cplusplus
}
#endif
#endif

3. C代码实现

接下来,在C语言中实现readRegister方法。在这里,我们假设寄存器通过内存映射的方式访问。

#include "RegisterReader.h"
#include <stdio.h>
#include <stdlib.h>

// 模拟寄存器的内存映射地址
#define REGISTER_BASE_ADDRESS 0x1000

JNIEXPORT jint JNICALL Java_RegisterReader_readRegister(JNIEnv *env, jobject obj, jint address) {
    // 模拟读取寄存器,实际实现应访问真实硬件哈
    int registerValue = (address - REGISTER_BASE_ADDRESS) * 2;  // 伪代码,用来模拟一下
    printf("Reading register at address: 0x%x\n", address);
    return registerValue;
}

4. 编译生成动态库

在Linux或macOS上,编译C代码并生成动态库:

gcc -shared -o libregister_reader.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux RegisterReader.c

在Windows上:

gcc -shared -o register_reader.dll -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" RegisterReader.c

5. 运行Java程序

确保编译生成的动态库位于Java的库路径中,然后运行Java程序:

java -Djava.library.path=. RegisterReader

结果

Java程序将调用本地C代码来读取寄存器的值,输出类似如下的结果:

Reading register at address: 0x1000
Register Value: 0

解释一下

  1. readRegister方法:在Java中调用时,会通过JNI调用C代码中的Java_RegisterReader_readRegister函数。
  2. 动态库加载System.loadLibrary("register_reader")加载名为register_reader的动态库,确保C函数可以被Java程序调用。

优点

  • 直接访问硬件:通过JNI可以直接访问寄存器或其他硬件设备,而不受JVM的限制。
  • 高性能:C/C++语言可以提供更高效的底层操作。

要注意的事

  • JNI涉及原生代码,因此需要注意平台兼容性和安全性问题。
  • 处理JNI时,通常需要了解设备的驱动接口和通信机制。

JSerialComm或RXTX等库

使用 JSerialComm 通过串口通信读取设备寄存器数据。
在一些嵌入式或工业设备中,使用串口(如RS232或RS485)进行数据通信是非常常见的。Java提供了多个库来实现串口通信,其中JSerialCommRXTX是两个常用的库。JSerialComm相对较新且维护良好,兼容性更好,因此我们以它为例介绍如何使用它进行串口通信。

实现步骤

  1. 添加 JSerialComm 依赖。
  2. 配置串口连接。
  3. 通过串口发送和接收数据。

1. 添加 JSerialComm 依赖

使用Maven管理项目时,可以在pom.xml中添加JSerialComm的依赖:

<dependency>
    <groupId>com.fazecast</groupId>
    <artifactId>jSerialComm</artifactId>
    <version>2.9.2</version> <!-- 根据需要选择版本 -->
</dependency>

或者,下载 jar 包并将其添加到项目的 classpath 中。

2. 示例代码:使用 JSerialComm 读取寄存器数据

import com.fazecast.jSerialComm.SerialPort;

public class SerialCommExample {
    public static void main(String[] args) {
        // 获取系统上的所有串口设备
        SerialPort[] ports = SerialPort.getCommPorts();
        System.out.println("可用串口设备列表:");
        for (int i = 0; i < ports.length; i++) {
            System.out.println(i + ": " + ports[i].getSystemPortName());
        }

        // 选择第一个串口设备并打开
        SerialPort serialPort = ports[0];
        serialPort.setBaudRate(9600); // 设置波特率
        serialPort.setNumDataBits(8); // 数据位
        serialPort.setNumStopBits(SerialPort.ONE_STOP_BIT); // 停止位
        serialPort.setParity(SerialPort.NO_PARITY); // 校验位

        if (serialPort.openPort()) {
            System.out.println("串口打开成功: " + serialPort.getSystemPortName());
        } else {
            System.out.println("无法打开串口");
            return;
        }

        // 等待串口设备准备好
        try {
            Thread.sleep(2000); // 等待2秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 发送命令给设备读取寄存器
        String command = "READ_REGISTER";  // 根据设备协议构建读取命令
        byte[] commandBytes = command.getBytes();
        serialPort.writeBytes(commandBytes, commandBytes.length);

        // 接收设备响应
        byte[] readBuffer = new byte[1024];
        int numRead = serialPort.readBytes(readBuffer, readBuffer.length);

        System.out.println("读取到的数据长度: " + numRead);
        System.out.println("数据内容:");
        for (int i = 0; i < numRead; i++) {
            System.out.print((char) readBuffer[i]);
        }

        // 关闭串口
        serialPort.closePort();
        System.out.println("\n串口关闭");
    }
}

解释一下代码

  1. 获取可用串口设备

    • SerialPort.getCommPorts() 获取系统上所有可用的串口设备,并打印其名称,方便选择要使用的端口。
  2. 串口配置

    • 设置串口的波特率数据位停止位校验位。这些参数必须根据你的设备文档设置。
    • 例如,波特率设置为9600,数据位为8,停止位为1,无校验位。
  3. 打开串口

    • serialPort.openPort() 打开串口,如果成功,程序会继续执行,否则输出错误并终止。
  4. 发送命令

    • serialPort.writeBytes(commandBytes, commandBytes.length) 向串口设备发送一个命令,这个命令通常由设备的通信协议决定。这里的命令 READ_REGISTER 是一个假设的示例,实际命令需要根据设备的手册来确定。
  5. 读取响应

    • serialPort.readBytes(readBuffer, readBuffer.length) 从串口设备接收响应数据。接收到的数据存储在 readBuffer 中,并逐字节打印出来。
  6. 关闭串口

    • 在完成操作后,使用 serialPort.closePort() 关闭串口设备。

运行时结果示例

可用串口设备列表:
0: COM3
串口打开成功: COM3
读取到的数据长度: 6
数据内容:
123456
串口关闭

要注意的事项有哪些

  1. 串口通信协议:串口设备之间的通信通常遵循某种协议,如Modbus RTU、自定义协议等。你需要根据设备手册实现特定的命令发送和数据解析。

  2. 波特率和其他参数设置:确保波特率、数据位、停止位和校验位的设置与设备匹配。错误的设置可能导致通信失败或数据乱码。

  3. 错误处理:串口通信可能会遇到各种错误,如通信超时、数据帧丢失等。需要根据具体情况进行错误处理。

RXTX 示例

RXTX是另一种用于串口通信的库,但由于维护不如JSerialComm积极,V哥建议使用JSerialComm。如果你还是要使用RXTX咋办?那 V 哥只能...上案例了,一个简单的串口通信示例:

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 portIdentifier = CommPortIdentifier.getPortIdentifier("COM3");
        SerialPort serialPort = (SerialPort) portIdentifier.open("SerialComm", 2000);

        // 设置串口参数
        serialPort.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);

        // 获取输入输出流
        InputStream inputStream = serialPort.getInputStream();
        OutputStream outputStream = serialPort.getOutputStream();

        // 发送命令
        String command = "READ_REGISTER";
        outputStream.write(command.getBytes());

        // 接收响应
        byte[] buffer = new byte[1024];
        int length = inputStream.read(buffer);
        System.out.println("读取到的数据长度: " + length);

        // 打印接收到的数据
        for (int i = 0; i < length; i++) {
            System.out.print((char) buffer[i]);
        }

        // 关闭串口
        serialPort.close();
    }
}

小结一下

通过 JSerialComm 库,咱们可以方便地在 Java 中实现串口通信。这个库简化了串口的配置和操作,且跨平台兼容性好,非常适合需要与硬件设备通过串口通信的项目。如果你在工业设备、嵌入式系统或物联网应用中需要使用串口通信,它是一个很好的选择。

总结一下三种方式的使用场景

以下是使用 JNIModbus协议串口通信库(JSerialComm或RXTX) 三种方式的场景总结:

1. JNI(Java Native Interface)

  • 场景

    • 当你需要直接访问硬件层或底层系统API时,使用JNI非常合适。
    • 适用于 Java 无法直接处理的硬件操作,例如与设备的寄存器、内存映射、驱动程序直接交互。
    • 适合需要极高性能或需要使用C/C++库与设备进行复杂通信的场景。
    • 如果设备的驱动程序只提供C/C++ API,你可以通过JNI将其集成到Java项目中。
  • 典型应用

    • 设备驱动程序的开发和使用。
    • 高性能、低延迟的硬件通信。
    • 操作系统特定的API调用,访问系统资源(例如寄存器、硬件接口)。
  • 优缺点

    • 优点:允许Java与本地系统代码通信;适合复杂的硬件控制。
    • 缺点:开发复杂,涉及C/C++代码,增加了跨平台复杂性。

2. Modbus协议

  • 场景

    • Modbus协议是用于工业设备之间通信的常见标准,适用于通过RS485/RS232串口以太网TCP与支持Modbus协议的设备进行通信。
    • 主要用于自动化控制系统,如PLC、传感器、变频器、HMI等工业设备的数据交换。
    • 适合需要通过标准工业协议与多个设备进行监控和数据采集的场景。
  • 典型应用

    • 工业自动化:读取设备状态、控制输出、获取传感器数据。
    • 物联网设备的监控和管理。
    • 远程控制和设备管理:如通过Modbus TCP读取远程设备数据。
  • 优缺点

    • 优点:标准化协议,兼容大量工业设备,简单易用。
    • 缺点:相对较慢的通信速率,适用于监控和控制而非实时复杂计算。

3. 串口通信库(JSerialComm或RXTX)

  • 场景

    • 当设备通过串口(RS232、RS485)进行通信时,可以使用串口通信库直接读取设备数据。
    • 适合与工业设备、嵌入式系统、传感器、仪表等需要基于串口进行通信的场景。
    • 如果设备使用的是自定义协议,且不支持标准的Modbus协议,可以通过这种方式实现与设备的通信。
    • 适合需要简单、直接的设备通信,尤其是在传统的嵌入式设备和工业场景中。
  • 典型应用

    • 通过串口与嵌入式设备通信,获取传感器数据。
    • 与PLC、工业控制系统、单片机等设备进行通信。
    • 物联网设备数据传输,尤其是需要通过串口传输的低速设备。
  • 优缺点

    • 优点:轻量、跨平台支持广泛、配置简单,适合与串口设备进行直接通信。
    • 缺点:仅适用于串口通信,缺乏复杂的协议支持。

总结对比:

  • JNI 适用于底层硬件访问高性能应用,如与操作系统或驱动程序直接交互。
  • Modbus协议工业标准协议,适用于需要通过串口或以太网与工业设备通信的场景。
  • JSerialComm/RXTX 适用于与串口设备通信,尤其是在嵌入式物联网设备中进行简单的设备交互。

选择哪种方式取决于设备的通信协议和项目的复杂性需求,如果是标准工业设备,Modbus协议 是首选。如果是自定义设备或嵌入式设备,使用 JSerialCommRXTX。如果需要高效底层硬件访问:JNI 可能是唯一选择。好了,今天的内容就到这里,欢迎关注威哥爱编程,点赞关注加收藏,让我们一起在 Java 路上越走越远。