Not a WDF driver course goes by that we aren’t asked by at least one student how to use a multi-interface device in KMDF. We always have to point them to a slide in the presentation that we think does the right thing, but without a multi-interface device to test we’ve been forced to be, shall we say, less than authoritative.
After many courses and unanswered prayers for students to stop asking a question we didn’t really know the answer to, we decided to set forth and actually try to get a multi-interface device working. To do this, we hacked the firmware on the OSR USB FX2 learning kit to add a second interface with an isochronous endpoint. It wasn’t pretty, but after a few sessions with our USB analyzer we actually got it to work.
EvtDevicePrepareHardware
Remember, when there are multiple interfaces in a USB device, each interface can be used in parallel and the endpoint addresses within each interface are unique within the configuration. As a result, there are only a few differences between a driver for a multi-interface device versus a single interface device. Almost all of the changes in the driver are likely to the contained within the EvtDevicePrepareHardware callback, where your driver selects a configuration and enumerates the various pipes on the device.
When you select your configuration, you need to supply a WDF_USB_DEVICE_SELECT_CONFIG_PARAMS structure. In the case of a multiple interface device, you will initialize this structure with WDF_USB_DEVICE_SELECT_ CONFIG_PARAMS_INIT_MULTIPLE_INTERFACE shown in Figure 1 and SettingPairs defined in Figure 2.
VOID WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_MULTIPLE_INTERFACES( IN OUT PWDF_USB_DEVICE_SELECT_CONFIG_PARAMS Params, IN OPTIONAL UCHAR NumberInterfaces, IN OPTIONAL PWDF_USB_INTERFACE_SETTING_PAIR SettingPairs );
typedef struct _WDF_USB_INTERFACE_SETTING_PAIR { WDFUSBINTERFACE UsbInterface; UCHAR SettingIndex; } WDF_USB_INTERFACE_SETTING_PAIR, *PWDF_USB_INTERFACE_SETTING_PAIR;
The WDK documentation doesn’t do such a great job with this routine and doesn’t give much insight into why one may or may not want to supply the optional parameters. Here’s what it says:
If the SettingPairs parameter is not NULL, this function sets the Type member to WdfUsbTargetDeviceSelect ConfigTypeInterfacesPairs. Otherwise, WDF_USB_DEVICE_SELECT_CONFIG _PARAMS_INIT_MULTIPLE_ INTERFACES sets the Type member to WdfUsbTargetDeviceSelectConfigType MultiInterface.
Inspecting the inline function shows that both NumberInterfaces and SettingPairs must be supplied together, otherwise SettingPairs will be ignored (See Figure 3).
VOID FORCEINLINE WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_MULTIPLE_INTERFACES( IN OUT PWDF_USB_DEVICE_SELECT_CONFIG_PARAMS Params, IN OPTIONAL UCHAR NumberInterfaces, IN OPTIONAL PWDF_USB_INTERFACE_SETTING_PAIR SettingPairs ) { RtlZeroMemory(Params, sizeof(WDF_USB_DEVICE_SELECT_CONFIG_PARAMS)); Params->Size = sizeof(WDF_USB_DEVICE_SELECT_CONFIG_PARAMS); if (SettingPairs != NULL && NumberInterfaces != 0) { Params->Type = WdfUsbTargetDeviceSelectConfigTypeInterfacesPairs; Params->Types.MultiInterface.NumberInterfaces = NumberInterfaces; Params->Types.MultiInterface.Pairs = SettingPairs; } else { Params->Type = WdfUsbTargetDeviceSelectConfigTypeMultiInterface; } }
So why would one want to use one version over the other? The answer lies in the SettingIndex member of the WDF_USB_INTERFACE_SETTING_PAIR structure. If you simply want to enable alternate setting zero on all of your interfaces, you can set the NumberInterfaces and SettingPairs parameters to NULL and KMDF will determine the number of interfaces you have an enable alternate setting zero on each. However, if you would like to enable an alternate setting on any of the interfaces you will need to create a WDF_USB_INTERFACE_SETTING_PAIR array and change the SettingIndex member appropriately.
For illustrative purposes, even though we’re only enabling alternate setting zero on both of our interfaces we’ll do so using a WDF_USB_INTERFACE_SETTING_PAIR array with the SettingIndex set to zero for each entry.
Configuring the WDF_USB_INTERFACE_SETTING_PAIR
Building the WDF_USB_INTERFACE_SETTING_PAIR structure is straightforward. We first dynamically determine the number of interfaces on the device and allocate a structure of the appropriate size (See Figure 4).
// // Get the number of interfaces on our device. // numInterfaces = WdfUsbTargetDeviceGetNumInterfaces(devContext->BasicUsbUsbDevice); // // Allocate enough memory for the WDF_USB_INTERFACE_SETTING_PAIR // array. // settingPairsSize = (sizeof(WDF_USB_INTERFACE_SETTING_PAIR) * numInterfaces); settingPairs = (PWDF_USB_INTERFACE_SETTING_PAIR) ExAllocatePoolWithTag(PagedPool, settingPairsSize, 'bRSO'); if(!settingPairs) { #if DBG DbgPrint("Failed to allocate memory for Setting Pairs 0x%0x\n", STATUS_INSUFFICIENT_RESOURCES); #endif return STATUS_INSUFFICIENT_RESOURCES; }
In our case we know that we only have two interfaces so we could have used a static array, but for a little more code we’re able to handle any future changes to the firmware.
Then we simply loop, filling in the UsbInterface field of the structure by calling WdfUsbTargetDeviceGetInterface and filling in the SettingIndex field with the alternate setting of the interface we would like to enable. In our case we’re just enabling alternate setting zero on each interface (Figure 5).
// Iterate through the number of interfaces and initialize the WDF_USB_INTERFACE_SETTING_PAIR array. // for (interfaceIndex = 0; interfaceIndex < numInterfaces; interfaceIndex++) { // // Get the WDFUSBINTERFACE for this index. // settingPairs[interfaceIndex].UsbInterface = WdfUsbTargetDeviceGetInterface( devContext->BasicUsbUsbDevice, interfaceIndex); // We want alternate setting zero on all interfaces. // settingPairs[interfaceIndex].SettingIndex = 0; }
Selecting the Configuration
From here, we’re ready to initialize our WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT structure and select our configuration (Figure 6).
// Init our WDF_USB_DEVICE_SELECT_CONFIG_PARAMS structure. // WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_MULTIPLE_INTERFACES( &selectConfigParams, numInterfaces, settingPairs); // // And actually select our configuration. // status = WdfUsbTargetDeviceSelectConfig(devContext->BasicUsbUsbDevice, WDF_NO_OBJECT_ATTRIBUTES, &selectConfigParams); if (!NT_SUCCESS(status)) { #if DBG DbgPrint("WdfUsbTargetDeviceSelectConfig failed 0x%0x\n", status); #endif ExFreePool(settingPairs); return status; }
Enumerating the Pipes
That’s pretty much it! The good news is that we haven’t been lying to our students all this time and it did work the way we thought.
The final change to the driver is to enumerate the pipes in each interface. To do this, we will have an outer loop and an inner loop. The outer loop will enumerate the configured interfaces while the inner loop will enumerate the configured pipes in the interface.
For our device, interface zero contains a bulk out and an interrupt in endpoint. Interface one contains a single isochronous out endpoint. Thus, we’ll simply loop looking for configured pipes of those types and store them away in our device context.
The start of the outer loop is shown in Figure 7.
// Since we have 2 interfaces for this configuration, we will go through each of them and get the pipes // in each. NOTE: we know that pipes are in each interface, since we defined the interfaces: // // Interface 0 has a bulk out and an interrupt pipe. // Interface 1 has an ISO in pipe. // // So we take advantage of that fact. // for(interfaceIndex = 0; interfaceIndex < numInterfaces; interfaceIndex++) { // // How many pipes were configure? // numPipes = WdfUsbInterfaceGetNumConfiguredPipes( settingPairs[interfaceIndex].UsbInterface);
Note that here we leverage the fact that we have already retrieved the interface for this index and stored it in the settingPairs array.
Once we have the number of pipes configured for this interface, we can loop over the pipes looking for out bulk, interrupt, and isochronous pipes (Figure 8).
// For all the pipes that were configured.... // for(pipeIndex = 0; pipeIndex < numPipes; pipeIndex++) { // We'll need to find out the type the pipe, which we'll do by supplying a pipe information // structure when calling WdfUsbInterfaceGetConfiguredPipe // WDF_USB_PIPE_INFORMATION_INIT(&pipeInfo); // Get the configured pipe. // configuredPipe = WdfUsbInterfaceGetConfiguredPipe( settingPairs[interfaceIndex].UsbInterface, pipeIndex, &pipeInfo); // First, let's see what type of pipe it is... // switch (pipeInfo.PipeType) { case WdfUsbPipeTypeBulk: { // Bulk pipe. Determine if it's an OUT pipe // if (WdfUsbTargetPipeIsOutEndpoint(configuredPipe)) { // Bulk OUT pipe. Should only ever get one of these... // ASSERT(devContext->BulkOutPipe == NULL); devContext->BulkOutPipe = configuredPipe; } break; } case WdfUsbPipeTypeInterrupt: { // We're only expecting an IN interrupt pipe // ASSERT(WdfUsbTargetPipeIsInEndpoint(configuredPipe)); // And we're only expected one of them // ASSERT(devContext->InterruptInPipe == NULL); devContext->InterruptInPipe = configuredPipe; break; } case WdfUsbPipeTypeIsochronous: { ASSERT(WdfUsbTargetPipeIsInEndpoint(configuredPipe)); ASSERT(devContext->IsochronousInPipe == NULL); devContext->IsochronousInPipe = configuredPipe; devContext->IsochronousInPacketSize = pipeInfo.MaximumPacketSize; break; } default: { // Don't know what it is, don't care what it is... #if DBG DbgPrint("Unexpected pipe type? 0x%x\n", pipeInfo.PipeType); #endif break; } }
And that’s it!
Much to our surprise, it really is that easy. KMDF comes through again with a straightforward approach and a well thought out API.
But what about the single interface setting? We won’t leave you hanging on that one. Check out the sidebar below for more information on single interface devices.
[infopane color=”6″ icon=”0182.png”]What About the Single Interface Alternate Setting?
It might be the case that you have a single interface device but would like to select an alternate setting. This also turns out to be quite simple in KMDF, requiring only one more API call.
After you have selected your configuration, all you will need do is initialize a WDF_USB_INTERFACE_SELECT_SETTING_ PARAMS structure and call WdfUsbInterfaceSelectSetting to select the alternate setting.
Select the Configuration
To start we just do the usual configuration selection process and get back our configured interface:
// The OSRFX2 device only has a single interface, so we'll // initialize our select configuration parameters structure // using the specially provided macro // WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_SINGLE_INTERFACE( &selectConfigParams); // And actually select our configuration. // status = WdfUsbTargetDeviceSelectConfig(devContext->BasicUsbUsbDevice, WDF_NO_OBJECT_ATTRIBUTES, &selectConfigParams); if (!NT_SUCCESS(status)) { #if DBG DbgPrint("WdfUsbTargetDeviceSelectConfig failed 0x%0x\n", status); #endif return status; } // Our single interface has been configured. Let's grab the // WDFUSBINTERFACE object so that we can get our pipes. // configuredInterface = selectConfigParams.Types.SingleInterface.ConfiguredUsbInterface;
Once we have our interface, we initialize our WDF_USB_INTERFACE_SELECT_SETTING_PARAMS structure using one of the available functions. By far the easiest is the WDF_USB_INTERFACE_SELECT_SETTING_PARAMS_INIT_SETTING function, which takes an interface number to enable:
// Configure the structure with alternate setting one. // WDF_USB_INTERFACE_SELECT_SETTING_PARAMS_INIT_SETTING( &interfaceSelectSetting, 1); And then simply select the setting for the configured interface by calling WdfUsbInterfaceSelectSetting: // // Select the setting. // status = WdfUsbInterfaceSelectSetting(configuredInterface, WDF_NO_OBJECT_ATTRIBUTES, &interfaceSelectSetting); if (!NT_SUCCESS(status)) { #if DBG DbgPrint("WdfUsbInterfaceSelectSetting failed 0x%0x\n", status); #endif ExFreePool(settingPairs); return status; }
[/infopane]