首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> VC 教學

Win32 多線程串口通訊實戰:從基礎 API 到完整工程範例

     編輯:VC 教學

前言

在工業控制與嵌入式系統開發中,串口通訊是最基礎也最關鍵的環節之一。工控機(通常基於 Windows 平台)須與單晶片透過 RS-232 介面交換資料,因此掌握 Windows 下的串口程式設計,是每位嵌入式工程師的必修課。本文將以一個完整的多線程串口通訊專案為例,詳細解析 Win32 API 如何實現串口的開啟、配置、讀寫,並示範如何利用多線程機制同時處理多個通訊埠,最後介紹一個成熟的 CSerialPort 類別封裝,助你快速構建健壯的串口應用。

一、Win32 串口操作三部曲

Win32 平台將通訊埠視為一種特殊的檔案裝置,操作流程與一般檔案 I/O 高度一致,核心步驟只有三個:

  1. 開啟通訊埠:呼叫 CreateFile 取得裝置控制代碼。
  2. 初始化參數:透過 GetCommState/SetCommState 設定鮑率、資料位、停止位、奇偶校驗等,對應 DCB 結構體。
  3. 讀寫資料:使用 ReadFile/WriteFile 進行資料傳輸,輔以事件驅動機制(SetCommMask、WaitCommEvent)實現非阻塞通訊。

1.1 開啟通訊埠

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。

1.2 設定裝置控制塊 (DCB)

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 套用。

1.3 事件驅動與非同步讀寫

為避免輪詢浪費 CPU,Win32 提供事件通知機制:

  • SetCommMask(hComm, EV_RXCHAR | EV_TXEMPTY):啟用「收到字元」與「發送緩衝區空」事件。
  • WaitCommEvent(hComm, &dwEvtMask, &ov):阻塞等待事件發生,配合 OVERLAPPED 結構可實現非同步等待。
  • 事件觸發後,依 dwEvtMask 判斷類型,呼叫 ReadFile 或 WriteFile 處理資料。

二、工程實例:雙埠多線程通訊程式

專案 MultiThreadCom 示範同時操作 COM1 與 COM2,介面簡潔,核心邏輯如下:

2.1 應用程式初始化:開啟並配置兩個串口

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
}

2.2 對話方塊啟動兩個工作者執行緒

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;
}

2.3 執行緒處理函數:事件迴圈與收發邏輯

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。

2.4 串口資料結構

typedef struct tagSerialPort {
    BYTE RxBuf[SPRX_BUFLEN]; WORD wRxPos, wRxLen;
    BYTE TxBuf[SPTX_BUFLEN]; WORD wTxPos, wTxLen;
} SerialPort, *LPSerialPort;

三、封裝優雅的 CSerialPort 類別

手工管理 Handle、事件、執行緒、同步物件極易出錯。CSerialPort(由 Remon Spekreijse 開發)將上述細節封裝,提供清晰的介面:

3.1 類別定義亮點

  • 非同步 I/O:建立時指定 FILE_FLAG_OVERLAPPED,搭配 OVERLAPPED 結構與事件陣列 m_hEventArray[3](關閉、接收、發送)實現高效等待。
  • 執行緒安全:使用 CRITICAL_SECTION 保護共用資源,m_bThreadAlive 標記執行緒狀態。
  • 訊息通知:定義 WM_COMM_RXCHAR、WM_COMM_TXEMPTY_DETECTED 等自訂訊息,透過 PostMessage 通知擁有者視窗,完美融入 MFC 訊息迴路。
  • 靈活配置:InitPort 一站式設定埠號、鮑率、校驗、資料位、停止位、事件遮罩、緩衝區大小。

3.2 核心實現剖析

建構子/解構子

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;
}

InitPort:完整的初始化流程

  1. 若舊執行緒存活,發送關閉事件並等待結束。
  2. 建立三個手動重設事件:關閉、接收(m_ov.hEvent)、發送(m_hWriteEvent)。
  3. 初始化 Critical Section,配置寫入緩衝區。
  4. 進入臨界區,呼叫 CreateFile(FILE_FLAG_OVERLAPPED)、SetCommTimeouts、SetCommMask、GetCommState/BuildCommDCB/SetCommState 完成硬體配置。
  5. PurgeComm 清空收發緩衝區,離開臨界區,啟動監控執行緒(StartMonitoring)。

監控執行緒 CommThread

執行緒主迴圈呼叫 WaitForMultipleObjects 等待事件陣列:

  • m_hShutdownEvent:優先級最高,收到即退出執行緒。
  • m_ov.hEvent:接收事件(EV_RXCHAR 等),呼叫 ReceiveChar 讀取資料並 PostMessage 通知上層。
  • m_hWriteEvent:發送緩衝區可寫,呼叫 WriteChar 從 m_szWriteBuffer 繼續發送。

3.3 使用範例

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 凍結。
  • 資源管理:Handle、事件、Critical Section、執行緒務必成對建立/釋放,善用 RAII 或 CSerialPort 這類成熟封裝。
  • 錯誤處理:每個 API 呼叫後檢查回傳值,GetLastError 取得詳細錯誤碼,記錄日誌便於診斷。
  • 緩衝區設計:應用層維護收發環狀緩衝區,與驅動層緩衝區解耦,應對突發流量。
  • 跨平台考量:若需支援 Linux/嵌入式,可抽象介面層,Windows 端實作委託 CSerialPort。

掌握 Win32 串口核心 API,並善用 CSerialPort 這類經實戰驗證的類別,能讓你專注於業務邏輯,快速交付穩定可靠的工業通訊軟體。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved