Win 32系統(tǒng)為串行通信提供了全新的服務(wù)。傳統(tǒng)的OpenComm、ReadComm、WriteComm、CloseComm等函數(shù)已經(jīng)過時,WM_COMMNOTIFY消息也消失了。取而代之的是文件I/O函數(shù)提供的打開和關(guān)閉通信資源句柄及讀寫操作的基本接口。
新的文件I/O函數(shù)(CreateFile、ReadFile、WriteFile等)支持重疊式輸入輸出,這使得線程可以從費時的I/O操作中解放出來,從而極大地提高了程序的運行效率。 12.3.1 串行口的打開和關(guān)閉 Win 32系統(tǒng)把文件的概念進行了擴展。無論是文件、通信設(shè)備、命名管道、郵件槽、磁盤、還是控制臺,都是用API函數(shù)CreateFile來打開或創(chuàng)建的。該函數(shù)的聲明為: HANDLE CreateFile( LPCTSTR lpFileName, // 文件名 DWORD dwDesiredAccess, // 訪問模式 DWORD dwShareMode, // 共享模式 LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 通常為NULL DWORD dwCreationDistribution, // 創(chuàng)建方式 DWORD dwFlagsAndAttributes, // 文件屬性和標志 HANDLE hTemplateFile // 臨時文件的句柄,通常為NULL
如果調(diào)用成功,那么該函數(shù)返回文件的句柄,如果調(diào)用失敗,則函數(shù)返回INVALID_HANDLE_VALUE。 如果想要用重疊I/O方式(參見12.3.3)打開COM2口,則一般應(yīng)象清單12.4那樣調(diào)用CreateFile函數(shù)。注意在打開一個通信端口時,應(yīng)該以獨占方式打開,另外要指定GENERIC_READ、GENERIC_WRITE、OPEN_EXISTING和FILE_ATTRIBUTE_NORMAL等屬性。如果要打開重疊I/O,則應(yīng)該指定 FILE_FLAG_OVERLAPPED屬性。
清單12.4 HANDLE hCom; DWORD dwError; hCom=CreateFile(“COM2”, // 文件名 GENERIC_READ | GENERIC_WRITE, // 允許讀和寫 0, // 獨占方式 NULL, OPEN_EXISTING, //打開而不是創(chuàng)建 FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, // 重疊方式 NULL ); if(hCom = = INVALID_HANDLE_VALUE) { dwError=GetLastError( ); . . . // 處理錯誤 } 當不再使用文件句柄時,應(yīng)該調(diào)用CloseHandle函數(shù)關(guān)閉之。 12.3.2 串行口的初始化 在打開通信設(shè)備句柄后,常常需要對串行口進行一些初始化工作。這需要通過一個DCB結(jié)構(gòu)來進行。DCB結(jié)構(gòu)包含了諸如波特率、每個字符的數(shù)據(jù)位數(shù)、奇偶校驗和停止位數(shù)等信息。在查詢或配置置串行口的屬性時,都要用DCB結(jié)構(gòu)來作為緩沖區(qū)。 調(diào)用GetCommState函數(shù)可以獲得串口的配置,該函數(shù)把當前配置填充到一個DCB結(jié)構(gòu)中。一般在用CreateFile打開串行口后,可以調(diào)用GetCommState函數(shù)來獲取串行口的初始配置。要修改串行口的配置,應(yīng)該先修改DCB結(jié)構(gòu),然后再調(diào)用SetCommState函數(shù)用指定的DCB結(jié)構(gòu)來設(shè)置串行口。 除了在DCB中的設(shè)置外,程序一般還需要設(shè)置I/O緩沖區(qū)的大小和超時。Windows用I/O緩沖區(qū)來暫存串行口輸入和輸出的數(shù)據(jù),如果通信的速率較高,則應(yīng)該設(shè)置較大的緩沖區(qū)。調(diào)用SetupComm函數(shù)可以設(shè)置串行口的輸入和輸出緩沖區(qū)的大小。 在用ReadFile和WriteFile讀寫串行口時,需要考慮超時問題。如果在指定的時間內(nèi)沒有讀出或?qū)懭胫付〝?shù)量的字符,那么ReadFile或WriteFile的操作就會結(jié)束。要查詢當前的超時設(shè)置應(yīng)調(diào)用GetCommTimeouts函數(shù),該函數(shù)會填充一個COMMTIMEOUTS結(jié)構(gòu)。調(diào)用SetCommTimeouts可以用某一個COMMTIMEOUTS結(jié)構(gòu)的內(nèi)容來設(shè)置超時。 有兩種超時:間隔超時和總超時。間隔超時是指在接收時兩個字符之間的最大時延,總超時是指讀寫操作總共花費的最大時間。寫操作只支持總超時,而讀操作兩種超時均支持。用COMMTIMEOUTS結(jié)構(gòu)可以規(guī)定讀/寫操作的超時,該結(jié)構(gòu)的定義為:
COMMTIMEOUTS結(jié)構(gòu)的成員都以毫秒為單位??偝瑫r的計算公式是: 總超時=時間系數(shù)×要求讀/寫的字符數(shù) + 時間常量 例如,如果要讀入10個字符,那么讀操作的總超時的計算公式為: 讀總超時=ReadTotalTimeoutMultiplier×10 + ReadTotalTimeoutConstant 可以看出,間隔超時和總超時的設(shè)置是不相關(guān)的,這可以方便通信程序靈活地設(shè)置各種超時。 如果所有寫超時參數(shù)均為0,那么就不使用寫超時。如果ReadIntervalTimeout為0,那么就不使用讀間隔超時,如果ReadTotalTimeoutMultiplier和ReadTotalTimeoutConstant都為0,則不使用讀總超時。如果讀間隔超時被設(shè)置成MAXDWORD并且兩個讀總超時為0,那么在讀一次輸入緩沖區(qū)中的內(nèi)容后讀操作就立即完成,而不管是否讀入了要求的字符。 在用重疊方式讀寫串行口時,雖然ReadFile和WriteFile在完成操作以前就可能返回,但超時仍然是起作用的。在這種情況下,超時規(guī)定的是操作的完成時間,而不是ReadFile和WriteFile的返回時間。 清單12.5列出了一段簡單的串行口初始化代碼。
清單12.5 打開并初始化串行口 HANDLE hCom; DWORD dwError; DCB dcb; COMMTIMEOUTS TimeOuts; hCom=CreateFile(“COM2”, // 文件名 GENERIC_READ | GENERIC_WRITE, // 允許讀和寫 0, // 獨占方式 NULL, OPEN_EXISTING, //打開而不是創(chuàng)建 FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, // 重疊方式 NULL ); if(hCom = = INVALID_HANDLE_VALUE) { dwError=GetLastError( ); . . . // 處理錯誤 }
SetupComm( hCom, 1024, 1024 ) //緩沖區(qū)的大小為1024
TimeOuts. ReadIntervalTimeout=1000; TimeOuts.ReadTotalTimeoutMultiplier=500; TimeOuts.ReadTotalTimeoutConstant=5000; TimeOuts.WriteTotalTimeoutMultiplier=500; TimeOuts.WriteTotalTimeoutConstant=5000; SetCommTimeouts(hCom, &TimeOuts); // 設(shè)置超時
GetCommState(hCom, &dcb); dcb.BaudRate=2400; // 波特率為2400 dcb.ByteSize=8; // 每個字符有8位 dcb.Parity=NOPARITY; //無校驗 dcb.StopBits=ONESTOPBIT; //一個停止位 SetCommState(hCom, &dcb);
12.3.3 重疊I/O 在用ReadFile和WriteFile讀寫串行口時,既可以同步執(zhí)行,也可以重疊(異步)執(zhí)行。在同步執(zhí)行時,函數(shù)直到操作完成后才返回。這意味著在同步執(zhí)行時線程會被阻塞,從而導(dǎo)致效率下降。在重疊執(zhí)行時,即使操作還未完成,調(diào)用的函數(shù)也會立即返回。費時的I/O操作在后臺進行,這樣線程就可以干別的事情。例如,線程可以在不同的句柄上同時執(zhí)行I/O操作,甚至可以在同一句柄上同時進行讀寫操作。“重疊”一詞的含義就在于此。 ReadFile函數(shù)只要在串行口輸入緩沖區(qū)中讀入指定數(shù)量的字符,就算完成操作。而WriteFile函數(shù)不但要把指定數(shù)量的字符拷入到輸出緩沖中,而且要等這些字符從串行口送出去后才算完成操作。 ReadFile和WriteFile函數(shù)是否為執(zhí)行重疊操作是由CreateFile函數(shù)決定的。如果在調(diào)用CreateFile創(chuàng)建句柄時指定了FILE_FLAG_OVERLAPPED標志,那么調(diào)用ReadFile和WriteFile對該句柄進行的讀寫操作就是重疊的,如果未指定重疊標志,則讀寫操作是同步的。 函數(shù)ReadFile和WriteFile的參數(shù)和返回值很相似。這里僅列出ReadFile函數(shù)的聲明: BOOL ReadFile(
需要注意的是如果該函數(shù)因為超時而返回,那么返回值是TRUE。參數(shù)lpOverlapped在重疊操作時應(yīng)該指向一個OVERLAPPED結(jié)構(gòu),如果該參數(shù)為NULL,那么函數(shù)將進行同步操作,而不管句柄是否是由FILE_FLAG_OVERLAPPED標志建立的。 當ReadFile和WriteFile返回FALSE時,不一定就是操作失敗,線程應(yīng)該調(diào)用GetLastError函數(shù)分析返回的結(jié)果。例如,在重疊操作時如果操作還未完成函數(shù)就返回,那么函數(shù)就返回FALSE,而且GetLastError函數(shù)返回ERROR_IO_PENDING。 在使用重疊I/O時,線程需要創(chuàng)建OVERLAPPED結(jié)構(gòu)以供讀寫函數(shù)使用。OVERLAPPED結(jié)構(gòu)最重要的成員是hEvent,hEvent是一個事件對象句柄,線程應(yīng)該用CreateEvent函數(shù)為hEvent成員創(chuàng)建一個手工重置事件,hEvent成員將作為線程的同步對象使用。如果讀寫函數(shù)未完成操作就返回,就那么把hEvent成員設(shè)置成無信號的。操作完成后(包括超時),hEvent會變成有信號的。 如果GetLastError函數(shù)返回ERROR_IO_PENDING,則說明重疊操作還為完成,線程可以等待操作完成。有兩種等待辦法:一種辦法是用象WaitForSingleObject這樣的等待函數(shù)來等待OVERLAPPED結(jié)構(gòu)的hEvent成員,可以規(guī)定等待的時間,在等待函數(shù)返回后,調(diào)用GetOverlappedResult。另一種辦法是調(diào)用GetOverlappedResult函數(shù)等待,如果指定該函數(shù)的bWait參數(shù)為TRUE,那么該函數(shù)將等待OVERLAPPED結(jié)構(gòu)的hEvent 事件。GetOverlappedResult可以返回一個OVERLAPPED結(jié)構(gòu)來報告包括實際傳輸字節(jié)在內(nèi)的重疊操作結(jié)果。 如果規(guī)定了讀/寫操作的超時,那么當超過規(guī)定時間后,hEvent成員會變成有信號的。因此,在超時發(fā)生后,WaitForSingleObject和GetOverlappedResult都會結(jié)束等待。WaitForSingleObject的dwMilliseconds參數(shù)會規(guī)定一個等待超時,該函數(shù)實際等待的時間是兩個超時的最小值。注意GetOverlappedResult不能設(shè)置等待的時限,因此如果hEvent成員無信號,則該函數(shù)將一直等待下去。 在調(diào)用ReadFile和WriteFile之前,線程應(yīng)該調(diào)用ClearCommError函數(shù)清除錯誤標志。該函數(shù)負責報告指定的錯誤和設(shè)備的當前狀態(tài)。 調(diào)用PurgeComm函數(shù)可以終止正在進行的讀寫操作,該函數(shù)還會清除輸入或輸出緩沖區(qū)中的內(nèi)容。
12.3.4 通信事件 在Windows 95/NT中,WM_COMMNOTIFY消息已經(jīng)取消,在串行口產(chǎn)生一個通信事件時,程序并不會收到通知消息。線程需要調(diào)用WaitCommEvent函數(shù)來監(jiān)視發(fā)生在串行口中的各種事件,該函數(shù)的第二個參數(shù)返回一個事件屏蔽變量,用來指示事件的類型。線程可以用SetCommMask建立事件屏蔽以指定要監(jiān)視的事件,表12.4列出了可以監(jiān)視的事件。調(diào)用GetCommMask可以查詢串行口當前的事件屏蔽。
表12.4 通信事件
WaitCommEvent即可以同步使用,也可以重疊使用。如果串口是用FILE_FLAG_OVERLAPPED標志打開的,那么WaitCommEvent就進行重疊操作,此時該函數(shù)需要一個OVERLAPPED結(jié)構(gòu)。線程可以調(diào)用等待函數(shù)或GetOverlappedResult函數(shù)來等待重疊操作的完成。 當指定范圍內(nèi)的某一事件發(fā)生后,線程就結(jié)束等待并把該事件的屏蔽碼設(shè)置到事件屏蔽變量中。需要注意的是,WaitCommEvent只檢測調(diào)用該函數(shù)后發(fā)生的事件。例如,如果在調(diào)用WaitCommEvent前在輸入緩沖區(qū)中就有字符,則不會因為這些字符而產(chǎn)生EV_RXCHAR事件。 如果檢測到輸入的硬件信號(如CTS、RTS和CD信號等)發(fā)生了變化,線程可以調(diào)用GetCommMaskStatus函數(shù)來查詢它們的狀態(tài)。而用EscapeCommFunction函數(shù)可以控制輸出的硬件信號(如DTR和RTS信號)。
為了使讀者更好地掌握本章的概念,這里舉一個具體實例來說明問題。如圖12.1所示,例子程序名為Terminal,是一個簡單的TTY終端仿真程序。讀者可以用該程序打開一個串行口,該程序會把用戶的鍵盤輸入發(fā)送給串行口,并把從串口接收到的字符顯示在視圖中。用戶通過選擇File->Connect命令來打開串行口,選擇File->Disconnect命令則關(guān)閉串行口。
圖12.1 Terminal終端仿真程序 當用戶選擇File->Settings...命令時,會彈出一個Communication settings對話框,如圖12.2所示。該對話框主要用來設(shè)置串行口,包括端口、波特率、每字節(jié)位數(shù)、校驗、停止位數(shù)和流控制。 圖12.2 Communication settings對話框
通過該對話框也可以設(shè)置TTY終端仿真的屬性,如果選擇New Line(自動換行),那么每當從串口讀到回車符(‘\r’)時,視圖中的正文就會換行,否則,只有在讀到換行符(‘\n’)時才會換行。如果選擇Local echo(本地回顯),那么發(fā)送的字符會在視圖中顯示出來。 終端仿真程序的特點是數(shù)據(jù)的傳輸沒有規(guī)律。因為鍵盤輸入速度有限,所以發(fā)送的數(shù)據(jù)量較小,但接收的數(shù)據(jù)源是不確定的,所以有可能會有大量數(shù)據(jù)高速涌入的情況發(fā)生。根據(jù)Terminal的這些特性,我們在程序中創(chuàng)建了一個輔助工作者線程專門來監(jiān)視串行口的輸入。由于寫入串行口的數(shù)據(jù)量不大,不會太費時,所以在主線程中完成寫端口的任務(wù)是可以的,不必另外創(chuàng)建線程。 現(xiàn)在就讓我們開始工作。請讀者按下面幾步進行:
表12.5 新菜單項
表12.6 通信設(shè)置對話框中的主要控件
表12.7 CSetupDlg類的數(shù)據(jù)成員
清單12.6 CTerminalDoc類的部分代碼 // TerminalDoc.h : interface of the CTerminalDoc class // /////////////////////////////////////////////////////////////////////////////
#define MAXBLOCK 2048 #define XON 0x11 #define XOFF 0x13
UINT CommProc(LPVOID pParam);
class CTerminalDoc : public CDocument { protected: // create from serialization only CTerminalDoc(); DECLARE_DYNCREATE(CTerminalDoc)
// Attributes public:
CWinThread* m_pThread; // 代表輔助線程 volatile BOOL m_bConnected; volatile HWND m_hTermWnd; volatile HANDLE m_hPostMsgEvent; // 用于WM_COMMNOTIFY消息的事件對象 OVERLAPPED m_osRead, m_osWrite; // 用于重疊讀/寫
volatile HANDLE m_hCom; // 串行口句柄 int m_nBaud; int m_nDataBits; BOOL m_bEcho; int m_nFlowCtrl; BOOL m_bNewLine; int m_nParity; CString m_sPort; int m_nStopBits;
// Operations public:
BOOL ConfigConnection(); BOOL OpenConnection(); void CloseConnection(); DWORD ReadComm(char *buf,DWORD dwLength); DWORD WriteComm(char *buf,DWORD dwLength); // Overrides . . . };
///////////////////////////////////////////////////////////////////////////// // TerminalDoc.cpp : implementation of the CTerminalDoc class //
#include "SetupDlg.h"
CTerminalDoc::CTerminalDoc() { // TODO: add one-time construction code here
m_bConnected=FALSE; m_pThread=NULL;
m_nBaud = 9600; m_nDataBits = 8; m_bEcho = FALSE; m_nFlowCtrl = 0; m_bNewLine = FALSE; m_nParity = 0; m_sPort = "COM2"; m_nStopBits = 0; }
CTerminalDoc::~CTerminalDoc() {
if(m_bConnected) CloseConnection(); // 刪除事件句柄 if(m_hPostMsgEvent) CloseHandle(m_hPostMsgEvent); if(m_osRead.hEvent) CloseHandle(m_osRead.hEvent); if(m_osWrite.hEvent) CloseHandle(m_osWrite.hEvent); }
BOOL CTerminalDoc::OnNewDocument() { if (!CDocument::OnNewDocument()) return FALSE; ((CEditView*)m_viewList.GetHead())->SetWindowText(NULL);
// TODO: add reinitialization code here // (SDI documents will reuse this document)
// 為WM_COMMNOTIFY消息創(chuàng)建事件對象,手工重置,初始化為有信號的 if((m_hPostMsgEvent=CreateEvent(NULL, TRUE, TRUE, NULL))==NULL) return FALSE; memset(&m_osRead, 0, sizeof(OVERLAPPED)); memset(&m_osWrite, 0, sizeof(OVERLAPPED)); // 為重疊讀創(chuàng)建事件對象,手工重置,初始化為無信號的 if((m_osRead.hEvent=CreateEvent(NULL, TRUE, FALSE, NULL))==NULL) return FALSE; // 為重疊寫創(chuàng)建事件對象,手工重置,初始化為無信號的 if((m_osWrite.hEvent=CreateEvent(NULL, TRUE, FALSE, NULL))==NULL) return FALSE; return TRUE; }
void CTerminalDoc::OnFileConnect() { // TODO: Add your command handler code here
if(!OpenConnection()) AfxMessageBox("Can't open connection"); }
void CTerminalDoc::OnFileDisconnect() { // TODO: Add your command handler code here
CloseConnection(); }
void CTerminalDoc::OnUpdateFileConnect(CCmdUI* pCmdUI) { // TODO: Add your command update UI handler code here
pCmdUI->Enable(!m_bConnected); }
void CTerminalDoc::OnUpdateFileDisconnect(CCmdUI* pCmdUI) { // TODO: Add your command update UI handler code here
pCmdUI->Enable(m_bConnected); }
// 打開并配置串行口,建立工作者線程 BOOL CTerminalDoc::OpenConnection() { COMMTIMEOUTS TimeOuts; POSITION firstViewPos; CView *pView;
firstViewPos=GetFirstViewPosition(); pView=GetNextView(firstViewPos); m_hTermWnd=pView->GetSafeHwnd();
if(m_bConnected) return FALSE; m_hCom=CreateFile(m_sPort, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL); // 重疊方式 if(m_hCom==INVALID_HANDLE_VALUE) return FALSE; SetupComm(m_hCom,MAXBLOCK,MAXBLOCK); SetCommMask(m_hCom, EV_RXCHAR);
// 把間隔超時設(shè)為最大,把總超時設(shè)為0將導(dǎo)致ReadFile立即返回并完成操作 TimeOuts.ReadIntervalTimeout=MAXDWORD; TimeOuts.ReadTotalTimeoutMultiplier=0; TimeOuts.ReadTotalTimeoutConstant=0; /* 設(shè)置寫超時以指定WriteComm成員函數(shù)中的 GetOverlappedResult函數(shù)的等待時間*/ TimeOuts.WriteTotalTimeoutMultiplier=50; TimeOuts.WriteTotalTimeoutConstant=2000; SetCommTimeouts(m_hCom, &TimeOuts); if(ConfigConnection()) { m_pThread=AfxBeginThread(CommProc, this, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED, NULL); // 創(chuàng)建并掛起線程 if(m_pThread==NULL) { CloseHandle(m_hCom); return FALSE; } else { m_bConnected=TRUE; m_pThread->ResumeThread(); // 恢復(fù)線程運行 } } else { CloseHandle(m_hCom); return FALSE; } return TRUE; }
// 結(jié)束工作者線程,關(guān)閉串行口 void CTerminalDoc::CloseConnection() { if(!m_bConnected) return; m_bConnected=FALSE;
//結(jié)束CommProc線程中WaitSingleObject函數(shù)的等待 SetEvent(m_hPostMsgEvent);
//結(jié)束CommProc線程中WaitCommEvent的等待 SetCommMask(m_hCom, 0);
//等待輔助線程終止 WaitForSingleObject(m_pThread->m_hThread, INFINITE); m_pThread=NULL; CloseHandle(m_hCom); }
// 讓用戶設(shè)置串行口 void CTerminalDoc::OnFileSettings() { // TODO: Add your command handler code here
CSetupDlg dlg; CString str;
dlg.m_bConnected=m_bConnected; dlg.m_sPort=m_sPort; str.Format("%d",m_nBaud); dlg.m_sBaud=str; str.Format("%d",m_nDataBits); dlg.m_sDataBits=str; dlg.m_nParity=m_nParity; dlg.m_nStopBits=m_nStopBits; dlg.m_nFlowCtrl=m_nFlowCtrl; dlg.m_bEcho=m_bEcho; dlg.m_bNewLine=m_bNewLine; if(dlg.DoModal()==IDOK) { m_sPort=dlg.m_sPort; m_nBaud=atoi(dlg.m_sBaud); m_nDataBits=atoi(dlg.m_sDataBits); m_nParity=dlg.m_nParity; m_nStopBits=dlg.m_nStopBits; m_nFlowCtrl=dlg.m_nFlowCtrl; m_bEcho=dlg.m_bEcho; m_bNewLine=dlg.m_bNewLine; if(m_bConnected) if(!ConfigConnection()) AfxMessageBox("Can't realize the settings!"); } }
// 配置串行口 BOOL CTerminalDoc::ConfigConnection() { DCB dcb;
if(!GetCommState(m_hCom, &dcb)) return FALSE; dcb.fBinary=TRUE; dcb.BaudRate=m_nBaud; // 波特率 dcb.ByteSize=m_nDataBits; // 每字節(jié)位數(shù) dcb.fParity=TRUE; switch(m_nParity) // 校驗設(shè)置 { case 0: dcb.Parity=NOPARITY; break; case 1: dcb.Parity=EVENPARITY; break; case 2: dcb.Parity=ODDPARITY; break; default:; } switch(m_nStopBits) // 停止位 { case 0: dcb.StopBits=ONESTOPBIT; break; case 1: dcb.StopBits=ONE5STOPBITS; break; case 2: dcb.StopBits=TWOSTOPBITS; break; default:; } // 硬件流控制設(shè)置 dcb.fOutxCtsFlow=m_nFlowCtrl==1; dcb.fRtsControl=m_nFlowCtrl==1? RTS_CONTROL_HANDSHAKE:RTS_CONTROL_ENABLE; // XON/XOFF流控制設(shè)置 dcb.fInX=dcb.fOutX=m_nFlowCtrl==2; dcb.XonChar=XON; dcb.XoffChar=XOFF; dcb.XonLim=50; dcb.XoffLim=50; return SetCommState(m_hCom, &dcb); }
// 從串行口輸入緩沖區(qū)中讀入指定數(shù)量的字符 DWORD CTerminalDoc::ReadComm(char *buf,DWORD dwLength) { DWORD length=0; COMSTAT ComStat; DWORD dwErrorFlags; ClearCommError(m_hCom,&dwErrorFlags,&ComStat); length=min(dwLength, ComStat.cbInQue); ReadFile(m_hCom,buf,length,&length,&m_osRead); return length;
}
// 將指定數(shù)量的字符從串行口輸出 DWORD CTerminalDoc::WriteComm(char *buf,DWORD dwLength) { BOOL fState; DWORD length=dwLength; COMSTAT ComStat; DWORD dwErrorFlags; ClearCommError(m_hCom,&dwErrorFlags,&ComStat); fState=WriteFile(m_hCom,buf,length,&length,&m_osWrite); if(!fState){ if(GetLastError()==ERROR_IO_PENDING) { GetOverlappedResult(m_hCom,&m_osWrite,&length,TRUE);// 等待 } else length=0; } return length; }
// 工作者線程,負責監(jiān)視串行口 UINT CommProc(LPVOID pParam) { OVERLAPPED os; DWORD dwMask, dwTrans; COMSTAT ComStat; DWORD dwErrorFlags; CTerminalDoc *pDoc=(CTerminalDoc*)pParam;
memset(&os, 0, sizeof(OVERLAPPED)); os.hEvent=CreateEvent(NULL, TRUE, FALSE, NULL); if(os.hEvent==NULL) { AfxMessageBox("Can't create event object!"); return (UINT)-1; } while(pDoc->m_bConnected) { ClearCommError(pDoc->m_hCom,&dwErrorFlags,&ComStat); if(ComStat.cbInQue) { // 無限等待WM_COMMNOTIFY消息被處理完 WaitForSingleObject(pDoc->m_hPostMsgEvent, INFINITE); ResetEvent(pDoc->m_hPostMsgEvent); // 通知視圖 PostMessage(pDoc->m_hTermWnd, WM_COMMNOTIFY, EV_RXCHAR, 0); continue; } dwMask=0; if(!WaitCommEvent(pDoc->m_hCom, &dwMask, &os)) // 重疊操作 { if(GetLastError()==ERROR_IO_PENDING) // 無限等待重疊操作結(jié)果 GetOverlappedResult(pDoc->m_hCom, &os, &dwTrans, TRUE); else { CloseHandle(os.hEvent); return (UINT)-1; } } } CloseHandle(os.hEvent); return 0; }
BOOL CTerminalDoc::CanCloseFrame(CFrameWnd* pFrame) { // TODO: Add your specialized code here and/or call the base class
SetModifiedFlag(FALSE); // 將文檔的修改標志設(shè)置成未修改 return CDocument::CanCloseFrame(pFrame); } 毫無疑問,CTerminalDoc類是研究重點。該類負責Terminal的通信任務(wù),主要包括設(shè)置通信參數(shù)、打開和關(guān)閉串行口、建立和終止輔助工作線程、用輔助線程監(jiān)視串行口等等。 在CTerminalDoc類的頭文件中,有些變量是用volatile關(guān)鍵字聲明的。當兩個線程都要用到某一個變量且該變量的值會被改變時,應(yīng)該用volatile聲明,該關(guān)鍵字的作用是防止優(yōu)化編譯器把變量從內(nèi)存裝入CPU寄存器中。如果變量被裝入寄存器,那么兩個線程有可能一個使用內(nèi)存中的變量,一個使用寄存器中的變量,這會造成程序的錯誤執(zhí)行。 成員m_bConnected用來表明當前是否存在一個通信連接。m_hTermWnd用來保存是視圖的窗口句柄。m_hPostMsgEvent事件對象用于WM_COMMNOTIFY消息的允許和禁止。m_pThread用來指向AfxBeginThread創(chuàng)建的CWinThread對象,以便對線程進行控制。OVERLAPPED結(jié)構(gòu)m_osRead和m_osWrite用于串行口的重疊讀/寫,程序應(yīng)該為它們的hEvent成員創(chuàng)建事件句柄。 CTerminalDoc類的構(gòu)造函數(shù)主要完成一些通信參數(shù)的初始化工作。OnNewDocument成員函數(shù)創(chuàng)建了三個事件對象,CTerminalDoc的析構(gòu)函數(shù)關(guān)閉串行口并刪除事件對象句柄。 OnFileSettings是File->Settings...的命令處理函數(shù),該函數(shù)彈出一個CSetupDlg對話框來設(shè)置通信參數(shù)。實際的設(shè)置工作由ConfigConnection函數(shù)完成,在OpenConnection和OnFileSettings中都會調(diào)用該函數(shù)。 OpenConnection負責打開串行口并建立輔助工作線程,當用戶選擇了File->Connect命令時,消息處理函數(shù)OnFileConnect將調(diào)用該函數(shù)。該函數(shù)調(diào)用CreateFile以重疊方式打開指定的串行口并把返回的句柄保存在m_hCom成員中。接著,函數(shù)對m_hCom通信設(shè)備進行各種設(shè)置。需要注意的是對超時的設(shè)定,將讀間隔超時設(shè)置為MAXDWORD并使其它讀超時參數(shù)為0會導(dǎo)致ReadFile函數(shù)立即完成操作并返回,而不管讀入了多少字符。設(shè)置超時就規(guī)定了GetOverlappedResult函數(shù)的等待時間,因此有必要將寫超時設(shè)置成適當?shù)闹?,這樣如果不能完成寫串口的任務(wù),GetOverlappedResult函數(shù)會在超過規(guī)定超時后結(jié)束等待并報告實際傳輸?shù)淖址麛?shù)。 如果對m_hCom設(shè)置成功,則函數(shù)會建立一個輔助線程并暫時將其掛起。在最后,調(diào)用CWinThread:: ResumeThread使線程開始運行。 OpenConnection調(diào)用成功后,線程函數(shù)CommProc就開始工作。該函數(shù)的主體是一個while循環(huán),在該循環(huán)內(nèi),混合了兩種方法監(jiān)視串行口輸入的方法。先是調(diào)用ClearCommError函數(shù)查詢輸入緩沖區(qū)中是否有字符,如果有,就向視圖發(fā)送WM_COMMNOTIFY消息通知其接收字符。如果沒有,則調(diào)用WaitCommEvent函數(shù)監(jiān)視EV_RXCHAR通信事件,該函數(shù)執(zhí)行重疊操作,緊接著調(diào)用的GetOverlappedResult函數(shù)無限等待通信事件,如果EV_RXCHAR事件發(fā)生(串口收到字符并放入輸入緩沖區(qū)中),那么函數(shù)就結(jié)束等待。 上述兩種方法的混合使用兼顧了線程的效率和可靠性。如果只用ClearCommError函數(shù),則輔助線程將不斷耗費CPU時間來查詢,效率較低。如果只用WaitCommEvent來監(jiān)視,那么由于該函數(shù)對輸入緩沖區(qū)中已有的字符不會產(chǎn)生EV_RXCHAR事件,因此在通信速率較高時,會造成數(shù)據(jù)的延誤和丟失。 注意到輔助線程用m_PostMsgEvent事件對象來同步WM_COMMNOTIFY消息的發(fā)送。在發(fā)送消息之前,WaitForSingleObject函數(shù)無限等待m_PostMsgEvent對象,WM_COMMNOTIFY的消息處理函數(shù)CTerminalView::OnCommNotify在返回時會把該對象置為有信號,因此,如果WaitForSingleObject函數(shù)返回,則說明上一個WM_COMMNOTIFY消息已被處理完,這時才能發(fā)下一個消息,在發(fā)消息前還要調(diào)用ResetEvent把m_PostMsgEvent對象置為無信號的,以供下次使用。 由于PostMessage函數(shù)在消息隊列中放入消息后會立即返回,所以如果不采取上述措施,那么輔助線程可能在主線程未處理之前重復(fù)發(fā)出WM_COMMNOTIFY消息,這會降低系統(tǒng)的效率。 可能有讀者會問,為什么不用SendMessage?該函數(shù)在發(fā)送的消息被處理完畢后才返回,這樣不就不用考慮同步問題了嗎?是的,本例中也可以使用SendMessage,但該函數(shù)會阻塞輔助線程的執(zhí)行直到消息處理完畢,這會降低效率。如果用PostMessage,那么在函數(shù)立即返回后線程還可以干別的事情,因此,考慮到效率問題,這里使用了PostMessage而不是SendMessage。 函數(shù)ReadComm和WriteComm分別用來從m_hCom通信設(shè)備中讀/寫指定數(shù)量的字符。ReadComm函數(shù)很簡單,由于對讀超時的特殊設(shè)定,ReadFile函數(shù)會立即返回并完成操作,并在length變量中報告實際讀入的字符數(shù)。此時,沒有必要調(diào)用等待函數(shù)或GetOverlappedResult。在WriteComm中,調(diào)用GerOverlappedResult來等待操作結(jié)果,直到超時發(fā)生。不管是否超時,該函數(shù)在結(jié)束等待后都會報告實際的傳輸字符數(shù)。 CloseConnection函數(shù)的主要任務(wù)是終止輔助線程并關(guān)閉m_hCom通信設(shè)備。為了終止線程,該函數(shù)設(shè)置了一系列信號,以結(jié)束輔助線程中的等待和循環(huán),然后調(diào)用WaitForSingleObject等待線程結(jié)束。
清單12.7 CTerminalView類的部分代碼 // TerminalView.h : interface of the CTerminalView class /////////////////////////////////////////////////////////////////////////////
class CTerminalView : public CEditView { . . . afx_msg LRESULT OnCommNotify(WPARAM wParam, LPARAM lParam); DECLARE_MESSAGE_MAP() };
// TerminalView.cpp : implementation of the CTerminalView class // BEGIN_MESSAGE_MAP(CTerminalView, CEditView) . . . ON_MESSAGE(WM_COMMNOTIFY, OnCommNotify) END_MESSAGE_MAP()
LRESULT CTerminalView::OnCommNotify(WPARAM wParam, LPARAM lParam) { char buf[MAXBLOCK/4]; CString str; int nLength, nTextLength; CTerminalDoc* pDoc=GetDocument(); CEdit& edit=GetEditCtrl(); if(!pDoc->m_bConnected || (wParam & EV_RXCHAR)!=EV_RXCHAR) // 是否是EV_RXCHAR事件? { SetEvent(pDoc->m_hPostMsgEvent); // 允許發(fā)送下一個WM_COMMNOTIFY消息 return 0L; } nLength=pDoc->ReadComm(buf,100); if(nLength) { nTextLength=edit.GetWindowTextLength(); edit.SetSel(nTextLength,nTextLength); //移動插入光標到正文末尾 for(int i=0;i<nLength;i++) { switch(buf[i]) { case '\r': // 回車 if(!pDoc->m_bNewLine) break; case '\n': // 換行 str+="\r\n"; break; case '\b': // 退格 edit.SetSel(-1, 0); edit.ReplaceSel(str); nTextLength=edit.GetWindowTextLength(); edit.SetSel(nTextLength-1,nTextLength); edit.ReplaceSel(""); //回退一個字符 str=""; break; case '\a': // 振鈴 MessageBeep((UINT)-1); break; default : str+=buf[i]; } } edit.SetSel(-1, 0); edit.ReplaceSel(str); // 向編輯視圖中插入收到的字符 } SetEvent(pDoc->m_hPostMsgEvent); // 允許發(fā)送下一個WM_COMMNOTIFY消息 return 0L; }
void CTerminalView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) { // TODO: Add your message handler code here and/or call default
CTerminalDoc* pDoc=GetDocument(); char c=(char)nChar;
if(!pDoc->m_bConnected)return; pDoc->WriteComm(&c, 1); if(pDoc->m_bEcho) CEditView::OnChar(nChar, nRepCnt, nFlags); // 本地回顯 } CTerminalView是CEditView的派生類,利用CEditView的編輯功能,可以大大簡化程序的設(shè)計。 OnChar函數(shù)對WM_CHAR消息進行處理,它調(diào)用CTerminalDoc::WriteComm把用戶鍵入的字符從串行口輸出。如果設(shè)置了Local echo,那么就調(diào)用CEditView::OnChar把字符輸出到視圖中。 OnCommNotify是WM_COMMNOTIFY消息的處理函數(shù)。該函數(shù)調(diào)用CTerminalDoc::ReadComm從串行口輸入緩沖區(qū)中讀入字符并把它們輸出到編輯視圖中。在輸出前,函數(shù)會對一些特殊字符進行處理。如果讀者對控制編輯視圖的代碼不太明白,那么請參見6.1.4。在函數(shù)返回時,要調(diào)用SetEvent把m_hPostMsgEvent置為有信號。
清單12.8 CSetupDlg類的部分代碼 // SetupDlg.h : header file // class CSetupDlg : public CDialog {
. . . public: BOOL m_bConnected; . . . };
// SetupDlg.cpp : implementation file //
BOOL CSetupDlg::OnInitDialog() { CDialog::OnInitDialog();
// TODO: Add extra initialization here
GetDlgItem(IDC_PORT)->EnableWindow(!m_bConnected); return TRUE; // return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FALSE } CSetupDlg的主要任務(wù)是配置通信參數(shù)。在OnInitDialog函數(shù)中,要根據(jù)當前是否連接來允許/禁止Port組合框。因為在打開一個連接后,顯然不能隨便改變端口。 |
|