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

VC++ 多串口多線程工業控制實戰:以水電站溫度監測為例

     編輯:VC 教學

專案背景

剛果 (布) 姆古古魯水電站:4 台發電機組,每組 20 個溫度測點,共 80 點。所有溫度表並聯在單一 RS-485 總線上,透過位址 (0-19) 輪詢讀取。需求:高即時性、容錯 (斷電/故障不阻塞)、可擴充多串口。

架構設計

  • 每串口一執行緒:CSerialPort 物件 + 監聽執行緒 + 超時控制執行緒
  • 協議解析:BYTE 陣列收發、位址輪詢、封包解析
  • 容錯機制:超時跳過、連續失敗計數、自動復位

核心類別改造:CSerialPort

// 修改收發為 BYTE 陣列
void CSerialPort::WriteToPort(BYTE* pBuffer, int nLen) {
    ASSERT(m_hComm);
    m_nWriteBufferSize = nLen;
    memcpy(m_bWriteBuffer, pBuffer, nLen);
    SetEvent(m_hWriteEvent); // 觸發寫入執行緒
}

void CSerialPort::ReadFromPort(BYTE* pBuffer, int& nLen) {
    // 從接收佇列取出資料...
}

視圖類關鍵成員

class CMyView : public CView {
    CSerialPort m_Port;           // 串口物件
    HANDLE m_hMonitorThread;      // 監聽執行緒
    HANDLE m_hTimeoutThread;      // 超時控制執行緒
    BYTE m_RecvBuf[1024];         // 接收緩衝
    int m_nRecvLen;               // 已收位元組數
    int m_nCurrentAddr;           // 當前輪詢位址 0-19
    float m_fTemp[20];            // 解析後溫度值
    CTime m_LastRecvTime[20];     // 各位址最後收包時間
    // ...
};

初始化與啟動

BOOL CMyView::InitPort() {
    m_nCurrentAddr = 0;
    m_nRecvLen = 0;
    memset(m_fTemp, 0, sizeof(m_fTemp));

    if (!m_Port.InitPort(this, 1, 4800, 'N', 8, 1, EV_RXCHAR | EV_RXFLAG, 1024))
        return FALSE;

    m_Port.StartMonitoring();        // 內部啟動接收執行緒
    SendReadCommand(0);              // 發送位址 0 讀取命令

    // 啟動外部超時控制執行緒
    UINT nThreadID;
    m_hTimeoutThread = (HANDLE)_beginthreadex(NULL, 0, TimeoutThread, this, CREATE_SUSPENDED, &nThreadID);
    ResumeThread(m_hTimeoutThread);
    return TRUE;
}

輪詢發送邏輯

void CMyView::SendReadCommand(int addr) {
    BYTE cmd[8] = {0x01, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00}; // Modbus RTU 範例
    cmd[0] = addr + 1; // 從站位址 1-20
    // 計算 CRC 填入 cmd[6], cmd[7]...
    m_Port.WriteToPort(cmd, 8);
    m_LastRecvTime[addr] = CTime::GetCurrentTime(); // 記錄發送時間
}

接收處理 (監聽執行緒回呼)

void CMyView::OnDataReceived(BYTE* pData, int nLen) {
    // 追加到接收緩衝
    memcpy(m_RecvBuf + m_nRecvLen, pData, nLen);
    m_nRecvLen += nLen;

    // 簡易幀檢測:最小長度、CRC 驗證...
    if (m_nRecvLen >= MIN_FRAME_LEN && CheckFrameComplete(m_RecvBuf, m_nRecvLen)) {
        int addr = m_RecvBuf[0] - 1; // 從站位址轉 0-based
        if (addr >= 0 && addr < 20) {
            ParseTemperatureFrame(m_RecvBuf, m_nRecvLen, m_fTemp[addr]);
            m_LastRecvTime[addr] = CTime::GetCurrentTime(); // 更新活躍時間
        }
        // 發送下一位址命令
        int nextAddr = (addr + 1) % 20;
        m_nCurrentAddr = nextAddr;
        SendReadCommand(nextAddr);
        // 清空緩衝
        m_nRecvLen = 0;
    }
}

超時控制執行緒 (核心容錯)

UINT __cdecl CMyView::TimeoutThread(LPVOID pParam) {
    CMyView* pView = (CMyView*)pParam;
    while (true) {
        Sleep(100); // 100ms 掃描間隔
        CTime now = CTime::GetCurrentTime();
        for (int i = 0; i < 20; ++i) {
            CTimeSpan span = now - pView->m_LastRecvTime[i];
            // 若超過 800ms 未收到回應,視為超時
            if (span.GetTotalSeconds() * 1000 + span.GetMilliseconds() > 800) {
                // 跳過該位址,發送下一個
                int next = (i + 1) % 20;
                pView->m_nCurrentAddr = next;
                pView->SendReadCommand(next);
                pView->m_LastRecvTime[i] = now; // 重置避免重複觸發
                // 可選:連續 N 次超時則標記該點故障、清零溫度值
            }
        }
    }
    return 0;
}

多串口擴充

將上述邏輯封裝為 CPortManager 類別,視圖類維護 CArray<CPortManager*>,每個管理器獨立擁有串口、執行緒、緩衝、狀態。主執行緒透過訊息或回呼接收解析後資料更新 UI。

關鍵經驗總結

挑戰解決方案
單串口多設備阻塞超時跳過機制 + 獨立超時執行緒
資料幀邊界不清協議定長/CRC/特徵碼 + 狀態機解析
執行緒安全存取共享資料CriticalSection 保護 m_fTemp、m_nRecvLen 等
UI 更新頻率過高導致卡頓資料變更才 PostMessage 通知 UI,或定時器批量刷新
串口斷開重連監聽執行緒偵測錯誤 → 關閉句柄 → 延遲重新 InitPort

此架構已在多個水電站、變電站 SCADA 系統穩定運行 10 年以上,是 VC++ 工業控制通訊的經典範式。

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