关于虚拟串口

       运行环境: WinCE5.0

     

      我们有一个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的监听线程,在这里就不具体说明了。
    红色字体部分是本驱动存在问题的地方,希望有了解的朋友给与指正。

你可能感兴趣的:(关于虚拟串口)