摘要:Windows具有多線程處理能力,應用程序中可以創建多個線程,每個線程能夠獨立完成一個子任務。在通信程序中使用多線程技術,可提高程序的執行效率和反應速度。結合應用實例,介紹了VC++環境下基于Win32 API的多線程、串口通信、異步I/O技術的原理和實現方法。
關鍵詞:多線程;串口通信;異步I/O
中圖分類號:TP393文獻標識碼:A文章編號:1009-3044(2009)27-7583-04
Multi Thread Application in Serial Communication
GUO Xiao-mei
(Nanjing Xiaozhuang College, Nanjing 210001, China)
Abstract: Windows system can create multi thread, each thread can do single task which could promote the execute efficiency and response speed.This paper introduces theory and example of multi thread, serial communication and asynchronous I/O technology base on Win32 API in Visual C++ environment.
Key words: multi thread; serial communication; asynchronous I/O
Windows是一個多任務操作系統,進程是應用程序的執行實例,線程是進程內部的一個執行單元,每一個進程至少包含一個由系統創建的主執行線程。根據需要,用戶可以在應用程序中創建多個線程,Win32系統中,多個線程可以實現并行處理,這意味著一個程序可以同時完成多個任務。實際上,對于單處理器(CPU)的計算機,操作系統為每個獨立線程安排了一些CPU時間片(約20μs),并以特定的方式在各線程之間切換,同一時間,只有一個線程在運行,由于時間片很小,因而這些線程仿佛在同時、并行的工作。一般的,通信程序應具有實現各種I/O操作和及時響應用戶請求的能力,為避免可能出現的I/O操作長時間占用CPU時間,影響對其它任務的處理,利用Win32的多線程和異步I/O特性,是設計通信程序的最佳選擇。
1 多線程與串口通信
Win32 API函數支持多線程的程序設計。MFC中的CWinThread類對Win32 API多線程函數進行了封裝。開發多線程應用程序,既可用Win32 API函數,也可用VC++提供的MFC類庫。線程相當于一個函數,包括用戶界面線程和工作線程兩種。用戶界面線程可以處理界面消息,工作線程一般用來處理后臺工作,這里主要對工作線程進行討論。
1.1 創建工作線程
創建工作線程,需先要編寫一個線程函數。通過全局函數AfxBeginThread創建線程并啟動后,在系統為該線程分配的時間片內,線程函數被自動調用。AfxBeginThread函數原型如下:
CWinThread* AfxBeginThread( AFX_THREADPROC pfnThreadProc, LPVOID pParam, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );
參數pfnThreadProc是一個指向線程函數的指針,參數pParam為傳遞給線程函數的指針,其它幾個參數用于設置線程的優先級、線程的堆棧大小、創建時是否立即啟動、線程的創建方式及線程的安全屬性。調用函數SuspendThread( )或ResumeThread( ),可使已創建的線程被掛起或恢復運行。
線程函數必須設計成全局函數或靜態成員函數,其返回值和參數類型應滿足如下的函數原型要求:UINT<自定義的線程函數名>(LPVOID pParam);
1.2 串口通信
編制串口通信程序的一般步驟為:打開串口,配置串口,超時設置和數據讀寫。
1) 打開串口
Win32系統中,串口和其他通信設備都被作為文件進行處理,使用前必須先將其打開。為保證串口通信數據傳輸的可靠性,串口打開時要設置為獨占模式,串口一旦被打開,其他應用程序將無法打開或使用。打開串口用函數CreateFile,其原型如下:
HANDLE CreateFile(
LPCTSTR lpFileName, // 文件名
DWORD dwDesiredAccess,// 訪問模式
DWORD dwShareMode,// 共享模式
LPSECURITY_ATTRIBUTES lpSecurityAttributes, //通常為NULL
DWORD dwCreationDisposition,// 創建方式
DWORD dwFlagsAndAttributes, // 文件屬性和標志
HANDLE hTemplateFile// 通常為NULL
);
2) 配置串口
串口進行數據通信前需對其進行配置,串口配置主要包括波特率,數據位數,停止位數,奇偶校驗,輸入/輸出緩沖區的設置等。串口配置時要用到設備控制塊DCB( Device Control Block),這是一個結構,其內包含了波特率等信息,調用SetCommState函數,用DCB結構變量作為參數,可進行串口參數設置。調用SetupComm函數,可設置輸入/輸出緩沖區的大小。
3) 超時設置
讀、寫串口時,要進行超時設置。如果在規定時間內沒有完成對串口的讀、寫,那么操作就會結束。超時設置通過改變COMMTIMEOUTS結構的成員變量值來實現,調用SetCommTimeouts函數,用COMMTIMEOUTS結構變量作為參數,可進行串口超時設置。
4) 數據讀/寫
用ReadFile和WriteFile函數可實現對串口的讀、寫操作。這兩個函數的參數和返回值很相似,以下是ReadFile的函數原型:
BOOL ReadFile(
HANDLE hFile,// 文件句柄,可表示串口
LPVOID lpBuffer, // 數據讀入后放入該指針指向的緩沖區
DWORD nNumberOfBytesToRead,// 要求讀入的字節數
LPDWORD lpNumberOfBytesRead, // 實際讀入的字節數
LPOVERLAPPED lpOverlapped// 指向OVERLAPPED結構的指針
);
對于WriteFile函數,寫往串口的數據放在lpBuffer指向的緩沖區內。
1.3 異步I/O
調用ReadFile和WriteFile時,如果最后一個參數lpOverlapped設置為NULL,則函數將進行同步操作,這意味著線程會阻塞在這里,直到讀、寫操作完成后,函數才會返回。在執行I/O這樣費時的操作時,很多時間往往浪費在等待函數的返回上,這將導致程序效率的下降。利用Win32的異步I/O特性,可很好的解決這一問題。異步執行時,即使讀、寫操作還沒完成,調用的函數也會立即返回,費時的I/O操作在后臺執行,線程可繼續處理其它事務。
用異步方式操作串口,在打開串口時,CreateFile函數的dwFlagsAndAttributes參數必須被設置為FILE_FLAG_OVERLAPPED 標志,同時ReadFile和WriteFile函數的lpOverlapped參數要指向一個OVERLAPPED結構。函數調用前,先為其創建一個OVERLAPPED結構變量,用以接收函數調用后的結果信息。異步操作時,ReadFile和WriteFile函數返回并不一定代表讀、寫操作完成,那么操作完成與否該如何判斷呢?當系統完成I/O操作后,會設置OVERLAPPED結構變量,通過在程序的適當位置調用WaitForSingleObject函數來等待這個I/O完成通知,在得到通知信號后,再調用GetOverlappedResult函數來查詢I/O操作結果,并進行相關處理。
2 VC++環境下基于API的串口通信程序
以下結合本人設計的串口通信程序,討論多線程在串口通信編程中的實現方法。該程序可發送或接收數據,實現兩個串行口之間的數據通信。主菜單中的“串口通信”用于連接和斷開串口,選擇“功能設置”中的“數據發送”命令后,隨機敲擊鍵盤,程序會將來自鍵盤的數據向串口發送;選擇“功能設置”中的“數據接收”命令,程序將從串口接收數據,為產生直觀效果,接收到的數據經處理后以圖形方式顯示。
程序在同一臺微機的兩個串行口上測試。連接COM1、COM2兩個串口后(用串口連接線或虛擬串口軟件),兩次啟動程序,分別選擇“數據發送”和“數據接收”命令,從發送界面隨機鍵入數據(如圖1),此時在接收界面可見到同步接收的數據(如圖2)。
2.1 設計思想
程序包含一個主線程和一個工作線程。分析發送端,程序通過鍵盤向串口發送數據,其特點為數據傳送沒有規律,速度不快,發送的數據量也不大;再分析接收端,接收的數據源不確定,大批數據快速涌入串口的情況可能發生。根據這一特點,在程序中增加一個工作線程,用于監視串口的數據接收情況。由于寫入串口的數據量不大,因而可在主線程中接收鍵盤輸入并寫入串口,不必再創建另一個線程。
程序采用文檔/視圖結構。文檔類CTRACOMDoc負責串口通信任務,主要包括打開/關閉串口、配置串口,超時設置、創建和終止工作線程、用工作線程監視串行口等,串口數據的讀、寫函數也在文檔類中實現。視圖類CTRACOMView由CView類派生,其主要任務是響應用戶鍵盤輸入、寫數據至串行口、接收串口數據處理后作圖。
2.2 關鍵代碼分析
首先為文檔類CTRACOMDoc添加串口通信所需的成員變量,其中m_sPort為串口名稱,發送數據時,其值設定為“COM1”,接收數據時設定為“COM2”。
class CTRACOMDoc : public CDocument
{…………
public:
CWinThread *m_pThread;//指向新增的工作線程
volatileBOOL m_bConnect;//標記串口是否連接
CStringm_sPort; //定義串口名稱
volatileHANDLE m_hCom;//串口句柄
//以下為串口通信參數定義,包括波特率、奇偶校驗等
…………};
1) 打開串口與創建線程
執行“連接串口”命令后,文檔類的成員函數OpenConnection( )被調用。該函數用CreateFile打開指定串口,為了以異步方式執行I/O操作,將它的dwFlagsAndAttributes參數置為FILE_FLAG_OVERLAPPED;調用自定義的ConfigConnection函數設置波特率等串口通信參數;調用全局函數AfxBeginThread創建工作線程,為了實現工作線程與主線程之間的通訊,將傳遞給線程函數的pParam參數置為this;結構體變量TimeOuts用于超時設置,SetCommMask函數用于設置指定串口發生的事件,它的第2個參數為EV_RXCHAR時,設定事件為輸入緩沖區收到了字符,工作線程中的WaitCommEvent函數將監視此事件的發生。OpenConnection( )函數的主要代碼如下:
BOOL CTRACOMDoc::OpenConnection( )
{COMMTIMEOUTS TimeOuts;
…………
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,2048,2048);//設置輸入/輸出緩沖區的大小
SetCommMask(m_hCom,EV_RXCHAR);//設置串口事件
//為TimeOuts的成員變量賦值
…………
SetCommTimeouts(m_hCom, TimeOuts); //設置超時,
if(ConfigConnection( ))
{m_pThread=AfxBeginThread(CommProc,this,THREAD_PRIORITY_NORMAL,0,
CREATE_SUSPENDED,NULL);//創建工作線程,并將其掛起
if(m_pThread==NULL)
{CloseHandle(m_hCom); //如果線程創建失敗,則關閉串口
return FALSE;}
else
{m_bConnect=TRUE;
m_pThread->ResumeThread( );//線程開始運行}
}
…………
return TRUE;
}
2) 線程函數
線程創建成功后就開始工作,它負責監視串行口。事件發生時(輸入緩沖區中收到字符)向視圖發送WM_COMMNOTIFY消息,由視圖處理該消息;當串口事件再次發生時,線程等待前一事件的完成信號(該信號由視圖發出),并再次發出WM_COMMNOTIFY消息,這是一個循環過程。兼顧到線程的效率和可靠性,用了兩種方法對串行口進行監視,調用ClearCommError函數查詢輸入緩沖區中是否有數據,如果有,則產生一個WM_COMMNOTIFY消息并向視圖發送,但前提是上一個WM_COMMNOTIFY消息已處理完畢;如緩沖區沒有數據,就調用WaitCommEvent函數監視EV_RXCHAR通信事件,該函數執行異步操作,即不管事件發生與否,線程不會在此阻塞,函數立即返回,接下來調用GetOverlappedResult函數等待通信事件發生,如果串口收到字符并放入輸入緩沖區,則函數結束等待,并返回一個OVERLAPPED結構來報告異步操作結果。以下是線程函數CommProc的主要代碼:
UINT CommProc(LPVOID pParam)
{OVERLAPPED os;// os結構變量用于保存異步I/O操作結果
DWORD dwMask,dwTrans;
COMSTAT ComStat;// 結構變量ComStat用于保存通信串口的當前狀態
DWORD dwErrorFlags;
…………
while(pDoc->m_bConnect)
{ClearCommError(pDoc->m_hCom,dwErrorFlags,ComStat);
if(ComStat.cbInQue) //當緩沖區有字符時
{ //等待前一個WM_COMMNOTIFY消息處理完畢
WaitForSingleObject(pDoc->m_hPostMsgEvent,INFINITE);
ResetEvent(pDoc->m_hPostMsgEvent);
//向視圖發送WM_COMMNOTIFY消息
PostMessage(pDoc->m_hTermWnd,WM_COMMNOTIFY,EV_RXCHAR,0);
continue;}
dwMask=0;
if(!WaitCommEvent(pDoc->m_hCom,dwMask,os)) //異步執行
{if(GetLastError()==ERROR_IO_PENDING)
//等待異步操作結束
GetOverlappedResult(pDoc->m_hCom,os,dwTrans,TRUE);
……………… }
}
return 0;
}
3) 數據發送
選擇“數據發送”并從鍵盤輸入數據時,視圖類的成員函數OnChar被調用,該函數接收鍵盤輸入,并調用文檔類的WriteComm函數,將鍵入的數據向串口輸出。函數通過調用WriteFile 向串口輸出數據,由于規定用異步方式操作串口,函數WriteFile的lpOverlapped參數設置為指向一個OVERLAPPED結構的事件對象。以下是WriteComm函數代碼:
DWORD CTRACOMDoc::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); //等待異步操作結果
elselength=0; }
return length;
}
4) 數據接收
執行“數據接收”命令,當線程函數監測到輸入緩沖區有數據時,向視圖發送WM_COMMNOTIFY消息,此時視圖類的OnCommNotify成員函數被調用,它調用文檔類的ReadComm函數并通過ReadFile從串口讀取數據,然后對讀取的數據進行處理,完成后調用SetEvent函數,允許線程發送下一個WM_COMMNOTIFY消息。以下為OnCommNotify函數的主要代碼:
LRESULT CTRACOMView::OnCommNotify(WPARAM wParam,LPARAM lParam)
{char buf[500];
CString str;
int nLength;
CTRACOMDoc *pDoc=GetDocument( );
………………
nLength=pDoc->ReadComm(buf,50);
if(nLength) //對收到的數據進行處理
{for(int i=0;i {point.x+=10; point.y=350-2*buf[i]; pDoc->AddLine(m_ptOld,point);//加入線段到指針數組 m_ptOld=point; } ff=2; Invalidate( );} SetEvent(pDoc->m_hPostMsgEvent); //處理完畢,允許發送下一個WM_COMMNOTIFY消息 return 0L; } 3 結束語 多線程技術應用于串口通信,可提高程序的執行效率,使程序并行工作。當系統既要進行費時的I/O操作,又同時 有其它任務要處理時(如及時響應用戶請求),線程是最好的工具。可以創建新的線程,結合異步I/O操作,來完成耗時的串口讀/寫任務,主線程則可以繼續處理其它事務。本文結合應用實例,對多線程、串口通信、異步I/O技術原理和實現方法進行了分析討論。 參考文獻: [1] 葛子昂.Windows核心編程[M].5版.北京:清華大學出版社,2008. [2] 侯俊杰.深入淺出MFC[M].2版.武漢:華中科技大學出版社,2001. [3] 龔建偉.Visual C++ /Turbo C串口通信編程實踐[M].北京:電子工業出版社,2008. [4] 辛長安.Visual C++權威剖析[M].北京:清華大學出版社,2008.