转载请标明是引用于 http://blog.csdn.net/chenyujing1234
欢迎大家拍砖
在我的一篇文章<<winCE中实现虚拟串口的方法 >>中,讲到在wince 下开发虚拟串口驱动的方法,现在介绍在windows XP下开发虚拟串口的方法。
可以开发一个虚拟串口,将读写请求传递给USB驱动,这样就可以利用现成的串口调试工具向USB设备读取了。
DDK对串口驱动提供了专门接口。只要编写的驱动满足这些接口,并按照串口标准的命名方法,不管是真实的串口设备,还是虚拟设备,Windows操作系统都会认为
这个设备是一个标准的串口设备。用标准的串口调试工具都可以与这个设备进行通信。
本章的实例程序是在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的符号链接,并且在注册表进行相应设置,系统会认为有这个串口驱动,用任何一个串口调试软件,都可以枚举到
该串口。
其实对于一个真实的串口驱动,或者这个介绍的虚拟串口驱动,都需要遵循一组接口。这组接口由微软事先定义好了,只要符合这组接口,
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数据结构表示。
typedef struct _SERIAL_LINE_CONTROL { UCHAR StopBits; UCHAR Parity; UCHAR WordLength; } 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。
case IOCTL_SERIAL_GET_LINE_CONTROL: { *((PSERIAL_LINE_CONTROL)(Irp->AssociatedIrp.SystemBuffer)) = pdx->Lc; Irp->IoStatus.Information = sizeof(SERIAL_LINE_CONTROL); break; }
(4)IOCTL_SERIAL_GET_CHARS
串口调试工具会接着向驱动发送IOCTL_SERIAL_GET_CHARS命令,这个命令是应用程序向驱动请求特殊字符,用来与控制信号握手,用数据结构SERIAL_CHARS表示。
typedef struct _SERIAL_CHARS { UCHAR EofChar; UCHAR ErrorChar; UCHAR BreakChar; UCHAR EventChar; UCHAR XonChar; UCHAR XoffChar; } SERIAL_CHARS,*PSERIAL_CHARS;
其中EofChar代表是否是传送结束、ErrorChar代码是否传送中有错误、BreadChar代码是否传送有停止等。
(5)IOCTL_SERIAL_GET_HANDFLOW
串口调试工具会接着向驱动发送IOCTL_SRIAL_GET_HANDFLOW命令,这个命令是负责向驱动程序获得串口驱动的握手信号,握手信号用SERIAL_HANDFLOW数据
结构表示:
typedef struct _SERIAL_HANDFLOW { ULONG ControlHandShake; ULONG FlowReplace; LONG XonLimit; LONG XoffLimit; } SERIAL_HANDFLOW,*PSERIAL_HANDFLOW;
(6)IOCTL_SERIAL_SET_WAIT_MASK
串口工具会接着向驱动发送IOCTL_SERIAL_SET_WAIT_MASK命令,这个命令主要是设置串口驱动的某些事件发生时,需要向应用程序通知,这些事件包括以下几种事件:
#define SERIAL_EV_RXCHAR 0x0001 // Any Character received #define SERIAL_EV_RXFLAG 0x0002 // Received certain character #define SERIAL_EV_TXEMPTY 0x0004 // Transmitt Queue Empty #define SERIAL_EV_CTS 0x0008 // CTS changed state #define SERIAL_EV_DSR 0x0010 // DSR changed state #define SERIAL_EV_RLSD 0x0020 // RLSD changed state #define SERIAL_EV_BREAK 0x0040 // BREAK received #define SERIAL_EV_ERR 0x0080 // Line status error occurred #define SERIAL_EV_RING 0x0100 // Ring signal detected #define SERIAL_EV_PERR 0x0200 // Printer error occured #define SERIAL_EV_RX80FULL 0x0400 // Receive buffer is 80 percent full #define SERIAL_EV_EVENT1 0x0800 // Provider specific event 1 #define SERIAL_EV_EVENT2 0x1000 // Provider specific event 2
case IOCTL_SERIAL_SET_WAIT_MASK: { PIRP pOldWaitIrp; PDRIVER_CANCEL pOldCancelRoutine; pdx->EventMask = *(PULONG)Irp->AssociatedIrp.SystemBuffer; KeAcquireSpinLock(&pdx->IoctlSpinLock, &OldIrql); pOldWaitIrp = pdx->pWaitIrp; if (pOldWaitIrp != NULL) { pOldCancelRoutine = IoSetCancelRoutine(pOldWaitIrp, NULL); //对以前没有进行完成例程的等待irp,进行完成 if (pOldCancelRoutine != NULL) { pOldWaitIrp->IoStatus.Information = sizeof(ULONG); *(PULONG)pOldWaitIrp->AssociatedIrp.SystemBuffer = 0; pOldWaitIrp->IoStatus.Status = STATUS_SUCCESS; pdx->pWaitIrp = NULL; } else { pOldWaitIrp = NULL; } } KeReleaseSpinLock(&pdx->IoctlSpinLock, OldIrql); if (pOldWaitIrp != NULL) { IoCompleteRequest(pOldWaitIrp, IO_NO_INCREMENT); } break; }
当设置新的阻塞事件时,哪果之前有等待的IRP,那么先进行完成。
(7)IOCTL_SERIAL_WAIT_ON_MASK
这个IO控制码是最重要的一个,当串口调试工具通过前面几个IO控制码初始华好后,就会发送这个请求。
在驱动程序中,应该阻塞在那里,即返回PENDING状态,且通过IoSetCancelRoutine设置取消例程,而不是完成这个IRP。
当IOCTL_SERIAL_SET_WAIT_MASK设置的事件中的一项发生时,阻塞状态改为完成,并通知应用程序是哪种事件发生了。
case IOCTL_SERIAL_WAIT_ON_MASK: { PDRIVER_CANCEL pOldCancelRoutine; KeAcquireSpinLock(&pdx->IoctlSpinLock, &OldIrql); //等待irp一定被清除,且eventMask一定不为0 if ((pdx->pWaitIrp != NULL) || (pdx->EventMask == 0)) ntStatus = STATUS_INVALID_PARAMETER; else if ((pdx->EventMask & pdx->HistoryEvents) != 0) { // Some events happened Irp->IoStatus.Information = sizeof(ULONG); *(PULONG)Irp->AssociatedIrp.SystemBuffer = pdx->EventMask & pdx->HistoryEvents; pdx->HistoryEvents = 0; ntStatus = STATUS_SUCCESS; }else { pdx->pWaitIrp = Irp; ntStatus = STATUS_PENDING; IoSetCancelRoutine(Irp, DriverCancelWaitIrp); if (Irp->Cancel) { pOldCancelRoutine = IoSetCancelRoutine(Irp, NULL); if (pOldCancelRoutine != NULL) { ntStatus = STATUS_CANCELLED; pdx->pWaitIrp = NULL; } else { IoMarkIrpPending(Irp); } } else { IoMarkIrpPending(Irp); } } KeReleaseSpinLock(&pdx->IoctlSpinLock, OldIrql); break; }
VOID DriverCancelWaitIrp(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { KdPrint(("DriverCancelWaitIrp\n")); PDEVICE_EXTENSION pExtension = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension; KIRQL OldIrql; IoReleaseCancelSpinLock(Irp->CancelIrql); KeAcquireSpinLock(&pExtension->IoctlSpinLock, &OldIrql); pExtension->pWaitIrp = NULL; KeReleaseSpinLock(&pExtension->IoctlSpinLock, OldIrql); Irp->IoStatus.Status = STATUS_CANCELLED; IoCompleteRequest(Irp, IO_NO_INCREMENT); }
关于删除例程的知识可以参考<<驱动程序的取消IRP >>
串口驱动除了需要完成处理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。
NTSTATUS HelloWDMWrite(IN PDEVICE_OBJECT fdo, IN PIRP Irp) { KdPrint(("HelloWDMWrite\n")); NTSTATUS ntStatus = STATUS_SUCCESS;// Assume success PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension; // 获得当前IO堆栈 PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation( Irp ); // 获取当前IO堆栈的操作字节数 ULONG DataLen = irpSp->Parameters.Write.Length; // 从IRP的缓冲区中得到数据 PUCHAR pData = (PUCHAR)Irp->AssociatedIrp.SystemBuffer; KIRQL OldIrql; PIRP pOldReadIrp = NULL; PDRIVER_CANCEL pOldCancelRoutine; // 设置IRP的操作字节数 Irp->IoStatus.Information = 0; ntStatus = STATUS_SUCCESS; if (DataLen == 0) { ntStatus = STATUS_SUCCESS; }else if (DataLen>COMBUFLEN) { ntStatus = STATUS_INVALID_PARAMETER; } else { KdPrint(("Write\n")); // 获取自旋锁 KeAcquireSpinLock(&pdx->WriteSpinLock, &OldIrql); // 复制内存块 RtlCopyMemory(pdx->Buffer,pData,DataLen); pdx->uReadWrite = DataLen; if (pdx->pReadIrp != NULL) // drop it out { // 记录IRP pOldReadIrp = pdx->pReadIrp; // 设置取消函数 pOldCancelRoutine = IoSetCancelRoutine(pOldReadIrp, NULL); if (pOldCancelRoutine != NULL) { pOldReadIrp->IoStatus.Information = 0; pOldReadIrp->IoStatus.Status = STATUS_SUCCESS; pdx->pReadIrp = NULL; } else { pOldReadIrp = NULL; } } // 检查事件 DriverCheckEvent(pdx, SERIAL_EV_RXCHAR | SERIAL_EV_RX80FULL); // DriverCheckEvent(pdx, SERIAL_EV_TXEMPTY); // 释放自旋锁 KeReleaseSpinLock(&pdx->WriteSpinLock, OldIrql); if (pOldReadIrp != NULL) IoCompleteRequest(pOldReadIrp, IO_NO_INCREMENT); } Irp->IoStatus.Status = ntStatus; Irp->IoStatus.Information = DataLen; IoCompleteRequest( Irp, IO_NO_INCREMENT ); return ntStatus; }
VOID DriverCheckEvent(IN PDEVICE_EXTENSION pExtension, IN ULONG events) { KdPrint(("DriverCheckEvent\n")); PIRP pOldWaitIrp = NULL; PDRIVER_CANCEL pOldCancelRoutine; KIRQL OldIrql; KeAcquireSpinLock(&pExtension->IoctlSpinLock, &OldIrql); pExtension->HistoryEvents |= events; events &= pExtension->EventMask; //相当于设置触发事件 if ((pExtension->pWaitIrp != NULL) && (events != 0)) { pOldWaitIrp = pExtension->pWaitIrp; pOldCancelRoutine = IoSetCancelRoutine(pOldWaitIrp, NULL); //是否已经被cancel掉? if (pOldCancelRoutine != NULL) { // Nein, also Request beenden pOldWaitIrp->IoStatus.Information = sizeof(ULONG); *(PULONG)pOldWaitIrp->AssociatedIrp.SystemBuffer = events; pOldWaitIrp->IoStatus.Status = STATUS_SUCCESS; pExtension->pWaitIrp = NULL; pExtension->HistoryEvents = 0; } else { //如果cancel掉,就不用IoCompleteRequest了 pOldWaitIrp = NULL; } } KeReleaseSpinLock(&pExtension->IoctlSpinLock, OldIrql); if (pOldWaitIrp != NULL) { KdPrint(("complete the wait irp\n")); IoCompleteRequest(pOldWaitIrp, IO_NO_INCREMENT); } }
对于虚拟串口的读工作,就变得相对简单。因为写IRP会负责通知让阻塞的线程继续运行,并且通知是何种事件的来临。串口调试软件得知SERAIL_EV_RXCHAR这个事件
,因此发起了读事件。在驱动中,就是进入读IRP的派遣函数。
在该派遣函数中,负责将存储在设备扩展中的数据通过IRP传送到应用程序。同时,还需要做一些同步处理。
NTSTATUS HelloWDMRead(IN PDEVICE_OBJECT fdo, IN PIRP Irp) { KdPrint(("HelloWDMRead\n")); NTSTATUS ntStatus = STATUS_SUCCESS;// Assume success PDEVICE_EXTENSION pExtension = (PDEVICE_EXTENSION)fdo->DeviceExtension; PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation( Irp ); ULONG BufLen = irpSp->Parameters.Read.Length; PCHAR pBuf = (PCHAR)Irp->AssociatedIrp.SystemBuffer; KIRQL OldIrql; PDRIVER_CANCEL pOldCancelRoutine; Irp->IoStatus.Information = 0; DbgPrint("DeviceObject:%08X Read\n",fdo); if (BufLen == 0) { ntStatus = STATUS_SUCCESS; } else { KeAcquireSpinLock(&pExtension->WriteSpinLock, &OldIrql); // 内存复制 RtlCopyMemory(pBuf,pExtension->Buffer,BufLen); Irp->IoStatus.Information = BufLen; if (BufLen==0 && pExtension->pReadIrp==NULL) // nothing, store { // 保存IRP pExtension->pReadIrp = Irp; Irp->IoStatus.Status = ntStatus = STATUS_PENDING; // 设置取消函数 IoSetCancelRoutine(Irp, DriverCancelCurrentReadIrp); // 重新设置取消函数 if (Irp->Cancel) { pOldCancelRoutine = IoSetCancelRoutine(Irp, NULL); if (pOldCancelRoutine != NULL) { // Nein, also IRP hier abbrechen Irp->IoStatus.Status = ntStatus = STATUS_CANCELLED; pExtension->pReadIrp = NULL; } else { // 标记IRP挂起 Ja, Cancel-Routine wird Request beenden IoMarkIrpPending(Irp); } } else { IoMarkIrpPending(Irp); } } KeReleaseSpinLock(&pExtension->WriteSpinLock, OldIrql); } Irp->IoStatus.Status = ntStatus; if (ntStatus != STATUS_PENDING) IoCompleteRequest( Irp, IO_NO_INCREMENT ); return ntStatus; }