Java與硬件通信:Modbus、JNI和串口庫實戰(zhàn)應用

2024-11-29 11:33 更新

大家好,我是V哥,程序員聊天真是三句不到離不開技術啊,這不前兩天跟一個哥們吃飯,他是我好多年前的學員了,一直保持著聯(lián)系,現(xiàn)在都李總了,在做工業(yè)互聯(lián)網(wǎng)相關的項目,真是只要 Java 學得好,能干一輩子,卷死的是那些半吊子。

感謝李總給我分享了工業(yè)互聯(lián)網(wǎng)項目的事情,收獲很多,今天的內(nèi)容來聊一聊 Java如何與底層硬件和工業(yè)設備輕松通信的事情。

Java讀取寄存器數(shù)據(jù)通常涉及與硬件設備的通信。這種操作通常是通過以下幾種方式來實現(xiàn)的:

使用 Modbus 協(xié)議讀取設備寄存器數(shù)據(jù)(使用 jLibModbus

Modbus 是一種用于工業(yè)自動化設備的通信協(xié)議。常見的Modbus通信方式包括:Modbus RTU(基于串行通信)和Modbus TCP(基于網(wǎng)絡通信)。在此示例中,我們將使用 Java 和 jLibModbus 庫通過 Modbus TCP 協(xié)議讀取設備的寄存器數(shù)據(jù)。

實現(xiàn)步驟

  1. 添加 jLibModbus 依賴。
  2. 設置 Modbus Master 客戶端。
  3. 通過 Modbus Master 讀取設備的寄存器數(shù)據(jù)。

1. 添加 jLibModbus 依賴

使用 Maven 管理項目時,可以在 pom.xml 中添加 jLibModbus 依賴:

<dependency>
    <groupId>com.intelligt.modbus</groupId>
    <artifactId>jlibmodbus</artifactId>
    <version>1.2.8.1</version> <!-- 根據(jù)具體需求設置版本 -->
</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 參數(shù)
            TcpParameters tcpParameters = new TcpParameters();
            InetAddress address = InetAddress.getByName("192.168.1.100"); // 設備的IP地址
            tcpParameters.setHost(address);
            tcpParameters.setPort(Modbus.TCP_PORT); // Modbus 默認TCP端口 502


            // 創(chuàng)建 ModbusMaster 實例
            ModbusMaster master = ModbusMasterFactory.createModbusMasterTCP(tcpParameters);
            master.connect();


            // 設備的 Slave ID(通常是從站地址)
            int slaveId = 1;
            // 讀取保持寄存器(Holding Registers),從地址 0 開始,讀取 10 個寄存器
            int startAddress = 0;
            int quantity = 10;


            try {
                // 讀取保持寄存器數(shù)據(jù)
                int[] registerValues = master.readHoldingRegisters(slaveId, startAddress, quantity);
                System.out.println("寄存器數(shù)據(jù):");
                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 參數(shù)
    • TcpParameters 用于配置 Modbus TCP 的主機和端口。默認的Modbus TCP端口是 502。
    • 使用 InetAddress.getByName("192.168.1.100") 設置設備的IP地址。

  1. Modbus Master 實例
    • ModbusMaster master = ModbusMasterFactory.createModbusMasterTCP(tcpParameters); 創(chuàng)建一個Modbus主機(Master),用于與從設備通信。
    • master.connect(); 連接到Modbus設備。

  1. 讀取保持寄存器
    • int[] registerValues = master.readHoldingRegisters(slaveId, startAddress, quantity); 讀取從設備的保持寄存器(Holding Registers),從 startAddress 開始,讀取 quantity 個寄存器。
    • 輸出寄存器值。

  1. 錯誤處理
    • 捕獲 ModbusProtocolException、ModbusNumberExceptionModbusIOException 以處理通信過程中可能出現(xiàn)的錯誤。

  1. 斷開連接
    • 使用 master.disconnect(); 在完成數(shù)據(jù)讀取后斷開與Modbus設備的連接。

3. 如何使用

  1. 設備配置:確保你有一個支持 Modbus TCP 的設備,并且設備的IP地址與端口正確配置。通常情況下,Modbus TCP設備監(jiān)聽502端口。
  2. 運行程序:運行此 Java 程序,程序?qū)⑦B接到設備并讀取指定寄存器的數(shù)據(jù)。
  3. 結(jié)果示例
    寄存器數(shù)據(jù):
    寄存器[0] = 1234
    寄存器[1] = 5678
    寄存器[2] = 910
    ...

使用時要注意的事項

  1. 設備通信參數(shù):確保設備支持Modbus TCP協(xié)議,并正確配置了IP地址、端口和從站地址(Slave ID)。
  2. 讀取不同類型的寄存器:在Modbus中,可以讀取不同類型的寄存器,比如輸入寄存器(Input Registers)或線圈(Coils)。根據(jù)需求調(diào)用對應的方法:
    • readInputRegisters(...):讀取輸入寄存器。
    • readCoils(...):讀取線圈狀態(tài)。
  3. Modbus地址偏移:一些Modbus設備使用1-based地址系統(tǒng),而程序中可能使用0-based地址,注意這點以防讀取錯誤地址。

小結(jié)一下

我們通過 jLibModbus 庫使用 Java 讀取支持 Modbus TCP 協(xié)議的設備的寄存器數(shù)據(jù)。Modbus是工業(yè)控制領域中廣泛應用的通信協(xié)議,利用Java實現(xiàn)設備通信可以用于各種自動化系統(tǒng)中。如果你的設備使用Modbus RTU協(xié)議,可以通過配置串口通信來實現(xiàn)類似的操作。

JNI(Java Native Interface)

Java Native Interface (JNI) 允許Java代碼與C/C++等本地語言編寫的代碼交互,可以用于實現(xiàn)高性能、直接的硬件訪問,如寄存器讀取。

JNI基本流程

  1. 在Java中聲明本地方法。
  2. 使用javac編譯Java類。
  3. 使用javah生成C/C++頭文件。
  4. 編寫C/C++代碼實現(xiàn)Java方法。
  5. 編譯生成動態(tài)庫。
  6. Java代碼加載動態(tài)庫并調(diào)用本地方法。

來看案例:使用JNI讀取寄存器數(shù)據(jù)

1. Java代碼

首先,定義一個Java類,該類聲明一個本地方法用于讀取寄存器數(shù)據(jù)。

public class RegisterReader {
    // 聲明本地方法,該方法將在C/C++代碼中實現(xiàn)
    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++中需要實現(xiàn)的方法聲明。

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代碼實現(xiàn)

接下來,在C語言中實現(xiàn)readRegister方法。在這里,我們假設寄存器通過內(nèi)存映射的方式訪問。

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


// 模擬寄存器的內(nèi)存映射地址
#define REGISTER_BASE_ADDRESS 0x1000


JNIEXPORT jint JNICALL Java_RegisterReader_readRegister(JNIEnv *env, jobject obj, jint address) {
    // 模擬讀取寄存器,實際實現(xiàn)應訪問真實硬件哈
    int registerValue = (address - REGISTER_BASE_ADDRESS) * 2;  // 偽代碼,用來模擬一下
    printf("Reading register at address: 0x%x\n", address);
    return registerValue;
}

4. 編譯生成動態(tài)庫

在Linux或macOS上,編譯C代碼并生成動態(tài)庫:

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程序

確保編譯生成的動態(tài)庫位于Java的庫路徑中,然后運行Java程序:

java -Djava.library.path=. RegisterReader

結(jié)果

Java程序?qū)⒄{(diào)用本地C代碼來讀取寄存器的值,輸出類似如下的結(jié)果:

Reading register at address: 0x1000
Register Value: 0

解釋一下

  1. readRegister方法:在Java中調(diào)用時,會通過JNI調(diào)用C代碼中的Java_RegisterReader_readRegister函數(shù)。
  2. 動態(tài)庫加載System.loadLibrary("register_reader")加載名為register_reader的動態(tài)庫,確保C函數(shù)可以被Java程序調(diào)用。

優(yōu)點

  • 直接訪問硬件:通過JNI可以直接訪問寄存器或其他硬件設備,而不受JVM的限制。
  • 高性能:C/C++語言可以提供更高效的底層操作。

要注意的事

  • JNI涉及原生代碼,因此需要注意平臺兼容性和安全性問題。
  • 處理JNI時,通常需要了解設備的驅(qū)動接口和通信機制。

JSerialComm或RXTX等庫

使用 JSerialComm 通過串口通信讀取設備寄存器數(shù)據(jù)。 在一些嵌入式或工業(yè)設備中,使用串口(如RS232或RS485)進行數(shù)據(jù)通信是非常常見的。Java提供了多個庫來實現(xiàn)串口通信,其中JSerialCommRXTX是兩個常用的庫。JSerialComm相對較新且維護良好,兼容性更好,因此我們以它為例介紹如何使用它進行串口通信。

實現(xiàn)步驟

  1. 添加 JSerialComm 依賴。
  2. 配置串口連接。
  3. 通過串口發(fā)送和接收數(shù)據(jù)。

1. 添加 JSerialComm 依賴

使用Maven管理項目時,可以在pom.xml中添加JSerialComm的依賴:

<dependency>
    <groupId>com.fazecast</groupId>
    <artifactId>jSerialComm</artifactId>
    <version>2.9.2</version> <!-- 根據(jù)需要選擇版本 -->
</dependency>

或者,下載 jar 包并將其添加到項目的 classpath 中。

2. 示例代碼:使用 JSerialComm 讀取寄存器數(shù)據(jù)

import com.fazecast.jSerialComm.SerialPort;


public class SerialCommExample {
    public static void main(String[] args) {
        // 獲取系統(tǒng)上的所有串口設備
        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); // 數(shù)據(jù)位
        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();
        }


        // 發(fā)送命令給設備讀取寄存器
        String command = "READ_REGISTER";  // 根據(jù)設備協(xié)議構(gòu)建讀取命令
        byte[] commandBytes = command.getBytes();
        serialPort.writeBytes(commandBytes, commandBytes.length);


        // 接收設備響應
        byte[] readBuffer = new byte[1024];
        int numRead = serialPort.readBytes(readBuffer, readBuffer.length);


        System.out.println("讀取到的數(shù)據(jù)長度: " + numRead);
        System.out.println("數(shù)據(jù)內(nèi)容:");
        for (int i = 0; i < numRead; i++) {
            System.out.print((char) readBuffer[i]);
        }


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

解釋一下代碼

  1. 獲取可用串口設備

    • SerialPort.getCommPorts() 獲取系統(tǒng)上所有可用的串口設備,并打印其名稱,方便選擇要使用的端口。

  2. 串口配置
    • 設置串口的波特率、數(shù)據(jù)位、停止位校驗位。這些參數(shù)必須根據(jù)你的設備文檔設置。
    • 例如,波特率設置為9600,數(shù)據(jù)位為8,停止位為1,無校驗位。

  1. 打開串口
    • serialPort.openPort() 打開串口,如果成功,程序會繼續(xù)執(zhí)行,否則輸出錯誤并終止。

  1. 發(fā)送命令
    • serialPort.writeBytes(commandBytes, commandBytes.length) 向串口設備發(fā)送一個命令,這個命令通常由設備的通信協(xié)議決定。這里的命令 READ_REGISTER 是一個假設的示例,實際命令需要根據(jù)設備的手冊來確定。

  1. 讀取響應
    • serialPort.readBytes(readBuffer, readBuffer.length) 從串口設備接收響應數(shù)據(jù)。接收到的數(shù)據(jù)存儲在 readBuffer 中,并逐字節(jié)打印出來。

  1. 關閉串口
    • 在完成操作后,使用 serialPort.closePort() 關閉串口設備。

運行時結(jié)果示例

可用串口設備列表:
0: COM3
串口打開成功: COM3
讀取到的數(shù)據(jù)長度: 6
數(shù)據(jù)內(nèi)容:
123456
串口關閉

要注意的事項有哪些

  1. 串口通信協(xié)議:串口設備之間的通信通常遵循某種協(xié)議,如Modbus RTU、自定義協(xié)議等。你需要根據(jù)設備手冊實現(xiàn)特定的命令發(fā)送和數(shù)據(jù)解析。

  2. 波特率和其他參數(shù)設置:確保波特率、數(shù)據(jù)位、停止位和校驗位的設置與設備匹配。錯誤的設置可能導致通信失敗或數(shù)據(jù)亂碼。

  1. 錯誤處理:串口通信可能會遇到各種錯誤,如通信超時、數(shù)據(jù)幀丟失等。需要根據(jù)具體情況進行錯誤處理。

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);


        // 設置串口參數(shù)
        serialPort.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);


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


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


        // 接收響應
        byte[] buffer = new byte[1024];
        int length = inputStream.read(buffer);
        System.out.println("讀取到的數(shù)據(jù)長度: " + length);


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


        // 關閉串口
        serialPort.close();
    }
}

小結(jié)一下

通過 JSerialComm 庫,咱們可以方便地在 Java 中實現(xiàn)串口通信。這個庫簡化了串口的配置和操作,且跨平臺兼容性好,非常適合需要與硬件設備通過串口通信的項目。如果你在工業(yè)設備、嵌入式系統(tǒng)或物聯(lián)網(wǎng)應用中需要使用串口通信,它是一個很好的選擇。

總結(jié)一下三種方式的使用場景

以下是使用 JNI、Modbus協(xié)議串口通信庫(JSerialComm或RXTX) 三種方式的場景總結(jié):

1. JNI(Java Native Interface)

  • 場景
    • 當你需要直接訪問硬件層或底層系統(tǒng)API時,使用JNI非常合適。
    • 適用于 Java 無法直接處理的硬件操作,例如與設備的寄存器、內(nèi)存映射、驅(qū)動程序直接交互。
    • 適合需要極高性能或需要使用C/C++庫與設備進行復雜通信的場景。
    • 如果設備的驅(qū)動程序只提供C/C++ API,你可以通過JNI將其集成到Java項目中。

  • 典型應用
    • 設備驅(qū)動程序的開發(fā)和使用。
    • 高性能、低延遲的硬件通信。
    • 操作系統(tǒng)特定的API調(diào)用,訪問系統(tǒng)資源(例如寄存器、硬件接口)。

  • 優(yōu)缺點
    • 優(yōu)點:允許Java與本地系統(tǒng)代碼通信;適合復雜的硬件控制。
    • 缺點:開發(fā)復雜,涉及C/C++代碼,增加了跨平臺復雜性。

2. Modbus協(xié)議

  • 場景
    • Modbus協(xié)議是用于工業(yè)設備之間通信的常見標準,適用于通過RS485/RS232串口以太網(wǎng)TCP與支持Modbus協(xié)議的設備進行通信。
    • 主要用于自動化控制系統(tǒng),如PLC、傳感器、變頻器、HMI等工業(yè)設備的數(shù)據(jù)交換。
    • 適合需要通過標準工業(yè)協(xié)議與多個設備進行監(jiān)控和數(shù)據(jù)采集的場景。

  • 典型應用
    • 工業(yè)自動化:讀取設備狀態(tài)、控制輸出、獲取傳感器數(shù)據(jù)。
    • 物聯(lián)網(wǎng)設備的監(jiān)控和管理。
    • 遠程控制和設備管理:如通過Modbus TCP讀取遠程設備數(shù)據(jù)。

  • 優(yōu)缺點
    • 優(yōu)點:標準化協(xié)議,兼容大量工業(yè)設備,簡單易用。
    • 缺點:相對較慢的通信速率,適用于監(jiān)控和控制而非實時復雜計算。

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

  • 場景
    • 當設備通過串口(RS232、RS485)進行通信時,可以使用串口通信庫直接讀取設備數(shù)據(jù)。
    • 適合與工業(yè)設備、嵌入式系統(tǒng)、傳感器、儀表等需要基于串口進行通信的場景。
    • 如果設備使用的是自定義協(xié)議,且不支持標準的Modbus協(xié)議,可以通過這種方式實現(xiàn)與設備的通信。
    • 適合需要簡單、直接的設備通信,尤其是在傳統(tǒng)的嵌入式設備和工業(yè)場景中。

  • 典型應用
    • 通過串口與嵌入式設備通信,獲取傳感器數(shù)據(jù)。
    • 與PLC、工業(yè)控制系統(tǒng)、單片機等設備進行通信。
    • 物聯(lián)網(wǎng)設備數(shù)據(jù)傳輸,尤其是需要通過串口傳輸?shù)牡退僭O備。

  • 優(yōu)缺點
    • 優(yōu)點:輕量、跨平臺支持廣泛、配置簡單,適合與串口設備進行直接通信。
    • 缺點:僅適用于串口通信,缺乏復雜的協(xié)議支持。

總結(jié)對比:

  • JNI 適用于底層硬件訪問高性能應用,如與操作系統(tǒng)或驅(qū)動程序直接交互。
  • Modbus協(xié)議工業(yè)標準協(xié)議,適用于需要通過串口或以太網(wǎng)與工業(yè)設備通信的場景。
  • JSerialComm/RXTX 適用于與串口設備通信,尤其是在嵌入式物聯(lián)網(wǎng)設備中進行簡單的設備交互。

選擇哪種方式取決于設備的通信協(xié)議和項目的復雜性需求,如果是標準工業(yè)設備,Modbus協(xié)議 是首選。如果是自定義設備或嵌入式設備,使用 JSerialCommRXTX。如果需要高效底層硬件訪問:JNI 可能是唯一選擇。好了,今天的內(nèi)容就到這里,歡迎關注威哥愛編程,點贊關注加收藏,讓我們一起在 Java 路上越走越遠。Java與硬件通信:Modbus、JNI和串口庫實戰(zhàn)應用Java與硬件通信:Modbus、JNI和串口庫實戰(zhàn)應用

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號