.NET SerialPort 類雖方便,但在高併發、特殊波特率、精確超時控制、Windows CE/嵌入式平台上仍有局限。本文提供基於 kernel32.dll 的完整封裝:CreateFile、DCB 結構體配置、COMMTIMEOUTS、ReadFile/WriteFile 異步重疊 I/O、十六進制工具類,適合工業級穩定通訊。
using System;
using System.Runtime.InteropServices;
namespace JustinIO
{
public class CommPort
{
// 公開配置屬性
public string PortName; // "COM1"
public int BaudRate = 9600;
public byte ByteSize = 8;
public byte Parity = 0; // 0=None,1=Odd,2=Even,3=Mark,4=Space
public byte StopBits = 0; // 0=1, 1=1.5, 2=2
public int ReadTimeout = 1000;
private int _hComm = -1;
public bool IsOpen => _hComm != -1;
// Win32 常量
private const uint GENERIC_READ = 0x80000000;
private const uint GENERIC_WRITE = 0x40000000;
private const int OPEN_EXISTING = 3;
private const int INVALID_HANDLE_VALUE = -1;
// DCB 結構體(按順序佈局,對應 C 結構體)
[StructLayout(LayoutKind.Sequential)]
public struct DCB
{
public int DCBlength;
public int BaudRate;
public int fBinary; public int fParity; public int fOutxCtsFlow; public int fOutxDsrFlow;
public int fDtrControl; public int fDsrSensitivity; public int fTXContinueOnXoff;
public int fOutX; public int fInX; public int fErrorChar; public int fNull;
public int fRtsControl; public int fAbortOnError; public int fDummy2;
public uint flags;
public ushort wReserved;
public ushort XonLim; public ushort XoffLim;
public byte ByteSize; public byte Parity; public byte StopBits;
public char XonChar; public char XoffChar; public char ErrorChar;
public char EofChar; public char EvtChar;
public ushort wReserved1;
}
[StructLayout(LayoutKind.Sequential)]
private struct COMMTIMEOUTS
{
public int ReadIntervalTimeout;
public int ReadTotalTimeoutMultiplier;
public int ReadTotalTimeoutConstant;
public int WriteTotalTimeoutMultiplier;
public int WriteTotalTimeoutConstant;
}
[StructLayout(LayoutKind.Sequential)]
private struct OVERLAPPED
{
public int Internal; public int InternalHigh; public int Offset; public int OffsetHigh; public int hEvent;
}
// API 導入
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern int CreateFile(string lpFileName, uint dwDesiredAccess, int dwShareMode,
int lpSecurityAttributes, int dwCreationDisposition, int dwFlagsAndAttributes, int hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetCommState(int hFile, ref DCB lpDCB);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool BuildCommDCB(string lpDef, ref DCB lpDCB);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetCommState(int hFile, ref DCB lpDCB);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetCommTimeouts(int hFile, ref COMMTIMEOUTS lpCommTimeouts);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetCommTimeouts(int hFile, ref COMMTIMEOUTS lpCommTimeouts);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool ReadFile(int hFile, byte[] lpBuffer, int nNumberOfBytesToRead,
ref int lpNumberOfBytesRead, ref OVERLAPPED lpOverlapped);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool WriteFile(int hFile, byte[] lpBuffer, int nNumberOfBytesToWrite,
ref int lpNumberOfBytesWritten, ref OVERLAPPED lpOverlapped);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(int hObject);
[DllImport("kernel32.dll")]
private static extern uint GetLastError();
}
}
public void Open()
{
var dcb = new DCB { DCBlength = Marshal.SizeOf() };
var timeouts = new COMMTIMEOUTS();
// 1. 打開設備句柄
_hComm = CreateFile(PortName, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, 0);
if (_hComm == INVALID_HANDLE_VALUE)
throw new ApplicationException($"無法打開串口 {PortName},錯誤碼: {GetLastError()}");
// 2. 設置超時
GetCommTimeouts(_hComm, ref timeouts);
timeouts.ReadTotalTimeoutConstant = ReadTimeout;
timeouts.ReadTotalTimeoutMultiplier = 0;
timeouts.WriteTotalTimeoutMultiplier = 0;
timeouts.WriteTotalTimeoutConstant = 0;
if (!SetCommTimeouts(_hComm, ref timeouts))
throw new ApplicationException("設置超時失敗");
// 3. 獲取當前 DCB 並修改
if (!GetCommState(_hComm, ref dcb))
throw new ApplicationException("獲取 DCB 失敗");
dcb.BaudRate = BaudRate;
dcb.flags = 1; // fBinary = 1
if (Parity > 0) dcb.flags |= 2; // fParity = 1
dcb.Parity = Parity;
dcb.ByteSize = ByteSize;
dcb.StopBits = StopBits;
if (!SetCommState(_hComm, ref dcb))
throw new ApplicationException($"設置 DCB 失敗,錯誤碼: {GetLastError()}");
// 可選:驗證設置
// var dcb2 = new DCB(); GetCommState(_hComm, ref dcb2);
}
public void Close()
{
if (_hComm != INVALID_HANDLE_VALUE)
{
CloseHandle(_hComm);
_hComm = INVALID_HANDLE_VALUE;
}
}
public byte[] Read(int count)
{
if (!IsOpen) throw new InvalidOperationException("串口未打開");
var buffer = new byte[count];
var overlapped = new OVERLAPPED();
int bytesRead = 0;
if (!ReadFile(_hComm, buffer, count, ref bytesRead, ref overlapped))
throw new ApplicationException($"讀取失敗,錯誤碼: {GetLastError()}");
if (bytesRead < count) Array.Resize(ref buffer, bytesRead);
return buffer;
}
public void Write(byte[] data)
{
if (!IsOpen) throw new InvalidOperationException("串口未打開");
var overlapped = new OVERLAPPED();
int written = 0;
if (!WriteFile(_hComm, data, data.Length, ref written, ref overlapped))
throw new ApplicationException($"寫入失敗,錯誤碼: {GetLastError()}");
if (written != data.Length) throw new ApplicationException("寫入字節數不足");
}
public static class HexConverter
{
public static string ToHexString(byte[] bytes, string separator = " ")
{
if (bytes == null) return "";
var sb = new StringBuilder(bytes.Length * 3);
foreach (byte b in bytes) sb.AppendFormat("{0:X2}{1}", b, separator);
return sb.ToString().TrimEnd();
}
public static byte[] FromHexString(string hex)
{
if (string.IsNullOrWhiteSpace(hex)) return Array.Empty();
var parts = hex.Split(new[] { ' ', '-', ':', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
var bytes = new byte[parts.Length];
for (int i = 0; i < parts.Length; i++)
bytes[i] = Convert.ToByte(parts[i], 16);
return bytes;
}
}
var port = new CommPort
{
PortName = "COM3",
BaudRate = 115200,
ByteSize = 8,
Parity = 0,
StopBits = 0,
ReadTimeout = 2000
};
try
{
port.Open();
// 發送指令:01 03 00 00 00 02 CRC
byte[] cmd = HexConverter.FromHexString("01 03 00 00 00 02 C4 0B");
port.Write(cmd);
// 讀取回應(假設 9 字節)
byte[] resp = port.Read(9);
Console.WriteLine("收到: " + HexConverter.ToHexString(resp));
}
catch (Exception ex)
{
Console.WriteLine("錯誤: " + ex.Message);
}
finally
{
port.Close();
}
將 CreateFile 旗標加上 FILE_FLAG_OVERLAPPED (0x40000000),ReadFile/WriteFile 傳入事件句柄的 OVERLAPPED,配合 GetOverlappedResult/WaitForSingleObject 實現真正非阻塞。代碼較長,此處略過,關鍵點:
OVERLAPPED.hEvent = CreateEvent(...)GetLastError()==ERROR_IO_PENDING (997) 為正常WaitForSingleObject(event, timeout) 等待完成GetOverlappedResult 獲取實際傳輸字節數Win32 串口類繞過 .NET SerialPort 封裝層,直接控制 DCB、COMMTIMEOUTS、重疊 I/O,適用於:非標準波特率、嚴格超時控制、Windows CE/嵌入式、高可靠工業協議。缺點是代碼量大、需自行管理非托管資源、跨平台困難。現代 .NET Core 3.0+ System.IO.Ports.SerialPort 已大幅改進,一般場景優先使用托管類,特殊場景再落地本類。