Êíèãà: Writing Windows WDM Device Drivers

Talking USB

Talking USB

Initializing a USB Device

There are several jobs that a USB client driver must do to initialize its connection to its device. The UsbKbd driver does these jobs in its Create IRP handler. However, most USB drivers will want to initialize their device when processing the Start Device Plug and Play IRP.

1. Check device enabled. Reset and enable the device, if necessary.

2. Select one interface in one of the configurations.

3. Possibly read other descriptors, such as the string-, class-, or vendor-specific descriptors.

4. Talk to your device to issue whatever commands are relevant, get device status, and initialize pipes.

Device Reset

Listing 21.3 shows how UsbKbd resets its device in routine UsbResetDevice when a Win32 program opens a handle to it. UsbGetPortStatus issues IOCTL_INTERNAL_USB_GET_PORT_STATUS to retrieve the port status bits. UsbResetDevice checks the USBD_PORT_CONNECTED and USBD_PORT_ENABLED bits, and calls UsbResetPort, if necessary. UsbResetPort simply issues IOCTL_INTERNAL_USB_RESET_PORT to the USB class drivers.

You may need to reset the port if some drastic communication problem has arisen with your device (e.g., if control transfers to the default pipe keep failing). However, if another pipe stalls, do not reset the port.

If a pipe stalls or USBDI detects a timeout, the pipe becomes halted. The USBD_HALTED macro detects this condition. All URBs linked to the current URB are cancelled. A halted pipe cannot accept any more transfers until a Reset Pipe URB is issued.

However, the default pipe can never be halted. If a timeout or stall occurs here, an error is reported, which is detectable using the USBD_ERROR macro, but not USBD_HALTED. The halt condition is cleared automatically to give transfers a chance of succeeding on this pipe. If they keep failing, you will have to reset the port.

Listing 21.3 Device reset routines

NTSTATUS UsbGetPortStatus(IN PUSBKBD_DEVICE_EXTENSION dx, OUT ULONG& PortStatus) {
 DebugPrintMsg("Getting port status");
 PortStatus = 0;
 NTSTATUS status = CallUSBDI(dx, &PortStatus, IOCTL_INTERNAL_USB_GET_PORT_STATUS);
 DebugPrint("Got port status %x", PortStatus);
 return status;
}
NTSTATUS UsbResetPort(IN PUSBKBD_DEVICE_EXTENSION dx) {
 DebugPrintMsg("Resetting port");
 NTSTATUS status = CallUSBDI(dx, NULL, IOCTL_INTERNAL_USB_RESET_PORT);
 DebugPrint("Port reset %x", status);
 return status;
}
NTSTATUS UsbResetDevice(IN PUSBKBD_DEVICE_EXTENSION dx) {
 ULONG PortStatus;
 NTSTATUS status = UsbGetPortStatus(dx, PortStatus);
 if (!NT_SUCCESS(status)) return status;
 // Give up if device not connected
 if (!(PortStatus & USBD_PORT_CONNECTED)) return STATUS_NO_SUCH_DEVICE;
 // Return OK if port enabled
 if (PortStatus & USBD_PORT_ENABLED) return status;
 // Port disabled so attempt reset
 status = UsbResetPort(dx);
 if (!NT_SUCCESS(status)) return status;
 // See if it is now working
 status = UsbGetPortStatus(dx, PortStatus);
 if (!NT_SUCCESS(status)) return status;
 if (!(PortStatus & USBD_PORT_CONNECTED) || !(PortStatus & USBD_PORT_ENABLED)) return STATUS_NO_SUCH_DEVICE;
 return status;
}

Issuing URBs

The Usb.cpp module contains many routines that build and send off URBs for processing by the USB class drivers. UsbGetDeviceDescriptor is used to retrieve the USB device descriptor. This routine is not essential in UsbKbd, but it is used to implement one of the IOCTLs that are available to user mode programs. UsbGetDeviceDescriptor allocates nonpaged memory for the device descriptor and puts the count of bytes transferred in its Size parameter. The routine that calls UsbGetDeviceDescriptor must free the memory using ExFreePool.

UsbGetDeviceDescriptor starts by allocating some nonpaged memory for the URB. The Get Descriptor URB function uses the URB_FUNCTION_GET_DESCRIPTOR_FROM_DEVICE function code. For this function code, the _URB_CONTROL_DESCRIPTOR_REQUEST structure is used.

Next, UsbGetDeviceDescriptor allocates memory for the device descriptor. Then, it calls UsbBuildGetDescriptorRequest to do the hard work of formatting the URB. The URB pointer and its size are passed as the first two parameters. The next parameter specifies the descriptor type, a device descriptor in this case. The following Index and LanguageId fields are only used when requesting configuration and string descriptors.

The following parameters to UsbBuildGetDescriptorRequest specify the descriptor buffer and its length. This buffer can be specified as a plain pointer to nonpaged memory. Alternatively, an MDL can be used. The final parameter is UrbLink, an optional link to a follow on URB.

UsbGetDeviceDescriptor sends off the built URB using CallUSBDI. It checks both the CallUSBDI NTSTATUS return status and the URB completion status.

The final job for UsbGetDeviceDescriptor is simply to save the count of bytes transferred. This was stored in the URB UrbControlDescriptorRequest.TransferBufferLength field. The USB memory is freed.

Listing 21.4 Getting a USB device descriptor

NTSTATUS UsbGetDeviceDescriptor(IN PUSBKBD_DEVICE_EXTENSION dx, OUT PUSB_DEVICE_DESCRIPT0R& deviceDescriptor, OUT ULONGS Size) {
 // Allocate memory for URB
 USHORT UrbSize = sizeof(struct _URB_CONTROL_DESCRIPTOR_REQUEST);
 PURB urb = (PURB)ExAllocatePool(NonPagedPool, UrbSize);
 if (urb==NULL) {
  DebugPrintMsg("No URB memory");
  return STATUS_INSUFFICIENT_RESOURCES;
 }
 // Allocate memory for device descriptor
 ULONG sizeDescriptor = sizeof(USB_DEVICE_DESCRIPTOR);
 deviceDescriptor = (PUSB_DEVICE_DESCRIPTOR)ExAllocatePool(NonPagedPool, sizeDescriptor);
 if (deviceDescriptor==NULL) {
  ExFreePool(urb);
  DebugPrintMsg("No descriptor memory");
  return STATUS_INSUFFICIENT_RESOURCES;
 }
 // Build the Get Descriptor URB
 UsbBuildGetDescriptorRequest(urb, UrbSize,
  USB_DEVICE_QESCRIPTOR_TYPE, 0, 0, // Types, Index & LanguageId
  deviceDescriptor, NULL, sizeDescriptor, // Transfer buffer
  NULL); // Link URB
 // Call the USB driver
 DebugPrintMsg("Getting device descriptor");
 NTSTATUS status = CallUSBDI(dx, urb);
 // Check statuses
 if (!NT_SUCCESS(status) || !USBD_SUCCESS( urb->UrbHeader.Status)) {
  DebugPrint("status %x URB status %x", status, urb->UrbHeader.Status);
  status = STATUS_UNSUCCESSFUL;
 }
 // Remember count of bytes actually transferred
 Size = urb->UrbControlDescriptorRequest.TransferBufferLength;
 ExFreePool(urb);
 return status;
}

Selecting an Interface

To start using a device, your driver must select one interface within one configuration. This is more of a job than you might expect. Routine UsbSelectConfiguration performs these steps, as shown in Listing 21.5.

1. Get the configuration, interface, endpoint descriptors, etc.

2. Find the appropriate interface descriptor.

3. Issue a select configuration URB.

4. Save the configuration handle and handles to any pipes that you use.

UsbGetConfigurationDescriptors (not listed) is used to retrieve all the descriptors that are needed. This routine issues a Get Descriptor request twice. The first call returns just the basic configuration descriptor. The wTotalLength field in there tells you how much memory to allocate to retrieve all the associated descriptors.

USBD_ParseConfigurationDescriptorEx is used to find an interface. The parameters to this routine specify the criteria that must match. In this case UsbKbd is not interested in matching the interface number or alternate settings, but is interested in the device class. The HID device class is 3 (USB_DEVICE_CLASS_HUMAN_INTERFACE). To match a HID keyboard the subclass and protocol interface descriptor fields must both be 1.

If USBD_ParseConfigurationDescriptorEx finds a matching interface, it returns the interface descriptor pointer. The next task is to build a suitable Select Configuration URB. The helper function USBD_CreateConfigurationRequestEx allocates and fills this URB. UsbSelectConfiguration specifies an array of USBD_INTERFACE_LIST_ENTRY structures as an input to this routine, with a NULL InterfaceDescriptor field indicating the end of the list. Each structure specifies the interface descriptor. When the Select Configuration URB has been run, the Interface field points to valid data.

The Select Configuration URB is then issued. This may fail if there is not enough USB bandwidth available. If it works, a configuration handle is returned. You can use this to select a different alternate interface setting. UsbKbd stores the configuration handle in its device extension, but does not use it.

UsbKbd does need to store the pipe handle for the HID keyboard's interrupt pipe. For a HID USB keyboard, this is the first and only pipe in the USBD_INTERFACE_INFORMATION structure. UsbKbd makes DebugPrint calls to display other potentially useful information. UsbKbd finally frees the URB and configuration descriptor memory.

Listing 21.5 Selecting a configuration and interface

NTSTATUS UsbSelectConfiguration(IN PUSBKBD_DEVICE_EXTENSION dx) {
 dx->UsbPipeHandle = NULL;
 // Get all first configuration descriptors
 PUSB_CONFIGURATIONDESCRIPTOR Descriptors = NULL;
 ULONG size;
 NTSTATUS status = UsbGetConfigurationDescriptors(dx, Descriptors, 0, size);
 if (!NT_SUCCESS(status)) {
  DebugPrint("UsbGetConfigurationDescriptors failed %x", status);
  FreeIfAllocated(Descriptors);
  return status;
 }
 // Search for an interface with HID keyboard device class
 PUSB_INTERFACE_DESCRIPTOR id = USBD_ParseConfigurationDescriptorEx(Descriptors, Descriptors,
  -1, –1, // Do not search by InterfaceNumber or AlternateSetting
  3, 1, 1); // Search for a HID device, boot protocol, keyboard
 if (id==NULL) {
  DebugPrintMsg("No matching interface found");
  FreeIfAllocated(Descriptors);
  return STATUS_NO_SUCH_DEVICE;
 }
 // Build list of interfaces we are interested in
 USBD_INTERFACE_LIST_ENTRY ilist[2];
 ilist[0].InterfaceDescriptor = id;
 ilist[0].Interface = NULL; // Will point to
                            // urb->UrbUsbSelectConfiguration.Interface
 ilist[1].InterfaceDescriptor = NULL;
 // Create select configuration URB
 PURB urb = USBD_CreateConfigurationRequestEx(Descriptors, ilist);
 // Call the USB driver
 DebugPrintMsg("Select ing configuration");
 status = CallUSBDI(dx, urb);
 // Check statuses
 if (!NT_SUCCESS(status) || !USBD_SUCCESS( urb->UrbHeader.Status)) {
  DebugPrint("status %x URB status %x", status, urb->UrbHeader.Status);
  status = STATUS_UNSUCCESSFUL;
 } else {
  // Select config worked
  DebugPrintMsg("Select configuration worked");
  dx->UsbConfigurationHandle = urb->UrbSelectConfiguration.ConfigurationHandle;
  // Find pipe handle of first pipe,
  // ie interrupt pipe that returns input HID reports
  PUSBD_INTERFACE_INFORMATION InterfaceInfo = &urb->UrbSelectConfiguration.Interface;
  DebugPrint("interface Class %d NumberOfPipes %d", InterfaceInfo->Class, InterfaceInfo->NumberOfPipes);
  if (InterfaceInfo->NumberOfPipes>0) {
   PUSBD_PIPE_INFORMATION pi = &InterfaceInfo->Pipes[0];
   dx->UsbPipeHandle = pi->PipeHandle;
   DebugPrint("PipeHandle = %x", dx->UsbPipeHandle);
   DebugPrint("Pipes[0] EndpointAddress %2x"
    "Interval %dms PipeType %d MaximumTransferSize %c",
    pi->EndpointAddress, pi->Interval, pi->PipeType, pi->MaximumTransferSize);
  }
  if (dx->UsbPipeHandle==NULL) status = STATUS_UNSUCCESSFUL;
 }
 FreeIfAllocated(urb);
 FreeIfAllocated(Descriptors);
 return status;
}

Other Initialization

As mentioned earlier, having selected your configuration and interface, there may be some more steps needed to initialize your USB peripheral. You may want to read other descriptors. The UsbKbd Create IRP handler shows how this might be done by getting the first string descriptor. This in fact just returns the language ID that the device supports. You will have to issue further Get Descriptor requests to get each String Descriptor. The UsbGetSpecifiedDescriptor routine in Usb.cpp can be used to get any descriptor. This routine is also used by the IOCTL_USBKBD_GET_SPECIFIED_DESCRIPTOR handler. The UsbKbdTest test program issues this IOCTL to get the HID Descriptor.

The UsbKbd Create IRP handler, UsbKbdCreate, also retrieves some general information about the USB bus, such as the amount of bandwidth used and the host controller device name. The required Internal IOCTLs are only implemented in Windows 2000, so preprocessor directives are used to remove the body of the function that does this job, UsbGetUsbInfo, when compiled for Windows 98.

Most USB device drivers will now want to talk to their devices over control or other pipes. Shortly, I describe how to perform such transfers.

Deselecting a Configuration

When a driver wants to stop accessing its device, it should deselect its configuration. In UsbKbd, the Close file handle IRP handler does this job. The UsbDeselectConfiguration routine uses UsbBuildSelectConfigurationRequest to build a Select Configuration URB with a NULL configuration descriptor. This disables the configuration and its interfaces.

Interrupt Transfers

The specification for UsbKbd says that it reports raw keyboard data in response to ReadFile Win32 requests. It also must implement a time-out, which defaults to 10 seconds.

Before I describe how to handle the Read IRPs, I must discuss how a USB HID keyboard produces data. The keyboard responds to USB Interrupt transfers with an 8-byte data report. The exact format of this block is discussed in the next chapter on Human Input Devices (HID).

A keyboard report is produced whenever a keypress or release occurs. A report is also produced regularly even if no keypresses occur. This is what happens on the USB bus. The USB class drivers initiate an Interrupt transfer regularly. My USB keyboard specifies that interrupt transfers should occur every 8ms. However, USB HID keyboards implement an idle rate. If there are no state changes during the idle period, the keyboard NAKs each Interrupt request. At the end of the idle period (every 500ms or so), the keyboard returns data regardless. The idle rate can be read and changed using class specific control transfers on the default pipe[53].

UsbKbd issues a Do Bulk or Interrupt Transfer URB to receive any keyboard reports. Keyboard reports are not "saved up" ready to fulfil any interrupt transfer requests. It is not clear whether the USB class drivers simply do not do any transfers, or whether they do the transfers but just dump the data. Any keypresses before an interrupt transfer is requested are ignored. I understand that the Microsoft keyboard drivers always try to leave two Interrupt transfer requests outstanding; the hope is that this should ensure that no keypresses are lost.

It turns out that the 8-byte keyboard report is all zeroes if no keys are pressed. The UsbKbd Read IRP handler ignores any keyboard reports that are all zero. A real driver would return read data when any key is pressed or released. A higher-level driver would have to implement an auto-repeat feature.

I can finally describe the Interrupt transfer routine, UsbDoInterruptTransfer, shown in Listing 21.6. This takes a buffer as input that must be at least 8 bytes long. UsbDoInterruptTransfer first checks that the USB pipe handle is available and that the input buffer is suitable.

In the same way as usual, memory is allocated for the URB, a _URB_BULK_OR_INTERRUPT_TRANSFER structure. UsbDoInterruptTransfer then loops until a non-zero keyboard report is returned, or until a time-out or another error occurs. UsbBuildInterruptOrBulkTransferRequest is used to build the URB each time[54]. UsbDoInterruptTransfer calls the USB class drivers in the usual way.

The time-out is checked using the KeQueryTickCount call[55]. This returns a ULONG count of "timer interrupts" since the operating system started. The timer interrupt interval is not the same in Windows 98 and Windows 2000. Use KeQueryTimeIncrement to find out this interval in units of 100ns. In both cases, there are just over 100 timer ticks per second.

UsbDoInterruptTransfer remembers the tick count before the first keyboard Interrupt request is generated. After each Interrupt request completes, it checks the tick count again, and gives up after the specified time-out.

UsbKbd does not handle IRP cancelling or the Cleanup routine. UsbKbd relies on the time-out to complete Read IRPs. If you do implement IRP cancelling, then you will need to use the Abort Pipe URB request to cancel all requests on the specified port.

Listing 21.6 Doing interrupt transfers

NTSTATUS UsbDoInterruptTransfer(IN PUSBKBD_DEVICE_EXTENSION dx, IN PVOID UserBuffer, ULONG& UserBufferSize) {
 // Check we're selected
 if (dx->UsbPipeHandle==NULL) return STATUS_INVALID_HANDLE;
 // Check input parameters
 ULONG InputBufferSize = UserBufferSize;
 UserBufferSize = 0;
 if (UserBuffer==NULL || InputBufferSize<8) return STATUS_INVALID_PARAMETER;
 // Keyboard input reports are always 8 bytes
 long NTSTATUS status = STATUS_SUCCESS;
 ULONG OutputBufferSize = 8;
 // Allocate memory for URB
 USHORT UrbSize = sizeof(struct _URB_BULK_OR_INTERRUPT_TRANSFER);
 PURB urb = (PURB)ExAllocatePool(NonPagedPool, UrbSize);
 if (urb==NULL) {
  DebugPrintMsg("No URB memory");
  return STATUS_INSUFFICIENT_RESOURCES;
 }
 // Remember when we started
 // Get start tick count and length of tick in 100ns units
 LARGE_INTEGER StartTickCount;
 KeQueryTickCount(&StartTickCount);
 ULONG UnitsOf100ns = KeQueryTimeIncrement();
 // Loop until non-zero report read, error, bad length, or timed out
 while(true) {
  // Build Do Bulk or Interrupt transfer request
  UsbBuildInterruptOrBulkTransferRequest(urb, UrbSize, dx->UsbPipeHandle, UserBuffer, NULL, OutputBufferSize, USBD_TRANSFER_DIRECTION_IN, NULL);
  // Call the USB driver
  status = CallUSBDI(dx, urb);
  // Check statuses
  if (!NT_SUCCESS(status) || !USBD_SUCCESS( urb->UrbHeader.Status)) {
   DebugPrint("status %x URB status %x", status, urb->UrbHeader.Status);
   status = STATUS_UNSUCCESSFUL;
   break;
  }
  // Give up if count of bytes transferred was not 8
  if (urb->UrbBulkOrInterruptTransfer.TransferBufferLength!= OutputBufferSize) break;
  // If data non-zero then exit as we have a keypress
  __int64* pData = (__int64 *)UserBuffer;
  if (*pData!=0i64) break;
  // Check for time-out
  LARGE_INTEGER TickCountNow;
  KeQueryTickCount(&TickCountNow);
  ULONG ticks = (ULONGKTickCountNow.QuadPart – StartTickCount.QuadPart);
  if (ticks*UnitsOf100ns/10000000 >= dx->UsbTimeout) {
   DebugPrint("Time-out %d 100ns", ticks*UnitsOf100ns);
   status = STATUS_NO_MEDIA_IN_DEVICE;
   break;
  }
 }
 UserBufferSize = urb->UrbBulkOrInterruptTransfer.TransferBufferLength;
 if (NT_SUCCESS(status)) {
  PUCHAR bd = (PUCHAR)UserBuffer;
  DebugPrint("Transfer data %2x %2x %2x %2x %2x %2x %2x %2x", bd[0], bd[1], bd[2], bd[3], bd[4], bd[5], bd[6], bd[7]);
 }
 ExFreePool(urb);
 return status;
}

Control Transfers

The UsbKbd driver lets its controlling application modify the state of the keyboard LEDs. This lets me illustrate how to do Control transfers on the default pipe to endpoint zero. The Write IRP handler sends the first byte of the write buffer to the keyboard as a HID "output report". Again, the next chapter fully defines the format of this report and the SET_REPORT command. The lower three bits of the report correspond to the NumLock, CapsLock, and ScrollLock keys.

The Write IRP handler calls UsbSendOutputReport to send the output report. As per usual, it allocates a suitable URB. The UsbBuildVendorRequest helper function is used to fill this URB. Control transfers to the default pipe can take many shapes and forms. First, there are "class" and "vendor" types. For each type, you can specify whether the destination is the "device", "interface", "endpoint", or "other".

The HID Specification tells us to send a "class" control transfer request to the "interface". The call to UsbBuildVendorRequest, therefore, uses the URB_FUNCTION_CLASS_INTERFACE URB function code. The TransferFlags parameter specifies the direction of the data transfer; set the USBD_TRANSFER_DIRECTION_IN flag for input transfers. The Request, Value, and Index parameters are set as instructed by the HID Specification. Finally, the output data buffer is given.

The URB is then sent off to the class drivers. Apart from noting the URB status and freeing the URB memory, there is nothing else to do.

The UsbGetIdleRate routine in Usb.cpp shows how to input data using a control transfer over the default pipe. This issues a GET_IDLE request. The current keyboard idle rate is printed in a DebugPrint trace statement.

Listing 21.7 Doing an output control transfer

const UCHAR SET_REPORT = 0x09;
NTSTATUS UsbSendOutputReport(IN PUSBKBD_DEVICE_EXTENSION dx, IN UCHAR OutputData) {
 // Allocate memory for URB
 USHORT UrbSize = sizeof(struct _URB_CONTROL_VENDOR_OR_CLASS_REQUEST);
 PURB urb = (PURB)ExAllocatePool(NonPagedPool, UrbSize);
 if (urb==NULL) {
  DebugPrintMsg("No URB memory");
  return STATUS_INSUFFICIENT_RESOURCES;
 }
 // Build URB to send Class interface control request on Default pipe
 UsbBuildVendorRequest(urb,
  URB_FUNCTION_CLASS_INTERFACE, UrbSize,
  USBD_TRANSFER_DIRECTION_OUT, // Direction out
  0, // Reserved bits
  SET_REPORT, // Request
  0x0200, // Output report type, Report id zero
  0, // interface index
  &OutputData, NULL, 1, // Output data
  NULL);
 // Call the USB driver
 DebugPrintMsg("Sending set report");
 NTSTATUS status = CallUSBDI(dx, urb);
 // Check statuses
 if (!NT_SUCCESS(status) || !USBD_SUCCESS(urb->UrbHeader.Status)) {
  DebugPrint("status %x URB status %x", status, urb->UrbHeader.Status);
  status = STATUS_UNSUCCESSFUL;
 }
 ExFreePool(urb);
 return status;
}

Other Issues

The UsbGetStatuses routine shows how to read status words from the device, interface, and endpoint using the Get Status URB. These words are bundled into a 6-byte buffer in response to the UsbKbd IOCTL_USBKBD_GET_STATUSES request.

The UsbKbd IOCTL_USBKBD_GET_FRAME_INFO request is handled in the UsbGetFrameInfo routine. UsbGetFrameInfo uses the Get Current Frame Number and Get Frame Length URBs to find out the required information. Both these URBs are built by hand, as there are no UsbBuild…Request helper macros. The frame numbers returned are 32-bit ULONGs, not the USB SOF number that goes from 0-0x7FF.

Îãëàâëåíèå êíèãè


Ãåíåðàöèÿ: 1.173. Çàïðîñîâ Ê ÁÄ/Cache: 3 / 1
ïîäåëèòüñÿ
Ââåðõ Âíèç