?? 實戰串行通訊.txt
字號:
實戰串行通訊
by 羅云彬,2001.1
--------------------------------------------------------------------------------
很長時間沒有寫編程文章了,最近為了編一個串行通訊的終端程序忙了近一個月,發現了在 Win32 的通訊 API 中有不少的問題,還有許多在其他串行通訊文章中沒有談到的問題,在這里準備集中談一下,大家請下載我這一個月的“成果”:迷你終端測試一下,看看還有沒有什么問題。有關串行通訊方面的基礎文章,可以參考《Serial communications in Microsoft Win32》和《串行通訊編程點滴》。本文不是全面的講述如何編寫串行通訊程序,而是討論一些實際遇到的問題。
1 選擇通訊方式 -- 同步還是非同步
正如在《Serial communications in Microsoft Win32》等文章中提到的,同步(NonOverLapped)方式是比較簡單的一種方式,編寫起來代碼的長度要明顯少于異步(OverLapped)方式,我開始用同步方式編寫了整個子程序,在 Windows98 下工作正常,但后來在 Windows2000下測試,發現接收正常,但一發送數據,程序就會停在那里,原因應該在于同步方式下如果有一個通訊 Api 在操作中,另一個會阻塞直到上一個操作完成,所以當讀數據的線程停留在 WaitCommEvent 的時候,WriteFile 就停在那里。我又測試了我手上所有有關串行通訊的例子程序,發現所有使用同步方式的程序在 Windows 2000 下全部工作不正常,對這個問題我一直找不到解決的辦法,后來在 Iczelion 站點上發現一篇文章提到 NT 下對串行通訊的處理和 9x 有些不同,根本不要指望在 NT 或 Windows 2000 下用同步方式同時收發數據,我只好又用異步方式把整個通訊子程序重新寫了一遍。
所以對于這個問題的建議是:如果程序只打算工作在 Win9x 下,為了簡單起見,可以用同步方式寫程序,如果程序打算在 NT 下也可以工作的話,就必須用異步方式寫。
2 Win32 通訊 API Bug 之一 --- CommConfigDialog
CommConfigDialog 是彈出系統內置串口設置對話框的 API,我們在設備管理器中設置串口參數的對話框就是這個,使用這個 API 時不用先打開端口,它并不針對一個已打開的端口,而是僅僅是把 DCB 的內容填寫到對話框中,當按了 OK 后把輸入的結果存回到 DCB 數據結構中,至于什么時候把結果設置到串口上,那就是你自己要做的事情了。
CommCinfigDialog 的定義如下:
BOOL CommConfigDialog(
LPTSTR lpszName, // pointer to device name string
HWND hWnd, // handle to window
LPCOMMCONFIG lpCC // pointer to comm. configuration structure
);
但在使用中發現,對話框有時能出來,有時出不來,最后總結的經驗是問題出在 COMMCONFIG 結構的 dwSize 字段上,COMMCONFIG 的定義如下:
typedef struct _COMM_CONFIG {
DWORD dwSize;
WORD wVersion;
WORD wReserved;
DCB dcb;
DWORD dwProviderSubType;
DWORD dwProviderOffset;
DWORD dwProviderSize;
WCHAR wcProviderData[1];
} COMMCONFIG, *LPCOMMCONFIG;
在參數中,wVersion 要填 100h,dwProviderSubType 要填 1,但 dwSize 就不能填 sizeof COMMCONFIG 了,我發現好象一定要把 dwSize 設置為比 sizeof COMMCONFIG 對話框才能出來,所以我用的代碼中定義了一個足夠大的緩沖區作為結構的地址:
_CommConfigDialog proc
local @stCC[256]:BYTE
pushad
invoke RtlZeroMemory,addr @stCC,sizeof @stCC
mov (COMMCONFIG ptr @stCC).dwSize,256
mov (COMMCONFIG ptr @stCC).wVersion,100h
mov (COMMCONFIG ptr @stCC).dwProviderSubType,1
invoke CommConfigDialog,addr [esi].szPortName,[esi].hWnd,addr @stCC
popad
ret
_CommConfigDialog endp
3 Win32 通訊 API Bug 之二--- BuildCommDCB
BuildCommDCB 的功能是把一個字符串如 com1:9600,n,8,1 這樣的轉換到具體的數據填寫到 DCB 中,但使用中也存在問題,我發現我用它轉換象 com1:9600,e,7,1 之類的帶校驗位的字符串,它總是無法把這個 e 給我轉換過去,設置好串口一看,成了 9600,n,7,1,而上面提到的 CommConfigDialog 返回的結果用來設置串口卻是正確的,經過比較,發現問題出在 DCB.fbits.fParity 這個 bit 上,只有把這個 bit 置 1,校驗位才是有效的,而 BuildCommDCB 恰恰是漏了這個 bit,所有如果你要使用 BuildCommDCB,別忘了補充把 DCB.fbits.fParity 設置回去,我用的代碼是:
_BuildCommDCB proc _lpszPara,_lpstDCB
pushad
mov esi,_lpstDCB
assume esi:ptr DCB
invoke RtlZeroMemory,esi,sizeof DCB
invoke BuildCommDCB,_lpszPara,esi
;********************************************************************
; 根據校驗位補充設置 DCB 中的 DCB.fbits.fParity 字段
;********************************************************************
mov dword ptr [esi].fbits,0010b
cld
@@:
lodsb
or al,al
jz @F
cmp al,'='
jz _BCD_Check
cmp al,','
jnz @B
_BCD_Check:
lodsb
or al,al
jz @F
or al,20h
cmp al,'n'
jnz @B
;********************************************************************
; 掃描到 =n 或 ,n 則取消校驗位
;********************************************************************
mov esi,_lpstDCB
and dword ptr [esi].fbits,not 0010b
@@:
popad
ret
_BuildCommDCB endp
4 Win32 通訊編程的一般流程
由于同步方式相對比較簡單,在這里講述的是異步方式的流程,在其他的很多文章里提到了 Windows 通訊 API 有二十多個,它們是:
BuildCommDCB
BuildCommDCBAndTimeouts
ClearCommBreak
ClearCommError
CommConfigDialog
EscapeCommFunction
GetCommConfig
GetCommMask
GetCommModemStatus
GetCommProperties
GetCommState
GetCommTimeouts
GetDefaultCommConfig
PurgeComm
SetCommBreak
SetCommConfig
SetCommMask
SetCommState
SetCommTimeouts
SetDefaultCommConfig
SetupComm
TransmitCommChar
WaitCommEvent
我剛看到這些 API 的時候,都不知道如何使用它們,但并不是所有這些 API 都是必須用的,比如說你要檢測當前串口的設置可以只用 SetCommState 而不用 GetCommProperties 和 GetCommConfig,雖然它們返回的信息可能更多。同樣,如果有些值你想用缺省的,比如緩沖區的大小和超時的時間等等,那么 SetupComm 和 BuildCommDCBAndTimeouts、SetCommTimeouts 也可以不用,TransmitCommChar 是馬上在發送序列中優先插入發送一個字符用的,平時也很少用到,下面講的是必須用到的 API 和使用步驟:
建立 Event -- 用 CreateEvent
invoke CreateEvent,NULL,TRUE,FALSE,NULL
用異步方式操作串口必須要定義 OVERLAPPED 結構,其中的 hEvent 必須自己建立,你要定義兩個 OVERLAPPED 結構,一個用于讀一個用于寫,當然也必須建立兩個 Event,把它們放入 OVERLAPPED.hEvent
打開串口 -- 用 CreateFile
invoke CreateFile,addr szPortName,GENERIC_READ or GENERIC_WRITE,0,NULL,OPEN_EXISTING,FILE_FLAG_OVERLAPPED,NULL
注意用異步方式必須指定 FILE_FLAG_OVERLAPPED,而文件方式必須 OPEN_EXISTING,讀寫必須是 GENERIC_READ or GENERIC_WRITE
設置串口參數 -- 用 SetCommState
invoke SetCommState,hCom,addr dcbx
hCom 是前面打開成功后返回的句柄,dcbx 是數據結構 DCB,里面包括了通訊的具體參數,至于這個參數的建立,你可以自己填寫,也可以用前面提到的 BuildCommDCB 或 CommConfigDialog 填寫
建立讀數據的線程
到這里,就可以開始讀數據了,一般我們是在主線程中寫數據,因為寫是我們可以控制的,而讀的時候我們不知道數據什么時候會到,所以要建立一個線程專門用來讀數據,在這個線程中,我們循環地用 ReadFile 讀串口,同時用 WaitCommEvent 檢測線路狀態。
如果要檢測通訊狀態,如 CTS 信號,RingIn 等等 -- 用 SetCommMask、WaitCommEvent、ClearCommError、GetCommModemStatus
invoke SetCommMask,hCom,EV_BREAK or EV_CTS or EV_DSR or EV_ERR or EV_RING or EV_RLSD or EV_RXCHAR or EV_RXFLAG or EV_TXEMPTY
SetCommMask 指定 WaitCommEvent 要等待的事件名稱,具體的參數請查手冊
invoke WaitCommEvent,hCom,addr dwEvent,NULL
WaitCommEvent 等待一直到 SetCommMask 指定事件之一發生
invoke ClearCommError,hCom,addr dwError,addr stComStat
在 WaitCommEvent 以后,要用 ClearCommError 清除事件的 Flag,以便進行下一輪 WaitCommEvent,同時這個 API 可以獲得更詳細的事件信息
invoke GetCommModemStatus,hCom,addr dwModemStatus
同樣,GetCommModemStatus 是用來獲得串口線路狀態的,如 CTS、RING 等等,當 WaitCommEvent 返回時,只是指出了如 CTS 等等狀態有變化,但具體是變成 On 還是 Off 了還要靠這個 API 去取得更詳細的信息
讀數據 -- 用 ReadFile
invoke ReadFile,hCom,addr szBuffer,sizeof szBuffer,addr dwBytesRead,addr stReadState
最后一個參數是開頭定義的 OVERLAPPED 結構的地址,指定了它就表示是用異步方式的讀方式,這個 API 會馬上返回,接下去要用
invoke GetOverlappedResult,hCom,addr stReadState,addr dwBytesRead,FALSE
將其余的數據讀完
結束時關閉端口 -- 停止 WaitCommEvent 的等待以及關閉端口 CloseHandle
平時程序會停留在 WaitCommEvent 的等待中,當要終止線程的時候,必須是程序從 WaitCommEvent 中退出來,這時候要用
按照 Win32 手冊上的說明,參數為 NULL 的 SetCommMask 會使另一個線程中的 WaitCommEvent 馬上返回,然后就是用 CloseHandle 關閉端口
invoke CloseHandle,hCom
5 Win32 通訊 API Bug 之二--- SetCommMask 和 WaitCommEvent
嚴格的說這不應該是 Bug,而是偶然的情況,我發現有些時候我的讀線程無法結束,跟蹤發現是停在了 WaitCommEvent 上,這說明有時候 invoke SetCommMask,hCom,NULL 并不能使 WaitCommEvent 退出,我最后使用的辦法是: 在 SetCommMask 以后再執行 invoke SetEvent,stReadState.hEvent,把讀的 OVERLAPPED 結構中的 Event 置位,讓 WaitCommEvent 認為有 Event 發生,它就會馬上返回,也許這并不是普遍的情況,但如果你的程序也是停在了 WaitCommEvent 的地方,不妨一試。
6 如何編寫讀線程中的循環
按照《Serial communications in Microsoft Win32》一文中的例程,讀循環可以用:
#define READ_TIMEOUT 500 // milliseconds
DWORD dwRes;
DWORD dwRead;
BOOL fWaitingOnRead = FALSE;
OVERLAPPED osReader = {0};
// Create the overlapped event. Must be closed before exiting
// to avoid a handle leak.
osReader.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (osReader.hEvent == NULL)
// Error creating overlapped event; abort.
if (!fWaitingOnRead) {
// Issue read operation.
if (!ReadFile(hComm, lpBuf, READ_BUF_SIZE, &dwRead, &osReader)) {
if (GetLastError() != ERROR_IO_PENDING) // read not delayed?
// Error in communications; report it.
else
fWaitingOnRead = TRUE;
}
else {
// read completed immediately
HandleASuccessfulRead(lpBuf, dwRead);
}
}
if (fWaitingOnRead) {
dwRes = WaitForSingleObject(osReader.hEvent, READ_TIMEOUT);
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -