大家好,我是V哥,程式設計師聊天真是三句不到離不開技術啊,這不前兩天跟一個哥們吃飯,他是我好多年前的學員了,一直保持著聯絡,現在都李總了,在做工業網際網路相關的專案,真是隻要 Java 學得好,能幹一輩子,卷死的是那些半吊子。
感謝李總給我分享了工業網際網路專案的事情,收穫很多,今天的內容來聊一聊 Java如何與底層硬體和工業裝置輕鬆通訊的事情。
Java讀取暫存器資料通常涉及與硬體裝置的通訊。這種操作通常是透過以下幾種方式來實現的:
使用 Modbus 協議讀取裝置暫存器資料(使用 jLibModbus
)
Modbus 是一種用於工業自動化裝置的通訊協議。常見的Modbus通訊方式包括:Modbus RTU(基於序列通訊)和Modbus TCP(基於網路通訊)。在此示例中,我們將使用 Java 和 jLibModbus
庫透過 Modbus TCP 協議讀取裝置的暫存器資料。
實現步驟
- 新增 jLibModbus 依賴。
- 設定 Modbus Master 客戶端。
- 透過 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();
}
}
}
來解釋一下程式碼哈
-
Modbus TCP 引數:
TcpParameters
用於配置 Modbus TCP 的主機和埠。預設的Modbus TCP埠是502
。- 使用
InetAddress.getByName("192.168.1.100")
設定裝置的IP地址。
-
Modbus Master 例項:
ModbusMaster master = ModbusMasterFactory.createModbusMasterTCP(tcpParameters);
建立一個Modbus主機(Master),用於與從裝置通訊。master.connect();
連線到Modbus裝置。
-
讀取保持暫存器:
int[] registerValues = master.readHoldingRegisters(slaveId, startAddress, quantity);
讀取從裝置的保持暫存器(Holding Registers),從startAddress
開始,讀取quantity
個暫存器。- 輸出暫存器值。
-
錯誤處理:
- 捕獲
ModbusProtocolException
、ModbusNumberException
和ModbusIOException
以處理通訊過程中可能出現的錯誤。
- 捕獲
-
斷開連線:
- 使用
master.disconnect();
在完成資料讀取後斷開與Modbus裝置的連線。
- 使用
3. 如何使用
- 裝置配置:確保你有一個支援 Modbus TCP 的裝置,並且裝置的IP地址與埠正確配置。通常情況下,Modbus TCP裝置監聽502埠。
- 執行程式:執行此 Java 程式,程式將連線到裝置並讀取指定暫存器的資料。
- 結果示例:
暫存器資料:
暫存器[0] = 1234
暫存器[1] = 5678
暫存器[2] = 910
...
使用時要注意的事項
- 裝置通訊引數:確保裝置支援Modbus TCP協議,並正確配置了IP地址、埠和從站地址(Slave ID)。
- 讀取不同型別的暫存器:在Modbus中,可以讀取不同型別的暫存器,比如輸入暫存器(Input Registers)或線圈(Coils)。根據需求呼叫對應的方法:
readInputRegisters(...)
:讀取輸入暫存器。readCoils(...)
:讀取線圈狀態。
- 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基本流程
- 在Java中宣告本地方法。
- 使用
javac
編譯Java類。 - 使用
javah
生成C/C++標頭檔案。 - 編寫C/C++程式碼實現Java方法。
- 編譯生成動態庫。
- 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
解釋一下
readRegister
方法:在Java中呼叫時,會透過JNI呼叫C程式碼中的Java_RegisterReader_readRegister
函式。- 動態庫載入:
System.loadLibrary("register_reader")
載入名為register_reader
的動態庫,確保C函式可以被Java程式呼叫。
優點
- 直接訪問硬體:透過JNI可以直接訪問暫存器或其他硬體裝置,而不受JVM的限制。
- 高效能:C/C++語言可以提供更高效的底層操作。
要注意的事
- JNI涉及原生程式碼,因此需要注意平臺相容性和安全性問題。
- 處理JNI時,通常需要了解裝置的驅動介面和通訊機制。
JSerialComm或RXTX等庫
使用 JSerialComm
透過串列埠通訊讀取裝置暫存器資料。
在一些嵌入式或工業裝置中,使用串列埠(如RS232或RS485)進行資料通訊是非常常見的。Java提供了多個庫來實現串列埠通訊,其中JSerialComm
和RXTX
是兩個常用的庫。JSerialComm
相對較新且維護良好,相容性更好,因此我們以它為例介紹如何使用它進行串列埠通訊。
實現步驟
- 新增
JSerialComm
依賴。 - 配置串列埠連線。
- 透過串列埠傳送和接收資料。
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串列埠關閉");
}
}
解釋一下程式碼
-
獲取可用串列埠裝置:
SerialPort.getCommPorts()
獲取系統上所有可用的串列埠裝置,並列印其名稱,方便選擇要使用的埠。
-
串列埠配置:
- 設定串列埠的波特率、資料位、停止位和校驗位。這些引數必須根據你的裝置文件設定。
- 例如,波特率設定為9600,資料位為8,停止位為1,無校驗位。
-
開啟串列埠:
serialPort.openPort()
開啟串列埠,如果成功,程式會繼續執行,否則輸出錯誤並終止。
-
傳送命令:
serialPort.writeBytes(commandBytes, commandBytes.length)
向串列埠裝置傳送一個命令,這個命令通常由裝置的通訊協議決定。這裡的命令READ_REGISTER
是一個假設的示例,實際命令需要根據裝置的手冊來確定。
-
讀取響應:
serialPort.readBytes(readBuffer, readBuffer.length)
從串列埠裝置接收響應資料。接收到的資料儲存在readBuffer
中,並逐位元組列印出來。
-
關閉串列埠:
- 在完成操作後,使用
serialPort.closePort()
關閉串列埠裝置。
- 在完成操作後,使用
執行時結果示例
可用串列埠裝置列表:
0: COM3
串列埠開啟成功: COM3
讀取到的資料長度: 6
資料內容:
123456
串列埠關閉
要注意的事項有哪些
-
串列埠通訊協議:串列埠裝置之間的通訊通常遵循某種協議,如Modbus RTU、自定義協議等。你需要根據裝置手冊實現特定的命令傳送和資料解析。
-
波特率和其他引數設定:確保波特率、資料位、停止位和校驗位的設定與裝置匹配。錯誤的設定可能導致通訊失敗或資料亂碼。
-
錯誤處理:串列埠通訊可能會遇到各種錯誤,如通訊超時、資料幀丟失等。需要根據具體情況進行錯誤處理。
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 中實現串列埠通訊。這個庫簡化了串列埠的配置和操作,且跨平臺相容性好,非常適合需要與硬體裝置透過串列埠通訊的專案。如果你在工業裝置、嵌入式系統或物聯網應用中需要使用串列埠通訊,它是一個很好的選擇。
總結一下三種方式的使用場景
以下是使用 JNI、Modbus協議 和 串列埠通訊庫(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協議 是首選。如果是自定義裝置或嵌入式裝置,使用 JSerialComm 或 RXTX。如果需要高效底層硬體訪問:JNI 可能是唯一選擇。好了,今天的內容就到這裡,歡迎關注威哥愛程式設計,點贊關注加收藏,讓我們一起在 Java 路上越走越遠。