轉自:http://tb.blog.csdn.net/TrackBack.aspx?PostId=1746599
在工業控制中,串口是常用的電腦與外部串列裝置之間的資料轉送通道,由於串列通訊方便易行,所以應用廣泛。
本文將介紹在Windows平台下串列通訊的工作機制和用Visual C++設計串列通訊程式的編程方法及通訊方式。
VC中實現串列通訊的編程技術
以下我們將介紹VC中幾種實現串列通訊的編程技術:
利用VC++的標準通訊函數_inp和_outp可實現串口通訊。下面是一個串口初始化的程式:
void init_com(PORT) {char i; outp(PORT+3,0x80); outp(PORT,0x0C); outp(PORT+1,0); outp(PORT+3 ,0x3a); outp(PORT+3 ,0x03); i=inp(PORT+5) && 0xfe; outp(PORT+5,i);} |
串列通訊控制項MSCOmm32.OCX提供了使用RS-232來進行資料通訊的所有協議,VC為該控制項提供了標準的事件處理函數、過程,並通過屬性和方法提供了串列通訊的設定。它使使用者能夠方便地訪問Windows串列通訊驅動程式的大多數特性,包括輸入、輸出緩衝區的大小及決定何時使用流量控制命令掛起資料轉送等。
在ClassWizard中為新建立的通訊控制項定義成員對象(CMSComm m_Serial),通過該對象便可以對串口屬性進行設定,MSComm控制項共有27個屬性。以下是通過設定控制項屬性對串口進行初始化的執行個體:
BOOL CSampleDlg:: PortOpen() { BOOL m_Opened; ...... m_Serial.SetCommPort(2); // 指定串口號 m_Serial.SetSettings("4800,N,8,1"); // 通訊參數設定 m_Serial.SetInBufferSize(1024); // 指定接收緩衝區大小 m_Serial.SetInBufferCount(0); // 清空接收緩衝區 m_Serial.InputMode(1); // 設定資料擷取方式 m_Serial.SetInputLen(0); // 設定讀取方式 m_Opened=m_Serail.SetPortOpen(1); // 開啟指定的串口 return m_Opened;} |
開啟所需串口後,我們需要考慮串口通訊的時機。在接收或發送資料過程中,可能需要監視並響應一些事件和錯誤,所以事件驅動是處理序列埠互動作用的一種非常有效方法。使用OnComm事件和CommEvent屬性捕捉並檢查通訊事件和錯誤的值。發生通訊事件或錯誤時將觸發OnComm事件,CommEvent屬性的值將被改變,應用程式通過檢查CommEvent屬性值並作出相應的反應。
控制項雖然簡單易用,但由於必須拿到對話方塊中使用,在一些需要線上程中實現通訊的應用場合下,控制項的使用顯得捉襟見肘。API是附帶在Windows內部的一個極其重要的組成部分。Windows的32位API主要是一系列很複雜的函數和訊息集合。它可以看作是Windows系統為在其下啟動並執行各種開發系統提供的開放式通用功能增強介面。
通訊程式在CreateFile處指定串口裝置及相關的操作屬性,再返回一個控制代碼,該控制代碼將被用於後續的通訊操作,並貫穿整個通訊過程。串口開啟後,其屬性被設定為預設值,根據具體需要,通過調用GetCommState(hComm,&&dcb)讀取當前串口裝置控制塊DCB設定,修改後通過SetCommState(hComm,&&dcb)將其寫入。運用ReadFile()與WriteFile()這兩個API函數實現串口讀寫操作,若為非同步通訊方式,兩函數中最後一個參數為指向OVERLAPPED結構的非null 指標,在讀寫函數傳回值為FALSE的情況下,調用GetLastError()函數,傳回值為ERROR_IO_PENDING,表明I/O操作懸掛,即操作轉入後台繼續執行。此時,可以用WaitForSingleObject()來等待結束訊號並設定最長等待時間,舉例如下:
BOOL bReadStatus; bReadStatus = ReadFile( m_hIDComDev, buffer, dwBytesRead, &&dwBytesRead, &&m_OverlappedRead ); if(!bReadStatus){ if(GetLastError()==ERROR_IO_PENDING){ WaitForSingleObject(m_OverlappedRead.hEvent,1000); return ((int)dwBytesRead);} return(0);} return ((int)dwBytesRead); |
多線程下實現串列通訊
Windows內部的搶先發送器在活動的線程之間分配CPU時間,Windows區分兩種不同類型的線程,一種是使用介面執行緒(User Interface Thread),它包含訊息迴圈或訊息泵,用於處理接收到的訊息;另一種是背景工作執行緒(Work Thread),它沒有訊息迴圈,用於執行背景工作、監視串口事件的線程即為背景工作執行緒。
多線程程式的編寫在連接埠的配置,串連部分與單線程的相同,在連接埠配置完畢後,最重要的是根據實際情況,建立多線程之間的同步對象,如號誌、臨界區和事件等。
一切就緒後即可啟動背景工作執行緒,程式如下:
CWinThrea CommThread = AfxBegin Thread(CommWatchThread, // 線程函數名 (LPVOID) m_pTTYInfo, // 傳遞的參數 THREAD_PRIORITY_ABOVE_NORMAL, // 設定線程優先順序 (UINT) 0, // 最大堆棧大小 (DWORD) CREATE_SUSPENDED , // 建立標誌 (LPSECURITY_ATTRIBUTES) NULL); if(WaitCommEvent(pTTYInfo->idComDev,&&dwEvtMask,NULL)) { if((dwEvtMask && pTTYInfo->dwEvtMask )== pTTYInfo->dwEvtMask) { WaitForSingleObject(pTTYInfo->hPostEvent,0xFFFFFFFF); ResetEvent(pTTYInfo->hPostEvent); // 置同步事件對象為非訊號態 ::PostMessage(CSampleView,ID_COM1_DATA,0,0); // 發送通知訊息}} BEGIN_MESSAGE_MAP(CSampleView, CView) //{{AFX_MSG_MAP(CSampleView) ON_MESSAGE(ID_COM1_DATA, OnProcessCom1Data) ON_MESSAGE(ID_COM2_DATA, OnProcessCom2Data) ..... //}}AFX_MSG_MAP END_MESSAGE_MAP() |
多線程的實現可以使得各連接埠獨立,準確地實現串列通訊,使串列通訊具有更廣泛的靈活性與嚴格性,且充分利用CPU時間。但在具體的即時監控系統中如何協調多個線程、線程之間以何種方式實現同步,這是多線程串列通訊程式實現的痛點。
串列通訊的操作方式
下面我們將介紹串列通訊的幾種操作方式:
同步方式中,讀串口的函數試圖在串口的接收緩衝區中讀取規定數目的資料,直到規定數目的資料全部被讀出或設定的逾時時間已到時才返回。例如:
COMMTIMEOUTS timeOver; memset(&&timeOver,0,sizeof(timeOver)); DWORD timeMultiplier,timeConstant; timeOver.ReadTotalTimeoutMultiplier=timeMultiplier; timeOver.ReadTotalTimeoutConstant=timeConstant; SetCommTimeouts(hComport,&&timeOver); …… ReadFile(hComport,inBuffer,nWantRead,&&nRealRead,NULL); |
COMMTIMEOUTS結構用於設定讀寫函數的等待時間。
在ReadFile函數中hComport為待讀串口控制代碼;inBuffer為輸入緩衝區大小;nWantRead為每次調用ReadFile時,函數試圖讀出的位元組數;nRealRead為實際讀出的位元組數;最後一個參數值NULL代表ReadFile將採用同步檔案讀寫的方式。
如果所規定的待讀取資料的數目nWantRead較大且設定的逾時時間也較長,而接收緩衝區中資料較少,則可能引起線程阻塞。解決這一問題的方法是檢查COMSTAT結構的cbInQue成員,該成員的大小即為接收緩衝區中處於等待狀態的資料的實際個數。如果令nWantRead的值等於COMSTAT.cbInQue,就能較好地防止線程阻塞。
查詢方式,即一個進程中的某一線程定時地查詢串口的接收緩衝區,如果緩衝區中有資料,就讀取資料;若緩衝區中沒有資料,該線程將繼續執行,因此會佔用大量的CPU時間,它實際上是同步方式的一種派生。例如:
COMMTIMEOUTS timeOver; memset(&&timeOver,0,sizeof(timeOver)); timeOver.ReadIntervalTimeout=MAXWORD; SetCommTimeouts(hComport.&&timeOver); …… ReadFile(hComport.inBuffer.nWantRead.&&nRealRead,NULL); |
除了COMMTIMEOUTS結構的變數timeOver設定不同外,查詢方式與同步方式在程式碼方面很類似,但二者的工作方式卻差別很大。儘管ReadFile採用的也是同步檔案讀寫方式,但由於timeOver的區間超過時間設定為MAXWORD,所以ReadFile每次將讀出接收隊列中的所有處於等待狀態的資料,一次最多可讀出nWantRead個位元組的資料。
非同步方式中,利用Windows的多線程結構,可以讓串口的讀寫操作在後台進行,而應用程式的其他部分在前台執行。例如:
OVERLAPPED wrOverlapped; COMMTIMEOUTS timeOver; memset(&&timeOver.0.sizeof(timeOver)); DWORDtimeMultiplier,timeConstant; timeOver.ReadTotalTimeoutMultiplier=timeMultiplier; timeOver.ReadTotalTimeoutConstant=timeConstant; SetCommTimeouts(hComport,&&timeOver); wrOverlapped.hEvent=CreateEvent(NULL.TRUE,FALSE,NULL); …… ReadFile(hComport,inBuffer,nWantRead,&&nRealRead,&&wrOverlapped); GetOverlappedResult(hComport,&&wrOverlapped,&& nRealRead,TRUE); …… ResetEvent(wrOverlapped.hEvent); |
上面代碼中的ReadFile由於採用了非同步方式,所以它只返回資料是否已開始讀入的狀態,並不返回實際的讀入資料,即ReadFile中的nRealRead無效。實際讀入的資料是由GetOverlappedResult函數返回的,該函數的最後一個參數值為TRUE,表示它等待非同步作業結束後才返回到應用程式,此時,GetOverlappedResult函數與WaitForSingleObject函數等效。
當採用非同步方式時,在用CreateFile開啟串口裝置時,CreateFile函數的參數fdwAttrsAndFlags必須設為FILE_FLAG_ OVERLAPPED。在Windows中,只有在串列裝置上才支援非同步檔案讀寫,並且,GetOverlappedResult函數也只支援串列裝置或用DeviceloControl函數開啟的檔案。
若對連接埠資料的回應時間要求較嚴格,可採用事件驅動方式。事件驅動方式通過設定事件通知,當所希望的事件發生時,Windows發出該事件已發生的通知,這與DOS環境下的中斷方式很相似。Windows定義了9種串口通訊事件,較常用的有以下三種:
EV_RXCHAR:接收到一個位元組,並放入輸入緩衝區;
EV_TXEMPTY:輸出緩衝區中的最後一個字元,發送出去;
EV_RXFLAG:接收到事件字元(DCB結構中EvtChar成員),放入輸入緩衝區。
在用SetCommMask()指定了有用的事件後,應用程式可調用WaitCommEvent()來等待事件的發生。SetCommMask(hComm,0)可使WaitCommEvent()中止。例如:
COMSTAT comStat; DWORD dwEvent; SetCommMask(hComport,EV_RXCHAR); …… if(WaitCommEvent(hComport,&&dwEvent,NULL)) if((dwEvent&&EV_RXCHAR)&&&&comstat.cbInQue) ReadFile(hComport,inBuffer,comstat.cbInQue,&&nRealRead,NULL); |
程式中,我們首先用SetCommMask函數設定事件代碼,上面的代碼中為EV_RXCHAR,表示接收到一個字元時觸發這一事件,然後調用WaitCommEvent函數等待該事件的發生。注意,WaitCommEvent函數第3個參數1pOverlapped可以是一個OVERLAPPED結構的變數指標,也可以是NULL,當用NULL時,表示該函數是同步的,否則表示該函數是非同步。
在一般要求情況下,查詢方式是一種最直接的讀串口方式。但定時查詢存在一個致命弱點,即查詢是定時發生的,可能發生得過早或過晚。在資料變化較快的情況下,特別是主控電腦的串口通過擴充板擴充至多個時,需定時地對所有串口輪流查詢,此時容易發生資料的丟失。雖然定時間隔越小,資料的即時性越高,但系統的資源也被佔去越多。
Windows中提出檔案讀寫的非同步方式,主要是針對檔案I/O相對較慢的速度而進行的改進,它利用了Windows的多線程結構。雖然在Windows中沒有實現任何對檔案I/O的非同步作業,但它卻能對串口進行非同步作業。採用非同步方式,可以提高系統的整體效能,在對系統強壯性要求較高的場合,建議採用這種方式。
事件驅動方式是一種高效的串口讀方式。這種方式的即時性較高,特別是對於擴充了多個串口的情況,並不要求像查詢方式那樣定時地對所有串口輪流查詢,而是像中斷方式那樣,只有當設定的事件發生時,應用程式得到Windows作業系統發出的訊息後,才進行相應處理,避免了資料丟失。在即時性要求較高的場合,筆者建議採用這種方式。