I/O Kit USB Support

USB support in the I/O Kit is provided by the IOUSBFamily, which is a dynamically loadable KEXT, identified by the bundle identifier com.apple.iokit.IOUSBFamily. The USB family provides the central core of USB handling in the kernel and contains drivers for the host controllers, as well as abstraction classes for representing USB devices, interfaces, and pipes. The class hierarchy of the USB Family is shown in Figure 8-4.

images

Figure 8-4. IOUSBFamily class hierarchy

Most of these classes are irrelevant if you only want to implement a driver for a USB device. The main classes used for USB driver development are shown in gray, in Figure 8-4, and include IOUSBPipe, IOUSBDevice, and IOUSBInterface, which we will discuss in detail later.

If you need to implement support for a new host controller, this can be done by inheriting from IOUSBController; however, kernel already provides drivers for UHCI-, OHCI-, and EHCI-compliant host controllers. Although not shown in Figure 8-4, these are subclasses of IOUSBController, and they are called AppleUSBUHCI, AppleUSBOHCI, and AppleUSBEHCI, respectively.

images Tip The USB Family is not part of the XNU source distribution, but is nevertheless available in source code form as a download from http://opensource.apple.com. The source package includes source for the entire USB Family, including the implementation of the UHCI, OCHI, and EHCI controllers. It also includes sample code for USB drivers, and how to enumerate and access USB devices from user space.

USB Device and Driver Handling

When a USB device is inserted, the USB Family will create an instance of the IOUSBDevice class, a subclass of IOService, and insert it into the I/O Registry. Exactly one instance of IOUSBDevice will be created for each device inserted onto the bus. The provider for an IOUSBDevice is the IOUSBController to which the device is attached. The IOUSBDevice class provides an abstraction of the USB device's device and configuration descriptors. Interface descriptors can be accessed from the IOUSBInterface class. The IOUSBDevice acts as a provider for IOUSBInterface classes, as seen in Figure 8-5.

images

Figure 8-5. USB device and driver provider relationships

Figure 8-5 shows three USB devices and how they relate to their higher-level providers:

  • The driver on the left: This is a driver for an audio device. You may have noticed that there is an additional driver between IOUSBDevice and IOUSBInterface called IOUSBCompositeDriver. This composite driver is matched against, and loaded for, any USB device that has its device class and subclass set to zero in its device descriptor and that has no other vendor specific driver matched against it. The name of the driver may suggest that it is only for composite drivers with multiple functions, but the driver is loaded even for devices with a single interface. The only function the composite driver performs is to select the device's active configuration (if it has multiple configuration descriptors), and then ensure that other drivers can be matched against the selected configuration's interfaces.
  • The driver in the middle: This is a vendor specific driver attached directly to the IOUSBDevice nub. The IOUSBCompositeDriver was not loaded as the device class, and the subclass fields in the device descriptor were set to 0xFF/0xFF, indicating a vendor specific device, and a driver was properly matched against the device.
  • The driver on the right: This has the same organization as on the left, with an IOUSBCompositeDriver attached to the IOUSBDevice nub. The composite driver will enumerate all device interfaces and ensure that they are made available for matching. In this case, there are two interfaces, each with an attached independent driver.

Loading USB Drivers

To have your driver loaded automatically when a device is inserted, you must configure your driver's Info.plist, as we learned in Chapter 4. As we saw in the previous section, a single driver may handle a USB device, or it may have several drivers, one for each interface (function) presented. For a USB device, a driver is matched against it using keys from the device's device descriptor. The I/O Kit follows the rules for driver matching set by the Universal Serial Bus Common Class Specification. The following combinations of keys are valid for matching a driver against a USB device:

  • idVendor & idProduct & bcdDevice
  • idVendor & idProduct
  • idVendor & bDeviceSubClass & bDeviceProtocol (only if bDeviceClass == 0xff)
  • idVendor & bDeviceSubClass (only if bDeviceClass == 0xff)
  • bDeviceClass & bDeviceSubClass & bDeviceProtocol (only if bDeviceClass != 0xff)
  • bDeviceClass & bDeviceSubClass (only if bDeviceClass != 0xff)

Each key represents an entry in the device's device descriptor. The bcdDevice field is used to store the device's revision number. If the bDeviceClass field is 0xff, it means the device class is vendor specific. A matching dictionary in Info.plist, which matches against a vendor ID, a product ID, and a revision number (bcdDevice), is shown in Listing 8-1.

Listing 8-1. Matching Dictionary for Matching Against Vendor ID, Product ID, and Device Revision

<key>MyUSBDriver</key>
<dict>
<key>CFBundleIdentifier</key>
    <string>com.osxkernel.MyUSBDriver</string>
    <key>IOClass</key>
    <string>com_osxkernel_MyUSBDriver</string>
    <key>IOProviderClass</key>
    <string>IOUSBDevice</string>
    <key>bcdDevice</key>
    <integer>1</integer>
    <key>idProduct</key>
    <integer>2323</integer>
    <key>idVendor</key>
    <integer>0001</integer>
</dict>

The entry must be located in the IOKitPersonalities section of your driver's Info.plist file to have any effect.

Devices that are not matched by the previous rules will be handled by the IOUSBCompositeDriver, which selects a device configuration, if the device has multiple configurations present, and then initiates matching against the device's interfaces instead. The keys that can be used to match against device interfaces are shown here:

  • idVendor & idProduct & bcdDevice & bConfigurationValue & bInterfaceNumber
  • idVendor & idProduct & bConfigurationValue & bInterfaceNumber
  • idVendor & bInterfaceSubClass & bInterfaceProtocol (only if bInterfaceClass == 0xff)
  • idVendor & bInterfaceSubClass (only if bInterfaceClass == 0xff)
  • bInterfaceClass & bInterfaceSubClass & bInterfaceProtocol (only if bInterfaceClass != 0xff)
  • bInterfaceClass & bInterfaceSubClass (only if bInterfaceClass != 0xff)

images Note You cannot create your own combinations of keys; you have to use one of the combinations shown above for either an interface or a device. However, you can add several personalities to your driver, which can each match against a different combination, but it has to be one of the valid combinations.

Each key represents a field in an interface descriptor. The matching rules are ordered according to how specific they are. The last rule, for example, which matches against the interface class and subclass, is used by Apple's USB Mass Storage driver to match all devices that conform to that interface, regardless of vendor or product ID. The Info.plist for the Apple mass storage driver is shown in Listing 8-2 (though some keys unrelated to matching were trimmed for readability).

Listing 8-2. Matching Dictionary for Matching Against a USB Interface Class and Subclass

<key>IOUSBMassStorageClass6</key>
<dict>
    <key>CFBundleIdentifier</key>
    <string>com.apple.iokit.IOUSBMassStorageClass</string>
    <key>IOClass</key>
    <string>IOUSBMassStorageClass</string>
    <key>IOProviderClass</key>
    <string>IOUSBInterface</string>
    <key>bInterfaceClass</key>
    <integer>8</integer>
    <key>bInterfaceSubClass</key>
    <integer>6</integer>
</dict>

Unlike the example in Listing 8-1, the IOProviderClass is specified as IOUSBInterface, which will be the provider passed to your driver's start() method instead of IOUSBDevice.

USB Prober

Before we start looking at actual code, it is worth mentioning a highly useful tool called USB Prober. USB Prober is a utility that is bundled with the Xcode distribution. The USB Prober tool is shown in Figure 8-6.

images

Figure 8-6. USB Prober utility

USB Prober allows you to probe available USB buses on your system and inspect the hierarchy of devices attached to each bus. It also allows you to inspect the device, configuration, interface, and endpoint descriptors. The IORegistry tab allows you to inspect the IOService plane of the I/O Registry as it pertains to USB devices, which is very useful during development of a USB driver, as it allows you to verify that your driver was matched correctly. USB prober can also perform USB specific tracing from the IOUSBFamily, which may be useful for debugging your driver in some cases. This requires some setup, including downloading the USB Debug Kit from Apple's developer website. The kit contains an alternate version of IOUSBFamily, which provides verbose logging. Access to the debug kit is restricted to members of the Mac developer program.

Driver Example: USB Mass Storage Device Driver

Let's put what we've learned so far into practice by putting together a simple USB-based driver, which will print log messages as various events occur. Now we could make a purely virtual driver, but that wouldn't be any fun, so let's instead create a driver that piggybacks on a real USB device so that we can see what happens when the device is plugged in and removed from the bus, but without interfering with the device's operation. The object-oriented nature of the I/O Kit makes it such that writing a device driver requires relatively little effort. Moreover, writing a device driver for a USB device is very similar to writing a driver for Firewire, PCI, or the virtual IOKitTest driver from Chapter 4.

To try this example, you need a thumb/flash drive or external USB hard drive. It doesn't have to be formatted for Mac, as we are not going to access the data.

images Caution It is recommended to try examples in this book on a Mac that is not being used to store important data. A kernel crash may corrupt your files or operating system. If you do not have a dedicated Mac for this purpose, ensure you have working backups of your data.

Our driver will be called MyFirstUSBDriver, and the class declaration is shown in Listing 8-3.

Listing 8-3. MyFirstUSBDriver.h: Class Declaration for MyFirstUSBDriver

#include <IOKit/usb/IOUSBDevice.h>

class com_osxkernel_MyFirstUSBDriver : public IOService
{
    OSDeclareDefaultStructors(com_osxkernel_MyFirstUSBDriver)
    
public:
    virtual bool init(OSDictionary *propTable);
    virtual IOService* probe(IOService *provider, SInt32 *score );
    virtual bool attach(IOService *provider);
    virtual void detach(IOService *provider);
    virtual bool start(IOService *provider);
    virtual void stop(IOService *provider);
    virtual bool terminate(IOOptionBits options = 0);
};

You will notice that the class is structurally nearly identical to the IOKitTest driver, with a few minor changes, which we will discuss later. The implementation of MyFirstUSBDriver is shown in Listing 8-4.

Listing 8-4. MyFirstUSBDriver.cpp: Implementation of MyFirstUSBDriver Class

#include <IOKit/IOLib.h>
#include <IOKit/usb/IOUSBInterface.h>
#include "MyFirstUSBDriver.h"

OSDefineMetaClassAndStructors(com_osxkernel_MyFirstUSBDriver, IOService)
#define super IOService

void logEndpoint(IOUSBPipe* pipe)
{
    IOLog("Endpoint #%d ", pipe->GetEndpointNumber());
    IOLog("--> Type: ");
    switch (pipe->GetType())
    {
        case kUSBControl: IOLog("kUSBControl "); break;
        case kUSBBulk: IOLog("kUSBBulk "); break;
        case kUSBIsoc: IOLog("kUSBIsoc "); break;
        case kUSBInterrupt: IOLog("kUSBInterrupt "); break;
    }
    IOLog("--> Direction: ");
    switch (pipe->GetDirection())
    {
        case kUSBOut: IOLog("OUT (kUSBOut) "); break;
        case kUSBIn: IOLog("IN (kUSBIn) "); break;
        case kUSBAnyDirn: IOLog("ANY (Control Pipe) "); break;
    }        
    IOLog("maxPacketSize: %d interval: %d ", pipe->GetMaxPacketSize(), pipe->GetInterval());    
}

bool com_osxkernel_MyFirstUSBDriver::init(OSDictionary* propTable)
{
    IOLog("com_osxkernel_MyFirstUSBDriver::init(%p) ", this);
    return super::init(propTable);
}

IOService* com_osxkernel_MyFirstUSBDriver::probe(IOService* provider, SInt32* score)
{
    IOLog("%s(%p)::probe ", getName(), this);
    return super::probe(provider, score);
}

bool com_osxkernel_MyFirstUSBDriver::attach(IOService* provider)
{
    IOLog("%s(%p)::attach ", getName(), this);
    return super::attach(provider);
}

void com_osxkernel_MyFirstUSBDriver::detach(IOService* provider)
{
    IOLog("%s(%p)::detach ", getName(), this);
    return super::detach(provider);
}

bool com_osxkernel_MyFirstUSBDriver::start(IOService* provider)
{
    IOUSBInterface* interface;
    IOUSBFindEndpointRequest request;
    IOUSBPipe* pipe = NULL;
    
    IOLog("%s(%p)::start ", getName(), this);

    if (!super::start(provider))
        return false;
    
    interface = OSDynamicCast(IOUSBInterface, provider);
    if (interface == NULL)
    {
        IOLog("%s(%p)::start -> provider not a IOUSBInterface ", getName(), this);
        return false;
    }
    
    // Mass Storage Devices use two bulk pipes, one for reading and one for writing.
    
    // Find the Bulk In Pipe.
    request.type = kUSBBulk;
    request.direction = kUSBIn;
    pipe = interface->FindNextPipe(NULL, &request, true);
    if (pipe)
    {
        logEndpoint(pipe);
        pipe->release();
    }
    
    // Find the Bulk Out Pipe.
    request.type = kUSBBulk;
    request.direction = kUSBOut;
    pipe = interface->FindNextPipe(NULL, &request, true);
    if (pipe)
    {
        logEndpoint(pipe);
        pipe->release();
    }  
    return true;
}

void com_osxkernel_MyFirstUSBDriver::stop(IOService *provider)
{
    IOLog("%s(%p)::stop ", getName(), this);
    super::stop(provider);
}

bool com_osxkernel_MyFirstUSBDriver::terminate(IOOptionBits options)
{
    IOLog("%s(%p)::terminate ", getName(), this);
    return super::terminate(options);
}

As you may see, there is very little logic in this driver, with the exception of logging when the various methods of our driver are called. The start() method will also attempt to find the bulk IN and bulk OUT endpoints, and log information about the endpoints. We will test the driver shortly, but first we have to create a matching dictionary so that the I/O Kit will know when to load our driver. The matching dictionary for MyFirstUSBDriver is shown in Listing 8-5.

Listing 8-5. Matching Dictionary for MyFirstUSBDriver

<key>IOKitPersonalities</key>
<dict>
    <key>MyFirstUSBDriver</key>
    <dict>
        <key>bInterfaceClass</key>
        <integer>8</integer>
        <key>bInterfaceSubClass</key>
        <integer>6</integer>
        <key>CFBundleIdentifier</key>
        <string>com.osxkernel.MyFirstUSBDriver</string>
        <key>IOClass</key>
        <string>com_osxkernel_MyFirstUSBDriver</string>
        <key>IOMatchCategory</key>
        <string>com_osxkernel_MyFirstUSBDriver</string>
        <key>IOProviderClass</key>
        <string>IOUSBInterface</string>
    </dict>
</dict>

The matching dictionary is more or less the same as the example in Listing 8-2. It will match against a USB interface rather than a USB device. We set bInterfaceClass to 8, which is the class code for mass storage devices, and we set bInterfaceSubClass to 6, which indicates that it uses the SCSI command set to communicate with the device (which doesn't necessarily imply that the drive/storage itself understands the SCSI protocol, but it is used to tunnel commands to the device over the bus, where another controller may translate it into, for example, ATA commands).

Because Apple's default IOUSBMassStorageClass matches against the same keys as us, we need to specify a match category so that our driver will also be loaded. We do this by adding the IOMatchCategory key. We set it to the name of our class, but it could be any string.

images Tip When you open a Info.plist file in Xcode, it is opening in the property list editor by default. If you wish to cut and paste to the property list, or you are curious about the format it is stored in, you can right-click the file and choose “Open as,” and then choose “Source Code,” which will present it in XML format.

There is one additional change we need to make to our driver's property list file, and that is to include our dependencies under the OSBundleLibraries dictionary; otherwise, the driver will fail to load. Dependencies can be found using the kextlibs tool. We used the following section to add a dependency to libkern and IOUSBFamily:

<key>OSBundleLibraries</key>
<dict>
        <key>com.apple.iokit.IOUSBFamily</key>
        <string>4.1.8</string>
        <key>com.apple.kernel.libkern</key>
        <string>6.0</string>
</dict>

We are now ready to load the driver, or rather to allow I/O Kit to load the driver for us. For the driver to load automatically, as the USB device is plugged in, it must be located in the directory /System/Library/Extensions, the standard location for all KEXTs.

When all is done, you should now be able to plug the device in. Before you do, however, you can bring up the Console application, and select kernel.log from the log list. Once you insert your compatible device, you should see the following entries being printed to the log in response to the insertion:


Jun 16 22:37:56 macbook kernel[0]: com_osxkernel_MyFirstUSBDriver::init(0x1361f900)
Jun 16 22:37:56 macbook kernel[0]: com_osxkernel_MyFirstUSBDriver(0x1361f900)::attach
Jun 16 22:37:56 macbook kernel[0]: com_osxkernel_MyFirstUSBDriver(0x1361f900)::probe
Jun 16 22:37:56 macbook kernel[0]: com_osxkernel_MyFirstUSBDriver(0x1361f900)::detach
Jun 16 22:37:56 macbook kernel[0]: com_osxkernel_MyFirstUSBDriver(0x1361f900)::attach
Jun 16 22:37:56 macbook kernel[0]: com_osxkernel_MyFirstUSBDriver(0x1361f900)::start
Jun 16 22:37:56 macbook kernel[0]: Endpoint #4 --> Type: kUSBBulk --> Direction: IN (kUSBIn)
maxPacketSize: 512 interval: 0
Jun 16 22:37:56 macbook kernel[0]: Endpoint #3 --> Type: kUSBBulk --> Direction: OUT
(kUSBOut) maxPacketSize: 512 interval: 0

The first five calls are part of the matching process. The first method attach() is used to connect our driver into the IOService plane of the I/O Registry, which in this case will attach us as a client of the IOUSBInterface nub, which again is the client of the IOUSBDevice we just plugged in. As we know from Chapter 4, the probe method is used for active matching, and allows us to further interrogate the device, or interface in this case, to determine if we are a match for it. I/O Kit then calls detach(), and the decision to which driver to load is made once all possible matches have been examined. It is usually not recommended to allocate any resources in attach() as it can be called multiple times. Usually, it is not necessary to override attach() or detach(), as the default ones provided by IOService are almost always sufficient. Once I/O Kit has selected our driver, which is guaranteed in our case (as we specified a unique IOMatchCategory), we will be getting a call to our attach() method again, and then finally the start() method of our driver. We can now use USB Prober to verify where in the hierarchy our driver was placed, as shown in Figure 8-7.

images

Figure 8-7. USB Prober showing the com_osxkernel_MyFirstUSBDriver attached to the IOService plane

You will notice that we are attached to the IOUSBInterface of the storage device, together with the IOUSBMassStorageClass driver, and that the USB device itself is managed by the composite driver IOUSBCompositeDriver.

Driver Startup

The implementation of our start() method in MyFirstUSBDriver is deliberately sparse because the IOUSBMassStorageClass driver is also managing the interface, and we do not wish to interfere with its use of the USB interface. We do a basic sanity check, which is commonly done in the start() method, to ensure that we get a provider that is of the type we expect. However, nothing is preventing you from writing a driver that can accept and work with multiple types of providers—for example, an IOUSBDevice and IOUSBInterface, or even an IOPCIDevice provider.

Here's an outline of the steps a USB driver typically must perform in its start() method:

  • Verify that the IOService provider object passed to us is of the type we expect. We do this with the help of the OSDynamicCast() macro, which works with I/O Kit's runtime type identification system, and returns a pointer to the object if the cast is successful, or NULL otherwise.
  • Store a pointer to the provider for later use.
  • Attempt to open the provider by calling its open(IOService* forClient, …) method.
  • If your driver is operating on an IOUSBDevice you may have to set the device's configuration. For IOUSBInterface-based drivers, the IOUSBCompositeDriver normally handles this. You can set the configuration using IOUSBDevice::SetConfiguration().
  • Find and verify the interfaces you will use for a driver that has an IOUSBDevice provider. And search for the appropriate endpoints needed by your driver. More about this in the section “Enumerating Device Resources.”
  • Interrogate the device for status information, and perform the needed configuration of the device by issuing control requests to it.
  • Allocate any driver specific resources you may need, for example I/O buffers or auxiliary classes needed by your driver.
  • If your driver is a nub and intends to provide services to other drivers, it needs to allocate and register these. For example, in the case of IOUSBMassStorageClass it will allocate IOSCSILogicalUnitNub objects for each logical unit provided by the interface, and for each of these call registerService(), a method inherited from IOService. The method ensures that matching will begin for each IOSCSILogicalUnitNub object.
  • If everything succeeds, start() should return true. If false is returned, the driver will obviously not be loaded, and the I/O Kit will try to load a new driver, if any, possibly one that “lost” and got a lower score previously. Be aware that stop() will not be called if you return false from start().

images Tip To uninstall MyFirstUSBDriver, simply use the command: sudo rm –rf /System/Library/Extensions/MyFirstUSBDriver.kext

Handling Device Removals

USB devices and drivers must be able to cope with the removal of a device at any point in time. When the IOUSBController detects that a device is removed, it will propagate this information recursively down the driver stack. The first notification to a driver is made by calling its terminate() method. The following sequence of calls is the result of unplugging the mass storage device that MyFirstUSBDriver is attached to:

Jun 16 22:58:46 macbook kernel[0]: com_osxkernel_MyFirstUSBDriver(0xb4e6100)::terminate
Jun 16 22:58:46 macbook kernel[0]: com_osxkernel_MyFirstUSBDriver(0xb4e6100)::stop
Jun 16 22:58:46 macbook kernel[0]: com_osxkernel_MyFirstUSBDriver(0xb4e6100)::detach

Any incomplete I/O can be cancelled using IOUSBPipe::Abort() and can be done when the terminate() method gets called, or in the willTerminate() or didTerminate() methods if overridden by the driver.

The next step in the removal process is that the driver's stop() method will be called, which should reverse actions taken in the start() method. After that detach() and finally free() will be called, which should clean up all remaining resources.

If your driver is opened by a user application, for example through a IOUserClient, it will not be deallocated (the free() method will not be called) until the application releases its reference to the device. If the device happens to be re-inserted at this time, the application is not able to resume using the device, as a new instance of the driver is created each time a device is inserted. The application can handle this by using notifications, as described in Chapter 5.

Enumerating Interfaces

During a USB driver's start() method, it is usually necessary to find and configure the endpoints and interfaces that will be used by the device. If your driver is based on the IOUSBDevice provider, chances are that you need to search for one or more of the interfaces that will be used by your driver. This can be done using the IOUSBDevice::FindNextInterface() method:

virtual IOUSBInterface* FindNextInterface(IOUSBInterface* current,
                                          IOUSBFindInterfaceRequest* request);

The first parameter can be specified to start the search from an existing IOUSBInterface instance and ignore any interfaces before it. NULL can be specified to start the search from the first interface.

The second parameter is a structure of the type IOUSBFindInterfaceRequest:

typedef struct {
    UInt16 bInterfaceClass;
    UInt16 bInterfaceSubClass;
    UInt16 bInterfaceProtocol;
    UInt16 bAlternateSetting;
} IOUSBFindInterfaceRequest;

To find an interface, you can fill out the IOUSBFindInterfaceRequest structure with the desired properties for the interface.

  • bInterfaceClass and bInterfaceSubClass can be filled in to search for an interface of a specific class and subclass. The values correspond to the codes in Table 8-3. The header file USBSpec.h in the IOUSBFamily source distribution define symbolic constants such as kUSBMassStorageInterfaceClass or kUSBPrintingClass.
  • The bInterfaceProtocol specifies the protocol used by the interface. The field is meaningless without the class and subclass. A HID (Human Interaction Device) may for example specify the protocol as kHIDKeyboardInterfaceProtocol or kHIDMouseInterfaceProtocol.
  • It is possible for an interface to have alternate versions of itself that uses a different set of endpoints, the bAlternateSetting field can therefore be set to request the specific interface desired.

Fields that does not matter can be set to kIOUSBFindInterfaceDontCare. Setting every field to this value will simply return the next interface regardless.

Listing 8-6 shows an extract from the Apple USB Ethernet driver which uses the FindNextInterface() method to search a USB device (IOUSBDevice) for an interface that supports Ethernet.

Listing 8-6. Searching a IOUSBDevice for a Interface (from USBCDCEthernet.cpp)

IOUSBFindInterfaceRequest               req;
IOUSBInterface*                         fCommInterface = NULL;

req.bInterfaceClass =    kUSBCommClass;
req.bInterfaceSubClass = kEthernetControlModel;
req.bInterfaceProtocol = kIOUSBFindInterfaceDontCare;
req.bAlternateSetting =  kIOUSBFindInterfaceDontCare;
    
fCommInterface = fpDevice->FindNextInterface(NULL, &req);    
if (!fCommInterface)
{
     // not found
     …
}

Enumerating Endpoints

An interface does not do anything useful by itself, so once the correct interface is retrieved by the driver, it must enumerate the interface's endpoints which are used for actual I/O. The enumeration/search is process is similar to that of finding an interface and is done with the IOUSBInterface::FindNextPipe() method:

virtual IOUSBPipe *FindNextPipe(IOUSBPipe *current, IOUSBFindEndpointRequest *request);
virtual IOUSBPipe* FindNextPipe(IOUSBPipe *current, IOUSBFindEndpointRequest *request,
        bool withRetain);

The first parameter if non-NULL tells the method to ignore pipes before it. The second parameter is a pointer to an IOUSBFindEndpointRequest:

typedef struct {
    UInt8 type;
    UInt8 direction;
    UInt16 maxPacketSize;
    UInt8 interval;
} IOUSBFindEndpointRequest;
  • The type field can be kUSBControl, kUSBIsoc, kUSBBulk, kUSBInterrupt, or kUSBAnyType.
  • The direction field must be set to kUSBOut, kUSBIn, or kUSBAnyDirn.
  • The maxPacketSize field is the max packet size in bytes that endpoint zero supports, and should be 8, 16, 32, or 64. It can be set to 0 if irrelevant.
  • The interval field can be used to search for an endpoint that has a specific polling interval. The polling interval only applies to isochronous and interrupts endpoints.

Listing 8-7 shows how the Apple USB Ethernet driver uses the FindNextPipe() method to enumerate endpoints.

Listing 8-7. Enumerating IOUSBPipe Instances for an Interface (from USBCDCEthernet.cpp)

IOUSBFindEndpointRequest        epReq;          // endPoint request struct on stack

// Open all the end points

epReq.type = kUSBBulk;
epReq.direction = kUSBIn;
epReq.maxPacketSize     = 0;
epReq.interval = 0;
fInPipe = fDataInterface->FindNextPipe(0, &epReq);
if (!fInPipe)
{
    …
    return false;
}

epReq.direction = kUSBOut;
fOutPipe = fDataInterface->FindNextPipe(0, &epReq);
if (!fOutPipe)
{
    …
    return false;
}
fOutPacketSize = epReq.maxPacketSize;

// Interrupt pipe - Comm Interface

epReq.type = kUSBInterrupt;
epReq.direction = kUSBIn;
fCommPipe = fCommInterface->FindNextPipe(0, &epReq);
if (!fCommPipe)
{
      ….
}

The driver in Listing 8-7 is a USB Ethernet driver. It uses three endpoints for its operation. The first is a bulk IN endpoint, which is used to read network data from the device. The second endpoint is a bulk OUT pipe, which is used to transmit packets to the device. The last end point is an interrupt IN endpoint, which is used to signal the arrival of a network packet and for notification of other events. In the following sections, we will look at how endpoints are used to perform I/O.

Performing Device Requests

Device requests are I/O requests to the default bi-directional default control pipe zero of the USB device, typically used for device configuration and accessing device registers. There are three classes of device requests:

  • Standard USB requests: These are standard requests implemented by all device. An example of a standard device request is querying a device's status. A list of symbolic constants for standard requests can be found in USBSpec.h.
  • Class specific requests: These are specific to a class of device. For example, an Ethernet device may provide a number of requests for configuring Ethernet related parameters.
  • Vendor specific requests

To perform a device request, both IOUSBDevice and IOUSBInterface provide a special DeviceRequest() convenience method, which under the hood uses the IOUSBPipe object, representing the default pipe, to transmit the request. If you wish, you can enumerate the IOUSBPipe instance for the zero endpoint and use it directly as well. The method is declared as follows:

DeviceRequest(IOUSBDevRequest *request, UInt32 noDataTimeout,
              UInt32 completionTimeout, IOUSBCompletion *completion);
DeviceRequest(IOUSBDevRequestDesc *, UInt32 noDataTimeout,
              UInt32 completionTimeout, IOUSBCompletion *completion);

In order to send a request, you must create an IOUSBDevRequest or IOUSBDeviceRequestDesc structure and fill in the appropriate fields.

typedef struct {                       typedef struct {
    UInt8 bmRequestType;                   UInt8 bmRequestType;
    UInt8 bRequest;                        UInt8 bRequest;
    UInt16 wValue;                         UInt16 wValue;
    UInt16 wIndex;                         UInt16 wIndex;
    UInt16 wLength;                        UInt16 wLength;
    void *pData;                           IOMemoryDescriptor *pData;
    UInt32 wLenDone;                       UInt32 wLenDone;
} IOUSBDevRequest;                     } IOUSBDevRequestDesc;
  • The bmRequestType field: Is a composite field that specifies the type of request, the direction, the type, and the recpient. The field can be generated by using the USBmakebmRequestType(direction, type, recpient) macro with the following paramters:
    • The direction will be either kUSBIn, kUSBOut, or kUSBNone.
    • The type will be either kUSBStandard, kUSBClass, or kUSBVendor.
    • The recpient will be either kUSBInterface, kUSBEndpoint, or kUSBDevice.
  • The bRequest field: This is a 8-bit value that selects the request to be performed.
  • The wValue and wIndex: These can be used to pass arguments along with the request. Their meaning depends on the request. For interface and endpoint requests, the wIndex number specifies the index number of the endpoint/interface to which the request is addressed. You can get the index number by calling either IOUSBPipe::GetEndpointNumber() or IOUSBInterface->GetInterfaceNumber().
  • The wLength field: This is the number of bytes for the pData field.
  • The pData field: This is either a pointer to a memory buffer or an IOMemoryDescriptor. The pData pointer may be set to NULL if no additional data is needed for the request. The buffer will either be read from or written to, depending on the direction of the request. If an IOMemoryDescriptor is used you should call prepare() on it first to ensure the memory is paged in and pinned down until the request is completed. The memory may come from user space if a memory descriptor is used. If the void* variant is used, the pointer must be in the kernel's virtual address space.
  • The wLenDone field: This should not be filled in, as it is used to return the number of bytes actually transferred.

Apart from the request parameters, the DeviceRequest() methods takes another three parameters.

  • noDataTimeout: This is the timeout, in milliseconds, to wait before aborting the request if no data has been sent/received.
  • completionTimeout: This specifies a timeout value for the entire command with data, and is also in milliseconds.
  • completion: This is optional, and if specified it allows us to perform the request asynchronously, which may often be desired to avoid blocking the calling thread. We will discuss asynchronous requests in more detail later in this chapter.

Let's look at an example of how a device request can be issued, again using the Apple USB Ethernet driver as an example. The code in Listing 8-8 is called by the driver from a periodic timer and is used to get statistics and status information from the Ethernet device, such as collisions, dropped packets, incoming packets, etc.

Listing 8-8. Device Request for Downloading Statistics From An Ethernet Device (USBCDCEthernet.cpp)

STREQ = (IOUSBDevRequest*)IOMalloc(sizeof(IOUSBDevRequest));
if (!STREQ)
{
     ...
} else {
     bzero(STREQ, sizeof(IOUSBDevRequest));
     // Now build the Statistics Request
    STREQ->bmRequestType = USBmakebmRequestType(kUSBOut, kUSBClass, kUSBInterface);
    STREQ->bRequest = kGet_Ethernet_Statistics;
    STREQ->wValue = currStat;
    STREQ->wIndex = fCommInterfaceNumber;
    STREQ->wLength = 4;
    STREQ->pData = &fStatValue;
        
    fStatsCompletionInfo.parameter = STREQ;
        
    rc = fpDevice->DeviceRequest(STREQ, &fStatsCompletionInfo);
    if (rc != kIOReturnSuccess)
    {
    ...
        IOFree(STREQ, sizeof(IOUSBDevRequest));
    } else {
       fStatInProgress = true;
    }
}

The request in Listing 8-8 is performed asynchronously. Because the IOUSBDevRequest structure must persist until the request finishes, it must not be allocated on the stack, although this is fine for a synchronous request. The request performed in Listing 8-8 is directed to a specific interface, and it is a class specific request, which means it will work the same on all interfaces with the same class code. The wValue field of the request is an index number specifying the statistic that should be transferred.

Control Requests

Device requests, discussed in the previous section, are I/O to the default control pipe (zero). The DeviceRequest() method cannot be used for control endpoints other than the default. If we wish to perform requests to another control endpoint, we must use the IOUSBPipe::ControlRequest() method instead. There are four ControlRequest() methods available:

virtual IOReturn ControlRequest(IOUSBDevRequestDesc* request,
        IOUSBCompletion* completion = 0);
virtual IOReturn ControlRequest(IOUSBDevRequest* request, IOUSBCompletion* completion = 0);
virtual IOReturn ControlRequest(IOUSBDevRequestDesc* request,
        UInt32 noDataTimeout,
        UInt32 completionTimeout,
        IOUSBCompletion* completion = 0);
virtual IOReturn ControlRequest(IOUSBDevRequest* request,
        UInt32 noDataTimeout,
        UInt32 completionTimeout,
        IOUSBCompletion* completion = 0);

The two first methods use the exact same arguments as the DeviceRequest() method discussed earlier. The two last also support the noDataTimeout and completionTimeout parameters.

Performing I/O to Bulk and Interrupt Endpoints

Sending and Receiving data is performed with the help of the IOUSBPipe class, which represents an endpoint. The IOUSBPipe class presents a simple interface for performing I/O, which is reminiscent of how user space performs file I/O. USB does not utilize DMA directly, although the host controller does use DMA to transfer data, but the details of this are abstracted away from us. This also means that we do not need to worry about memory alignment, if the memory is physically contiguous, is in the correct address range, or translating memory addresses to physical addresses. We can also perform I/O from a user space buffer.

The IOUSBPipe class supports I/O to all endpoint types: control, bulk, interrupt, and isochronous.

The methods for performing bulk and interrupt I/O are the Read() and Write() methods:

virtual IOReturn Read(IOMemoryDescriptor* buffer,
                      UInt32 noDataTimeout,
                      UInt32 completionTimeout,
                      IOByteCount reqCount,
                      IOUSBCompletion* completion = 0,
                      IOByteCount* bytesRead = 0);

virtual IOReturn Write(IOMemoryDescriptor* buffer,
                       UInt32 noDataTimeout,
                       UInt32 completionTimeout,
                       IOByteCount reqCount,
                       IOUSBCompletion* completion = 0);
  • The buffer is an IOMemoryDescriptor containing the buffer for which data should be read or written. The memory descriptor should have its prepare() method called to ensure memory is paged in and pinned down. The memory may be in the kernel or a user task's address space.
  • The noDataTimeout argument specifies the amount of time, in milliseconds, to wait for data transfer on the bus before the request is considered unsuccessful.
  • The completionTimeout is the time to allow, in milliseconds, for the entire request to complete before it is considered unsuccessful.
  • The reqCount is the amount of data, in bytes, that should be read or written. It must be less or equal to the size of the buffer, as returned by IOMemoryDescriptor::getLength().
  • The completion parameter is a structure of the type IOUSBCompletion, and is used for asynchronous requests. The parameter can be specified as NULL to perform the request synchronously, in which case the call will block until the request is complete or times out. We will look at asynchronous I/O later.
  • For the Read() method bytesRead will return the number of bytes that were read. It may be less than what was requested. The value is only set for synchronous requests.

Listing 8-9 shows example invocations of the Read() and Write() methods.

Listing 8-9. Examples of Synchronous Read() and Write() to a Bulk Pipe

UInt32 bytesRead;
IOMemoryDescriptor* readBuffer;
IOMemoryDescriptor* writeBuffer;

if (myBulkPipeIn->Read(readBuffer, 1000, 5000,
                       readBuffer->GetLength(), 0, &bytesRead) != kIOReturnSuccess)
{
     // Handle error
}
else
    IOLog(“We read: %u bytes ”, bytesRead);

if (myBulkPipeOut->Write(writeBuffer, 1000, 5000,
                         writeBuffer->GetLength()) != kIOReturnSuccess)
{
    // Handle error
}

Since we didn't specify the completion argument for either method, they will both be executed synchronously, which means that the request will be executed in its entirety by the time the method returns control to us. Recall that all pipes are uni-directional, with the exception of the default control pipe, so the IN and OUT requests are performed on two separate pipes.

images Note Another overloaded set of Read() and Write() exists that does not accept a reqCount parameter, but rather uses the GetLength() method of the IOMemoryDescriptor. These methods are now deprecated and should not be used.

The example in Listing 8-9 will also work for an interrupt endpoint. There is no special programming interface needed to work with interrupt endpoints. I/O is handled in the same way as with bulk endpoints. The difference is in behavior. An interrupt endpoint provides bounded latency and the host controller guarantees to poll the device for data no less often than what is requested in the endpoint's descriptor. The minimum-polling interval is 125 microseconds. Interrupt transfers use reserved bandwidth, which guarantees that the requests make it through even in the event that there are high amounts of activity on the bus. Unlike bulk transfers, interrupt transfers are not suitable for transferring large amounts of data and are limited to 8, 64, or, 1024 bytes for low-speed, full-speed, and high-speed, respectively. Note that interrupt endpoints are not related to system interrupts in any way. I/O to interrupt endpoints is performed in a normal kernel thread.

Dealing with Errors and Pipe Stalls

When an endpoint is unable to transmit or receive data due to an error, the host or device may set the HALT bit. Communicating with an endpoint in this state, or an endpoint with an error, will return a STALL handshake packet. An error needs to be resolved before I/O can continue on the endpoint. The IOUSBPipe class provides two methods for clearing a pipe stall and allowing I/O to resume:

virtual IOReturn ClearStall(void);
virtual IOReturn ClearPipeStall(bool withDeviceRequest);

The second version clears the error (toggle bit) on the controller, but it does not send out a device request to the endpoint if withDeviceRequest is false. Both methods will cause outstanding I/O to be completed with the return code kIOUSBTransactionReturned.

Isochronous I/O

Isochronous transfers are continuous in nature and are suitable for use with devices, such as audio and video, where information is continuously streaming and there is a need for guaranteed bandwidth and bounded latency. Data integrity can be verified using a CRC, but corrupted data is never re-sent automatically. The amount of bandwidth needed by a device is specified in the isochronous endpoint descriptor. If the host controller is unable to guarantee enough bandwidth to support the device, which can happen if another device already has reserved bandwidth on the bus, the device may be unable to function. If the device is able to operate with less bandwidth, it can define alternate interface descriptors with more conservative requirements. Maximum payloads for isochronous transfers are as follows:

  • High-speed devices have a maximum packet size of 1024 bytes.
  • Full-speed devices have a maximum packet size of 1023 bytes.
  • Low-speed devices do not support isochronous transfers.

Isochronous transfers use the concept of microframes. A microframe is 125 microseconds long. For high-speed devices, up to three packets can be transmitted per microframe, giving a maximum data-rate of 3 x 1024 x 8000 microframes per second = 24 MB/s. This is slightly lower than the maximum bandwidth possible over a bulk endpoint.

A microframe is represented by the IOUSBIsocFrame structure:

typedef struct IOUSBIsocFrame {
    IOReturn              frStatus;
    UInt16                frReqCount;
    UInt16                frActCount;
} IOUSBIsocFrame;

The structure describes how many bytes of data should be transmitted or received fromt the I/O buffer in each microframe. The frReqCount field is the amount of bytes requested, whereas the frActCount is the count actually transferred. The structure also contains a status field.

The methods for reading and writing to an isochronous endpoint are similar to those used to read and write from interrupt and bulk endpoints:

virtual IOReturn Read(IOMemoryDescriptor* buffer, UInt64 frameStart, UInt32 numFrames,
                      IOUSBIsocFrame* frameList, IOUSBIsocCompletion* completion = 0);
virtual IOReturn Write(IOMemoryDescriptor* buffer, UInt64 frameStart, UInt32 numFrames,
                       IOUSBIsocFrame *frameList, IOUSBIsocCompletion * completion = 0);

The methods take the following arguments:

  • The buffer argument is a virtually contiguous buffer containing the data to be transferred. The memory descriptor should have its prepare() method called to ensure memory is paged in and pinned down for the duration of the transfer. There are no special requirements otherwise for the memory and it can be either user space or kernel memory.
  • The frameStart argument specifies the index of the USB frame from which to start. One USB frame corresponds to 8 microframes.
  • The numFrames argument is a count of the microframe descriptors contained in the frameList array.
  • The frameList argument is a pointer to an array of IOUSBIsocFrame structures.
  • An optional completion structure. If specified, this will perform the transfer asynchronously.
Asynchronous Requests

It is often necessary to perform requests to USB devices asynchronously—for example, when performing large bulk requests to a hard drive. Instead of having the caller thread blocked, the request can be handled by the USB controller, and it will notify us, through a callback method, when the request is completed.

To do this, you must supply an IOUSBCompletion structure to the Read(), Write(), DeviceRequest(), or ControlRequest() methods:

typedef struct IOUSBCompletion {
    void* target;
    IOUSBCompletionAction action;
    void* parameter;
} IOUSBCompletion;  
  • The target field is a pointer that can contain user-defined data. Often it is used to pass the pointer to the class that sent the request, so that you can cast the pointer back to the original class in the completion function.
  • The action field is the actual callback, and should be a pointer to a function matching the IOUSBCompletionAction prototype. The method will be called once the request completes.
  • The parameter field can carry an additional parameter, which will also be passed to the completion function.

The IOUSBCompletionAction callback has the following prototype:

typedef void ( *IOUSBCompletionAction)(void* target, void* parameter,
                                       IOReturn status, UInt32 bufferSizeRemaining);

As you can see, the target and parameter fields of the IOUSBCompletion structure are passed directly to the callback. The callback will also get the status of the transfer, and the bufferSizeRemaining field will contain the number of bytes left to transfer if the request was not fully completed.

Asynchronous requests are completed on the IOUSBFamily work loop thread, which means that if you access data in the callback from your own driver, you must ensure that this access is properly synchronized.

Generally speaking, USB drivers never operate in a primary interrupt context, with the exception of the low latency versions of the isochronous Read() and Write() methods, which allows asynchronous isochronous I/O to have the completion callback called at primary interrupt time. In this case, extreme care needs to be taken to avoid calling code that may block. The use of low latency isochronous I/O should be used sparingly, and is generally not required even for audio and video drivers.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.148.144.139