windows USB读卡器驱动详解

目标

windows 2000及以上版本的操作系统中,开发usb读卡器驱动。实现数据批量传输功能,同时需要根据特定的协议(协议可自定义)进行通信。实现一个基于此usb驱动的动态库(统一接口),供上层应用程序调用。

具体功能包括:查询IC卡类型、读扇区数据、写扇区数据、删除IC卡数据、USB热插拔。

开发环境

操作系统:windows xp

开发软件:DDKdriverStudio

调试工具:Ellisys USB分析仪,Debug view, irptrace, driverMonitor, SymLinks

基本技能

熟悉usb2.0协议,熟悉windows下驱动开发流程(包括windows驱动调试技术、内存管理、驱动程序的同步、IRP、分层驱动模型、设备的即插即用等)。

与传统PC总线(PCI总线)设备的驱动程序相比,USB设备驱动程序从不直接与硬件对话。相反,它仅靠创建URB(USB请求块)并把URB提交到总线驱动程序就可完成硬件操作。

可以把USBD.SYS看作是接受URB的实体,向USBD的调用被转化为带有主功能代码为IRP_MJ_INTERNAL_DEVICE_CONTROLIRP。然后USBD再调度总线时间,发出URB中指定的操作。

初始化请求

为了创建一个URB,你首先应该为URB分配内存,然后调用初始化例程把URB结构中的各个域填入请求要求的内容,例如,当你为响应IRP_START_DEVICE请求而配置设备时,首要的任务就是读取该设备的设备描述符。下面代码片段可以完成这个任务:

USB_DEVICE_DESCRIPTOR dd;

URB urb;

UsbBuildGetDescriptorRequest(&urb,

                         sizeof(_URB_CONTROL_DESCRIPTOR_REQUEST),

                         USB_DEVICE_DESCRIPTOR_TYPE,

                         0,

                         0,

                            &dd,

                         NULL,

                         sizeof(dd),

                         NULL);

我们首先声明一个局部变量urb来保存URB数据结构。URBUSBDI.H中声明,是一个多子结构的联合,我们将使用UrbControlDescriptorRequest子结构,它的类型是_URB_CONTROL_DESCRIPTOR_REQUEST。使用自动变量当然是可以的,但你事先必须了解系统堆栈上是否有足够的空间装下最大的URB,并且在这个URB被完成前,你不能离开当前函数,否则自动变量将被释放。

当然,你还可以在系统堆上为URB动态地分配内存:

PURB urb = (PURB) ExAllocatePool(NonPagedPool,  sizeof(_URB_CONTROL_DESCRIPTOR_REQUEST));

if (!urb)

      return  STATUS_INSUFFICIENT_RESOURCES;

UsbBuildGetDescriptorRequest(urb, …);

ExFreePool(urb);

UsbBuildGetDescriptorRequest看上去象一个正常的服务例程,实际上它是一个宏(USBDLIB.H中声明),用于生成读描述符请求子结构各个域的初始化语句。在DDK头文件中定义了这些创建各种URB的宏,见表1。因为它们是预处理宏,所以在它们的参数中应避免使用有侧效的表达式。

1用于创建URB的辅助宏

辅助宏

事务类型

UsbBuildInterruptOrBulkTransferRequest

对中断或批量端点的输入和输出

UsbBuildGetDescriptorRequest

端点0GET_DESCRIPTOR控制请求

UsbBuildGetStatusRequest

对设备、接口、端点的GET_STATUS请求

UsbBuildFeatureRequest

对设备、接口、端点的SET_FEATURECLEAR_FEATURE请求

UsbBuildSelectConfigurationRequest

SET_CONFIGURATION

UsbBuildSelectInterfaceRequest

SET_INTERFACE

UsbBuildVendorRequest

任何厂商定义的控制请求

在前面的代码片段中,我们指定把设备描述符信息接收到一个局部变量(dd)中,其地址和长度我们在参数中都提供了。涉及到数据传输的URB可以指定两种方式的非分页数据缓冲区。你可以象上面代码那样指定缓冲区的虚拟地址和长度。另一种方法是提供一个内存描述符列表(MDL),但事先必须调用MmProbeAndLockPages函数对这个MDL进行探测并锁定(probe-and-lock)

发送URB

创建完URB后,你需要创建并发送一个内部I/O控制(IOCTL)请求到USBD驱动程序,USBD驱动程序位于驱动程序层次结构的低端。在大多数情况下,你需要等待设备回应,可以使用下面辅助函数:

NTSTATUS usb_icSubmitUrbSynch(

    IN  PUSB_IC_DEVICE_EXTENSION   DeviceExtension,

    IN  PURB                        Urb

    )

{

     NTSTATUS            status;

     PIRP                irp;

     IO_STATUS_BLOCK     ioStatus;

     KEVENT              event;               

     PIO_STACK_LOCATION  irpStack;

 

     usb_icDebugPrint(DBG_IO, DBG_INFO, __FUNCTION__"++");

 

     KeInitializeEvent(&event, NotificationEvent, FALSE);    ß 1

 

    irp =  IoBuildDeviceIoControlRequest(                  ß 2

             IOCTL_INTERNAL_USB_SUBMIT_URB,

             DeviceExtension->LowerDeviceObject,

             NULL,

             0,

             NULL,

             0,

            TRUE,

             &event,

             &ioStatus

             );

 

    if (irp  != NULL)

    {

         irpStack = IoGetNextIrpStackLocation(irp);                   

         irpStack->Parameters.Others.Argument1 = Urb;                 ß 3

 

         status = IoCallDriver(DeviceExtension->LowerDeviceObject, irp);  ß 4

        if  (status == STATUS_PENDING)

        {

             KeWaitForSingleObject(

                 &event,

                 Executive,

                 KernelMode,

                 FALSE,

                 NULL

                 );

            

             status = ioStatus.Status;   

        }

    }

    else

    {

         status = STATUS_INSUFFICIENT_RESOURCES;

    }

 

     usb_icDebugPrint(DBG_IO, DBG_INFO, __FUNCTION__"–. STATUS  %x", status);

 

    return  status;

}

1.  我们将等待URB完成,所以我们必须先创建一个内核事件对象。

2. 创建内部IOCTL请求最简单的的方法是调用IoBuildDeviceIoControlRequest函数,第一个参数(IOCTL_INTERNAL_USB_SUBMIT_URB)指出I/O控制代码。第二个参数(DeviceExtension->LowerDeviceObject)指定接收请求的设备对象;IoBuildDeviceIoControlRequest使用这个指针来决定需要接收多少个堆栈单元。接下来的四个参数描述输入输出缓冲区,提交这种URB并不需要这些信息,所以例子中将它们置成NULL0值。第七个参数为TRUE,它指出我们创建的是IRP_MJ_INTERNAL_DEVICE_CONTROL请求而不是IRP_MJ_DEVICE_CONTROL请求。最后两个参数指出等待URB完成的事件和一个接收该操作最终状态的结构IO_STATUS_BLOCK

3.  被提交的URB的地址被填入Parameters.OthersArgument1域。对于普通的IOCTL请求,该偏移对应OutputBufferLength域。

4.  我们用IoCallDriver把请求发送到下一层驱动程序。USBD将处理该URB请求并完成,然后I/O管理器将那个IRP删除并置事件信号。由于我们没有提供自己的完成例程,所以不能确定I/O管理器在所有可能的完成情况下都置事件信号。我们仅当低级派遣例程返回STATUS_PENDING时才等待那个事件。

配置

USB总线驱动程序自动检测新插入的USB设备。然后它读取设备内的设备描述符以查明插入的是何种设备,描述符中的厂商和产品标识以及其它描述符一同决定具体安装哪一个驱动程序。

配置管理器调用驱动程序的AddDevice函数。AddDevice做所有你已知的任务:创建设备对象,把设备对象连接到驱动程序堆栈上,等等。最后,配置管理器向驱动程序发送一个即插即用请求IRP_MN_START_DEVICE。通过调用一个名为StartDevice的辅助函数并传递一些参数,这些参数描述了赋予设备的经过转换的和未经转换的I/O资源。

StartDevice的执行过程大致如下,首先为设备选择一个配置。选定了某个配置后,接着应该选择配置中的一个或多个接口。顺便说一下,支持多接口的设备并不少见。选定了一个配置和一组接口后,你应该向总线驱动程序发送配置选择URB。最后,总线驱动程序向设备发出命令使能选定的配置和接口。总线驱动程序负责创建管道和用于访问管道的句柄,管道提供功能驱动程序与选定接口端点之间的通信,它同时还创建配置句柄和接口句柄。你可以从完成的URB中提取这些句柄并保存为以后使用。至此,设备的配置过程全部结束。

读取配置描述符

可以把固定大小的配置描述符看做是某个可变长结构的头,这个可变长结构描述了一个配置和该配置的所有接口、以及每个接口的所有端点。

因为硬件不允许直接访问接口和端点描述符,所以必须把整个可变长结构读入一个连续的内存区。不幸的是,在读之前我们并不能确切地知道这个结构有多长。下面代码演示了如何用两个URB来读取配置描述符:

[usb_ic.c +386]

        UsbBuildGetDescriptorRequest(

            urb,

            (USHORT)sizeof(struct  _URB_CONTROL_DESCRIPTOR_REQUEST),

             USB_CONFIGURATION_DESCRIPTOR_TYPE,

            0,

            0,

            configDescriptor,

            NULL,

             sizeof(USB_CONFIGURATION_DESCRIPTOR),

            NULL

            );

 

        status =  usb_icSubmitUrbSynch(DeviceExtension, urb);

        if (!NT_SUCCESS(status))

        {

            break;

        }

 

        size =  configDescriptor->wTotalLength;

        ExFreePool(configDescriptor);

 

        configDescriptor =  (PUSB_CONFIGURATION_DESCRIPTOR)ExAllocatePoolWithTag(

                                                             NonPagedPool,

                                                             size,

                                                             USB_IC_POOL_TAG

                                                             );

        if (configDescriptor == NULL)

        {

            status =  STATUS_INSUFFICIENT_RESOURCES;

            break;

        }

 

        UsbBuildGetDescriptorRequest(

            urb,

            (USHORT)sizeof(struct  _URB_CONTROL_DESCRIPTOR_REQUEST),

             USB_CONFIGURATION_DESCRIPTOR_TYPE,

            0,

            0,

            configDescriptor,

            NULL,

            size,

            NULL

            );

 

        status =  usb_icSubmitUrbSynch(DeviceExtension, urb);

        if (!NT_SUCCESS(status))

        {

            break;

        }

在这段代码中,我们先发出一个URB,把配置描述符读取到临时描述符缓冲区configDescriptor,我指定配置号0,它是第一个配置描述符。这个描述符包含了可变长结构的总长度(wTotalLength)。然后我们分配该长度的内存块,并发出了第二个URB读取整个描述符。最后,configDescriptor变量指向包含整个配置信息的内存区。(注意,真正的应用代码应该有错误检测)

读配置描述符时使用的描述符索引来自于设备固件响应GET_DESCRIPTOR控制请求的结果,而不受USB规范的限定。

选择配置

通过向设备发送一系列控制命令来选择配置及使能需要的接口。我们将使用USBD_CreateConfigurationRequestEx函数创建这种URB。其中的一个参数指向要使能的接口描述符数组。下一步就是准备这个数组。

回想一下,当我们读配置描述符时,我们同时也把该配置的所有接口描述符读入了调整后的内存区中。这个内存区域包含了一系列描述符:一个配置描述符、一个接口描述符及其所有端点,下一个接口描述符和其所有端点,等等。有一种方法可以从这些描述符中提取你感兴趣的接口,总线驱动程序提供的USBD_ParseConfigurationDescriptorEx函数可以简化这个过程:

PUSB_INTERFACE_DESCRIPTOR pid;

pid = USBD_ParseConfigurationDescriptorEx(pcd,

                                     StartPosition,

                                     InterfaceNumber,

                                     AlternateSetting,

                                     InterfaceClass,

                                     InterfaceSubclass,

                                     InterfaceProtocol);

在这个函数中,pcd是那个复合配置描述符的地址。StartPosition可以是配置描述符的地址,或者是起始搜索的描述符地址。其余参数指定描述符搜索准则。值-1表示在搜索中不使用该准则。下面这些准则可以在搜索中指定:

  • InterfaceNumber
  • AlternateSetting
  • InterfaceClass
  • InterfaceSubclass
  • InterfaceProtocol

当USBD_ParseConfigurationDescriptorEx返回一个接口描述符时,你应该把它保存在USBD_INTERFACE_LIST_ENTRY数组元素的InterfaceDescriptor成员中。然后,你跨过这个接口描述符前进到下一个接口。这个接口表项数组最后将成为调用USBD_CreateConfigurationRequestEx函数的一个参数,数组元素由下面结构定义:

typedef struct _USBD_INTERFACE_LIST_ENTRY {

     PUSB_INTERFACE_DESCRIPTOR InterfaceDescriptor;

     PUSBD_INTERFACE_INFORMATION Interface;

} USBD_INTERFACE_LIST_ENTRY,  *PUSBD_INTERFACE_LIST_ENTRY;

当初始化数组中的表项时,你应该把要使能接口的接口描述符地址赋予InterfaceDescriptor成员,并把Interface成员置NULL。你应该为每个接口定义一个表项,并在最后加入一个InterfaceDescriptorNULL的结束表项。

配置过程的下一步是创建URB并发送到设备:

urb = USBD_CreateConfigurationRequestEx(configDescriptor,  interfaceBuffer);

除了创建URBUSBD_CreateConfigurationRequestEx还初始化USBD_INTERFACE_LIST表项中的Interface成员,使其指向一个USBD_INTERFACE_INFORMATION结构。这个结构与URB在同一物理内存区,在调用ExFreePool释放URB时该结构一同被释放。该结构描述如下:

typedef struct  _USBD_INTERFACE_INFORMATION {

  USHORT Length;

  UCHAR InterfaceNumber;

  UCHAR AlternateSetting;

  UCHAR Class;

  UCHAR SubClass;

  UCHAR Protocol;

  UCHAR Reserved;

  USBD_INTERFACE_HANDLE InterfaceHandle;

  ULONG NumberOfPipes;

  USBD_PIPE_INFORMATION Pipes[1];

} USBD_INTERFACE_INFORMATION,  *PUSBD_INTERFACE_INFORMATION;

其中管道信息结构是我们在此真正关心的,因为结构中的其它域是在提交URB后由USBD填充的。管道结构描述如下:

typedef struct _USBD_PIPE_INFORMATION {

  USHORT  MaximumPacketSize;

  UCHAR  EndpointAddress;

  UCHAR  Interval;

   USBD_PIPE_TYPE PipeType;

   USBD_PIPE_HANDLE PipeHandle;

  ULONG  MaximumTransferSize;

  ULONG  PipeFlags;

} USBD_PIPE_INFORMATION, *PUSBD_PIPE_INFORMATION;

这样,我们已经有了一组USBD_INTERFACE_LIST表项,每一项中的USBD_INTERFACE_INFORMATION结构又包含一组USBD_PIPE_INFORMATION结构。接下来的任务是填充管道信息结构中的MaximumTransferSize成员,如果不填充这个成员,USBD将使用默认值USBD_DEFAULT_MAXIMUM_TRANSFER_SIZE,其值等于DDK中的PAGE_SIZE。这个成员值与端点在单事务中的最大传输量(在一个总线事务中可以传输多少字节)或端点接收量(由设备上有多少有效内存决定)不直接相关。相反,它代表单一URB中能携带的最大数据量。这个值可以小于应用程序向设备或设备向应用程序发送的数据量的最大值,在这种情况下,驱动程序必须把应用程序的请求分成不大于这个值的小块。

提供最大传输量的原因是由于一个调度算法,主控制器驱动程序使用这个算法把URB请求分成总线帧中的事务。如果要发送大量数据,我们的数据可能占满整个帧而其它设备的数据会被挤出去。所以,通过为URB指定一个适当的单次传输最大值来均衡总线带宽的使用。

一旦你完成管道信息结构的初始化,就可以提交这个配置URB

status = usb_icSubmitUrbSynch(DeviceExtension,  urb);

寻找句柄

配置选择URB成功完成后,我们应该把一些句柄保存下来供以后使用:

URB成员UrbSelectConfiguration.ConfigurationHandle返回该配置句柄。

USBD_INTERFACE_INFORMATION结构中的InterfaceHandle返回接口句柄。

每个USBD_PIPE_INFORMATION结构中都含有与每个端点对应的管道句柄PipeHandle

管理批量传输管道

在本程序中,有三个批量传输端点,其中一个是输入端点,两个输出端点。定义在usb.c中。

[usb.c  +424]

         UsbBuildInterruptOrBulkTransferRequest(

             urb,

             sizeof(struct _URB_BULK_OR_INTERRUPT_TRANSFER),

             DeviceExtension->InterfaceInformation->Pipes[0].PipeHandle,

             DeviceExtension->Pipe0Data,

             NULL,

             sizeof(DeviceExtension->Pipe0Data),

             USBD_TRANSFER_DIRECTION_IN | USBD_SHORT_TRANSFER_OK,

            NULL

             );

这些代码运行在DeviceIoControl调用的处理程序中,所以IRPSystemBuffer域指向应提交的数据。count变量是我们要填充的缓冲区的大小。

        //  initialize irp

         IoReuseIrp(irp, STATUS_SUCCESS);

 

         irpStack = IoGetNextIrpStackLocation(irp);

         irpStack->MajorFunction = IRP_MJ_INTERNAL_DEVICE_CONTROL;

         irpStack->Parameters.Others.Argument1 = urb;

         irpStack->Parameters.DeviceIoControl.IoControlCode =  IOCTL_INTERNAL_USB_SUBMIT_URB;

 

         IoSetCompletionRoutine(

             irp,

             usb_icOnPipe0Complete,

             DeviceExtension,

             TRUE,

             TRUE,

             TRUE

             );

 

         status = usb_icIncrementIoCount(&DeviceExtension->IoLock);

        if  (NT_SUCCESS(status))

        {

             status = IoCallDriver(DeviceExtension->LowerDeviceObject, irp);

        }

通过IoSetCompletionRoutine( )设置完成例程,然后通过IoCallDriver( )irp发送到底层的设备栈中,当管道中有数据到达时,会调用函数usb_icOnPipe0Complete( )

[usb.c  +679]

NTSTATUS usb_icPipe1IoCompletionRoutine(

    IN  PDEVICE_OBJECT DeviceObject,

    IN  PIRP           Irp,

    IN   PVOID          Context

    )

{

     PUSB_IC_IO_CONTEXT      ioContext;

     NTSTATUS                         status;

     ULONG                            transferLength;

     PIO_STACK_LOCATION               irpStack;

 

     ioContext = (PUSB_IC_IO_CONTEXT)Context;

    status  = Irp->IoStatus.Status;

 

     usb_icDebugPrint(DBG_IO, DBG_TRACE, __FUNCTION__"++. IRP %p  STATUS %x", Irp, status);

 

    if  (NT_SUCCESS(status))

    {

         ioContext->TotalTransferCount +=  ioContext->Urb->UrbBulkOrInterruptTransfer.TransferBufferLength;

 

        if  (ioContext->RemainingLength)

        {

             if (ioContext->RemainingLength >  USBD_DEFAULT_MAXIMUM_TRANSFER_SIZE)

             {

                 transferLength = USBD_DEFAULT_MAXIMUM_TRANSFER_SIZE;

             }

            else

             {

                 transferLength = ioContext->RemainingLength;

             }

 

             MmPrepareMdlForReuse(ioContext->Mdl);

             IoBuildPartialMdl(Irp->MdlAddress, ioContext->Mdl,  ioContext->NextVirtualAddress, transferLength);

 

             ioContext->Urb->UrbBulkOrInterruptTransfer.TransferBufferLength  = transferLength;

 

             ioContext->RemainingLength -= transferLength;

             ioContext->NextVirtualAddress += transferLength;

 

             irpStack = IoGetNextIrpStackLocation(Irp);

             irpStack->MajorFunction = IRP_MJ_INTERNAL_DEVICE_CONTROL;

             irpStack->Parameters.Others.Argument1 = ioContext->Urb;

             irpStack->Parameters.DeviceIoControl.IoControlCode =  IOCTL_INTERNAL_USB_SUBMIT_URB;

 

            IoSetCompletionRoutine(

                 Irp,

                 usb_icPipe1IoCompletionRoutine,

                 ioContext,

                 TRUE,

                 TRUE,

                 TRUE

                 );

 

             IoCallDriver(ioContext->DeviceExtension->LowerDeviceObject,  Irp);

 

             usb_icDebugPrint(DBG_IO, DBG_TRACE, __FUNCTION__"–. Next  Stage");

 

             return STATUS_MORE_PROCESSING_REQUIRED;

        }

         else

        {

             Irp->IoStatus.Information = ioContext->TotalTransferCount;

        }

    }

 

    // free  the resources

     ExFreePool(ioContext->Urb);

     IoFreeMdl(ioContext->Mdl);

     usb_icDecrementIoCount(&ioContext->DeviceExtension->IoLock);

     ExFreePool(ioContext);

 

     usb_icDebugPrint(DBG_IO, DBG_TRACE, __FUNCTION__"–. Done");

 

    return  STATUS_SUCCESS;

}

在这个回调函数中,我们主要做两件事情,一是将传输的数据保持到缓冲区,二是递归调用IoSetCompletionRoutine( )IoCallDriver( )来侦听新的批量传输数据。

错误恢复

当你用批量端点传输数据时,总线或总线驱动程序自动重试失败的传输。结果,如果URB表明成功完成,那么你可以确信数据已被正确传输。如果发生错误,你的驱动程序需要做某种恢复操作。第一步就是使处于停止状态的端点脱离停止状态,这样才可以再通信。下面是一个名为ResetPipe的辅助函数:

NTSTATUS usb_icResetPipe(

    IN  PUSB_IC_DEVICE_EXTENSION   DeviceExtension,

    IN  USBD_PIPE_HANDLE                    PipeHandle

    )

{

     NTSTATUS                     status;

    struct  _URB_PIPE_REQUEST    urb;

 

     usb_icDebugPrint(DBG_IO, DBG_INFO, __FUNCTION__"++");

 

     urb.Hdr.Length = (USHORT)sizeof(struct _URB_PIPE_REQUEST);

     urb.Hdr.Function = URB_FUNCTION_RESET_PIPE;

     urb.PipeHandle = PipeHandle;

 

    status  = usb_icSubmitUrbSynch(DeviceExtension, (PURB)&urb);

 

     usb_icDebugPrint(DBG_IO, DBG_INFO, __FUNCTION__"–. STATUS  %x", status);

 

    return  status;

}

上面就是提交管道重置URB的全部过程。由于这个辅助例程间接地等待URB被完成,所以你必须在PASSIVE_LEVEL级上调用它。这个URB所做的工作用USB术语来说,就是清除该管道端点的ENDPOINT_HALT特征。

如果重置管道失败,则需要重置整个设备,使用ResetDevice函数:

NTSTATUS usb_icResetDevice(

    IN  PUSB_IC_DEVICE_EXTENSION   DeviceExtension

    )

{

     NTSTATUS            status;

     PIRP                irp;

     KEVENT              event;

     IO_STATUS_BLOCK     ioStatus;

 

     usb_icDebugPrint(DBG_IO, DBG_INFO, __FUNCTION__"++");

 

     KeInitializeEvent(&event, NotificationEvent, FALSE);

 

    irp =  IoBuildDeviceIoControlRequest(

             IOCTL_INTERNAL_USB_RESET_PORT,

             DeviceExtension->LowerDeviceObject,

             NULL,

             0,

             NULL,

            0,

             TRUE,

             &event,

             &ioStatus

             );

 

    if(irp  != NULL)

    {

         status = IoCallDriver(DeviceExtension->LowerDeviceObject, irp);

        if  (status == STATUS_PENDING)

        {

             KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL);

             status = ioStatus.Status;

        }

    }

    else

    {

         status = STATUS_INSUFFICIENT_RESOURCES;

    }

 

     usb_icDebugPrint(DBG_IO, DBG_INFO, __FUNCTION__"–. STATUS  %x", status);

 

    return status;

}

hub口重置命令可以使hub驱动程序在重新初始化设备的同时保留设备当前配置。如果该命令失败,hub驱动程序将返回STATUS_UNSUCCESSFUL

管理中断管道

我们程序中有一个中断端点,其作用是接收usb设备的反馈信息,用于PC与读卡器的数据同步。

从设备方面看,中断管道与批量管道几乎完全相同。唯一的不同是,主机将以某个有保证的频率循检中断端点,除非中断端点有中断产生,否则设备将以NAK响应主机循检。报告中断事件时,设备以ACK应答并提供少量数据。

从驱动程序这边看,管理中断管道仅比管理批量管道稍复杂一点。当驱动程序读写批量管道时,它仅仅是创建URB并发送到总线驱动程序。但中断管道是用于向主机通知硬件事件,驱动程序需要始终维持一个未完成的读请求。这里不推荐使用系统循检线程,因为电源管理问题将使独立线程的管理大大复杂化。维持一个读请求最好是用一个完成例程使URB循环提交。

USBINT例子演示了怎样用循环URB管理中断管道。我写了一些辅助例程来协助这个工作:

usb_icDisarmPipe0删除URB。无论何时,当我们关闭设备时就调用这个函数来释放IRPURB占用的内存。

usb_icArmPipe0发送一个URB来循检设备的中断端点。无论何时当我们激活设备时就调用这个函数。

usb_icOnPipe0Complete是一个标准的I/O完成函数,作为设备的中断服务例程。它看起来象这样:

NTSTATUS usb_icOnPipe0Complete(

    IN  PDEVICE_OBJECT      DeviceObject,

    IN  PIRP                Irp,

    IN  PVOID               Context

    )

{

     PUSB_IC_DEVICE_EXTENSION deviceExtension;

     NTSTATUS                status;

       ULONG  cnt;

 

     deviceExtension = (PUSB_IC_DEVICE_EXTENSION)Context;

    status  = Irp->IoStatus.Status;

 

     usb_icDebugPrint(DBG_IO, DBG_TRACE, __FUNCTION__"++. STATUS  %x", status);

 

     InterlockedExchange(&deviceExtension->Pipe0Armed, FALSE);     

      

      

       deviceExtension->SavedPipe0Data[deviceExtension->SavedPipe0DataIndex++]  = deviceExtension->Pipe0Data[0];                                          ß 1

 

       usb_icDebugPrint(DBG_IO,  DBG_TRACE, __FUNCTION__"deviceExtension->SavedPipe0Data[0] =  %d", deviceExtension->SavedPipe0Data[0]);

 

 

       InterlockedIncrement(&deviceExtension->Pipe0ArmedCnt);

       InterlockedExchange(&deviceExtension->ResetPipe0Data,  FALSE);

 

    if  (NT_SUCCESS(status))

    {

         //*****************************************************************

         //*****************************************************************

        //  TODO: deal with the interrupt and rearm it

         //*****************************************************************

         //*****************************************************************

         usb_icArmPipe0(deviceExtension);                                   ß 2

    }

    else

    {

    }

 

     usb_icDecrementIoCount(&deviceExtension->IoLock);

 

     usb_icDebugPrint(DBG_IO, DBG_TRACE, __FUNCTION__"–");

 

    return  STATUS_MORE_PROCESSING_REQUIRED;                          ß 3

}

1.     数据备份。

2.     中断处理,我们用相同的URB初始化另一轮中断循检。

3.     返回STATUS_MORE_PROCESSING_REQUIRED,因为我们不希望IoCompleteRequest做任何事。

与用户程序交互

读、写、删除读卡器数据等操作都是由应用程序触发的,因此驱动程序中有相应的接口函数(系统调用)来响应应用程序的请求。我们这里主要用了三个系统调用:read(), write()ioctl()

[iorw.c  +28]

NTSTATUS usb_icReadDispatch(

    IN  PDEVICE_OBJECT  DeviceObject,

    IN  PIRP            Irp

    )

{

     PUSB_IC_DEVICE_EXTENSION    deviceExtension;

     NTSTATUS                         status;

     PIO_STACK_LOCATION               irpStack;

     PVOID                            readBuffer;

     ULONG                            readLength;

 

     usb_icDebugPrint(DBG_IO, DBG_TRACE, __FUNCTION__"++. IRP  %p", Irp);

 

     deviceExtension =  (PUSB_IC_DEVICE_EXTENSION)DeviceObject->DeviceExtension;

 

    status  = usb_icCheckIoLock(&deviceExtension->IoLock, Irp);

    if  (!NT_SUCCESS(status) || (status == STATUS_PENDING))

    {

         usb_icDebugPrint(DBG_IO, DBG_WARN, __FUNCTION__"–. IRP %p STATUS  %x", Irp, status);

 

         return status;

    }

    status  = usb_icPipe2Io(

                 deviceExtension,

                 Irp,

                 &deviceExtension->InterfaceInformation->Pipes[2]

                );

 

//     usb_icDecrementIoCount(&deviceExtension->IoLock);

 

     usb_icDebugPrint(DBG_IO, DBG_TRACE, __FUNCTION__"–. IRP %p  STATUS %x", Irp, status);

 

    return  status;

}

read()系统调用用于读取读卡器批量数据,使用的是输入批量传输管道,即端点2。因此这里直接调用usb_icPipe2Io()

同样write()系统调用用于写入读卡器批量数据,使用的是端点3,因此在usb_icWriteDispatch()函数中直接调用usb_icPipe3Io().

除了读写数据,我们还需要其他控制操作,比如初始化读卡器、读取读卡器状态及关闭卡插座等等,这些操作都在ioctl()系统调用中完成。

NTSTATUS usb_icDeviceIoControlDispatch(

    IN  PDEVICE_OBJECT  DeviceObject,

    IN  PIRP            Irp

    )

{

… …

    switch  (irpStack->Parameters.DeviceIoControl.IoControlCode)

    {

 

case  PIPE1_OUT_COMMAND:

… …

    status = usb_icPipe1Io(

                deviceExtension,

                Irp,

                 &deviceExtension->InterfaceInformation->Pipes[1]

                );

        break;

case PIPE1_IN_STATUS:

… …

       switch(deviceExtension->CmdType)

       {

}

break;

case PIPE2_IN_STATUS:

    status = usb_icPipe2Io(

                deviceExtension,

                Irp,

                 &deviceExtension->InterfaceInformation->Pipes[2]

                );

        break;

这里一共有三个ioctl的系统调用:

PIPE1_OUT_COMMAND  对应端点1的输出,即发送请求指令。

PIPE1_IN_STATUS  对应端点1的输入,获取反馈状态。

PIPE2_IN_STATUS  对应端点2的输入,获取反馈状态。

开发流程

DriverStadio生成驱动的相关信息

a.      设备序列号

USB Vendor ID 0471

USB Product ID 2663

b.     端点信息

Pipe name

Pipe0

Pipe1

Pipe2

Pipe3

Endpoint type

Bulk

Bulk

Bulk

Bulk

Endpoint address

1

1

2

2

Transfer direction

In

Out

In

Out

Maxmum packet size

16

16

64

64

Maxmum transfer size{per UBR}

4096

4096

4096

4096

c.      IRP类型

IRP_MJ_SYSTEM_CONTROL

IRP_MJ_PNP

IRP_MJ_CLOSE

IRP_MJ_POWER

IRP_MJ_CLEANUP

IRP_MJ_CREATE

IRP_MJ_READ

IRP_MJ_WRITE

IRP_MJ_DEVICE_CONTROL

d.     IOCTL

PIPE1_OUT_COMMAND

PIPE1_IN_STATUS

PIPE2_IN_STATUS

SymbolicLinkusbICDevice

通信协议

a.      命令格式

命令由PC机发往读卡器,共9个字节组成。        

 

x

x

x

x

x

x

x

x

x

byte

0

1

2

3

4

5

6

7

8

各字节含义说明

byte0

操作卡类型

0x00      未知卡

0x02      扇区大小为264bytes的卡

0x05      扇区大小为258bytes的卡

byte1

操作类型

0x11      查询卡类型

0x13      关闭卡座电源

0x03      写一个扇区

0x82      读一个扇区

0x04      整卡删除

byte2,byte3

扇区地址,若扇区地址为0xfffe,则代表是对密码寄存器进行操作

扇区地址 = byte3  * 0x100 + byte2

byte4,byte5

扇区内偏移地址

偏移地址 = byte5  * 0x100 + byte4

byte6,byte7

传输数据长度

数据长度 = byte7  * 0x100 + byte6

byte8

校验位

byte8 = byte0 ^ byte1 ^ … ^ byte7

b.     卡类型代号

卡类型

卡存储容量(byte)

卡扇区大小(byte)

扇区数目

卡类型代码

AT45DB041B

512k

256+8

2048

0x02

AT45DB161B

2M

512+16

4096

0x05

AT45DB161D

2M

512+16

4096

0x06

AT45DB321D

4M

512+16

8192

0x07

c.      要删除的卡类型

PC机发出的卡类型代码分两类:

0x02代表扇区大小为264byte的一类卡

0x05代表扇区大小为528byte的一类卡

AT45DB041B代码为0x02,而AT45DB161B,AT45DB161D,AT45DB321D代码都是0x05

查询卡类型

out1

00 11 00 00 00 00 00 11

in1

0x99(命令正确接收)  0x88(无卡)  0x6(错误命令)

in1

0x90(操作正常)

in2

0102(byte0 表示卡类型代码,byte1中的1表示卡存在,0表示卡不存在)

流程如下:

读扇区

in1

0x99(操作正常)  0x88(无卡)

in1

0x90(操作正常)  0x80(操作异常)

in2

传输一个扇区数据

流程如下:

写扇区

in1

0x99(命令接收正确)  0x66(重发)

out2

写数据

in1

0x90(操作正常)  0x80(操作错误)

流程如下

删除卡

out1

05 04 00 00 00 00 00 00 xx(xx为校验位)

in1

0x99(操作正常)  0x79(错误操作)

in1

0x90(操作正常)

 

参考文献

[1] 张帆.Windows驱动开发技术详解.北京:电子工业出版社,2009

[2] Art Baker.Windows200设备驱动程序设计指南.北京:机械工业出版社,2001

[3] Walter Oney.Programming the Microsoft windows driver model.Microsoft Press,1999

 

Published by

风君子

独自遨游何稽首 揭天掀地慰生平