WINCE下虚拟串口驱动设计

时间:2022-04-28 18:04:25

出处:http://blog.csdn.net/firehood_/article/details/6195558 

现在的车载和PND设备都有自动校正系统时间的功能,实现方法一般是通过GPS较时(当然对于有CMMB模块的设备也可以通过CMMB校时)。 但由于串口设备是一个独占设备,GPS串口不能同时被导航软件和校时程序使用。如果此时导航软件正在运行,GPS校时程序是无法访问GPS串口的。

       在这样的情况下,我们就需要创建一个非独占性质的串口设备,将一个物理串口虚拟成多个串口来使用。 虚拟串口驱动通过打开物理串口并监听物理串口事件,当物理串口有数据过来是将数据保存到缓存*多个不同的虚拟串口使用。这样通过虚拟的方式实现了同一物理串口在多个程序之间的数据共享。

       基于上述思想,采用了如下设计。(这里设定驱动的前缀为COM,与物理串口驱动的前缀保持一致,这样应用层可以像访问物理串口驱动一样访问虚拟串口驱动。)

        1. 在COM_Open()函数中打开物理串口,并创建线程MonitorCommEventProc用于监听并处理物理串口数据。

        2. 定义两个Buffer,分别保存两个虚拟串口的数据信息,Buffer的设计采用循环队列的方式。

        3. 在监听线程MonitorCommEventProc中监听物理串口事件,当物理串口有数据过来时读取数据并将数据分别保存到两个Buffer里。

        4. 在COM_Read()函数中实现读取Buffer中的数据。

        5. 在COM_IOControl()函数中实现相应控制码接口。如IOCTL_SERIAL_SET_WAIT_MASK,IOCTL_SERIAL_WAIT_ON_MASK、IOCTL_SERIAL_GET_COMMSTATUS、IOCTL_SERIAL_PURGE等。

MonitorCommEventProc监听线程参考代码:

[cpp]  view plain copy
  1. //we use this program to always read the  physical serial data  
  2. DWORD MonitorCommEventProc(LPVOID pParam)  
  3. {  
  4.      InterlockedExchange(reinterpret_cast<LONG *>(&g_bMonitorProcRunning),TRUE);     
  5.      unsigned char vtBufRead[READ_BUFFER_LENGTH];  
  6.      while(TRUE)  
  7.      {     
  8.         if(g_bExitMonitorProc != FALSE)  
  9.         {  
  10.             RETAILMSG(TRUE,(TEXT("MonitorCommEventProc Stop!/r/n")));  
  11.             //if com is closed,we will stop to read  
  12.             break;  
  13.         }     
  14.   
  15.         DWORD dwEvtMask = 0;     
  16.         SetCommMask (g_hCom, g_dwWaitMask | EV_RXCHAR);  
  17.         // wait the physical serial event     
  18.         if(WaitCommEvent(g_hCom,&dwEvtMask,NULL)&&(dwEvtMask & EV_RXCHAR))  
  19.         {   
  20.             SetCommMask (g_hCom, g_dwWaitMask | EV_RXCHAR);        {   
  21.             COMSTAT cmState;  
  22.             DWORD dwReadErrors;  
  23.             ClearCommError(g_hCom,&dwReadErrors,&cmState);  
  24.             DWORD willReadLen = cmState.cbInQue ;  
  25.             if (willReadLen <= 0)  
  26.                 continue;  
  27.             DWORD dwRead = 0;   
  28.             ReadFile(g_hCom,vtBufRead,willReadLen,&dwRead,NULL);      
  29.             EnterCriticalSection(&g_csRead1);  
  30.             EnterCriticalSection(&g_csRead2);  
  31.             if(g_bComState1==COM_STATUS_OPEN)  
  32.             {  
  33.                 WriteBuffer(g_ucDateBuf1,READ_BUFFER_LENGTH,vtBufRead,dwRead, &g_dwDateBufHead1,&g_dwDateBufTail1);  
  34.             }  
  35.             if(g_bComState2==COM_STATUS_OPEN)  
  36.             {  
  37.                 WriteBuffer(g_ucDateBuf2,READ_BUFFER_LENGTH,vtBufRead,dwRead, &g_dwDateBufHead2,&g_dwDateBufTail2);  
  38.             }  
  39.                         LeaveCriticalSection(&g_csRead2);  
  40.             LeaveCriticalSection(&g_csRead1);  
  41.             InterlockedExchange(reinterpret_cast<LONG *>(&g_dwEvtMask),dwEvtMask);     
  42.             PulseEvent(g_hCanReadEvent);  
  43.             printf("PulseEvent g_hCanReadEvent.../n");  
  44.             // sleep for other thread to respond to the event,very important..  
  45.             Sleep(1);  
  46.         }  
  47.     }  
  48.      InterlockedExchange(reinterpret_cast<LONG *>(&g_bMonitorProcRunning),FALSE);  
  49.      return 0;  
  50. }  

 

        COM_IOControl()函数多数控制码的实现可以直接调用物理串口DeviceIoControl()来处理,但有些控制码需要自己实现,比如清空缓存,不能直接调用DeviceIoControl()来清空物理串口的数据,而是应该清空虚拟串口Buffer中的数据。几个重要控制码实现的参考代码:

[cpp]  view plain copy
  1. BOOL  
  2. COM_IOControl(HANDLE  dwHandle,  
  3.               DWORD dwIoControlCode, PBYTE pBufIn,  
  4.               DWORD dwBufInSize, PBYTE pBufOut, DWORD dwBufOutSize,  
  5.               PDWORD pBytesReturned)  
  6.   
  7. {   
  8.     switch(dwIoControlCode)  
  9.     {  
  10.         case IOCTL_SERIAL_SET_WAIT_MASK:  
  11.             //printf("Serial Command: SERIAL_SET_WAIT_MASK/n");  
  12.             if(g_uiOpenCount==1)  
  13.             {  
  14.                 g_dwWaitMask = *reinterpret_cast<DWORD *>(pBufIn);   
  15.                 return DeviceIoControl(g_hCom, IOCTL_SERIAL_SET_WAIT_MASK,pBufIn, dwBufInSize,pBufOut,dwBufOutSize,pBytesReturned,NULL);  
  16.             }  
  17.             else  
  18.                 return TRUE;  
  19.         case IOCTL_SERIAL_WAIT_ON_MASK:  
  20.             {  
  21.                 PVSP_INFO pOpenHead = (PVSP_INFO) dwHandle;  
  22.                 // return immediately if the buffer has the available data  
  23.                 if(*(pOpenHead->BufferHead)!=*(pOpenHead->BufferTail))  
  24.                     return TRUE;  
  25.                 if(dwBufOutSize < sizeof(DWORD) || WaitForSingleObject(g_hCanReadEvent,INFINITE) == WAIT_TIMEOUT)     
  26.                 {     
  27.                     *pBytesReturned = 0;        
  28.                     return FALSE;     
  29.                 }     
  30.                 else    
  31.                 {     
  32.                     InterlockedExchange(reinterpret_cast<LONG *>(pBufOut),g_dwEvtMask);  
  33.                     *pBytesReturned = sizeof(DWORD);           
  34.                     return TRUE;     
  35.                 }          
  36.             }  
  37.         case IOCTL_SERIAL_PURGE:  
  38.             {  
  39.                 //clean the virtual serial buffer  
  40.                 PVSP_INFO pOpenHead = (PVSP_INFO) dwHandle;  
  41.                 *(pOpenHead->BufferHead)=0;  
  42.                 *(pOpenHead->BufferTail)=0;  
  43.                 return TRUE;  
  44.             }  
  45.         .....     
  46.     }  
  47.     return FALSE;  
  48. }  

 

Buffer(循环队列)读写操作参考代码:

[cpp]  view plain copy
  1. void WriteBuffer(PUCHAR targetBuffer,DWORD LongOfTarget,PUCHAR sourceBuffer,  
  2.     DWORD NumInSource,DWORD *BufferHead,DWORD *BufferTail)  
  3. {  
  4.     BOOL ChangeHead = FALSE;  
  5.     DWORD i=*BufferTail;  
  6.     DWORD j=0;  
  7.     if(NumInSource >= LongOfTarget)  
  8.     {  
  9.         memcpy(targetBuffer,sourceBuffer,LongOfTarget);  
  10.         *BufferHead=0;  
  11.         *BufferTail=LongOfTarget-1;  
  12.         return;  
  13.     }  
  14.     else  
  15.     {  
  16.         for(;j<NumInSource;j++)  
  17.         {  
  18.             targetBuffer[i++]=sourceBuffer[j];  
  19.             if(i>=LongOfTarget)  
  20.             {  
  21.                 i=0;  
  22.             }  
  23.             if(i==(*BufferHead))  
  24.             {  
  25.                 ChangeHead=TRUE;  
  26.             }     
  27.         }  
  28.         if(ChangeHead==FALSE)  
  29.         {  
  30.               
  31.             *BufferTail=i;  
  32.             return;  
  33.         }  
  34.         else  
  35.         {  
  36.             *BufferTail=i;  
  37.             *BufferHead=i+1;  
  38.             return;  
  39.         }  
  40.     }  
  41. }  
  42.   
  43. DWORD ReadBuffer(PUCHAR targetBuffer,PUCHAR sourceBuffer,DWORD SizeOfTargeBuf,  
  44.     DWORD LongOfSourceBuf,DWORD *BufferHead,DWORD *BufferTail)  
  45. {  
  46.     DWORD i=0;  
  47.     DWORD j=*BufferHead;  
  48.     BOOL IsEmpty=FALSE;  
  49.     for(i=0;i<SizeOfTargeBuf;i++)  
  50.     {  
  51.         if(j==(*BufferTail))  
  52.         {  
  53.             IsEmpty=TRUE;  
  54.             break;  
  55.         }  
  56.         targetBuffer[i]=sourceBuffer[j++];  
  57.         if(j>=LongOfSourceBuf)  
  58.         {  
  59.             j=0;  
  60.         }  
  61.     }  
  62.     if(IsEmpty==FALSE)  
  63.     {  
  64.         *BufferHead=j;  
  65.     }  
  66.     else  
  67.     {  
  68.         (*BufferHead)=(*BufferTail)=0;  
  69.     }  
  70.     return i;  
  71. }  
 

    参考资料:《WinCE虚拟串口驱动》http://blog.csdn.net/norains/archive/2009/03/28/4032257.aspx  作者:norains 感谢 norains大侠无私的奉献。



关于虚拟串口

出处:http://blog.csdn.net/lovelynn/article/details/4466215

 我们有一个GPS模块连接在COM3上,现在有两个应用程序都需要读取COM3的内容,然而WinCE的串口为独占式的串口,因此我们需要一个驱动程序,将COM3虚拟成COM4和COM5来供应用程序使用。下面我来介绍一下驱动程序的设计。

 
       首先我们要解决虚拟串口驱动加载的问题
       加载方法一:
        在本程序中,加载过程需要两个函数来完成,一个是虚拟串口驱动的 COM_Init(),另一个是
RegisterDevice(),我们将在应用程序中使用RegisterDevice()来启动COM_Init()完成虚拟串口驱动的加载。在应用程序中加载虚拟串口驱动的代码如下:
    DWORD VirComNO =4;
    HANDLE hRes = RegisterDevice (L"COM", VirComNO, L"GPSCOM.dll", (DWORD) VirComNO);
    RegisterDevice函数的用法参见文档说明。
    通过这个函数我们就会调用device.exe在系统中添加了一个名为COM4的设备,GPSCOM.dll中的流接口COM_Init()会被调用。
  加载方法二:
  当然我们也可以在系统启动时,让device.exe直接加载本驱动。
下面我们来看COM_Init()的实现:
    
HANDLE
COM_Init(
        ULONG   Identifier
        )
{
        
    PHW_INDEP_INFO  pSerialHead = NULL;
    // Allocate our control structure.
    //创建一个结构体用来记录设备信息
    pSerialHead  =  (PHW_INDEP_INFO)LocalAlloc(LPTR, sizeof(HW_INDEP_INFO));
    pSerialHead->pAccessOwner = NULL;
       
     ......

      //add com Identifier
        //如果我们创建的是COM5这个设备,那么把COM5的相关信息记录在pSerialHead中。
        if(5==Identifier)
        {
              RETAILMSG(DEBUG_COM2,(L" PLATFORM      fwq  COM_init5  /r/n"));
                pSerialHead->COMNUM = 5;
                g_pCircleBuffer5 =CP_CreateCircleBuffer(8192);
                
        }
        //如果我们创建的是COM4这个设备,那么把COM4的相关信息记录在pSerialHead中。        
        if(4==Identifier)
        {
              RETAILMSG(DEBUG_COM2,(L" PLATFORM      fwq  COM_init 4  /r/n"));
              pSerialHead->COMNUM =  4;
                // init circlebuffer for com4
                g_pCircleBuffer4 =CP_CreateCircleBuffer(8192);
        }

        ......
        //返回pSerialHead,这个pSerialHead将会被COM_Open()所得到。
        return(pSerialHead);
 }
     通过RegisterDevice()和COM_Init()的配合我们可以看到,每添加一个设备,COM_Init()就会在device.exe的进程空间内分配一段空间用来存放相应设备的信息,这些信息被记录在pSerialHead所指向的结构体中。
      至此,设备的加载过程就完成,我们可以灵活的根据我们的需要在pSerialHead所指向的结构体中,添加需要的变量,这个结构体也可以我们自己来定义,但在本程序中,我直接引用了系统代码中定义好的结构体,并在此结构体中添加了自己需要的变量。
 
      第二,驱动程序加载成功之后,我们就可以通过应用程序来打开虚拟串口了。下面我们来完成COM_Open()函数。
      
HANDLE
COM_Open(
        HANDLE  pHead,          // @parm Handle returned by COM_Init.
        DWORD   AccessCode,     // @parm access code.
        DWORD   ShareMode       // @parm share mode - Not used in this driver.
        )

{
    RETAILMSG(DEBUG_COM2,(L" PLATFORM      fwq  COM_Open  /r/n"));
    // 系统会根据,CreateFile的第一个参数,把devcie.exe内存空间中的与具体设备相关的     //PHW_INDEP_INFO结构体,通过pHead参数传递过来。
    //比如CreateFile的第一个参数是COM4,文件系统就回把我们在COM_Init()中创建好的用来存    //贮COM4设备信息的PHW_INDEP_INFO结构体地址传递过来。
    PHW_INDEP_INFO  pSerialHead = (PHW_INDEP_INFO)pHead;
    PHW_OPEN_INFO   pOpenHead = NULL;
    
    ......


    // 为pOpenHead分配空间,这个空间内用来存放,打开设备的一些信息,比如运行时的状态等都可以存储在此空间内。
    pOpenHead    =  (PHW_OPEN_INFO)LocalAlloc(LPTR, sizeof(HW_OPEN_INFO));
        RETAILMSG(DEBUG_COM,(L"PLATFORM   **()()()**        pOpenHead=%d ",pOpenHead));
        if ( !pOpenHead ) {
        DEBUGMSG( DEBUG_COM,
                 (TEXT("  PLATFORMError allocating memory for pOpenHead, COM_Open failed/n/r")));
        return(NULL);
    }

    // Init the structure
    //我们要把在COM_Init()中和当前打开设备相关的pSerialHead的地址保存在pOpenHead中。
   // 设备打开后,其他流接口函数被调用时,都会获得pOpenHead所指向的结构体地址。这样我们就可以在驱动中控制
    //应用程序打开的设备状态了
    pOpenHead->pSerialHead = pSerialHead;   
    ......
    //InitializeCriticalSection(&(pOpenHead->CommEvents.EventCS));

    EnterCriticalSection(&g_csOpen);
// 如果串口3已经被打开了 ,就不在继续打开。
 if(g_uiOpenCount != 0)   
 {     
  goto SET_SUCCEED_FLAG;   
 }  
 BOOL res=FALSE;
 // 打开串口3
 res = g_SerialPort.Open(3,4800);
 if(res == FALSE )   
 {   
  RETAILMSG(DEBUG_COM,(TEXT("Failed to map 3/r/n")));   
  goto CleanUp;   
 }   
 else  
 {   
  RETAILMSG(DEBUG_COM,(TEXT("Succeed to map to 3/r/n")));   
 }  
  g_hReadEvent4 = CreateEvent(NULL,FALSE,FALSE,L"WaitCommGPS4");
SET_SUCCEED_FLAG:   
        
        if(pSerialHead->COMNUM == 4)
        {   
             RETAILMSG(DEBUG_COM3,(L" PLATFORM       open com4  /r/n"));
              g_ComOpenFlag4 =1;        
              pSerialHead->COMOpenFlag =1;
        }

        if(pSerialHead->COMNUM == 5)
        {
              RETAILMSG(DEBUG_COM3,(L" PLATFORM       open com5  /r/n"));
              g_ComOpenFlag5 =1;
                pSerialHead->COMOpenFlag =1;
        }
 // 记录串口3被打开的次数  
 g_uiOpenCount ++;       
    
 LeaveCriticalSection(&g_csOpen);
/////////////////////////////////////////////////////////////////////////////

    return(pOpenHead);

 CleanUp:
        LeaveCriticalSection(&g_csOpen);

        RETAILMSG(DEBUG_COM,(L" PLATFORM      readqueue faild  and exit COM_Open/r/n"));
         return(NULL);

}
 
   第三,现在我们可以在应用程序中通过打开的串口来监听串口数据了,应用程序会通过
WaitCommEvent函数来等待串口事件。
   那么驱动程序中,我们是如何知道应用程序在等待哪个串口呢?应用程序调用WaitCommEvent函数实际上是掉用,驱动的 COM_IOControl()函数。
   下面来看流接口COM_IOControl()的设计
    
BOOL
COM_IOControl(PHW_OPEN_INFO pOpenHead,
              DWORD dwCode, PBYTE pBufIn,
              DWORD dwLenIn, PBYTE pBufOut, DWORD dwLenOut,
              PDWORD pdwActualOut){


      ......
    // 通过文件系统传来的pOpenHead参数获得应用程序所调用设备的结构体指针,获取当前设       备信息。
    PHW_INDEP_INFO  pSerialHead= pOpenHead->pSerialHead;
    switch ( dwCode ) {
                
    case IOCTL_SERIAL_WAIT_ON_MASK :
                // 应用程序调用WaitCommEvent()函数会进入这个分支
                if(4 == pSerialHead->COMNUM)
                {    
                     // WaitCommEvent4调用了WaitForSingleObject来等待事件
                     WaitCommEvent4(pOpenHead, (DWORD *)pBufOut, NULL);
                }

                if(5 == pSerialHead->COMNUM)
                {    
                     WaitCommEvent5(pOpenHead, (DWORD *)pBufOut, NULL);
                }
              *pdwActualOut = sizeof(DWORD);
        break;
    default :
       
        RETAILMSG (DEBUG_COM, (TEXT("  PLATFORM    Invalid ioctl %d/r/n"), dwCode));
        
        break;
    }
   ......
    return TRUE;
}
    通过这个COM_IOControl()函数,我们就能对COM4和COM5两个串口事件分别进行监听。
 
 
    第四,监听COM3 并将COM3的数据,分别发送给COM4和COM5,针对COM4和COM5我分别为其分配了一段循环缓冲区,当COM3有数据时,如果COM4为打开状态,就将数据放入COM4的循环缓冲区中,COM5同理。
    这部分工作由一个线程来完成,下面是COM3数据监听线程代码。
    
static DWORD WINAPI     ThreadReadCOM(LPVOID lpParam)
{
        RETAILMSG(DEBUG_CODE,(L"ThreadReadCOM/r/n"));
        DWORD   dwCommModemStatus       = 0;
        BOOL            bRet = FALSE;
        int             pdwBytesRead =0;
        BYTE            pBuffer [1024];

        _try
        {
                
                while(INVALID_HANDLE_VALUE != g_hComFile)
                {
                        //RETAILMSG(DEBUG_CODE,(L"COM1 Wait Comm Event/r/n"));
                        SetCommMask(g_hComFile,EV_RXCHAR);
                        WaitCommEvent (g_hComFile,&dwCommModemStatus,0);
                        
                        //RETAILMSG(DEBUG_CODE,(L"第%d次收到数据:/r/n",count));
                        
                        bRet =          ReadFile(g_hComFile,pBuffer,128,(LPDWORD)&pdwBytesRead,0);
                        
                    if(1 == g_ComOpenFlag4)
                    {
                        // 设置事件有效,通过COM4收到数据
                        SetEvent(g_hReadEvent4);
                        //将数据存放在专为COM4准备的循环缓冲区中
                        g_pCircleBuffer4->Write(g_pCircleBuffer4,pBuffer,pdwBytesRead);
                        
                    }

                    if(1 == g_ComOpenFlag5)
                    {
                         // 设置事件有效,通过COM5收到数据
                        // 这里不添加SetEvent语句,串口5依然会得到响应,而且响应速度很快,添加了
                        // SetEvent后com5的响应反而会变慢,这里我猜测是实串口驱动的SetEvent事件引起
                        //了应用程序对com5的WaitCommEvent响应,希望有了解的朋友给与指正
                        SetEvent(g_hReadEvent5);
                        //将数据存放在专为COM5准备的循环缓冲区中
                        g_pCircleBuffer5->Write(g_pCircleBuffer5,pBuffer,pdwBytesRead);
                        
                    }

                }
        }
                __except(EXCEPTION_EXECUTE_HANDLER)
        {
                RETAILMSG(DEBUG_CODE,(L"Exception! call Serial Thread/r/n"));
        }
     RETAILMSG(DEBUG_CODE,(L"readcom Thread exit/r/n"));

        return 1;
}
 
 
    第五,实现COM_Read(),应用程序调用ReadFile后驱动中的COM_Read()会被调用,下面是具体实现代码,
    
ULONG
COM_Read(
        HANDLE      pHead,          //@parm [IN]         HANDLE returned by COM_Open   
        PUCHAR      pTargetBuffer,  //@parm [IN,OUT] Pointer to valid memory.     
        ULONG       BufferLength    //@parm [IN]         Size in bytes of pTargetBuffer.
        )
{
    PHW_OPEN_INFO   pOpenHead = (PHW_OPEN_INFO)pHead;
    PHW_INDEP_INFO  pSerialHead= pOpenHead->pSerialHead;
    ULONG           BytesRead = 0;
 
         ......

         if(4 ==pSerialHead->COMNUM)
         {
             //从COM4的循环缓冲区中读取数据
             g_pCircleBuffer4->Read(g_pCircleBuffer4,pTargetBuffer,BufferLength,(unsigned int*)&BytesRead);    
             
             return BytesRead;
         }
                 
          if(5 ==pSerialHead->COMNUM)
         {   
             //从COM5的循环缓冲区中读取数据
             g_pCircleBuffer5->Read(g_pCircleBuffer5,pTargetBuffer,BufferLength,(unsigned int*)&BytesRead);    
           
             return BytesRead;
         }
        return -1;
   
}
 
    至此,我们的虚拟串口驱动,就基本完成,当然目前本驱动只具备串口读功能,如果要实现写功能还需要完成COM_Write的 代码。
还有一个重要的函数COM_Close()需要实现,在这个函数中我们要把被关闭的串口占用的资源释放,将某些状态位设为默认值等,还要在所有虚拟串口都关闭后,关闭实串口COM3,结束COM3的监听线程,在这里就不具体说明了。
    红色字体部分是本驱动存在问题的地方,希望有了解的朋友给与指正。