在工業控制與嵌入式系統開發中,串口通訊是最基礎也最關鍵的環節之一。工控機(通常基於 Windows 平台)須與單晶片透過 RS-232 介面交換資料,因此掌握 Windows 下的串口程式設計,是每位嵌入式工程師的必修課。本文將以一個完整的多線程串口通訊專案為例,詳細解析 Win32 API 如何實現串口的開啟、配置、讀寫,並示範如何利用多線程機制同時處理多個通訊埠,最後介紹一個成熟的 CSerialPort 類別封裝,助你快速構建健壯的串口應用。
Win32 平台將通訊埠視為一種特殊的檔案裝置,操作流程與一般檔案 I/O 高度一致,核心步驟只有三個:
CreateFile 取得裝置控制代碼。GetCommState/SetCommState 設定鮑率、資料位、停止位、奇偶校驗等,對應 DCB 結構體。ReadFile/WriteFile 進行資料傳輸,輔以事件驅動機制(SetCommMask、WaitCommEvent)實現非阻塞通訊。HANDLE CreateFile(
PCTSTR lpFileName, // 例如 "COM1"
DWORD dwDesiredAccess, // GENERIC_READ | GENERIC_WRITE
DWORD dwShareMode, // 串口獨佔,必須為 0
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition, // OPEN_EXISTING
DWORD dwFlagsAndAttributes, // FILE_FLAG_OVERLAPPED 可啟用非同步 I/O
HANDLE hTemplateFile
);
成功時返回有效 Handle,失敗返回 INVALID_HANDLE_VALUE。
DCB 結構包含所有串口參數:
typedef struct _DCB {
DWORD DCBlength; // sizeof(DCB)
DWORD BaudRate; // 鮑率,如 9600, 19200
DWORD fBinary:1; // 二進位模式
DWORD fParity:1; // 啟用奇偶校驗
BYTE ByteSize; // 資料位 4-8
BYTE Parity; // 0-4: 無、奇、偶、標記、空間
BYTE StopBits; // 0,1,2 = 1, 1.5, 2 停止位
// ... 流控制、特殊字元等欄位
} DCB;
常用流程:GetCommState 取得預設值 → 修改 BaudRate 等欄位 → SetCommState 套用。
為避免輪詢浪費 CPU,Win32 提供事件通知機制:
SetCommMask(hComm, EV_RXCHAR | EV_TXEMPTY):啟用「收到字元」與「發送緩衝區空」事件。WaitCommEvent(hComm, &dwEvtMask, &ov):阻塞等待事件發生,配合 OVERLAPPED 結構可實現非同步等待。dwEvtMask 判斷類型,呼叫 ReadFile 或 WriteFile 處理資料。專案 MultiThreadCom 示範同時操作 COM1 與 COM2,介面簡潔,核心邏輯如下:
BOOL CMultiThreadComApp::InitInstance() {
// 開啟 COM1
hComm1 = CreateFile("COM1", GENERIC_READ|GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, 0, NULL);
if (hComm1 == INVALID_HANDLE_VALUE) { AfxMessageBox("開啟 COM1 失敗"); return FALSE; }
DCB dcb; GetCommState(hComm1, &dcb);
dcb.BaudRate = 9600; SetCommState(hComm1, &dcb);
PurgeComm(hComm1, PURGE_TXCLEAR);
// 開啟 COM2(同樣流程)
hComm2 = CreateFile("COM2", GENERIC_READ|GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, 0, NULL);
// ... 設定 9600 鮑率、清空緩衝區
return FALSE; // 對話式應用程式回傳 FALSE
}
BOOL CMultiThreadComDlg::OnInitDialog() {
// ... 圖示、選單初始化 ...
// 執行緒 1 處理 COM1
DWORD tid1;
hCommThread1 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Com1ThreadProcess,
m_hWnd, 0, &tid1);
// 執行緒 2 處理 COM2
DWORD tid2;
hCommThread2 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Com2ThreadProcess,
m_hWnd, 0, &tid2);
return TRUE;
}
DWORD WINAPI Com1ThreadProcess(HWND hWnd) {
DWORD dwEvent;
SetCommMask(hComm1, EV_RXCHAR | EV_TXEMPTY);
while (TRUE) {
if (!WaitCommEvent(hComm1, &dwEvent, NULL)) continue;
if (dwEvent == 0) { CloseHandle(hCommThread1); ExitThread(0); }
switch (dwEvent) {
case EV_TXEMPTY: // 發送緩衝區空,繼續發送下一個位元組
if (com1Data.wTxPos < com1Data.wTxLen) {
DWORD cnt;
WriteFile(hComm1, &com1Data.TxBuf[com1Data.wTxPos], 1, &cnt, NULL);
com1Data.wTxPos++;
}
break;
case EV_RXCHAR: // 收到字元
if (com1Data.wRxPos < com1Data.wRxLen) {
DWORD cnt;
ReadFile(hComm1, &com1Data.RxBuf[com1Data.wRxPos], 1, &cnt, NULL);
com1Data.wRxPos++;
if (com1Data.wRxPos == com1Data.wRxLen)
PostMessage(hWnd, COM_SENDCHAR, 0, 1); // 通知主視窗
}
break;
}
}
return 0;
}
Com2ThreadProcess 邏輯完全對稱,僅換用 hComm2 與 com2Data。
typedef struct tagSerialPort {
BYTE RxBuf[SPRX_BUFLEN]; WORD wRxPos, wRxLen;
BYTE TxBuf[SPTX_BUFLEN]; WORD wTxPos, wTxLen;
} SerialPort, *LPSerialPort;
手工管理 Handle、事件、執行緒、同步物件極易出錯。CSerialPort(由 Remon Spekreijse 開發)將上述細節封裝,提供清晰的介面:
FILE_FLAG_OVERLAPPED,搭配 OVERLAPPED 結構與事件陣列 m_hEventArray[3](關閉、接收、發送)實現高效等待。CRITICAL_SECTION 保護共用資源,m_bThreadAlive 標記執行緒狀態。WM_COMM_RXCHAR、WM_COMM_TXEMPTY_DETECTED 等自訂訊息,透過 PostMessage 通知擁有者視窗,完美融入 MFC 訊息迴路。InitPort 一站式設定埠號、鮑率、校驗、資料位、停止位、事件遮罩、緩衝區大小。CSerialPort::CSerialPort() {
m_hComm = NULL; m_ov.hEvent = m_hWriteEvent = m_hShutdownEvent = NULL;
m_szWriteBuffer = NULL; m_bThreadAlive = FALSE;
}
CSerialPort::~CSerialPort() {
do { SetEvent(m_hShutdownEvent); } while (m_bThreadAlive); // 等待執行緒結束
delete[] m_szWriteBuffer;
}
m_ov.hEvent)、發送(m_hWriteEvent)。CreateFile(FILE_FLAG_OVERLAPPED)、SetCommTimeouts、SetCommMask、GetCommState/BuildCommDCB/SetCommState 完成硬體配置。PurgeComm 清空收發緩衝區,離開臨界區,啟動監控執行緒(StartMonitoring)。CommThread執行緒主迴圈呼叫 WaitForMultipleObjects 等待事件陣列:
m_hShutdownEvent:優先級最高,收到即退出執行緒。m_ov.hEvent:接收事件(EV_RXCHAR 等),呼叫 ReceiveChar 讀取資料並 PostMessage 通知上層。m_hWriteEvent:發送緩衝區可寫,呼叫 WriteChar 從 m_szWriteBuffer 繼續發送。CSerialPort myPort;
myPort.InitPort(this, 1, 19200, 'N', 8, 1, EV_RXCHAR | EV_CTS, 512);
myPort.StartMonitoring();
// 發送資料
char cmd[] = "AT\r\n";
myPort.WriteToPort(cmd);
// 在視窗類別中處理 WM_COMM_RXCHAR 訊息接收資料
FILE_FLAG_OVERLAPPED + 事件驅動,避免 UI 凍結。CSerialPort 這類成熟封裝。GetLastError 取得詳細錯誤碼,記錄日誌便於診斷。CSerialPort。掌握 Win32 串口核心 API,並善用 CSerialPort 這類經實戰驗證的類別,能讓你專注於業務邏輯,快速交付穩定可靠的工業通訊軟體。