USB、藍牙、Wi-Fi、LoRa、5G 層出不窮,但 **RS-232/RS-485/RS-422 串行通訊** 依然統治著:PLC/變頻器/儀表/條碼掃描器/醫療設備/智能電表/工業機器人/舊系統改造。Java 標準庫無串口 API,**javax.comm** (Sun 1998, 早已停維) 、**RXTX** (開源移植, 2010 後停更) 、**jSerialComm** (Fazecast, 純 Java + JNA, 現役主力)、**PureJavaComm** (純 Java, 輕量) 成為四大選擇。本文以 **jSerialComm** 為主,系統講解串口基礎、參數配置、讀寫模式、协议解析、多埠口管理。
| 腳位 | 縮寫 | 全稱 | 方向 (DTE 視角: PC) | 功能 |
|---|---|---|---|---|
| 1 | DCD | Data Carrier Detect | 輸入 | 數據載波檢測 (Modem 就緒) |
| 2 | RXD | Receive Data | 輸入 | **接收數據** (關鍵) |
| 3 | TXD | Transmit Data | 輸出 | **發送數據** (關鍵) |
| 4 | DTR | Data Terminal Ready | 輸出 | 數據終端就緒 |
| 5 | GND | Signal Ground | — | **信號地** (關鍵) |
| 6 | DSR | Data Set Ready | 輸入 | 數據設備就緒 |
| 7 | RTS | Request To Send | 輸出 | 請求發送 (硬體流控) |
| 8 | CTS | Clear To Send | 輸入 | 允許發送 (硬體流控) |
| 9 | RI | Ring Indicator | 輸入 | 振鈴指示 |
最小三線制:TXD ↔ RXD (交叉)、GND ↔ GND。**必須交叉連接** (PC TXD → 設備 RXD)。
9600-8-N-1 (9600 baud, 8 data, No parity, 1 stop)。| 類別/介面 | 核心方法 | 說明 |
|---|---|---|
SerialPort | getCommPorts() (靜態列舉所有埠口)、getCommPort(String name)、openPort()/closePort()、setBaudRate(int)、setNumDataBits(int)、setNumStopBits(int)、setParity(int)、setFlowControl(int)、setComPortTimeouts(int mode, int readTimeout, int writeTimeout)、getInputStream()/getOutputStream()、addDataListener(SerialPortDataListener)、removeDataListener() | 埠口控制核心,非執行緒安全,單執行緒操作 |
SerialPortDataListener | int getListeningEvents() (返回 LISTENING_EVENT_DATA_RECEIVED / LISTENING_EVENT_DATA_WRITTEN)、void serialEvent(SerialPortEvent event) | **非阻塞事件驅動模式** 推薦介面,避免輪詢執行緒 |
SerialPortEvent | getReceivedData() (byte[])、getEventType()、getSerialPort() | 事件載體 |
SerialPort port = SerialPort.getCommPort("COM3"); // Windows
// Linux: "/dev/ttyUSB0", "/dev/ttyS0"
// Mac: "/dev/tty.usbserial-XXXX"
port.setBaudRate(9600);
port.setNumDataBits(8);
port.setNumStopBits(1);
port.setParity(SerialPort.NO_PARITY);
port.setFlowControl(SerialPort.FLOW_CONTROL_DISABLED);
port.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING, 1000, 1000); // 阻塞模式
if (!port.openPort()) throw new IllegalStateException("Open port failed");
try (OutputStream out = port.getOutputStream();
InputStream in = port.getInputStream()) {
// 發送指令 (例: Modbus RTU 讀保持寄存器)
byte[] cmd = {0x01, 0x03, 0x00, 0x00, 0x00, 0x02, 0xC4, 0x0B}; // CRC16
out.write(cmd);
out.flush();
// 讀取回應 (阻塞直到滿足超時或資料到達)
byte[] buffer = new byte[256];
int len = in.read(buffer); // 可能不足預期長度,需協議層組裝
System.out.println("Received: " + bytesToHex(buffer, len));
} finally {
port.closePort();
}
缺點:阻塞執行緒、難處理變長幀、需自行管理讀取執行緒。
port.setComPortTimeouts(SerialPort.TIMEOUT_NONBLOCKING, 0, 0); // 非阻塞
// 或 TIMEOUT_READ_SEMI_BLOCKING: 有資料即返回,無資料等待 timeout
// 需自建讀取執行緒輪詢
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
byte[] buffer = new byte[1024];
while (port.isOpen()) {
int len = port.getInputStream().read(buffer);
if (len > 0) {
// 送入協議解碼器 (如 ByteToMessageDecoder)
protocolDecoder.decode(buffer, len);
} else {
Thread.sleep(10); // 避免空轉佔 CPU
}
}
});
port.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 100, 0); // 配合事件模式
port.addDataListener(new SerialPortDataListener() {
@Override
public int getListeningEvents() {
return SerialPort.LISTENING_EVENT_DATA_RECEIVED;
}
@Override
public void serialEvent(SerialPortEvent event) {
if (event.getEventType() != SerialPort.LISTENING_EVENT_DATA_RECEIVED) return;
byte[] newData = event.getReceivedData();
// 直接送入協議解碼器 (需執行緒安全)
protocolDecoder.decode(newData);
}
});
// 發送數據 (非阻塞)
port.getOutputStream().write(requestFrame);
port.getOutputStream().flush();
優勢:無輪詢執行緒、資料到達即回調、延遲最低、CPU 佔用極低。**生產環境首選。**
串口是**位元組流**,無訊息邊界。應用層必須實現**幀同步**:
\n/\r\n)、特定字元 (Modbus RTU 靜默間隔 3.5 字元時間)。0x7E (HDLC)、0x68 (DL/T 645)。推薦架構:ByteBuf (Netty) / RingBuffer 累積位元組 → **狀態機解碼器** 解析完整幀 → 交付業務層。參考 Netty ByteToMessageDecoder 設計模式。
public class SerialPortManager {
private final Map ports = new ConcurrentHashMap<>();
private final ProtocolDecoder decoder;
public SerialPortManager(ProtocolDecoder decoder) { this.decoder = decoder; }
public synchronized SerialPort openPort(String portName, SerialPortConfig config) {
SerialPort port = SerialPort.getCommPort(portName);
config.applyTo(port); // 封裝參數設定
port.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 100, 0);
port.addDataListener(new SerialPortDataListener() {
@Override public int getListeningEvents() { return SerialPort.LISTENING_EVENT_DATA_RECEIVED; }
@Override public void serialEvent(SerialPortEvent event) {
if (event.getEventType() == SerialPort.LISTENING_EVENT_DATA_RECEIVED) {
decoder.decode(event.getReceivedData(), portName); // 攜帶埠口標識
}
}
});
if (!port.openPort()) throw new IllegalStateException("Failed to open " + portName);
ports.put(portName, port);
return port;
}
public void send(String portName, byte[] data) {
SerialPort port = ports.get(portName);
if (port != null && port.isOpen()) {
try { port.getOutputStream().write(data); port.getOutputStream().flush(); }
catch (IOException e) { handleError(portName, e); }
}
}
public void closeAll() { ports.values().forEach(SerialPort::closePort); ports.clear(); }
}
closePort() 必須在 `finally` / `try-with-resources` / 關閉鉤子中調用,否則埠口被佔用需重啟機器/拔插設備。Java 串口開發,**選 jSerialComm、用事件驅動模式、建立協議解碼器、封裝埠口管理器、外部化配置、嚴格資源釋放**。掌握 RS-232/485 物理層知識、波特率/校驗/流控參數、幀同步機制、Modbus RTU/DL-T645/自定義協議解析,即可從容應對工控、醫療、電力、物聯網邊緣端的串口整合挑戰。