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