//========================================================================
//TITLE:
// WinCE虛擬串口驅(qū)動(dòng)(一)
//AUTHOR:
// norains
//DATE:
// Saturday 28-March-2009
//Environment:
// WINDOWS CE 5.0
//========================================================================
用過(guò)串口進(jìn)行開(kāi)發(fā)的朋友應(yīng)該都知道,串口驅(qū)動(dòng)是一個(gè)典型的獨(dú)占設(shè)備。簡(jiǎn)單點(diǎn)來(lái)說(shuō),就是在成功地調(diào)用CreateFile打開(kāi)串口之后,沒(méi)有通過(guò)CloseHandle進(jìn)行關(guān)閉,是無(wú)論如何都不能再次調(diào)用CreateFile來(lái)再次打開(kāi)相同的串口。
有的朋友可能會(huì)覺(jué)得莫名奇妙,為什么微軟要在這上面做限制呢?但其實(shí)從另一個(gè)角度來(lái)講,微軟這么做是非常有道理的。以接收數(shù)據(jù)為例子,在驅(qū)動(dòng)里面會(huì)有一定的緩存,用來(lái)保留一定量的數(shù)據(jù)。當(dāng)通過(guò)ReadFile來(lái)獲取數(shù)據(jù)時(shí),驅(qū)動(dòng)就會(huì)將緩存給清空,然后再繼續(xù)接收數(shù)據(jù)。如果串口不是獨(dú)占設(shè)備,可以多次打開(kāi),那么在讀取數(shù)據(jù)上面就會(huì)有問(wèn)題:應(yīng)該什么時(shí)候才清空緩存?比方說(shuō),其中一個(gè)線程通過(guò)ReadFile來(lái)獲得了數(shù)據(jù),那么驅(qū)動(dòng)應(yīng)不應(yīng)該將緩沖清空?如果清空,那另一個(gè)線程也想獲得同樣的數(shù)據(jù)進(jìn)行分析,那就會(huì)產(chǎn)生數(shù)據(jù)丟失;如果不清空,萬(wàn)一之前已經(jīng)通過(guò)ReadFile獲取數(shù)據(jù)的線程再次進(jìn)行讀取,那么它將會(huì)得到同樣重復(fù)的數(shù)據(jù)。如果想要在這多個(gè)進(jìn)程中維持?jǐn)?shù)據(jù)的同步,肯定要額外增加相應(yīng)的標(biāo)識(shí),但這樣就會(huì)加大了驅(qū)動(dòng)的復(fù)雜度,并且也無(wú)法和別的驅(qū)動(dòng)保持一致。因此,微軟對(duì)串口實(shí)行獨(dú)占設(shè)備的策略,是非常正確的。
但,正確并不代表放之四海而皆準(zhǔn),在某些特殊的情況下,我們還是需要非獨(dú)占性質(zhì)的串口。簡(jiǎn)單地舉個(gè)例子,在手持PND GPS設(shè)備中,導(dǎo)航軟件肯定是必須要能通過(guò)串口進(jìn)行數(shù)據(jù)獲取來(lái)定位;可另一方面,我的另一個(gè)應(yīng)用程序又想獲得GPS數(shù)據(jù)進(jìn)行系統(tǒng)時(shí)間的校準(zhǔn)。在這情形之下,我們就必須使用一個(gè)非獨(dú)占性質(zhì)的串口設(shè)備。
為了簡(jiǎn)化設(shè)計(jì),該串口設(shè)備的驅(qū)動(dòng)我們約定如下:
1.同一時(shí)間只能有一個(gè)進(jìn)程對(duì)外輸出數(shù)據(jù),其余進(jìn)程只能在該進(jìn)程輸出完畢之后才能進(jìn)行。
2.程序不應(yīng)該主動(dòng)調(diào)用ReadFile來(lái)輪詢獲取數(shù)據(jù)。而是通過(guò)WaitCommEvent進(jìn)行檢測(cè),當(dāng)返回的狀態(tài)中具備EV_RXCHAR時(shí)才調(diào)用ReadFile。并且該調(diào)用必須在一定的時(shí)間間隔之內(nèi),而且為了不丟失數(shù)據(jù),緩沖大小一定要等于或大于READ_BUFFER_LENGTH。
之所以有如上約束,完全是出于設(shè)計(jì)簡(jiǎn)便考慮。
非獨(dú)占式串口驅(qū)動(dòng)主要是處理數(shù)據(jù)的分發(fā),可以和具體的硬件分開(kāi),換句話說(shuō),該驅(qū)動(dòng)是基于原有的串口驅(qū)動(dòng)之上,實(shí)際上并“沒(méi)有”該設(shè)備,因此我們將該非獨(dú)占式串口稱(chēng)之為“虛擬串口驅(qū)動(dòng)”。這樣設(shè)計(jì)的優(yōu)勢(shì)很明顯,可以不用理會(huì)具體的硬件規(guī)格,只要采用的是WinCE系統(tǒng),并且原來(lái)已經(jīng)具備了完善的串口驅(qū)動(dòng),那么該虛擬串口驅(qū)動(dòng)就能工作正常。
接下來(lái)我們來(lái)看看該虛擬串口的具體實(shí)現(xiàn)。
麻雀雖小,五官俱全,雖然說(shuō)該驅(qū)動(dòng)是“虛擬”的,但畢竟還是“驅(qū)動(dòng)”,該有的部分我們還是要具備的。
驅(qū)動(dòng)的前綴為VSP,取自于Virtual Serial Port之意。
該驅(qū)動(dòng)必須實(shí)現(xiàn)如下函數(shù):
- VSP_Close
- VSP_Deinit
- VSP_Init
- VSP_IOControl
- VSP_Open
- VSP_PowerDown
- VSP_PowerUp
- VSP_Read
- VSP_Seek
- VSP_Write
因?yàn)榇隍?qū)動(dòng)是流設(shè)備,又和具體的電源管理五官,故VSP_Seek,VSP_PowerDown,VSP_PowerUp這些函數(shù)可以不用處理,直接返回即可。
現(xiàn)在來(lái)看一下VSP_Open函數(shù)。
VSP_Open函數(shù)我們大致需要如下流程處理事情:
1.判斷當(dāng)前的是否已經(jīng)打開(kāi)串口,如果已經(jīng)打開(kāi),直接跳到4.
2.獲取需要打開(kāi)的串口序號(hào),并打開(kāi)該串口。如果打開(kāi)失敗,直接跳到5.
3.打開(kāi)數(shù)據(jù)監(jiān)視進(jìn)程(注:該部分在數(shù)據(jù)讀取部分進(jìn)行分析)。
4.標(biāo)識(shí)記數(shù)(即g_uiOpenCount)增加1。
5.函數(shù)返回
流程1:
全局變量g_uiOpenCount用來(lái)保存打開(kāi)的記數(shù),所以只要判斷該數(shù)值是否為0即可確定是否應(yīng)該打開(kāi)串口:
- if(g_uiOpenCount != 0)
- {
- goto SET_SUCCEED_FLAG;
- }
流程2:
為了讓程序更具備靈活性,所打開(kāi)的串口序號(hào)我們不直接在驅(qū)動(dòng)中設(shè)定,而是通過(guò)讀取注冊(cè)表的數(shù)值獲得:
- if(reg.Open(REG_ROOT_KEY,REG_DEVICE_SUB_KEY) == FALSE)
- {
- RETAILMSG(TRUE,(TEXT("[VSP]:Failed to open the registry/r/n")));
- goto LEAVE_CRITICAL_SECTION;
- }
-
- //Get the MAP_PORT name
- reg.GetValueSZ(REG_MAP_PORT_NAME,&vtBuf[0],vtBuf.size());
接下來(lái)便是打開(kāi)具體的串口:
- g_hCom = CreateFile(&vtBuf[0],GENERIC_READ | GENERIC_WRITE ,0,NULL,OPEN_EXISTING,0,NULL);
- if(g_hCom == INVALID_HANDLE_VALUE )
- {
- RETAILMSG(TRUE,(TEXT("[VSP]Failed to map to %s/r/n"),&vtBuf[0]));
- goto LEAVE_CRITICAL_SECTION;
- }
- else
- {
- RETAILMSG(TRUE,(TEXT("[VSP]Succeed to map to %s/r/n"),&vtBuf[0]));
- }
流程3:
創(chuàng)建進(jìn)程來(lái)監(jiān)視數(shù)據(jù):
- InterlockedExchange(reinterpret_cast<LONG *>(&g_bExitMonitorProc),FALSE);
- CloseHandle(CreateThread(NULL,NULL,MonitorCommEventProc,NULL,NULL,NULL));
流程4:
成功打開(kāi)記數(shù)
- SET_SUCCEED_FLAG:
- g_uiOpenCount ++;
- bResult = TRUE;
流程5:
函數(shù)返回:
- LEAVE_CRITICAL_SECTION:
- LeaveCriticalSection(&g_csOpen);
- return bResult;
和VSP_Open密切對(duì)應(yīng)的是VSP_Close,該函數(shù)流程基本和VSP_Open相反處理:
1.打開(kāi)記數(shù)(g_uiOpenCount)減1。如果g_uiOpenCount為不為0,跳轉(zhuǎn)3。
2.退出監(jiān)視數(shù)據(jù)進(jìn)程,并且關(guān)閉打開(kāi)的串口。
3.函數(shù)返回。
流程1和流程2處理如下:
- g_uiOpenCount --;
- if(g_uiOpenCount == 0)
- {
- //Notify the monitor thread to exit.
- InterlockedExchange(reinterpret_cast<LONG *>(&g_bExitMonitorProc),TRUE);
- DWORD dwMask = 0;
- GetCommMask(g_hCom,&dwMask);
- SetCommMask(g_hCom,dwMask);
-
- while(InterlockedExchange(reinterpret_cast<LONG *>(&g_bMonitorProcRunning),TRUE) == TRUE)
- {
- Sleep(20);
- }
- InterlockedExchange(reinterpret_cast<LONG *>(&g_bMonitorProcRunning),FALSE);
-
- CloseHandle(g_hCom);
- g_hCom = NULL;
- }
我們必須確保VSP_Open和VSP_Close中的某一個(gè)必須要全部處理完才能再次調(diào)用,否則在處理過(guò)程中如果又再次調(diào)用本函數(shù)或相對(duì)應(yīng)的加載或卸載函數(shù),那么一定會(huì)引發(fā)我們不可預(yù)料的情況,所以我們?cè)谶@兩個(gè)函數(shù)中增加了關(guān)鍵段,以維持處理上的同步:
- EnterCriticalSection(&g_csOpen);
- ...
- LeaveCriticalSection(&g_csOpen);
其余的接口,算起來(lái)最簡(jiǎn)單的是VSP_Write,只要確定同一時(shí)間只能有唯一的一個(gè)進(jìn)程進(jìn)行輸出即可:
- EnterCriticalSection(&g_csWrite);
- DWORD dwWrite = 0;
- WriteFile(g_hCom,pBuffer,dwNumBytes,&dwWrite,NULL);
- LeaveCriticalSection(&g_csWrite);
在完成VSP_Read之前,我們先來(lái)看另外一個(gè)函數(shù):WaitCommEvent。這是串口驅(qū)動(dòng)特有的,目的是有某些時(shí)間發(fā)生時(shí),能夠第一時(shí)間激活線程。該函數(shù)和驅(qū)動(dòng)的MMD層有關(guān),是MDD層的應(yīng)用程序級(jí)別接口。具體串口的PDD層,WaitCommEvent函數(shù)體內(nèi)也僅僅是調(diào)用了COM_IOControl接口,然后傳入IOCTL_SERIAL_WAIT_ON_MASK控制碼而已。也就是說(shuō),調(diào)用WaitCommEvent的代碼,就相當(dāng)于如此調(diào)用COM_IOControl:
- DeviceIoControl(hCom,
- IOCTL_SERIAL_WAIT_ON_MASK,
- NULL,
- 0,
- pOutBuf,
- dwOutBufLen,
- &dwReturn,
- NULL);
換句話說(shuō),如果想讓虛擬串口驅(qū)動(dòng)支持WaitCommEvent函數(shù),我們只需要在VSP_IOControl處理IOCTL_SERIAL_WAIT_ON_MASK控制碼即可:
- BOOL VSP_IOControl(
- DWORD dwHandle,
- DWORD dwIoControlCode,
- PBYTE pBufIn,
- DWORD dwBufInSize,
- PBYTE pBufOut,
- DWORD dwBufOutSize,
- PDWORD pBytesReturned
- )
- {
- ...
-
- switch(dwIoControlCode)
- {
- ...
-
- case IOCTL_SERIAL_WAIT_ON_MASK:
-
- ...
- break;
-
- ...
- }
- }
-
推而廣之,像SetCommState,SetCommTimeouts等串口特有的函數(shù),都僅僅只是對(duì)COM_IOControl函數(shù)進(jìn)行的一層封裝而已。
我們?cè)倩氐絎aitCommEvent函數(shù)??赡苡械呐笥阎苯诱J(rèn)為,我們只要在IOCTL_SERIAL_WAIT_ON_MASK段直接簡(jiǎn)單調(diào)用原有的WaitCommEvent即可:
- switch(dwIoControlCode)
- {
- ...
-
- case IOCTL_SERIAL_WAIT_ON_MASK:
- {
- //直接調(diào)用原生的WaitCommEvent,但實(shí)際是錯(cuò)誤的
- if(dwBufOutSize < sizeof(DWORD) || WaitCommEvent(g_hCom,reinterpret_cast<DWORD *>(pBufOut),NULL) == FALSE)
- {
- *pBytesReturned = 0;
- return FALSE;
- }
- else
- {
- *pBytesReturned = sizeof(DWORD);
- return TRUE;
- }
- }
-
- ...
- }
但實(shí)際上這樣是不行的。查看文檔關(guān)于WaitCommEvent函數(shù)的描述,注意事項(xiàng)中有這么一條:Only one WaitCommEvent can be used for each open COM port handle. This means that if you have three threads in your application and each thread needs to wait on a specific comm event, each thread needs to open the COM port and then use the assigned port handle for their respective WaitCommEvent calls.
也就是說(shuō),WaitCommEvent只能被一個(gè)線程調(diào)用。如果多線程都同時(shí)調(diào)用該函數(shù),會(huì)發(fā)生什么情況呢?經(jīng)過(guò)實(shí)際測(cè)試,如果多線程都調(diào)用相同的WaitCommEvent,那么在某個(gè)線程調(diào)用WaitCommEvent時(shí),之前已經(jīng)有其余的線程通過(guò)調(diào)用該函數(shù)進(jìn)行等待狀態(tài)的話,那等待的線程立馬會(huì)喚醒。簡(jiǎn)單點(diǎn)來(lái)說(shuō),就是同一時(shí)間只能有唯一的一個(gè)線程通過(guò)WaitCommEvent函數(shù)進(jìn)入等待狀態(tài)。所以,對(duì)于IOCTL_SERIAL_WAIT_ON_MASK控制碼,我們不能簡(jiǎn)單地調(diào)用WaitCommEvent函數(shù)。
在這里我們采用這么一種設(shè)計(jì),對(duì)于IOCTL_SERIAL_WAIT_ON_MASK的處理,我們是通過(guò)調(diào)用WaitForSingleObject進(jìn)行線程等待。而虛擬串口驅(qū)動(dòng),會(huì)額外開(kāi)放一個(gè)線程,該線程主要是通過(guò)調(diào)用WaitCommEvent來(lái)獲取原生串口的狀態(tài),當(dāng)狀態(tài)有通知時(shí),再發(fā)送event給等待的線程。因此,對(duì)于IOCTL_SERIAL_WAIT_ON_MASK控制碼的處理可以所作如下:
- switch(dwIoControlCode)
- {
- ...
-
- case IOCTL_SERIAL_WAIT_ON_MASK:
- {
- if(dwBufOutSize < sizeof(DWORD) || WaitForSingleObject(g_hEventComm,INFINITE) == WAIT_TIMEOUT)
- {
- *pBytesReturned = 0;
- return FALSE;
- }
- else
- {
- InterlockedExchange(reinterpret_cast<LONG *>(pBufOut),g_dwEvtMask);
- *pBytesReturned = sizeof(DWORD);
- return TRUE;
- }
- }
-
- ...
- }
驅(qū)動(dòng)額外的等待線程所做如是:
- DWORD MonitorCommEventProc(LPVOID pParam)
- {
- ...
-
- while(TRUE)
- {
- DWORD dwEvtMask = 0;
- BOOL bWaitRes = WaitCommEvent(g_hCom,&dwEvtMask,NULL);
-
- if(g_bExitMonitorProc != FALSE)
- {
- break;
- }
-
- if(bWaitRes == FALSE)
- {
- continue;
- }
-
- ...
-
- InterlockedExchange(reinterpret_cast<LONG *>(&g_dwEvtMask),dwEvtMask);
- PulseEvent(g_hEventComm);
-
- ...
-
- }
-
- ...
-
- return 0;
- }
現(xiàn)在是到考慮ReadFile實(shí)現(xiàn)的時(shí)候了。我們需要考慮到,不同進(jìn)程,在同時(shí)讀取數(shù)據(jù)時(shí),應(yīng)該能獲得相同的數(shù)據(jù)。但對(duì)于原生的串口驅(qū)動(dòng),如果再次調(diào)用ReadFile,所獲得的數(shù)據(jù)絕對(duì)是不會(huì)和之前的一樣,否則就亂套了。于是,和IOCTL_SERIAL_WAIT_ON_MASK一樣,我們這么也不能粗暴簡(jiǎn)單地調(diào)用原生的ReadFile完事。
我們轉(zhuǎn)換個(gè)思維,對(duì)于“不同進(jìn)程,在同時(shí)讀取數(shù)據(jù)時(shí),應(yīng)該能獲得相同的數(shù)據(jù)”,我們應(yīng)該是這么理解:“不同進(jìn)程,相當(dāng)短的間隔內(nèi)讀取數(shù)據(jù),應(yīng)該能獲得相同的數(shù)據(jù)”。如果要做到這點(diǎn),我們只需要設(shè)置一個(gè)讀取緩存,當(dāng)上級(jí)程序想要獲取數(shù)據(jù)時(shí),我們只需要簡(jiǎn)單地將數(shù)據(jù)返回即可。那么接下來(lái)最關(guān)鍵的是,我們應(yīng)該什么時(shí)候讀取數(shù)據(jù)?什么時(shí)候該刷新緩存呢?
分開(kāi)來(lái)說(shuō),最簡(jiǎn)單的方式,就是在監(jiān)視進(jìn)程MonitorCommEventProc中讀取數(shù)據(jù)并刷新緩存。因?yàn)樵摼€程會(huì)調(diào)用WaitCommEvent函數(shù)進(jìn)行等待,它能夠充分知道什么時(shí)候有數(shù)據(jù)進(jìn)來(lái)。只要有數(shù)據(jù)進(jìn)來(lái),我們就進(jìn)行讀取。如果之前的緩存已經(jīng)被讀取過(guò),我們就清空緩存,存入新的數(shù)據(jù);否則就在舊緩存之后添加我們新的數(shù)據(jù)。故此,完善的MonitorCommEventProc實(shí)現(xiàn)就應(yīng)該如此:
- DWORD MonitorCommEventProc(LPVOID pParam)
- {
- InterlockedExchange(reinterpret_cast<LONG *>(&g_bMonitorProcRunning),TRUE);
-
- RETAILMSG(TRUE,(TEXT("[VSP]:MonitorCommEventProc Running!/r/n")));
-
- std::vector<BYTE> vtBufRead(g_vtBufRead.size(),0);
- while(TRUE)
- {
- DWORD dwEvtMask = 0;
- BOOL bWaitRes = WaitCommEvent(g_hCom,&dwEvtMask,NULL);
-
- if(g_bExitMonitorProc != FALSE)
- {
- break;
- }
-
- if(bWaitRes == FALSE)
- {
- continue;
- }
-
- DWORD dwRead = 0;
- if(dwEvtMask & EV_RXCHAR)
- {
- EnterCriticalSection(&g_csRead);
-
- ReadFile(g_hCom,&g_vtBufRead[0],vtBufRead.size(),&dwRead,NULL);
- if(dwRead == vtBufRead.size() || g_bReaded != FALSE)
- {
- g_dwLenReadBuf = dwRead;
- g_vtBufRead.swap(vtBufRead);
- }
- else if(dwRead != 0)
- {
- if(g_dwLenReadBuf + dwRead <= g_vtBufRead.size())
- {
- g_dwLenReadBuf += dwRead;
- g_vtBufRead.insert(g_vtBufRead.end(),vtBufRead.begin(),vtBufRead.begin() + dwRead);
- }
- else
- {
- DWORD dwCover = g_dwLenReadBuf + dwRead - g_vtBufRead.size();
- std::copy(g_vtBufRead.begin() + dwCover,g_vtBufRead.begin() + g_dwLenReadBuf,g_vtBufRead.begin());
- std::copy(vtBufRead.begin(),vtBufRead.begin() + dwRead,g_vtBufRead.begin() + (g_dwLenReadBuf - dwCover));
- g_dwLenReadBuf = g_vtBufRead.size();
- }
- }
-
- g_bReaded = FALSE;
-
- DEBUGMSG(TRUE,(TEXT("[VSP]:Read data : %d/r/n"),dwRead));
-
- LeaveCriticalSection(&g_csRead);
- }
-
- if(dwEvtMask == EV_RXCHAR && ((g_dwWaitMask & EV_RXCHAR) == 0 || dwRead == 0))
- {
- //The return event mask is only EV_RXCHAR and there is not EV_RXCHAR in the wait mask.
- continue;
- }
-
- InterlockedExchange(reinterpret_cast<LONG *>(&g_dwEvtMask),dwEvtMask);
- PulseEvent(g_hEventComm);
-
- //Sleep for other thread to respond to the event
- Sleep(100);
-
- DEBUGMSG(TRUE,(TEXT("[VSP]:PulseEvent! The event-mask is 0x%x/r/n"),dwEvtMask));
-
- }
-
- RETAILMSG(TRUE,(TEXT("[VSP]:Exit the MonitorCommEventProc/r/n")));
- InterlockedExchange(reinterpret_cast<LONG *>(&g_bMonitorProcRunning),FALSE);
-
- return 0;
- }
正因?yàn)樽x取是如此實(shí)現(xiàn),所以我們才有文章開(kāi)頭的第二點(diǎn)約定:
程序不應(yīng)該主動(dòng)調(diào)用ReadFile來(lái)輪詢獲取數(shù)據(jù)。而是通過(guò)WaitCommEvent進(jìn)行檢測(cè),當(dāng)返回的狀態(tài)中具備EV_RXCHAR時(shí)才調(diào)用ReadFile(如果一直采用ReadFile來(lái)輪詢接收數(shù)據(jù),很可能會(huì)讀取重復(fù)的數(shù)據(jù))。并且該調(diào)用必須在一定的時(shí)間間隔之內(nèi)(如果間隔太久,很可能因?yàn)榫彺嬉呀?jīng)刷新,數(shù)據(jù)丟失),而且為了不丟失數(shù)據(jù),緩沖大小一定要等于或大于READ_BUFFER_LENGTH(因?yàn)橹灰x取一次數(shù)據(jù),讀取的標(biāo)識(shí)就會(huì)被設(shè)置,當(dāng)有新數(shù)據(jù)到達(dá)時(shí),會(huì)刷新緩存,導(dǎo)致數(shù)據(jù)丟失)。
這也同時(shí)解釋了MonitorCommEventProc進(jìn)程為何在PulseEvent之后會(huì)調(diào)用Sleep函數(shù)進(jìn)行短暫的休眠,其作用主要是讓驅(qū)動(dòng)的讀取進(jìn)程歇歇,好讓上級(jí)等待進(jìn)程能在等待事件返回時(shí)有足夠的時(shí)間來(lái)讀取獲得的數(shù)據(jù)。