剛果 (布) 姆古古魯水電站:4 台發電機組,每組 20 個溫度測點,共 80 點。所有溫度表並聯在單一 RS-485 總線上,透過位址 (0-19) 輪詢讀取。需求:高即時性、容錯 (斷電/故障不阻塞)、可擴充多串口。
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++ 工業控制通訊的經典範式。