一、串口API
1. 打開串口
使用CreateFile函數可以打開串口。通常有兩種方式可以打開,一種是同步方式(NonOverlapped),另外一種異步方式(Overlapped)。
HANDLE hComm;
hComm = CreateFile( gszPort, //串口名
GENERIC_READ|GENERIC_WRITE //讀寫
0, //注意:串口為不可共享設備,本參數須為0
0,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED, //異步方式
0);
if(hComm == INVALID_HANDLE_VALUE) //打開串口失敗處理
······
2. 配置串口
DCB(Device Control Block)結構定義了串口通信設備的控制設置,有3種方式可以初始化DCB。
- 通過GetCommState函數得到DCB的初始值:
DCB dcb;
memset(&dcb, 0, sizeof(dcb));
if(!GetCommState(hComm, &dcb)) …… //錯誤處理
else …… //已准備就緒
- 用BuildCommDCB函數初始化DCB結構:
DCB dcb;
memset(&dcb, 0, sizeof(dcb));
dcb.DCBlength = sizeof(dcb);
if(!BuildCommDCB(“9600,n,8,1”, &dcb)) …… //參數配置錯誤
else …… //已准備就緒
- 用SetCommState函數手動設置DCB初值:
DCB dcb;
memset(&dcb, 0, sizeof(dcb));
if(!GetCommState(hComm, &dcb)) return FALSE;
dcb.BaudRate = CBR_9600;
3. 流控設備
流控制有如下兩種設置:
- 硬件流控制:硬件流控有兩種,DTE/DSR方式和RTS/CTS方式。這與DCB結構的初始化有關系,建議采用標准流行的流控方式,采用硬件流控時,DTE、DSR、RTS、CTS的邏輯位直接影響到數據的讀寫及收發數據的緩沖區控制。
- 軟件流控制:串口通信中采用特殊字符XON和XOFF作為控制串口數據的收發。
注意:在不設置流控制方式或軟件流控的情況下,基本上不會出現什么問題,但在硬件流控下,規范的RTS_CONTROL_HANDSHAKE流控方式的含義本來是當緩沖區快滿的時候RTS會自動OFF通知對方暫停發送,當緩沖區重新空出來的時候,RTS會自動ON,但很多時候當RTS變OFF以后即使已經清空了緩沖區,RTS也不會自動的ON,造成對方停在那里不發送了。所以,如果要用硬件流控制的話,還要在接收后最好加上檢測緩沖區大小的判斷,具體做法是使用ClearCommError后返回COMSTAT.cbInQue,當緩沖區已經空出來的時候,要使用invoke(EscapeCommFunction,hComm,SETRTS)重新將RTS設置為ON。
4. 串口讀寫操作
串口讀寫有兩種方式:同步方式(NonOverlapped)和異步方式(Overlapped)。同步方式指必須完成了讀寫操作,函數才返回,這可能會使程序無響應,因為如果在讀寫時發生了錯誤,永遠不返回就會出錯,可能線程將停在原地。而異步方式則靈活的多,一旦讀寫不成功,就將讀寫掛起,函數直接返回,可以通過GetLastError函數得知讀寫未成功的原因,所以串口讀寫常常采用異步方式操作。
ReadFile()函數用於完成讀操作,異步方式的讀操作為:
DWORD dwRead;
BOOL fWaitingOnRead = FALSE;
OVERLAPPED osReader;
memset(&osReader, 0, sizeof(osReader));
osReader.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if(osReader.hEvent == NULL) …… //錯誤處理
if(!fWaitingOnRead)
{
if(!ReadFile(hComm, lpBuf, READ_BUF_SIZE, &dwRead, &osReader)) //讀串口
{
if(GetLastError() != ERROR_IO_PENDING) …… //報告錯誤
else fWaitingOnRead = TRUE;
}
}
else
{
//讀取完成,不必在調用GetOverlappedResults函數
HandleASuccessfulRead(lpBuf, dwRead);
}
//如果讀操作被掛起,可以調用WaitForSingleObject()函數或
//WaitForMuntilpleObjects()等待讀操作完成或者超時發生,
//再調用GetOverlappedResult()得到想要的信息。
if(fWaitingOnRead)
{
dwRes = WaitForSingleObject(osReader.hEvent, READ_TIMEOUT);
switch(dwRes)
{
case WAIT_OBJECT_0: //完成讀操作
if(!GetOverlappedResult(hComm, &osReader, &dwRead, FALSE)) …… //錯誤
else …… //全部讀取成功
HandleASuccessfulRead(lpBuf, dwRead);
fWaitintOnRead = FALSE;
break;
case WAIT_TIMEOUT: //操作尚未完成
……. //處理其他任務
break;
default:
…… //出現錯誤
break;
}
}
注意上述代碼在處理多線程串口在windows系列下存在一些問題,修改完成后代碼參考1.4節。
5. 關閉串口
程序結束或需要釋放串口資源時,必須正確關閉串口。調用CloseHandle函數關閉串口的句柄即可,
CloseHandle(hComm);
值得注意的是,在關閉串口前必須保證讀寫串口線程已經退出,否則會引起誤操作,一般采用的辦法是使用事件驅動機制,啟動一事件,通知串口讀寫線程強制退出。
6. 其他問題
串口通信中其他必須處理的問題主要有如下幾個:
- 檢測通信事件:用SetCommMask()設置想要得到的通信事件的掩碼,再調用WaitCommEvent()檢測通信事件的發生。可設置事件標志有EV_BREAK \ EV_VTS \ EV_DSR \ EV_ERR \ EV_RING \ EV_RLSD \ EV_RXCHAR \ EV_RXFLAG \ EV_TXEMPTY。
- 處理通信超時:在通信中,超時是一個很重要的考慮因素,因為數據接收過程中由於某種原因突然中斷或停止,如果不采取超時控制機制,將會使得I/O線程被掛起或無限阻塞。超時設置分兩步,首先設置COMMTIMEOUTS結構的5個變量,然后調用SetcommTimeouts()設置超時值,對於使用異步方式讀寫的操作,如果操作掛起后,異步成功完成了讀寫,WaitForSingleObject()或WaitForMultipleObjects()將返回WAIT_OBJECT_0,GetOverlappedResult()返回TRUE。其實還可以用GetCommTimeouts()得到系統初始值。
- 錯誤處理和通信狀態:在串口通信中,可以會產生很多的錯誤,使用ClearCommError()可以檢測錯誤並且清除錯誤條件。
- WaitCommEvent()返回時,只是指出了如CTS等等狀態有變化。但要了解具體變化情況必須使用GetCommModemStatus()獲得串口線路狀態更詳細的信息。
二、串口操作方式
1. 同步方式
同步(NonOverlapped)方式是比較簡單的一種方式,編寫代碼長度明顯少於異步(Overlapped)方式。同步方式中,讀串口的函數試圖在串口的接收緩沖區中讀取規定數據的數據,直到規定數據的數據全部被讀出或設定超時時間已到時才返回。例如:
COMMTIMEOUTS timeOver;
memset(&timeOver, 0, sizeof(timeOver));
DWORD timeMultiplier, timeConstant;
……
timeOver.ReadTotalTimeoutMultiplier = timeMultiplier;
timeOver.ReadTotalTimeoutConstant = timeConstant;
SetCommTimeouts(hComm, &timeOver);
……
ReadFile(hComm, inBuffer, nWantRead, &nRealRead, NULL); //NULL指采用同步文件讀寫
如果所規定的待讀取數據的數目nWantRead較大且設定的超時時間較長,而接收緩沖區中數據較少,則可能引起線程阻塞。解決這一問題的方法是檢查COSTAT結構的cbInQue成員,該成員的大小即為接收緩沖區中處於等待狀態的實際個數。如果令nWantRead的值等於COMSTAT.cbInQue,就能很好的防止線程阻塞。
2. 異步方式
在異步方式中,利用Windows的多線程結構,可以讓串口的讀寫操作在后台進行,而應用程序的其他部分在前台執行。例如:
OVERLAPPED wrOverlapped;
COMMTIMEOUTS timeOVer;
memset(&timeOver, 0, sizeof(timeOver));
DWORD timeMultiplier, timeConstant;
…… //給timeMultiplier, timeConstant賦值
timeOver.ReadTotalTimeoutMultiplier = timeMultiplier;
timeOver.ReadTotalTimeoutConstant = timeConstant;
SetCommTimeouts(hComm, &timeOver);
wrOverlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
……
ReadFile(hComm, inBuffer, nWantRead, &nRealRead, &wrOverlapped);
GetOverlappedResult(hComm, &wrOverlapped, &nRealRead,TRUE);
……
ResetEvent(wrOverlapped.hEvent);
上面代碼中的ReadFile由於采用了異步方式,所以只返回數據是否已經開始讀入的狀態,並不返回實際的讀入數據,即ReadFile中的nRealRead無效。實際讀入的數據由GetOverlappedResult返回的,該函數的最后一個參數值為TRUE,表示它等待異步操作結束后才返回到應用程序,此時,GetOverlappedResult與WaitForSingleObject函數無效。
3. 查詢方式
即一個進程中的某一線程定時地查詢串口的接收緩沖區,如果緩沖區中有數據,就讀取數據;若緩沖區沒有數據,該線程將繼續執行,因此會占用大量的CPU時間,它實際上是同步方式的一種派生。例如:
COMMTIMEOUTS timeOver,
Memset(&timeOver, 0, sizeof(timeOver));
timeOver.ReadIntervalTimeout = MAXWORD;
SetCommTimeouts(hComm, &timeOver);
……ReadFile(hComm, inBuffer, nWantRead, &nRealRead, NULL);
除了COMMTIMEOUTS結構的變量timeOver設置不同外,查詢方式與同步方式在程序代碼方面很類似,但二者的工作方式卻差別很大。盡管ReadFile采用的也是同步文件讀寫方式,但由於timeOver的區間超過時間設置為MAXWORD,所以ReadFile每次將讀出接收隊列中的所有處於等待狀態的數據,一次最多可讀出nWantRead個字節的數據。
4. 事件驅動方式
若對端口數據的響應時間要求較嚴格,可采用事件驅動方式。事件驅動方式通過設置事件通知,當所希望的事件發生時,Windows發出該事件已經發生的通知。Windows定義了9中串口通信事件,常用的有以下3中:
- EV_RXCHAR:接收到一個字節,並放入輸入緩沖區。
- EV_TXEMPTY:輸出緩沖區中的最后一個字符,發送出去。
- EV_RXFLAG:接收到事件字符(DCB結構中的EvtChar成員),放入輸入緩沖區。
在用SetCommMask()制定了有用的事件后,應用程序可調用WaitCommEvent()來等待事件的發生。SetCommMask可使WaitCommEvent()中止。例如:
COMSTAT comStat;
DWORD dwEvent;
SetCommMask(hComm, EV_RXCHAR);
……
if(WaitCommEvent(hComm, &dwEvent, NULL))
if((dwEvent & EV_RXCHAR) && comstat.cbInQue)
ReadFile(hComm, inBuffer, comstat.cbInQue, &nRealRead, NULL);
5. 總結
一般要求情況下,查詢方式是一種最直接的讀串口的方式。但定時查詢存在一個致命的弱點,即查詢是定時發生的,可能發生的過早或過晚。在數據變化較快的情況下,特別是主控計算機的串口通過擴展板擴展多個時,需定時對所有串口輪流查詢,容易發生數據的丟失。雖然定時間隔越小,數據的實時性越高,但系統的資源也被占用越多。
Windows中提出文件讀寫的異步方式,主要是針對文件IO相對較慢的速度而進行的改進,它利用了系統的多線程結構,雖然在Windows中沒有實現任何對文件IO的異步操作,但它卻能對串口進行異步操作。采用異步方式,可以提高系統整體性能,在對系統強壯性要求高的場合,建議采用這種方式。
事件驅動方式是一種高效的串口讀方式。這種方式實時性較高,特別對擴展了多個串口的情況,並不要求像查詢方式那樣定時地對所有串口輪詢,而像中斷方式那樣,只有當設定的事件發生時,應用程序得到windows操作系統發出的消息后,才進行相應處理,以免數據丟失。