在前幾篇文章中,我們已見識過 WIN32 API 的底層複雜性與 MSComm ActiveX 控制項的便利性。雖然 MSComm 以事件驅動方式屏蔽了多執行緒細節,實現了非同步存取,但其資料傳輸仍需在 VARIANT 型別與字串間轉換,使用上仍有不便。為追求更簡潔高效的開發體驗,許多優秀開發者自行封裝了串口類別。本文將介紹由程式設計師 llbird 開發的 cnComm 類別——一個極具代表性的輕量級串口封裝庫。
llbird 的代碼風格簡潔緊湊,類別宣告與實現全部定義在單一標頭檔 cnComm.h 中,使用者只需引入此檔案即可。以下是核心架構亮點:
cnComm(bool fAutoBeginThread = true, DWORD dwIOMode = FILE_FLAG_OVERLAPPED)
: _dwIOMode(dwIOMode), _fAutoBeginThread(fAutoBeginThread) { Init(); }
virtual ~cnComm() { Close(); UnInit(); }
支援同步/非同步 (Overlapped I/O) 兩種模式,預設啟用監控執行緒。
IsOpen()、operator bool()GetHandle()、operator HANDLE()GetState()/SetState(),支援字串格式 "9600, 8, n, 1"GetTimeouts()/SetTimeouts()、SetBufferSize()SetWnd(HWND)、SetNotifyNum(DWORD)BeginThread()/SuspendThread()/ResumeThread()/EndThread()// 開啟串口 (預設 9600,8,n,1)
bool Open(DWORD dwPort);
bool Open(DWORD dwPort, DWORD dwBaudRate);
bool Open(DWORD dwPort, char *szSetStr); // 如 "9600, 8, n, 1"
// 讀取資料
DWORD Read(LPVOID Buffer, DWORD dwBufferLength, DWORD dwWaitTime = 10);
char *ReadString(char *szBuffer, DWORD dwBufferLength, DWORD dwWaitTime = 20);
// 寫入資料
DWORD Write(LPVOID Buffer, DWORD dwBufferLength);
DWORD Write(const char *szBuffer); // ANSI 字串
DWORD Write(char *szBuffer, DWORD dwBufferLength, char *szFormat, ...); // 格式化
DWORD Write(char *szBuffer, char *szFormat, ...); // 格式化 (不檢查長度)
// 同步版本 (適用於特定場景)
DWORD ReadSync(LPVOID Buffer, DWORD dwBufferLength);
DWORD WriteSync(LPVOID Buffer, DWORD dwBufferLength);
提供 SetDTR()、SetRTS()、SetBreak() 等模擬信號控制。核心創新在於內建監控執行緒,透過 WaitCommEvent 等待事件,並將 EV_RXCHAR、EV_TXEMPTY、EV_CTS 等事件轉發為自訂視窗訊息 (ON_COM_RECEIVE 等),實現了類似 MSComm 的事件驅動模型,但介面更原生、效能更高。
ThreadFunc()virtual DWORD ThreadFunc() {
if (!::SetCommMask(_hCommHandle, _dwMaskEvent)) { /* 錯誤處理 */ return 1; }
COMSTAT Stat; DWORD dwError;
for (DWORD dwLength, dwMask = 0; _fRunFlag && IsOpen(); dwMask = 0) {
if (!::WaitCommEvent(_hCommHandle, &dwMask, &_WaitOverlapped)) {
if (::GetLastError() == ERROR_IO_PENDING)
::GetOverlappedResult(_hCommHandle, &_WaitOverlapped, &dwLength, TRUE);
else continue;
}
if (dwMask == 0) continue;
switch (dwMask) {
case EV_RXCHAR:
::ClearCommError(_hCommHandle, &dwError, &Stat);
if (Stat.cbInQue >= _dwNotifyNum) OnReceive();
break;
case EV_TXEMPTY: OnTXEmpty(); break;
case EV_CTS: OnCTS(); break;
case EV_DSR: OnDSR(); break;
case EV_RING: OnRing(); break;
case EV_RLSD: OnRLSD(); break;
case EV_BREAK: OnBreak(); break;
case EV_ERR: OnError(); break;
}
}
return 0;
}
功能與界面同前文 (WIN32 API/MSComm 版本) 一致,僅替換串口實作層。界面截圖:

#define ON_COM_RECEIVE WM_USER + 618 (對應 cnComm 內部定義)cnComm com;、afx_msg void OnCommRecv(WPARAM, LPARAM);ON_MESSAGE(ON_COM_RECEIVE, OnCommRecv)BOOL CSerialPortClassDlg::OnInitDialog() {
// ... 標準初始化 ...
com.Open(1); // 開啟 COM1,預設參數
com.SetWnd(AfxGetMainWnd()->m_hWnd); // 設定通知視窗
return TRUE;
}
com.Write(m_send); (一行搞定)void CSerialPortClassDlg::OnCommRecv(WPARAM wParam, LPARAM lParam) {
UpdateData(TRUE);
char str[100];
com.ReadString(str, 100); // 讀取字串
m_recv += str; // 追加到顯示區
UpdateData(FALSE);
}
使用 cnComm 類別後,串口初始化、讀寫操作幾乎都只需 1~2 行代碼。這正是我們專門用一期連載介紹第三方類別的原因——優秀的封裝能讓開發者專注於業務邏輯,而非纏繞於底層細節。筆者在網路程式設計中同樣傾向使用第三方網路庫而非 MFC 的 CSocket,因為它們提供了更簡潔明快的介面,這也是值得主流框架借鑑的地方。