工業控制常需透過 RS-232/485 進行主從式通訊。本文提供一個 PortData 類別,封裝 SerialPort,支援事件式非同步接收、同步指令回覆 (SendCommand)、錯誤處理與緩衝區管理,適合 Moxa 等工業級裝置開發。
using System;
using System.IO.Ports;
using System.Threading;
public class PortData
{
// 事件定義
public event EventHandler Received;
public event EventHandler Error;
public SerialPort Port { get; private set; }
public bool ReceiveEventFlag = false; // true=暫停事件接收
public PortData(string portName, int baudRate, Parity parity, SerialInterface.SerialMode mode)
{
Port = new SerialPort(portName, baudRate, parity, 8, StopBits.One);
Port.RtsEnable = true;
Port.ReadTimeout = 3000;
Port.DataReceived += OnDataReceived;
Port.ErrorReceived += OnErrorReceived;
}
~PortData() => Close();
public void Open() { if (!Port.IsOpen) Port.Open(); }
public void Close() { if (Port.IsOpen) Port.Close(); }
// 發送資料
public void SendData(byte[] data) => SendData(data, 0, data.Length);
public void SendData(byte[] data, int offset, int count)
{
if (Port.IsOpen) Port.Write(data, offset, count);
}
// 同步指令:發送 -> 等待回覆 (含逾時)
public int SendCommand(byte[] sendData, ref byte[] receiveData, int overtimeMs)
{
if (!Port.IsOpen) return -1;
ReceiveEventFlag = true; // 暫停事件,避免干擾同步讀取
Port.DiscardInBuffer(); // 清空接收緩衝區
Port.Write(sendData, 0, sendData.Length);
int elapsed = 0;
while (elapsed < overtimeMs)
{
if (Port.BytesToRead >= receiveData.Length) break;
Thread.Sleep(1);
elapsed++;
}
int ret = 0;
if (Port.BytesToRead >= receiveData.Length)
ret = Port.Read(receiveData, 0, receiveData.Length);
ReceiveEventFlag = false; // 恢復事件
return ret;
}
private void OnErrorReceived(object sender, SerialErrorReceivedEventArgs e)
=> Error?.Invoke(sender, e);
private void OnDataReceived(object sender, SerialDataReceivedEventArgs e)
{
if (ReceiveEventFlag) return; // 同步模式下忽略事件
byte[] data = new byte[Port.BytesToRead];
Port.Read(data, 0, data.Length);
Received?.Invoke(sender, new PortDataReciveEventArgs(data));
}
public bool IsOpen() => Port.IsOpen;
}
// 事件參數
public class PortDataReciveEventArgs : EventArgs
{
public byte[] Data { get; set; }
public PortDataReciveEventArgs() { Data = null; }
public PortDataReciveEventArgs(byte[] data) { Data = data; }
}
BytesToRead/Read 導致資料撕裂。while (elapsed < overtimeMs) + Thread.Sleep(1) 簡單實作阻塞等待,精度約 1ms。"COMx:",COM10 以上舊版建議 "\\\\.\\COMx:",Moxa 裝置需 "$device\\COM{num}\\0" 格式。
// 初始化
PortData port = new PortData("COM1", 9600, Parity.None, SerialInterface.SerialMode.RS232);
port.Received += (s, e) => { /* 非同步處理 e.Data */ };
port.Error += (s, e) => { /* 錯誤處理 */ };
port.Open();
// 發送指令並等待回覆 (例如 Modbus RTU)
byte[] cmd = { 0x01, 0x03, 0x00, 0x00, 0x00, 0x02, 0xC4, 0x0B }; // 讀保持暫存器
byte[] resp = new byte[9]; // 預期 9 位元組回覆
int len = port.SendCommand(cmd, ref resp, 500); // 500ms 逾時
if (len > 0) { /* 驗證 CRC、解析資料 */ }
// 關閉
port.Close();
SerialPort.BaseStream.ReadAsync 實作真正非同步 SendCommandAsync。PortData 專注於傳輸層。PortManager 統一管理多個 PortData 實例,提供依裝置 ID 取得通道功能。PortData 類別展示了工業序列埠通訊的核心模式:事件式非同步監聽 + 同步指令回覆雙軌並行。ReceiveEventFlag 切換機制簡單有效,解決了同步/非同步並存的資料完整性問題。配合正確的 COM 埠命名與逾時處理,可直接應用於 Moxa、Advantech 等工業級硬體整合專案。