首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

Java 串口通訊完整實現:RXTX/NRJavaSerial 驅動、Modbus 協議解析、CRC16 校驗與浮點數轉換實戰

     編輯:Java 綜合教學

Java 串口通訊完整實現指南

工業控制、儀表采集、POS 設備等場景仍大量依賴 RS232/RS485 串口通訊。本文補全原文代碼結構,系統講解 Java 串口開發全棧:驅動選型、通訊參數配置、異步事件監聽、Modbus RTU 幀構造、CRC16 校驗算法、IEEE754 浮點數解析、線程安全與異常恢復機制。

一、驅動選型與依賴

庫狀態特點推薦場景
RXTX停止維護標準 API、跨平台、需原生庫維護老項目
jSerialComm活躍維護無原生依賴、API 簡潔、自動掃描新項目首選
NRJavaSerial活躍RXTX 兼容、Maven Central 發布遷移 RXTX 項目
PureJavaComm活躍純 Java、支援藍牙、USB CDC嵌入式/無原生環境

Maven 依賴(jSerialComm 推薦)

<dependency>
    <groupId>com.fazecast</groupId>
    <artifactId>jSerialComm</artifactId>
    <version>2.10.4</version>
</dependency>

二、核心通訊類設計

配置參數封裝

public class SerialConfig {
    private String portName;           // COM1 / /dev/ttyUSB0
    private int baudRate = 9600;       // 2400/4800/9600/19200/38400/57600/115200
    private int dataBits = 8;          // 5/6/7/8
    private int stopBits = 1;          // 1/1.5/2
    private int parity = 0;            // 0=NONE, 1=ODD, 2=EVEN, 3=MARK, 4=SPACE
    private int timeoutMs = 2000;      // 讀超時
    // getter/setter + builder
}

通訊管理器(單例/連接池)

public class SerialManager {
    private final SerialConfig config;
    private SerialPort serialPort;
    private final Object lock = new Object();
    private final List listeners = new CopyOnWriteArrayList<>();
    
    public SerialManager(SerialConfig config) { this.config = config; }
    
    public boolean open() {
        synchronized (lock) {
            if (serialPort != null && serialPort.isOpen()) return true;
            
            serialPort = SerialPort.getCommPort(config.getPortName());
            serialPort.setBaudRate(config.getBaudRate());
            serialPort.setNumDataBits(config.getDataBits());
            serialPort.setNumStopBits(config.getStopBits());
            serialPort.setParity(config.getParity());
            serialPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 
                                          config.getTimeoutMs(), 0);
            
            if (!serialPort.openPort()) return false;
            
            // 註冊異步監聽
            serialPort.addDataListener(new SerialPortDataListener() {
                @Override
                public int getListeningEvents() { return SerialPort.LISTENING_EVENT_DATA_AVAILABLE; }
                @Override
                public void serialEvent(SerialPortEvent event) {
                    if (event.getEventType() == SerialPort.LISTENING_EVENT_DATA_AVAILABLE) {
                        byte[] buffer = new byte[serialPort.bytesAvailable()];
                        int numRead = serialPort.readBytes(buffer, buffer.length);
                        if (numRead > 0) {
                            byte[] data = Arrays.copyOf(buffer, numRead);
                            notifyListeners(data);
                        }
                    }
                }
            });
            return true;
        }
    }
    
    public void send(byte[] data) {
        synchronized (lock) {
            if (serialPort != null && serialPort.isOpen()) {
                serialPort.writeBytes(data, data.length);
            }
        }
    }
    
    public void addListener(SerialDataListener listener) { listeners.add(listener); }
    public void removeListener(SerialDataListener listener) { listeners.remove(listener); }
    
    private void notifyListeners(byte[] data) {
        for (SerialDataListener l : listeners) l.onDataReceived(data);
    }
    
    public void close() {
        synchronized (lock) {
            if (serialPort != null) {
                serialPort.removeDataListener();
                serialPort.closePort();
                serialPort = null;
            }
        }
    }
    
    @FunctionalInterface
    public interface SerialDataListener {
        void onDataReceived(byte[] data);
    }
}

三、Modbus RTU 幀構造與解析

幀格式

[從站地址:1B] [功能碼:1B] [數據域:N B] [CRC16:2B 低位在前]
例:01 03 00 00 00 02 C4 0B  (讀保持寄存器)

幀構建器

public class ModbusRtuFrame {
    public static byte[] buildRequest(int slaveId, int functionCode, int startAddr, int quantity) {
        ByteBuffer buf = ByteBuffer.allocate(6).order(ByteOrder.BIG_ENDIAN);
        buf.put((byte) slaveId);
        buf.put((byte) functionCode);
        buf.putShort((short) startAddr);
        buf.putShort((short) quantity);
        byte[] pdu = buf.array();
        return appendCrc16(pdu);
    }
    
    public static byte[] buildWriteSingleRegister(int slaveId, int addr, int value) {
        ByteBuffer buf = ByteBuffer.allocate(6).order(ByteOrder.BIG_ENDIAN);
        buf.put((byte) slaveId);
        buf.put((byte) 0x06);  // 功能碼 06
        buf.putShort((short) addr);
        buf.putShort((short) value);
        return appendCrc16(buf.array());
    }
    
    private static byte[] appendCrc16(byte[] data) {
        int crc = CRC16.compute(data);
        byte[] result = Arrays.copyOf(data, data.length + 2);
        result[data.length] = (byte) (crc & 0xFF);        // 低位
        result[data.length + 1] = (byte) ((crc >> 8) & 0xFF); // 高位
        return result;
    }
    
    public static boolean validateCrc(byte[] frame) {
        if (frame.length < 3) return false;
        byte[] data = Arrays.copyOf(frame, frame.length - 2);
        int expected = CRC16.compute(data);
        int actual = (frame[frame.length - 1] << 8) | (frame[frame.length - 2] & 0xFF);
        return expected == actual;
    }
}

四、CRC16 校驗算法(Modbus 標準)

public class CRC16 {
    private static final int POLYNOMIAL = 0xA001;  // x^16 + x^15 + x^2 + 1
    
    // 查表法(高性能)
    private static final int[] TABLE = new int[256];
    static {
        for (int i = 0; i < 256; i++) {
            int crc = i;
            for (int j = 0; j < 8; j++) {
                crc = (crc & 1) != 0 ? (crc >>> 1) ^ POLYNOMIAL : (crc >>> 1);
            }
            TABLE[i] = crc;
        }
    }
    
    public static int compute(byte[] data) {
        int crc = 0xFFFF;
        for (byte b : data) {
            crc = (crc >>> 8) ^ TABLE[(crc ^ b) & 0xFF];
        }
        return crc;
    }
    
    // 逐位法(教學用)
    public static int computeBitwise(byte[] data) {
        int crc = 0xFFFF;
        for (byte b : data) {
            crc ^= b & 0xFF;
            for (int i = 0; i < 8; i++) {
                if ((crc & 1) != 0) crc = (crc >>> 1) ^ POLYNOMIAL;
                else crc >>>= 1;
            }
        }
        return crc;
    }
}

五、IEEE754 浮點數解析(Modbus 32-bit Float)

Modbus 寄存器為 16 位,浮點數佔兩個寄存器(32 位),常見兩種字節序:

  • Big-Endian (ABCD):高字節在前,高寄存器在前
  • Little-Endian (CDAB):低字節在前,低寄存器在前(常見)
public class ModbusFloat {
    // CDAB 格式:低寄存器在前
    public static float parseCDAB(int lowReg, int highReg) {
        int bits = (highReg << 16) | (lowReg & 0xFFFF);
        return Float.intBitsToFloat(bits);
    }
    
    // ABCD 格式:高寄存器在前
    public static float parseABCD(int highReg, int lowReg) {
        int bits = (highReg << 16) | (lowReg & 0xFFFF);
        return Float.intBitsToFloat(bits);
    }
    
    // 字節序交換(BADC/DCBA 等變體)
    public static float parseSwapped(byte[] fourBytes, ByteOrder order) {
        return ByteBuffer.wrap(fourBytes).order(order).getFloat();
    }
    
    // 反向轉換
    public static int[] toRegistersCDAB(float value) {
        int bits = Float.floatToIntBits(value);
        return new int[] { bits & 0xFFFF, (bits >>> 16) & 0xFFFF };
    }
}

六、完整請求-響應協程範例

public class ModbusClient {
    private final SerialManager serial;
    private final int slaveId;
    private final CountDownLatch latch = new CountDownLatch(1);
    private byte[] response;
    private volatile boolean timeout = false;
    
    public ModbusClient(SerialManager serial, int slaveId) {
        this.serial = serial;
        this.slaveId = slaveId;
        serial.addListener(this::onData);
    }
    
    public float readFloatRegister(int address) throws Exception {
        byte[] request = ModbusRtuFrame.buildRequest(slaveId, 0x03, address, 2);
        response = null;
        timeout = false;
        
        serial.send(request);
        
        if (!latch.await(2, TimeUnit.SECONDS)) {
            timeout = true;
            throw new SocketTimeoutException("響應超時");
        }
        if (response == null || response.length < 5) {
            throw new IOException("響應數據無效");
        }
        if (!ModbusRtuFrame.validateCrc(response)) {
            throw new IOException("CRC 校驗失敗");
        }
        // 響應格式:[地址][功能碼][字節數][數據...][CRC]
        int byteCount = response[2] & 0xFF;
        if (byteCount != 4) throw new IOException("數據長度異常");
        
        int lowReg = ((response[3] & 0xFF) << 8) | (response[4] & 0xFF);
        int highReg = ((response[5] & 0xFF) << 8) | (response[6] & 0xFF);
        return ModbusFloat.parseCDAB(lowReg, highReg);
    }
    
    private void onData(byte[] data) {
        if (timeout) return;
        this.response = data;
        latch.countDown();
    }
}

七、線程安全與異常恢復策略

  1. 發送鎖: synchronized 保證幀完整發送,避免交錯
  2. 響應匹配: 生產環境需引入序列號/請求 ID,支持併發多請求
  3. 自動重連: 監聽 SerialPortEvent.PORT_CLOSED 觸發指數退避重連
  4. 心跳檢測: 定時發送 0x03 讀保持寄存器,連續 N 次失敗判定斷線
  5. 緩衝區管理: 環形緩衝區累積數據,按幀長度/超時切分

八、測試與調試工具

  • 虛擬串口: com0com (Windows)、socat (Linux) 建立虛擬回環對
  • Modbus 模擬器: Modbus Slave / Modbus Poll / diagslave
  • 抓包分析: 串口監聽器 (PortMon)、Saleae Logic Analyzer 硬件抓包
  • 單元測試: Mock SerialPort 接口,驗證幀構造、CRC、浮點解析邏輯
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved