精华内容
下载资源
问答
  • windows虚拟串口驱动开发windows下驱动程序设计。
  • Windows 2000下虚拟串口 WDM 驱动程序的开发 孙筱萌,夏 斌,韩德红,方 晓 (空军雷达学院电子对抗系,武汉 430019 摘 要:针对传统 RS-232串行通信存在的通信距离 端口数量等多个方面的限制, 提出了在 Windows 2000 操作...
  • WindowsXP下虚拟串口驱动源码

    热门讨论 2009-06-11 14:59:41
    使用VC的编译环境编译,虚拟的串口7。系统加载驱动后,必须在注册表中手动添加才行,具体位置是HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM,在这里加入新项目,名为...向虚拟串口写入的数据会自动发回来。
  • 开发虚拟串口驱动程序

    千次阅读 2010-03-17 16:29:00
    转自:http://www2.ccw.com.cn/05/0501/d/0501d04_1.asp 虚拟串口就是当本地并没有...本文作者给出了一种在Windows平台上实现虚拟串口的方法,由此实现的“串口”具有真实串口完全相同的系统调用接口。 在很多应用中

       转自:http://www2.ccw.com.cn/05/0501/d/0501d04_1.asp

     

       虚拟串口就是当本地并没有对应的串口硬件设备,而为应用层提供串口设备一样的系统调用接口,以兼容原本使用本地串口的应用软件的“虚”设备。本文作者给出了一种在Windows平台上实现虚拟串口的方法,由此实现的“串口”具有真实串口完全相同的系统调用接口。

    在很多应用中需要用到虚拟串口,如在Modem卡出现之前,已经有了接在计算机串口上的外部Modem,而且各种拔号程序也是通过串口与外部Modem通信的。为了让已有的拔号程序不做修改,像使用外部Modem一样使用内置卡,就需要内置卡的驱动程序虚拟一个串口设备。又如当前工业界使用的一些串口服务器,往往有8个或16个甚至更多的串口,以连接多个串口设备,再通过一个网卡直接连入以太网。与它在同一网络上的计算机就通过以太网与串口服务器上挂接的串口设备通信。为了让计算机中原来使用本地串口的软件兼容,就需要在计算机上提供虚拟串口驱动。

    虚拟串口的设计关键在于,该“串口”实现后必须具有与真实串口完全相同的系统调用接口。要做到这点,从已有的串口设备驱动程序上做修改是最佳捷径。下文就介绍以Windows NT上的串口驱动程序为基础,开发可运行于Windows NT、Windows 2000、Windows XP的各个版本虚拟串口驱动程序。

    串口驱动中使用的几个链表

    由于串口是双工设备,在一个读请求发出来还没有完成之前,同时可以发出写请求,加上在驱动程序层所有I/O请求都要求异步完成,即前一个请求尚没有完成,下一个相同的请求可能又来了。为此,串口驱动程序需要使用多个双向链表数据结构来处理各种IRP(I/O Request Packet,I/O请求包)。当收到一个IRP,先判断是否可立即完成,可以马上处理并返回,如果不允许则将IRP插在相应链表尾,在适当的时候如设备有空闲时处理,这时往往会产生一个硬件中断,激发DPC(Deferred Procedure Call,暂缓过程调用)过程,由DPC处理函数逐个从链表头取出IRP并试着完成它。串口驱动中有以下几个链表和DPC(在serial.h中有定义):

    ReadQueue 和 CompleteReadDpc

    用于保存Read IRP的链表和用于调度的DPC,与DPC对应的处理函数是SerialCompleteRead,它在read.c文件中,该函数的主要任务就是从ReadQueue中提取下一个IRP,并试着完成它。

    WriteQueue 和 CompleteWriteDpc

    用于保存Write IRP的链表和对应的DPC,与DPC对应的函数是SeriaCompleteWrite,它的实现在write.c中,该函数负责从WriteQueue中提取IRP,并试着完成它。

    MaskQueue 和 CommWaitDpc

    这一对链表用于处理Windows串口驱动的一个特性:事件驱动机制。它允许应用程序预设一个事件标志,而后等待与标志对应事件发生。DPC所调用的函数是SerialCompleteWait,它实现在Waitmask.c文件中,该函数也是试着从MaskQueue中提取IRP并完成它。

    PurgeQueue

    该链表与前面几个稍有不同,它没有与之相对应的DPC机制,而是在每次收到Purge请求时从PurgeQueue中逐个提取IRP并试着完成,因某种原因不能完成时则插入链表。相应的函数是purge.c文件中的SerialStartPurge。

    以上机制是串口驱动程序的重要实现方法,在虚拟串口驱动中需要保留,但不同的是,硬件串口驱动中是ISR(中断服务程序)根据收、发或MODEM中断来激发相应的DPC,而在虚拟串口驱动中将因实际情况不同会有不同的激发机制。

    DriverEntry的实现

    DriverEntry是驱动程序的入口函数,相当于应用程序C语言中的main函数,开发一个虚拟串口驱动首先要修改的就是它。它的函数实体在initunlo.c文件中。只是在虚拟串口驱动中由于不与具体的硬件打交道,就不存在硬件资源分析、硬件初始化、判断其工作状态等处理,只需要为虚拟串建立设备对象、符号链接和初始化数据结构。一个典型函数实现大体如下:

    NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)

    {

    /*填写DriverObject->MajorFunction[]数组*/

    /*建立设备对象*/

    /*初始化SERIAL_DEVCIE_EXETENSION数据结构*/

    Status = IoCreateDevice(DriverObject, sizeof(SERIAL_DEVICE_EXTENSION), &uniNameString, FILE_DEVICE_SERIAL_PORT, 0,TRUE,&deviceObject);

    //初始化所有链表

    InitializeListHead(&extension->ReadQueue);

    InitializeListHead(…);

    …;

    //初始化所有DPC

    KeInitializeDpc(&extension->CompleteReadDpc,SerailCompleteRead,extension);

    KeInitializeDpc(…);

    /*建立符号链接*/

    SerialSetupExternalNaming(extension);

    return Status;

    }

    SerialRead和SerialCompleteRead的实现

    函数SerailRead和SerialCompleteRead决定了对Read IRP的响应策略,它们都存于read.c中。以串口服务器要用的虚拟串口为例,当串口服务器收到来自外部数据时将通过网络发至计算机,计算机则产生相应的网络中断并进行协议数据处理。网络接收线程缓存新收到的数据并激活CompleteReadDpc,从而SerialCompleteReadIrp得到调用,它再调用CompleteReadIrp对每个IRP进行处理。它们的实现大体如下:

    NTSTATUS SerialRead(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp)

    {

    /*此处略去变量声明和初始化*/

    /*提取IRP中相关的数据*/

    stack = IoGetCurrentIrpStackLocation(Irp);

    ReadLen = stack->Parameters.Read.Length;

    /*先看本地缓冲有数据否?有的话先读取*/

    if(Extension->InCounter > 0 )

    { //注意这里要加锁,以防数据访问冲突

    KeAcquireSpinLock(&Extension->

    ReadBufferLock,&lIrql);

    FirstRead = (ReadLen>Extension->

    InCounter)? Extension->InCounter: ReadLen;

    RtlCopyMemory(Irp->AssociatedIrp.

    SystemBuffer,Extension->pInBuffer,FirstRead);

    Extension->InCounter -= FirstRead;

    ReadLen -= FirstRead;

    KeReleaseSpinLock(&Extension->

    ReadBufferLock,lIrql);//释放锁

    }

    /*是否已读到足够数据?是的话则完成该IRP*/

    if( 0 == ReadLen)

    {

    status=STATUS_SUCCESS;

    Irp->IoStatus.Status = status;

    Irp->IoStatus.Information = FirstRead;

    IoCompleteRequest(Irp,0);

    return status;

    }

    /*没有则将IRP插入队列中,通过网络向串口服务器发出读数据请求*/

    IoMarkIrpPending(Irp);

    InsertWaitList(Extension->ReadQueue,Irp);

    status = TdiSendAsync(Extension->ComChannel,pAckPacket,PacketLen(pAckPacket),(PVOID)ReadAckComplete,Irp);

    /*返回PENDING,表示该IRP尚没有完成*/

    return STATUS_PENDING;

    }

    Void CompleteReadIrp(IN PSERIAL_DEVICE_EXTENSION extension,IN PIRP Irp,IN PUCHAR pInData,IN ULONG Length )

    {

    /*此处略去变量声明和初始化*/

    /*读取新数据*/

    ReadLen = (ReadLen > Length)? Length : ReadLen;

    if(ReadLen != 0)

    {

    RtlCopyMemory(pReadAsync->

    pReadBuffer,pInData,ReadLen);

    pReadAsync->pReadBuffer += ReadLen;

    pReadAsync->ReadAlready += ReadLen;

    extension->PerfStats.ReceivedCount +=

    ReadLen;

    }

    else

    {

    /*因为串口服务器端只有在已经有了相应的数据或超过时间(此时,Length=0)才会发来应答并激活本DPC过程,所以此时已经超时,为了便于结束本IRP,这里有意改变TotalNeedRead,造成接收完毕的假象*/

    pReadAsync->TotalNeedRead =

    pReadAsync->ReadAlready;

    }

    if(pReadAsync->TotalNeedRead == pReadAsync->ReadAlready)

    {

    /*该IRP是否已经接收完毕,是的话则结束该

    IRP*/

    EndReadIrp(Irp);

    /*从ReadQueue中取下一个IRP*/

    }

    /*本IRP没有完成也没有超时,则继续等待本DPC下次被激活,注意此时要判断IRP是否被要求取消*/

    }

    SerialWrite和SerailCompleteWrite的实现

    SerialWrite和SerailCompleteWrite决定了Write IRP的实现。在SerialWrite中调用了网络发送函数TdiSendAsync,当该发送完成后将激活CompleteWriteDpc,调度SerialCompleteWrite函数,而它主要就是取出当前的WriteIRP,设置已经发送的数据数量,调用CompleteWriteIrp做该IRP的进一步处理。它们大体如下:

    NTSTATUS SerialWrite(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp)

    {

    /*此处略去变量声明和初始化*/

    /*从IRP中提取有关数据*/

    stack=IoGetCurrentIrpStackLocation(Irp);

    SendLen = stack->Parameters.Write.Length;

    /*为网络发送和异步操作分配缓冲,在CompleteWrite中全部数据发送完后释放*/

    pWriteAsync = ExAllocatePool(NonPagedPool,

    SendLen+PACKET_HEADER_LEN+sizeof(WRITE_ASYNC));

    if(pWriteAsync == NULL)

    {

    //错误处理

    }

    //保存异步数据

    //设置网络发送数据包

    BuildDataPacket(pPacket,WRITE,(USHORT)SendLen,pWriteAsync->pWriteBuffer);

    /*先将IRP暂时阻塞并插入队列,在CompleteWrite中完成*/

    IoMarkIrpPending(Irp);

    InsertWaitList(extension->WriteQueue, Irp);

    /*将写请求和相关数据通过网络发向串口服务器,由它负责将数据传到具体串口设备*/

    status = TdiSendAsync(Extension->ComChannel,pPacket,PacketLen(pPacket),(PVOID)CompleteWriteIrp,Irp);

    //统计数据累加

    Extension->PerfStats.TransmittedCount += SendLen;

    return STATUS_PENDING;

    }

     

    NTSTATUS CompleteWriteIrp(IN PDEVICE_OBJECT deviceobject,IN PIRP pIrp,IN PVOID context)

    {

    /*此处略去变量声明和初始化*/

    SendLen=pWriteAsync->TotalNeedWrite - pWriteAsync->WroteAlready;

    if(SendLen == 0)//全部数据发送完毕

    {

    EndWaitWriteIrp(pWriteIrp,STATUS_SUCCESS,

    pWriteAsync->WroteAlready,pWriteAsync);

    //从WriteQueue中取下一个IRP;

    }

    else //发送剩余数据

    {

    if(pWriteIrp->Cancel)

      {

    //IRP被要求取消,完成WriteIrp

    EndWaitWriteIrp(pWriteIrp,STATUS_CANCELLED,

    pWriteAsync->WroteAlready,pWriteAsync);

    return STATUS_CANCELED;

    }

    else

    {

    //再次设置网络数据包并发送

    BuildDataPacket(…);

    status = TdiSendAsync(…);

    //统计数据累加

    Extension->PerfStats.TransmittedCount +=

    SendLen;

    return STATUS_MORE_PROCESSING_REQUIRED;

    }

    }

    }

    其他几个接口函数的实现

    除Read/Write外,SerialUnload、SerialCreateOpen、 SerialClose、SerialCleanup、SerailFlush等调用接口是硬件相关性比较弱的接口函数,基本不要修改,直接删除原来操作硬件的部分即可。复杂一点就是SerialIoControl,该接口函数包含有大量设置、读取串口硬件状态的处理,可建立一个本地数据结构随时保存虚拟串口的当前硬件状态。同时为了保证串口服务器端的真实串口状态和上层软件要求的一致,需要将所有设置请求通过网络发送到服务器端,由它负责改变真实硬件的状态。

    展开全文
  • 虚拟串口驱动

    热门讨论 2011-11-05 11:48:59
    在很多应用环境中,终端设备的管理...基于串口的设备能够适应新的网络环境,我们开发了TCP/IP转 RS-232/485/422串口服务器设备,这样既能够方便的解决串口终端设 备的网络通信问题,又极大程度的保护了用户的原有投资。
  • 针对传统RS.232串行通信存在的通信距离、端口数量等多个方面的限制,提出了在Windows2000操作系统下使用DriverStudio开发工具编写符合WDM模式的虚拟串口设备驱动程序结构和实现方法.使用结果表明,此设计方法可以...
  • Windows驱动开发——虚拟串口设备

    千次阅读 2013-10-14 19:45:24
    DDK对串口驱动提供了专门接口。只要编写的驱动满足这些接口,并按照串口标准的命名方法,不管是真实的串口设备,还是虚拟设备,Windows操作系统都会认为 这个设备是一个标准的串口设备。用标准的串口调试工具都可以...

    文章转自:http://blog.csdn.net/chenyujing1234/article/details/7896364

    1、DDK串口开发框架

    DDK对串口驱动提供了专门接口。只要编写的驱动满足这些接口,并按照串口标准的命名方法,不管是真实的串口设备,还是虚拟设备,Windows操作系统都会认为

    这个设备是一个标准的串口设备。用标准的串口调试工具都可以与这个设备进行通信

    1、1 串口驱动的入口函数

    本章的实例程序是在HelloWDM驱动的基础上修改而来,入口函数依然是DriverEntry,在DriverEntry函数中指定各种IRP的派遣函数,以及AddDevice 例程、卸载例程等。

    /************************************************************************
    * 函数名称:DriverEntry
    * 功能描述:初始化驱动程序,定位和申请硬件资源,创建内核对象
    * 参数列表:
          pDriverObject:从I/O管理器中传进来的驱动对象
          pRegistryPath:驱动程序在注册表的中的路径
    * 返回 值:返回初始化驱动状态
    *************************************************************************/
    #pragma INITCODE 
    extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject,
    								IN PUNICODE_STRING pRegistryPath)
    {
    	KdPrint(("Enter DriverEntry\n"));
    
    	pDriverObject->DriverExtension->AddDevice = HelloWDMAddDevice;
    	pDriverObject->MajorFunction[IRP_MJ_PNP] = HelloWDMPnp;
    	pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = HelloWDMDispatchControlp;
    	pDriverObject->MajorFunction[IRP_MJ_CREATE] = HelloWDMCreate;
    	pDriverObject->MajorFunction[IRP_MJ_CLOSE] = HelloWDMClose;
    	pDriverObject->MajorFunction[IRP_MJ_READ] = HelloWDMRead;
    	pDriverObject->MajorFunction[IRP_MJ_WRITE] = HelloWDMWrite;
    	pDriverObject->DriverUnload = HelloWDMUnload;
    
    	KdPrint(("Leave DriverEntry\n"));
    	return STATUS_SUCCESS;
    }

    其中在AddDevice例程中,需要创建设备对象,这些都是和以前的HelloWDM驱动程序类似。在创建完设备对象后,需要将设备对象指定一个符号链接,该符号链接必须是

    COM开头,并接一下数字,如本例就采用了COM7。因为COM1和COM2在有些计算机中有时会被占用,因此,当该设备对象在指定符号链接时,应该避免采用这些名称。

    /************************************************************************
    * 函数名称:HelloWDMAddDevice
    * 功能描述:添加新设备
    * 参数列表:
          DriverObject:从I/O管理器中传进来的驱动对象
          PhysicalDeviceObject:从I/O管理器中传进来的物理设备对象
    * 返回 值:返回添加新设备状态
    *************************************************************************/
    #pragma PAGEDCODE
    NTSTATUS HelloWDMAddDevice(IN PDRIVER_OBJECT DriverObject,
                               IN PDEVICE_OBJECT PhysicalDeviceObject)
    { 
    	PAGED_CODE();
    	KdPrint(("Enter HelloWDMAddDevice\n"));
    
    	NTSTATUS status;
    	PDEVICE_OBJECT fdo;
    	UNICODE_STRING devName;
    	RtlInitUnicodeString(&devName,L"\\Device\\MyWDMDevice");
    	status = IoCreateDevice(
    		DriverObject,
    		sizeof(DEVICE_EXTENSION),
    		&(UNICODE_STRING)devName,
    		FILE_DEVICE_UNKNOWN,
    		0,
    		FALSE,
    		&fdo);
    	if( !NT_SUCCESS(status))
    		return status;
    	PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension;
    	pdx->fdo = fdo;
    	pdx->NextStackDevice = IoAttachDeviceToDeviceStack(fdo, PhysicalDeviceObject);
    	UNICODE_STRING symLinkName;
    	RtlInitUnicodeString(&symLinkName,L"\\DosDevices\\COM7");
    
    	pdx->ustrDeviceName = devName;
    	pdx->ustrSymLinkName = symLinkName;
    	status = IoCreateSymbolicLink(&(UNICODE_STRING)symLinkName,&(UNICODE_STRING)devName);
    
    	if( !NT_SUCCESS(status))
    	{
    		IoDeleteSymbolicLink(&pdx->ustrSymLinkName);
    		status = IoCreateSymbolicLink(&symLinkName,&devName);
    		if( !NT_SUCCESS(status))
    		{
    			return status;
    		}
    	}
    	// 设置为缓冲区设备
    	fdo->Flags |= DO_BUFFERED_IO | DO_POWER_PAGABLE;
    	fdo->Flags &= ~DO_DEVICE_INITIALIZING;
    
    	KdPrint(("Leave HelloWDMAddDevice\n"));
    	return STATUS_SUCCESS;
    }
    

    在创建完符号链接后,还不能保证应用程序能找出这个虚拟的串口设备,还需要进一步修改注册表。具体位置是HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM,可以在这里加入新项目。本例的项目名是MyWDMDevice,类型为REG_SZ,内容是COM7。

    在上述步骤后,即在AddDevice例程中创建COM7的符号链接,并且在注册表进行相应设置,系统会认为有这个串口驱动,用任何一个串口调试软件,都可以枚举到

    该串口。

    1、2  应用程序与串口驱动的通信

    其实对于一个真实的串口驱动,或者这个介绍的虚拟串口驱动,都需要遵循一组接口。这组接口由微软事先定义好了,只要符合这组接口,

    windows就会认为这是一个串口设备。这里所指的接口就是应用程序发的IO控制码和读写命令,因此对于串口驱动只要对这些IRP的派遣函数编写适当,就能实现一个串口驱动。

    首先用IRPTrace看一下,需要对哪些IRP进行处理,笔者加载本章已经介绍的虚拟串口驱动,并用IRPTrace 拦截其IRP处理信息,在打开串口工具后,会发现IRPTrace立刻跟踪到若干个IO控制码。

    下面依次解释这些IO控制码,理解这些IO控制码,并处理好这些控制码,是编写串口驱动的核心。关于这些IO控制码在ntddser.h文件中,都有相应的定义,并且还有

    相应的数据结构定义。

    (1)IOCTL_SERIAL_SET_QUEUE_SIZE

    这个控制码是应用程序向驱动请求设置串口驱动内部的缓冲区大小,它是向驱动传递SEARIAL_QUEUE_SIZE 数据结构来进行设置的,对于虚拟串口驱动来说,这是不需要

    关心的。用IRPTrace可以看出,串口调试工具会向驱动发送的请求是0x400大小的缓冲区大小。

    (2)IOCTL_SERIAL_GET_BAUD_RATE

    串口调试工具会接着向驱动发送IOCTL_SERIAL_GET_BAUD_RATE命令,这主要是询问驱动这个设备的波特率。驱动应该回应应用程序SEARIAL_BAUD_RATE数据结构,来通知波特率的数值。

    (3)IOCTL_SERIAL_GET_LINE_CONTROL

    串口调试工具接着向驱动发送IOTCL_SERIAL_GET_LINE_CONTROL命令,这主要是为了返回串口的行控制信息,行控制信息用SERIAL_LINE_CONTROL数据结构表示。

    [cpp]  view plain copy
    1. typedef struct _SERIAL_LINE_CONTROL {  
    2.     UCHAR StopBits;  
    3.     UCHAR Parity;  
    4.     UCHAR WordLength;  
    5.     } SERIAL_LINE_CONTROL,*PSERIAL_LINE_CONTROL;  

    其中StopBits是停止位,可以是STOP_BIT_1、STOP_BITS_1_5、STOP_BITS_2等取值。

    Parity代表校验位,可以是NO_PARITY、ODD——PARITY、EVEN_PARITY、MARK——PARITY、SPACE_PARITY。WorkLength是数据位,可以是5、6、7、8。

    [cpp]  view plain copy
    1. case IOCTL_SERIAL_GET_LINE_CONTROL:   
    2. {  
    3.     *((PSERIAL_LINE_CONTROL)(Irp->AssociatedIrp.SystemBuffer)) = pdx->Lc;  
    4.   
    5.           Irp->IoStatus.Information = sizeof(SERIAL_LINE_CONTROL);  
    6.     break;  
    7. }  

    (4)IOCTL_SERIAL_GET_CHARS

    串口调试工具会接着向驱动发送IOCTL_SERIAL_GET_CHARS命令,这个命令是应用程序向驱动请求特殊字符,用来与控制信号握手,用数据结构SERIAL_CHARS表示。

    [cpp]  view plain copy
    1. typedef struct _SERIAL_CHARS {  
    2.     UCHAR EofChar;  
    3.     UCHAR ErrorChar;  
    4.     UCHAR BreakChar;  
    5.     UCHAR EventChar;  
    6.     UCHAR XonChar;  
    7.     UCHAR XoffChar;  
    8.     } SERIAL_CHARS,*PSERIAL_CHARS;  


    其中EofChar代表是否是传送结束、ErrorChar代码是否传送中有错误、BreadChar代码是否传送有停止等。

    (5)IOCTL_SERIAL_GET_HANDFLOW

    串口调试工具会接着向驱动发送IOCTL_SRIAL_GET_HANDFLOW命令,这个命令是负责向驱动程序获得串口驱动的握手信号,握手信号用SERIAL_HANDFLOW数据

    结构表示:

    [cpp]  view plain copy
    1. typedef struct _SERIAL_HANDFLOW {  
    2.     ULONG ControlHandShake;  
    3.     ULONG FlowReplace;  
    4.     LONG XonLimit;  
    5.     LONG XoffLimit;  
    6.     } SERIAL_HANDFLOW,*PSERIAL_HANDFLOW;  


    (6)IOCTL_SERIAL_SET_WAIT_MASK

    串口工具会接着向驱动发送IOCTL_SERIAL_SET_WAIT_MASK命令,这个命令主要是设置串口驱动的某些事件发生时,需要向应用程序通知,这些事件包括以下几种事件:

    [cpp]  view plain copy
    1. #define SERIAL_EV_RXCHAR           0x0001  // Any Character received  
    2. #define SERIAL_EV_RXFLAG           0x0002  // Received certain character  
    3. #define SERIAL_EV_TXEMPTY          0x0004  // Transmitt Queue Empty  
    4. #define SERIAL_EV_CTS              0x0008  // CTS changed state  
    5. #define SERIAL_EV_DSR              0x0010  // DSR changed state  
    6. #define SERIAL_EV_RLSD             0x0020  // RLSD changed state  
    7. #define SERIAL_EV_BREAK            0x0040  // BREAK received  
    8. #define SERIAL_EV_ERR              0x0080  // Line status error occurred  
    9. #define SERIAL_EV_RING             0x0100  // Ring signal detected  
    10. #define SERIAL_EV_PERR             0x0200  // Printer error occured  
    11. #define SERIAL_EV_RX80FULL         0x0400  // Receive buffer is 80 percent full  
    12. #define SERIAL_EV_EVENT1           0x0800  // Provider specific event 1  
    13. #define SERIAL_EV_EVENT2           0x1000  // Provider specific event 2  
    [cpp]  view plain copy
    1. case IOCTL_SERIAL_SET_WAIT_MASK:  
    2.         {  
    3.             PIRP            pOldWaitIrp;  
    4.             PDRIVER_CANCEL  pOldCancelRoutine;  
    5.   
    6.             pdx->EventMask = *(PULONG)Irp->AssociatedIrp.SystemBuffer;  
    7.   
    8.             KeAcquireSpinLock(&pdx->IoctlSpinLock, &OldIrql);  
    9.   
    10.             pOldWaitIrp = pdx->pWaitIrp;  
    11.             if (pOldWaitIrp != NULL)  
    12.             {  
    13.                 pOldCancelRoutine = IoSetCancelRoutine(pOldWaitIrp, NULL);  
    14.   
    15.                 //对以前没有进行完成例程的等待irp,进行完成  
    16.                 if (pOldCancelRoutine != NULL)  
    17.                 {  
    18.                     pOldWaitIrp->IoStatus.Information = sizeof(ULONG);  
    19.                     *(PULONG)pOldWaitIrp->AssociatedIrp.SystemBuffer = 0;  
    20.   
    21.                     pOldWaitIrp->IoStatus.Status = STATUS_SUCCESS;  
    22.   
    23.                     pdx->pWaitIrp = NULL;  
    24.                 }  
    25.                 else  
    26.                 {  
    27.                     pOldWaitIrp = NULL;  
    28.                 }  
    29.             }  
    30.   
    31.             KeReleaseSpinLock(&pdx->IoctlSpinLock, OldIrql);  
    32.   
    33.             if (pOldWaitIrp != NULL)  
    34.             {  
    35.                 IoCompleteRequest(pOldWaitIrp, IO_NO_INCREMENT);  
    36.             }  
    37.   
    38.             break;  
    39.         }  

    当设置新的阻塞事件时,哪果之前有等待的IRP,那么先进行完成

    (7)IOCTL_SERIAL_WAIT_ON_MASK

    这个IO控制码是最重要的一个,当串口调试工具通过前面几个IO控制码初始华好后,就会发送这个请求。

    在驱动程序中,应该阻塞在那里,即返回PENDING状态,且通过IoSetCancelRoutine设置取消例程,而不是完成这个IRP。

    当IOCTL_SERIAL_SET_WAIT_MASK设置的事件中的一项发生时,阻塞状态改为完成,并通知应用程序是哪种事件发生了。

    [cpp]  view plain copy
    1. case IOCTL_SERIAL_WAIT_ON_MASK:  
    2.         {  
    3.             PDRIVER_CANCEL  pOldCancelRoutine;  
    4.   
    5.             KeAcquireSpinLock(&pdx->IoctlSpinLock, &OldIrql);  
    6.   
    7.             //等待irp一定被清除,且eventMask一定不为0  
    8.             if ((pdx->pWaitIrp != NULL) || (pdx->EventMask == 0))  
    9.                 ntStatus = STATUS_INVALID_PARAMETER;  
    10.             else if ((pdx->EventMask & pdx->HistoryEvents) != 0)  
    11.             {  
    12.                 // Some events happened  
    13.                 Irp->IoStatus.Information = sizeof(ULONG);  
    14.                 *(PULONG)Irp->AssociatedIrp.SystemBuffer = pdx->EventMask & pdx->HistoryEvents;  
    15.                 pdx->HistoryEvents = 0;  
    16.                 ntStatus = STATUS_SUCCESS;  
    17.             }else  
    18.             {  
    19.                 pdx->pWaitIrp = Irp;  
    20.   
    21.                 ntStatus = STATUS_PENDING;  
    22.   
    23.                 IoSetCancelRoutine(Irp, DriverCancelWaitIrp);  
    24.   
    25.                 if (Irp->Cancel)  
    26.                 {  
    27.                     pOldCancelRoutine = IoSetCancelRoutine(Irp, NULL);  
    28.   
    29.                     if (pOldCancelRoutine != NULL)  
    30.                     {  
    31.                         ntStatus = STATUS_CANCELLED;  
    32.   
    33.                         pdx->pWaitIrp = NULL;  
    34.                     }  
    35.                     else  
    36.                     {  
    37.                         IoMarkIrpPending(Irp);  
    38.                     }  
    39.                 }  
    40.                 else  
    41.                 {  
    42.                     IoMarkIrpPending(Irp);  
    43.                 }  
    44.             }  
    45.   
    46.             KeReleaseSpinLock(&pdx->IoctlSpinLock, OldIrql);  
    47.             break;  
    48.   
    49.         }  

     

    [cpp]  view plain copy
    1. VOID DriverCancelWaitIrp(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)  
    2. {  
    3.     KdPrint(("DriverCancelWaitIrp\n"));  
    4.     PDEVICE_EXTENSION pExtension = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension;  
    5.     KIRQL                   OldIrql;  
    6.   
    7.     IoReleaseCancelSpinLock(Irp->CancelIrql);  
    8.   
    9.     KeAcquireSpinLock(&pExtension->IoctlSpinLock, &OldIrql);  
    10.   
    11.     pExtension->pWaitIrp = NULL;          
    12.   
    13.     KeReleaseSpinLock(&pExtension->IoctlSpinLock, OldIrql);  
    14.   
    15.     Irp->IoStatus.Status = STATUS_CANCELLED;  
    16.     IoCompleteRequest(Irp, IO_NO_INCREMENT);  
    17. }  


    关于删除例程的知识可以参考<<驱动程序的取消IRP >>

    1、3  写的实现

    串口驱动除了需要完成处理IO控制码外,还需要对读写IRP进行处理。一般情况下,作为应用程序的串口调试工具会开启多个线程,其中主线程负责与串口驱动初始化的IO控制码通信。

    另外一个很重要的线程就是发送IOCTL_SERIAL_WAIT_ON_MASK请求,对于没有数据传输的情况下,这个IO控制码请求会PENDING在那里,即阻塞。当有传送的请求时,相应的事件被触发,刚才因为IOCTL_SERAIL_WAIT_ON_MASK的IRP被阻塞的线程得以继续运行,

    如果应用程序得知该事件是被写入了一个字符,会去发出一个读请求 ,对于驱动则是读的IRP。如果循环过程,从而实现了一个虚拟摄像头回写的例子。

    在对于写IRP的派遣函数中,主要是将写的数据存储在设备扩展中,以便以后读的时候将这些内容返回到应用程序。

    另外一个很重要的内容,就是阻塞的IO控制苏醒过来。在本例中调用DriverCheckEvent函数,该函数将阻塞的IRP完成,使应用程序的线程得以继续进行。并且这个线程还知道了SERIAL_EV_RXCHAR和SERIAL_EV_RX80FULL事件的到来,从而发起一个读请求,传送到驱动中就是读IRP。

    [cpp]  view plain copy
    1. NTSTATUS HelloWDMWrite(IN PDEVICE_OBJECT fdo,  
    2.                         IN PIRP Irp)  
    3. {  
    4.     KdPrint(("HelloWDMWrite\n"));  
    5.       
    6.     NTSTATUS ntStatus = STATUS_SUCCESS;// Assume success  
    7.   
    8.     PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension;  
    9.     // 获得当前IO堆栈  
    10.     PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation( Irp );  
    11.     // 获取当前IO堆栈的操作字节数  
    12.     ULONG DataLen = irpSp->Parameters.Write.Length;  
    13.     // 从IRP的缓冲区中得到数据  
    14.     PUCHAR pData = (PUCHAR)Irp->AssociatedIrp.SystemBuffer;  
    15.     KIRQL OldIrql;  
    16.     PIRP            pOldReadIrp = NULL;  
    17.     PDRIVER_CANCEL  pOldCancelRoutine;  
    18.     // 设置IRP的操作字节数  
    19.     Irp->IoStatus.Information = 0;  
    20.     ntStatus = STATUS_SUCCESS;  
    21.   
    22.     if (DataLen == 0)  
    23.     {  
    24.         ntStatus = STATUS_SUCCESS;  
    25.     }else if (DataLen>COMBUFLEN)  
    26.     {  
    27.         ntStatus = STATUS_INVALID_PARAMETER;  
    28.     }  
    29.     else  
    30.     {  
    31.         KdPrint(("Write\n"));  
    32.         // 获取自旋锁  
    33.         KeAcquireSpinLock(&pdx->WriteSpinLock, &OldIrql);  
    34.         // 复制内存块  
    35.         RtlCopyMemory(pdx->Buffer,pData,DataLen);  
    36.   
    37.         pdx->uReadWrite = DataLen;  
    38.   
    39.         if (pdx->pReadIrp != NULL) // drop it out  
    40.         {  
    41.             // 记录IRP  
    42.             pOldReadIrp = pdx->pReadIrp;  
    43.             // 设置取消函数  
    44.             pOldCancelRoutine = IoSetCancelRoutine(pOldReadIrp, NULL);  
    45.   
    46.             if (pOldCancelRoutine != NULL)  
    47.             {  
    48.                 pOldReadIrp->IoStatus.Information = 0;  
    49.   
    50.                 pOldReadIrp->IoStatus.Status = STATUS_SUCCESS;  
    51.   
    52.                 pdx->pReadIrp = NULL;  
    53.             }  
    54.             else  
    55.             {  
    56.                 pOldReadIrp = NULL;  
    57.             }  
    58.   
    59.         }  
    60.         // 检查事件  
    61.         DriverCheckEvent(pdx, SERIAL_EV_RXCHAR | SERIAL_EV_RX80FULL);  
    62.   
    63. //      DriverCheckEvent(pdx, SERIAL_EV_TXEMPTY);  
    64.         // 释放自旋锁  
    65.         KeReleaseSpinLock(&pdx->WriteSpinLock, OldIrql);  
    66.   
    67.         if (pOldReadIrp != NULL)  
    68.             IoCompleteRequest(pOldReadIrp, IO_NO_INCREMENT);  
    69.     }  
    70.   
    71.     Irp->IoStatus.Status = ntStatus;  
    72.     Irp->IoStatus.Information = DataLen;  
    73.     IoCompleteRequest( Irp, IO_NO_INCREMENT );  
    74.   
    75.     return ntStatus;  
    76. }  


     

    [cpp]  view plain copy
    1. VOID DriverCheckEvent(IN PDEVICE_EXTENSION pExtension, IN ULONG events)  
    2. {  
    3.     KdPrint(("DriverCheckEvent\n"));  
    4.     PIRP            pOldWaitIrp = NULL;  
    5.     PDRIVER_CANCEL  pOldCancelRoutine;  
    6.     KIRQL           OldIrql;  
    7.   
    8.     KeAcquireSpinLock(&pExtension->IoctlSpinLock, &OldIrql);  
    9.   
    10.     pExtension->HistoryEvents |= events;  
    11.   
    12.     events &= pExtension->EventMask;  
    13.   
    14.     //相当于设置触发事件  
    15.     if ((pExtension->pWaitIrp != NULL) && (events != 0))  
    16.     {  
    17.         pOldWaitIrp = pExtension->pWaitIrp;  
    18.   
    19.         pOldCancelRoutine = IoSetCancelRoutine(pOldWaitIrp, NULL);  
    20.   
    21.         //是否已经被cancel掉?  
    22.         if (pOldCancelRoutine != NULL)  
    23.         {  
    24.             // Nein, also Request beenden  
    25.             pOldWaitIrp->IoStatus.Information = sizeof(ULONG);  
    26.             *(PULONG)pOldWaitIrp->AssociatedIrp.SystemBuffer = events;  
    27.   
    28.             pOldWaitIrp->IoStatus.Status = STATUS_SUCCESS;  
    29.   
    30.             pExtension->pWaitIrp      = NULL;  
    31.             pExtension->HistoryEvents = 0;  
    32.         }  
    33.         else  
    34.         {  
    35.             //如果cancel掉,就不用IoCompleteRequest了  
    36.             pOldWaitIrp = NULL;  
    37.         }  
    38.     }  
    39.   
    40.     KeReleaseSpinLock(&pExtension->IoctlSpinLock, OldIrql);  
    41.   
    42.     if (pOldWaitIrp != NULL)  
    43.     {  
    44.         KdPrint(("complete the wait irp\n"));  
    45.         IoCompleteRequest(pOldWaitIrp, IO_NO_INCREMENT);  
    46.     }  
    47. }  


    1、4  读的实现

    对于虚拟串口的读工作,就变得相对简单。因为写IRP会负责通知让阻塞的线程继续运行,并且通知是何种事件的来临。串口调试软件得知SERAIL_EV_RXCHAR这个事件

    ,因此发起了读事件。在驱动中,就是进入读IRP的派遣函数。

    在该派遣函数中,负责将存储在设备扩展中的数据通过IRP传送到应用程序。同时,还需要做一些同步处理。

    [cpp]  view plain copy
    1. NTSTATUS HelloWDMRead(IN PDEVICE_OBJECT fdo,  
    2.                         IN PIRP Irp)  
    3. {  
    4.     KdPrint(("HelloWDMRead\n"));  
    5.   
    6.     NTSTATUS ntStatus = STATUS_SUCCESS;// Assume success  
    7.   
    8.     PDEVICE_EXTENSION pExtension = (PDEVICE_EXTENSION)fdo->DeviceExtension;  
    9.   
    10.     PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation( Irp );  
    11.   
    12.     ULONG BufLen = irpSp->Parameters.Read.Length;  
    13.     PCHAR pBuf = (PCHAR)Irp->AssociatedIrp.SystemBuffer;  
    14.   
    15.     KIRQL OldIrql;  
    16.   
    17.     PDRIVER_CANCEL pOldCancelRoutine;  
    18.   
    19.     Irp->IoStatus.Information = 0;  
    20.       
    21.     DbgPrint("DeviceObject:%08X Read\n",fdo);  
    22.   
    23.     if (BufLen == 0)  
    24.     {  
    25.         ntStatus = STATUS_SUCCESS;  
    26.     }  
    27.     else  
    28.     {  
    29.         KeAcquireSpinLock(&pExtension->WriteSpinLock, &OldIrql);  
    30.         // 内存复制  
    31.         RtlCopyMemory(pBuf,pExtension->Buffer,BufLen);  
    32.   
    33.         Irp->IoStatus.Information = BufLen;  
    34.   
    35.         if (BufLen==0 && pExtension->pReadIrp==NULL) // nothing, store  
    36.         {  
    37.             // 保存IRP  
    38.             pExtension->pReadIrp = Irp;  
    39.             Irp->IoStatus.Status = ntStatus = STATUS_PENDING;  
    40.             // 设置取消函数  
    41.             IoSetCancelRoutine(Irp, DriverCancelCurrentReadIrp);  
    42.   
    43.             // 重新设置取消函数  
    44.             if (Irp->Cancel)  
    45.             {  
    46.                 pOldCancelRoutine = IoSetCancelRoutine(Irp, NULL);  
    47.   
    48.                 if (pOldCancelRoutine != NULL)  
    49.                 {  
    50.                     // Nein, also IRP hier abbrechen  
    51.                     Irp->IoStatus.Status = ntStatus = STATUS_CANCELLED;  
    52.   
    53.                     pExtension->pReadIrp = NULL;  
    54.                 }  
    55.                 else  
    56.                 {  
    57.                     // 标记IRP挂起   Ja, Cancel-Routine wird Request beenden  
    58.                     IoMarkIrpPending(Irp);  
    59.                 }  
    60.             }  
    61.             else  
    62.             {  
    63.                     IoMarkIrpPending(Irp);  
    64.             }  
    65.         }  
    66.   
    67.         KeReleaseSpinLock(&pExtension->WriteSpinLock, OldIrql);  
    68.       
    69.     }  
    70.   
    71.     Irp->IoStatus.Status = ntStatus;  
    72.     if (ntStatus != STATUS_PENDING)  
    73.         IoCompleteRequest( Irp, IO_NO_INCREMENT );  
    74.   
    75.     return ntStatus;  
    76. }
    展开全文
  • 第19章,虚拟串口驱动,求讲解。有意的加我qq:545039712.我出钱。 需要讲解的代码如下: NTSTATUS HelloWDMRead(IN PDEVICE_OBJECT fdo, IN PIRP Irp) { KdPrint(("HelloWDMRead\n")); NTSTATUS ntStatus...
  • 本文件是STM32 USB串口最新驱动, 支持WIN7/WIN8和WIN10. 本人已在WIN10下测试通过, 可正常识别使用.内有详细使用说明文档.
  • windows USB 虚拟串口的 PC驱动

    千次阅读 2012-12-25 13:06:07
     l 成功在windows的超级终端用 usb-serial串口进入步骤:  1. 按照上面编译内核及其文件系统/dev , /etc/inittab文件。      2. 用以前的方式打开超级终端,启动9261系统到内核启动完毕,此时连通...
    LINUX端内核配置需加入USBgadget支持,9261UDP支持以及usb-serial.支持。 
    

        

         i. /linux-kernel/driver/usb/gadget/serial.c中

        #define GS_DEFAULT_USE_ACM 0该为

        #define GS_DEFAULT_USE_ACM 1

         ii. 需在/dev/下加入ttygs0 c 127 0

        命令为mknod /dev/ttygs0 c 127 0

         iii. /etc/inittab中最后加入B:2345:respawn:/sbin/getty 115200 ttygs0

        

        

         windows端需要加入usb- serial驱动支持

        提供文件名为gserial.inf和usbser.sys

        

        l 成功在windows的超级终端用 usb-serial串口进入步骤:

        1. 按照上面编译内核及其文件系统/dev , /etc/inittab文件。

        

        

        2. 用以前的方式打开超级终端,启动9261系统到内核启动完毕,此时连通usb线,在windows端,会出现“发现新硬件,安装驱动的提示,”,手动安装即可,确认成功,在“我的电脑”右击鼠标,查看属性,找到“硬件设备管理器”,并在“端口”项目中找到“Gadget Serial”。

        

        

        3. 重新启动9261系统,进入sh提示符后,等待直到

        gs_open: (0,c0014000,c0cb2ee0) device is not connected

        gs_close: NULL port pointer

        出现,如未出现,说明前面操作有问题。在等待字符出现后,重新拔插一次USB线,会立即出现:

        gs_disconnect: Gadget Serial disconnected

        gs_setup_complete: status error, status=0, actual=16, length=18

        gs_disconnect: Gadget Serial disconnected

        gs_set_config: Gadget Serial configured, full speed CDC-ACM config

        此时你可以新建一个超级终端,选择端口为“gadget serial“,我这里是com5

        其他配置与以前超级终端一样,bd 115200 之后回车进入,就有如下界面:

        Familiar Linux v0.8.3 ebd9261 ttygs0

        

        ebd9261 login:

        

        

        

        

        

        

        附录[一]:

        

        gserial . inf come from :/linux/document/usb/gadget_serial.txt

        

        [Version]

        Signature="$Windows NT$"

        Class=Ports

        ClassGuid={4D36E978-E325-11CE-BFC1-08002BE10318}

        Provider=%LINUX%

        DriverVer=08/17/2004,0.0.2.0

        ; Copyright (C) 2004 Al Borchers (

        [email=alborchers@steinerpoint.com]alborchers@steinerpoint.com[/email]

        )

        [Manufacturer]

        %LINUX%=GSerialDeviceList

        [GSerialDeviceList]

        %GSERIAL%=GSerialInstall, USB\VID_0525&PID_A4A7

        [DestinationDirs]

        DefaultDestDir=10,System32\Drivers

        [GSerialInstall]

        CopyFiles=GSerialCopyFiles

        AddReg=GSerialAddReg

        [GSerialCopyFiles]

        usbser.sys

        [GSerialAddReg]

        HKR,,DevLoader,,*ntkern

        HKR,,NTMPDriver,,usbser.sys

        HKR,,EnumPropPages32,,"MsPorts.dll,SerialPortPropPageProvider"

        [GSerialInstall.Services]

        AddService = usbser,0x0002,GSerialService

        [GSerialService]

        DisplayName = %GSERIAL_DISPLAY_NAME%

        ServiceType = 1 ; SERVICE_KERNEL_DRIVER

        StartType = 3 ; SERVICE_DEMAND_START

        ErrorControl = 1 ; SERVICE_ERROR_NORMAL

        ServiceBinary = %10%\System32\Drivers\usbser.sys

        LoadOrderGroup = Base

        [Strings]

        LINUX = "Linux"

        GSERIAL = "Gadget Serial"

        GSERIAL_DISPLAY_NAME = "USB Gadget Serial Driver"

        

        附录[二]:usbser.sys

        

        你可以在\WINDOWS\Driver Cache\i386\sp2.cab中找到

        或者g.cn 找找

    展开全文
  • WinCE虚拟串口驱动

    2011-12-12 16:33:52
    //======================================================================== ... // WinCE虚拟串口驱动(一)  //AUTHOR:  // norains  //DATE:  // Saturday 28-March-2009  //Environment
     
    

      //========================================================================
      //TITLE:
      //    WinCE虚拟串口驱动(一)
      //AUTHOR:
      //    norains
      //DATE:
      //    Saturday 28-March-2009
      //Environment:
      //    WINDOWS CE 5.0
      //========================================================================
      
      用过串口进行开发的朋友应该都知道,串口驱动是一个典型的独占设备。简单点来说,就是在成功地调用CreateFile打开串口之后,没有通过CloseHandle进行关闭,是无论如何都不能再次调用CreateFile来再次打开相同的串口。
      
      有的朋友可能会觉得莫名奇妙,为什么微软要在这上面做限制呢?但其实从另一个角度来讲,微软这么做是非常有道理的。以接收数据为例子,在驱动里面会有一定的缓存,用来保留一定量的数据。当通过ReadFile来获取数据时,驱动就会将缓存给清空,然后再继续接收数据。如果串口不是独占设备,可以多次打开,那么在读取数据上面就会有问题:应该什么时候才清空缓存?比方说,其中一个线程通过ReadFile来获得了数据,那么驱动应不应该将缓冲清空?如果清空,那另一个线程也想获得同样的数据进行分析,那就会产生数据丢失;如果不清空,万一之前已经通过ReadFile获取数据的线程再次进行读取,那么它将会得到同样重复的数据。如果想要在这多个进程中维持数据的同步,肯定要额外增加相应的标识,但这样就会加大了驱动的复杂度,并且也无法和别的驱动保持一致。因此,微软对串口实行独占设备的策略,是非常正确的。
      
      但,正确并不代表放之四海而皆准,在某些特殊的情况下,我们还是需要非独占性质的串口。简单地举个例子,在手持PND GPS设备中,导航软件肯定是必须要能通过串口进行数据获取来定位;可另一方面,我的另一个应用程序又想获得GPS数据进行系统时间的校准。在这情形之下,我们就必须使用一个非独占性质的串口设备。
      
      为了简化设计,该串口设备的驱动我们约定如下:
      
      1.同一时间只能有一个进程对外输出数据,其余进程只能在该进程输出完毕之后才能进行。
      
      2.程序不应该主动调用ReadFile来轮询获取数据。而是通过WaitCommEvent进行检测,当返回的状态中具备EV_RXCHAR时才调用ReadFile。并且该调用必须在一定的时间间隔之内,而且为了不丢失数据,缓冲大小一定要等于或大于READ_BUFFER_LENGTH。
      
      之所以有如上约束,完全是出于设计简便考虑。
      
      
      非独占式串口驱动主要是处理数据的分发,可以和具体的硬件分开,换句话说,该驱动是基于原有的串口驱动之上,实际上并“没有”该设备,因此我们将该非独占式串口称之为“虚拟串口驱动”。这样设计的优势很明显,可以不用理会具体的硬件规格,只要采用的是WinCE系统,并且原来已经具备了完善的串口驱动,那么该虚拟串口驱动就能工作正常。
      
      
      接下来我们来看看该虚拟串口的具体实现。
      
      麻雀虽小,五官俱全,虽然说该驱动是“虚拟”的,但毕竟还是“驱动”,该有的部分我们还是要具备的。
      
      驱动的前缀为VSP,取自于Virtual Serial Port之意。
      
      该驱动必须实现如下函数:

    1. VSP_Close  
    2. VSP_Deinit  
    3. VSP_Init  
    4. VSP_IOControl  
    5. VSP_Open  
    6. VSP_PowerDown  
    7. VSP_PowerUp  
    8. VSP_Read  
    9. VSP_Seek  
    10. VSP_Write         

     

      因为串口驱动是流设备,又和具体的电源管理五官,故VSP_Seek,VSP_PowerDown,VSP_PowerUp这些函数可以不用处理,直接返回即可。
      
      
      现在来看一下VSP_Open函数。
      
      VSP_Open函数我们大致需要如下流程处理事情:
      
      1.判断当前的是否已经打开串口,如果已经打开,直接跳到4.
      
      2.获取需要打开的串口序号,并打开该串口。如果打开失败,直接跳到5.
      
      3.打开数据监视进程(注:该部分在数据读取部分进行分析)。
      
      4.标识记数(即g_uiOpenCount)增加1。
      
      5.函数返回
      
      
      流程1:
      
      全局变量g_uiOpenCount用来保存打开的记数,所以只要判断该数值是否为0即可确定是否应该打开串口: 

    1. if(g_uiOpenCount != 0)  
    2. {         
    3. goto SET_SUCCEED_FLAG;  
    4. }  


      流程2:
      
      为了让程序更具备灵活性,所打开的串口序号我们不直接在驱动中设定,而是通过读取注册表的数值获得:

    1. if(reg.Open(REG_ROOT_KEY,REG_DEVICE_SUB_KEY) == FALSE)  
    2. {  
    3.     RETAILMSG(TRUE,(TEXT("[VSP]:Failed to open the registry/r/n")));  
    4.     goto LEAVE_CRITICAL_SECTION;  
    5. }  
    6.           
    7. //Get the MAP_PORT name   
    8. reg.GetValueSZ(REG_MAP_PORT_NAME,&vtBuf[0],vtBuf.size());  

     


      接下来便是打开具体的串口:

    1. g_hCom = CreateFile(&vtBuf[0],GENERIC_READ | GENERIC_WRITE ,0,NULL,OPEN_EXISTING,0,NULL);  
    2. if(g_hCom == INVALID_HANDLE_VALUE )  
    3. {  
    4.     RETAILMSG(TRUE,(TEXT("[VSP]Failed to map to %s/r/n"),&vtBuf[0]));  
    5.     goto LEAVE_CRITICAL_SECTION;  
    6. }  
    7. else  
    8. {  
    9.     RETAILMSG(TRUE,(TEXT("[VSP]Succeed to map to %s/r/n"),&vtBuf[0]));  
    10. }     


      流程3:
      
      创建进程来监视数据:

    1. InterlockedExchange(reinterpret_cast<LONG *>(&g_bExitMonitorProc),FALSE);  
    2. CloseHandle(CreateThread(NULL,NULL,MonitorCommEventProc,NULL,NULL,NULL));  


      流程4:
      
      成功打开记数

    1. SET_SUCCEED_FLAG:     
    2.     g_uiOpenCount ++;  
    3.     bResult = TRUE;  

      

     

     

      流程5:
      
      函数返回:

    1. LEAVE_CRITICAL_SECTION:       
    2.     LeaveCriticalSection(&g_csOpen);      
    3.     return bResult;   




      和VSP_Open密切对应的是VSP_Close,该函数流程基本和VSP_Open相反处理:
      
      1.打开记数(g_uiOpenCount)减1。如果g_uiOpenCount为不为0,跳转3。
      
      2.退出监视数据进程,并且关闭打开的串口。
      
      3.函数返回。
      
      
      流程1和流程2处理如下:
    1. g_uiOpenCount --;     
    2. if(g_uiOpenCount == 0)  
    3. {         
    4.     //Notify the monitor thread to exit.      
    5.     InterlockedExchange(reinterpret_cast<LONG *>(&g_bExitMonitorProc),TRUE);  
    6.     DWORD dwMask = 0;  
    7.     GetCommMask(g_hCom,&dwMask);  
    8.     SetCommMask(g_hCom,dwMask);       
    9.               
    10.     while(InterlockedExchange(reinterpret_cast<LONG *>(&g_bMonitorProcRunning),TRUE) == TRUE)  
    11.     {  
    12.         Sleep(20);  
    13.     }  
    14.     InterlockedExchange(reinterpret_cast<LONG *>(&g_bMonitorProcRunning),FALSE);  
    15.               
    16.     CloseHandle(g_hCom);  
    17.     g_hCom = NULL;  
    18. }  


    我们必须确保VSP_Open和VSP_Close中的某一个必须要全部处理完才能再次调用,否则在处理过程中如果又再次调用本函数或相对应的加载或卸载函数,那么一定会引发我们不可预料的情况,所以我们在这两个函数中增加了关键段,以维持处理上的同步: 
    1. EnterCriticalSection(&g_csOpen);  
    2. ...  
    3. LeaveCriticalSection(&g_csOpen);  



      其余的接口,算起来最简单的是VSP_Write,只要确定同一时间只能有唯一的一个进程进行输出即可:
    1. EnterCriticalSection(&g_csWrite);  
    2. DWORD dwWrite = 0;  
    3. WriteFile(g_hCom,pBuffer,dwNumBytes,&dwWrite,NULL);  
    4. LeaveCriticalSection(&g_csWrite);  

      

        在完成VSP_Read之前,我们先来看另外一个函数:WaitCommEvent。这是串口驱动特有的,目的是有某些时间发生时,能够第一时间激活线程。该函数和驱动的MMD层有关,是MDD层的应用程序级别接口。具体串口的PDD层,WaitCommEvent函数体内也仅仅是调用了COM_IOControl接口,然后传入IOCTL_SERIAL_WAIT_ON_MASK控制码而已。也就是说,调用WaitCommEvent的代码,就相当于如此调用COM_IOControl:

    1. DeviceIoControl(hCom,  
    2.                                 IOCTL_SERIAL_WAIT_ON_MASK,  
    3.                                     NULL,  
    4.                                     0,  
    5.                                     pOutBuf,  
    6.                                     dwOutBufLen,  
    7.                                     &dwReturn,  
    8.                                     NULL);  



      换句话说,如果想让虚拟串口驱动支持WaitCommEvent函数,我们只需要在VSP_IOControl处理IOCTL_SERIAL_WAIT_ON_MASK控制码即可:

    1. BOOL VSP_IOControl(  
    2.    DWORD dwHandle,  
    3.    DWORD dwIoControlCode,  
    4.    PBYTE pBufIn,  
    5.    DWORD dwBufInSize,  
    6.    PBYTE pBufOut,  
    7.    DWORD dwBufOutSize,  
    8.    PDWORD pBytesReturned  
    9.    )  
    10. {  
    11.     ...  
    12.       
    13.     switch(dwIoControlCode)   
    14.     {  
    15.         ...  
    16.                   
    17.         case IOCTL_SERIAL_WAIT_ON_MASK:  
    18.                       
    19.             ...                   
    20.             break;  
    21.               
    22.         ...  
    23.     }  
    24. }  
    25.           



      推而广之,像SetCommState,SetCommTimeouts等串口特有的函数,都仅仅只是对COM_IOControl函数进行的一层封装而已。
      
      我们再回到WaitCommEvent函数。可能有的朋友直接认为,我们只要在IOCTL_SERIAL_WAIT_ON_MASK段直接简单调用原有的WaitCommEvent即可:

    1. switch(dwIoControlCode)   
    2. {  
    3.     ...  
    4.               
    5.     case IOCTL_SERIAL_WAIT_ON_MASK:  
    6.     {                 
    7.         //直接调用原生的WaitCommEvent,但实际是错误的  
    8.         if(dwBufOutSize < sizeof(DWORD) || WaitCommEvent(g_hCom,reinterpret_cast<DWORD *>(pBufOut),NULL) == FALSE)  
    9.         {  
    10.             *pBytesReturned = 0;              
    11.             return FALSE;  
    12.         }  
    13.         else  
    14.         {  
    15.             *pBytesReturned = sizeof(DWORD);  
    16.             return TRUE;  
    17.         }                     
    18.     }  
    19.                   
    20.     ...  
    21. }  


     

     但实际上这样是不行的。查看文档关于WaitCommEvent函数的描述,注意事项中有这么一条: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.

      
      也就是说,WaitCommEvent只能被一个线程调用。如果多线程都同时调用该函数,会发生什么情况呢?经过实际测试,如果多线程都调用相同的WaitCommEvent,那么在某个线程调用WaitCommEvent时,之前已经有其余的线程通过调用该函数进行等待状态的话,那等待的线程立马会唤醒。简单点来说,就是同一时间只能有唯一的一个线程通过WaitCommEvent函数进入等待状态。所以,对于IOCTL_SERIAL_WAIT_ON_MASK控制码,我们不能简单地调用WaitCommEvent函数。
      
      在这里我们采用这么一种设计,对于IOCTL_SERIAL_WAIT_ON_MASK的处理,我们是通过调用WaitForSingleObject进行线程等待。而虚拟串口驱动,会额外开放一个线程,该线程主要是通过调用WaitCommEvent来获取原生串口的状态,当状态有通知时,再发送event给等待的线程。因此,对于IOCTL_SERIAL_WAIT_ON_MASK控制码的处理可以所作如下:

    1. switch(dwIoControlCode)   
    2. {  
    3.     ...  
    4.                   
    5.     case IOCTL_SERIAL_WAIT_ON_MASK:  
    6.     {                 
    7.         if(dwBufOutSize < sizeof(DWORD) ||   WaitForSingleObject(g_hEventComm,INFINITE) == WAIT_TIMEOUT)  
    8.                 {  
    9.                     *pBytesReturned = 0;              
    10.                     return FALSE;  
    11.                 }  
    12.                 else  
    13.                 {  
    14.                     InterlockedExchange(reinterpret_cast<LONG *>(pBufOut),g_dwEvtMask);  
    15.                     *pBytesReturned = sizeof(DWORD);                          
    16.                     return TRUE;  
    17.                 }                     
    18.             }  
    19.                   
    20.             ...  
    21.         }  



      驱动额外的等待线程所做如是:
    1. DWORD MonitorCommEventProc(LPVOID pParam)  
    2. {             
    3.     ...  
    4.               
    5.     while(TRUE)  
    6.     {     
    7.         DWORD dwEvtMask = 0;  
    8.         BOOL bWaitRes = WaitCommEvent(g_hCom,&dwEvtMask,NULL);                
    9.                   
    10.         if(g_bExitMonitorProc != FALSE)  
    11.         {  
    12.             break;  
    13.         }                     
    14.                   
    15.         if(bWaitRes == FALSE)  
    16.         {  
    17.             continue;  
    18.         }         
    19.                   
    20.         ...  
    21.               
    22.         InterlockedExchange(reinterpret_cast<LONG *>(&g_dwEvtMask),dwEvtMask);  
    23.         PulseEvent(g_hEventComm);         
    24.                   
    25.         ...  
    26.                   
    27.     }  
    28.               
    29.     ...  
    30.               
    31.     return 0;  
    32. }  


      现在是到考虑ReadFile实现的时候了。我们需要考虑到,不同进程,在同时读取数据时,应该能获得相同的数据。但对于原生的串口驱动,如果再次调用ReadFile,所获得的数据绝对是不会和之前的一样,否则就乱套了。于是,和IOCTL_SERIAL_WAIT_ON_MASK一样,我们这么也不能粗暴简单地调用原生的ReadFile完事。
      
      我们转换个思维,对于“不同进程,在同时读取数据时,应该能获得相同的数据”,我们应该是这么理解:“不同进程,相当短的间隔内读取数据,应该能获得相同的数据”。如果要做到这点,我们只需要设置一个读取缓存,当上级程序想要获取数据时,我们只需要简单地将数据返回即可。那么接下来最关键的是,我们应该什么时候读取数据?什么时候该刷新缓存呢?
      
      分开来说,最简单的方式,就是在监视进程MonitorCommEventProc中读取数据并刷新缓存。因为该线程会调用WaitCommEvent函数进行等待,它能够充分知道什么时候有数据进来。只要有数据进来,我们就进行读取。如果之前的缓存已经被读取过,我们就清空缓存,存入新的数据;否则就在旧缓存之后添加我们新的数据。故此,完善的MonitorCommEventProc实现就应该如此:

    1. DWORD MonitorCommEventProc(LPVOID pParam)  
    2. {  
    3.     InterlockedExchange(reinterpret_cast<LONG *>(&g_bMonitorProcRunning),TRUE);  
    4.       
    5.     RETAILMSG(TRUE,(TEXT("[VSP]:MonitorCommEventProc Running!/r/n")));  
    6.       
    7.     std::vector<BYTE> vtBufRead(g_vtBufRead.size(),0);          
    8.     while(TRUE)  
    9.     {     
    10.         DWORD dwEvtMask = 0;  
    11.         BOOL bWaitRes = WaitCommEvent(g_hCom,&dwEvtMask,NULL);                
    12.           
    13.         if(g_bExitMonitorProc != FALSE)  
    14.         {  
    15.             break;  
    16.         }                     
    17.           
    18.         if(bWaitRes == FALSE)  
    19.         {  
    20.             continue;  
    21.         }         
    22.           
    23.         DWORD dwRead = 0;             
    24.         if(dwEvtMask & EV_RXCHAR)  
    25.         {  
    26.             EnterCriticalSection(&g_csRead);                      
    27.               
    28.             ReadFile(g_hCom,&g_vtBufRead[0],vtBufRead.size(),&dwRead,NULL);       
    29.             if(dwRead == vtBufRead.size() || g_bReaded != FALSE)  
    30.             {  
    31.                 g_dwLenReadBuf = dwRead;  
    32.                 g_vtBufRead.swap(vtBufRead);  
    33.             }  
    34.             else if(dwRead != 0)  
    35.             {  
    36.                 if(g_dwLenReadBuf + dwRead <= g_vtBufRead.size())  
    37.                 {  
    38.                     g_dwLenReadBuf += dwRead;  
    39.                     g_vtBufRead.insert(g_vtBufRead.end(),vtBufRead.begin(),vtBufRead.begin() + dwRead);  
    40.                 }  
    41.                 else  
    42.                 {  
    43.                     DWORD dwCover = g_dwLenReadBuf + dwRead - g_vtBufRead.size();  
    44.                     std::copy(g_vtBufRead.begin() + dwCover,g_vtBufRead.begin() + g_dwLenReadBuf,g_vtBufRead.begin());  
    45.                     std::copy(vtBufRead.begin(),vtBufRead.begin() + dwRead,g_vtBufRead.begin() + (g_dwLenReadBuf - dwCover));  
    46.                     g_dwLenReadBuf = g_vtBufRead.size();  
    47.                 }  
    48.             }  
    49.               
    50.             g_bReaded = FALSE;  
    51.               
    52.             DEBUGMSG(TRUE,(TEXT("[VSP]:Read data : %d/r/n"),dwRead));     
    53.           
    54.             LeaveCriticalSection(&g_csRead);  
    55.         }  
    56.       
    57.         if(dwEvtMask == EV_RXCHAR && ((g_dwWaitMask & EV_RXCHAR) == 0 || dwRead == 0))  
    58.         {  
    59.             //The return event mask is only EV_RXCHAR and there is not EV_RXCHAR in the wait mask.  
    60.             continue;  
    61.         }  
    62.       
    63.         InterlockedExchange(reinterpret_cast<LONG *>(&g_dwEvtMask),dwEvtMask);  
    64.         PulseEvent(g_hEventComm);         
    65.           
    66.         //Sleep for other thread to respond to the event  
    67.         Sleep(100);  
    68.           
    69.         DEBUGMSG(TRUE,(TEXT("[VSP]:PulseEvent! The event-mask is 0x%x/r/n"),dwEvtMask));      
    70.           
    71.     }  
    72.       
    73.     RETAILMSG(TRUE,(TEXT("[VSP]:Exit the MonitorCommEventProc/r/n")));    
    74.     InterlockedExchange(reinterpret_cast<LONG *>(&g_bMonitorProcRunning),FALSE);  
    75.       
    76.     return 0;  
    77. }  


      正因为读取是如此实现,所以我们才有文章开头的第二点约定:
      
      程序不应该主动调用ReadFile来轮询获取数据。而是通过WaitCommEvent进行检测,当返回的状态中具备EV_RXCHAR时才调用ReadFile(如果一直采用ReadFile来轮询接收数据,很可能会读取重复的数据)。并且该调用必须在一定的时间间隔之内(如果间隔太久,很可能因为缓存已经刷新,数据丢失),而且为了不丢失数据,缓冲大小一定要等于或大于READ_BUFFER_LENGTH(因为只要读取一次数据,读取的标识就会被设置,当有新数据到达时,会刷新缓存,导致数据丢失)。
      

      这也同时解释了MonitorCommEventProc进程为何在PulseEvent之后会调用Sleep函数进行短暂的休眠,其作用主要是让驱动的读取进程歇歇,好让上级等待进程能在等待事件返回时有足够的时间来读取获得的数据。



    //========================================================================
      //TITLE:
      //    WinCE虚拟串口驱动(二)
      //AUTHOR:
      //    norains
      //DATE:
      //    Saturday 28-March-2009
      //Environment:
      //    WINDOWS CE 5.0
      //========================================================================
      
      虚拟串口驱动的完整代码如下:
      

    1. // VirtualSerial.cpp : Defines the entry point for the DLL application.  
    2. //  
    3.   
    4. #include "windows.h"  
    5. #include "reg.h"  
    6. #include <vector>  
    7. #include <Pegdser.h>  
    8. #include "algorithm"  
    9.   
    10. //--------------------------------------------------------------------------  
    11. //Macro  
    12. #define REG_ROOT_KEY     HKEY_LOCAL_MACHINE  
    13. #define REG_DEVICE_SUB_KEY  TEXT("Drivers//Builtin//VirtualSerial")  
    14. #define REG_MAP_PORT_NAME   TEXT("Map_Port")  
    15.   
    16. //The buffer length for storing the read data.  
    17. #define READ_BUFFER_LENGTH  MAX_PATH  
    18. //--------------------------------------------------------------------------  
    19. //Gloabal variable  
    20. HANDLE g_hCom = INVALID_HANDLE_VALUE;  
    21. unsigned int g_uiOpenCount = 0;  
    22. CRITICAL_SECTION g_csOpen;  
    23. CRITICAL_SECTION g_csRead;  
    24. CRITICAL_SECTION g_csWrite;  
    25. std::vector<BYTE> g_vtBufRead(READ_BUFFER_LENGTH,0);  
    26. DWORD g_dwLenReadBuf = 0;  
    27. DWORD g_dwEvtMask = 0;  
    28. DWORD g_dwWaitMask = 0;  
    29. HANDLE g_hEventComm = NULL;  
    30. BOOL g_bMonitorProcRunning = FALSE;  
    31. BOOL g_bExitMonitorProc = FALSE;  
    32. BOOL g_bReaded = FALSE;  
    33. //--------------------------------------------------------------------------  
    34.   
    35. BOOL WINAPI DllEntry(HANDLE hInstDll, DWORD dwReason, LPVOID lpvReserved)  
    36. {  
    37.     switch ( dwReason )   
    38.     {  
    39.         case DLL_PROCESS_ATTACH:  
    40.         break;  
    41.     }  
    42.     return TRUE;  
    43. }  
    44.   
    45. DWORD MonitorCommEventProc(LPVOID pParam)  
    46. {  
    47.  InterlockedExchange(reinterpret_cast<LONG *>(&g_bMonitorProcRunning),TRUE);  
    48.    
    49.  RETAILMSG(TRUE,(TEXT("[VSP]:MonitorCommEventProc Running!/r/n")));  
    50.    
    51.  std::vector<BYTE> vtBufRead(g_vtBufRead.size(),0);    
    52.  while(TRUE)  
    53.  {   
    54.   DWORD dwEvtMask = 0;  
    55.   BOOL bWaitRes = WaitCommEvent(g_hCom,&dwEvtMask,NULL);      
    56.     
    57.   if(g_bExitMonitorProc != FALSE)  
    58.   {  
    59.    break;  
    60.   }       
    61.     
    62.   if(bWaitRes == FALSE)  
    63.   {  
    64.    continue;  
    65.   }    
    66.     
    67.   DWORD dwRead = 0;     
    68.   if(dwEvtMask & EV_RXCHAR)  
    69.   {  
    70.    EnterCriticalSection(&g_csRead);       
    71.      
    72.    ReadFile(g_hCom,&g_vtBufRead[0],vtBufRead.size(),&dwRead,NULL);    
    73.    if(dwRead == vtBufRead.size() || g_bReaded != FALSE)  
    74.    {  
    75.     g_dwLenReadBuf = dwRead;  
    76.     g_vtBufRead.swap(vtBufRead);  
    77.    }  
    78.    else if(dwRead != 0)  
    79.    {  
    80.     if(g_dwLenReadBuf + dwRead <= g_vtBufRead.size())  
    81.     {  
    82.      g_dwLenReadBuf += dwRead;  
    83.      g_vtBufRead.insert(g_vtBufRead.end(),vtBufRead.begin(),vtBufRead.begin() + dwRead);  
    84.     }  
    85.     else  
    86.     {  
    87.      DWORD dwCover = g_dwLenReadBuf + dwRead - g_vtBufRead.size();  
    88.      std::copy(g_vtBufRead.begin() + dwCover,g_vtBufRead.begin() + g_dwLenReadBuf,g_vtBufRead.begin());  
    89.      std::copy(vtBufRead.begin(),vtBufRead.begin() + dwRead,g_vtBufRead.begin() + (g_dwLenReadBuf - dwCover));  
    90.      g_dwLenReadBuf = g_vtBufRead.size();  
    91.     }  
    92.    }  
    93.