USB Device Drivers

Dr. Dobb's Journal April 2004

Putting a low-cost, flexible serial bus to work

By Dean A. Gereaux

Dean is a consultant engineer and principal of Golden Bits Software developing WIndows and Linux device drivers. He can be contacted at deang@goldenbits.com.
Pipe Handles

The Universal Serial Bus (USB) has become common on everything from MP3 players, PDAs, and digital cameras, to printers, keyboards, and mice. What has made USB popular is its low cost, flexibility, and ease of use. In this article, I examine the details of writing USB device drivers for Windows. In the process, I present a working driver; the source code for the driver is available from DDJ (see "Resource Center," page 5) and at http://www.goldenbits.com/newsletter/ issue2/usbsample.zip.

Before diving into the driver itself, however, it's important to have a grasp of the basics of USB technology. USB is a serial bus (there are only four signals) logically organized in a hierarchical manner—your basic tree structure. The tree can be extended by the addition of hubs, and each hub can support additional devices. USB can support up to 127 devices but, in practice, the most I've ever seen is four. At the top of the tree is the root hub and host controller, which controls all of the device configuration and traffic on the bus. Figure 1 illustrates the USB topology.

USB is a polled bus, meaning the controller constantly polls all of the devices to see if the device has any data to transfer to the host and also to initiate a transfer to the device. This polling enables the root hub/controller to control the bandwidth of the bus, which is important when performing isochronous transfers. During an initial setup phase, each device on the bus is assigned an address by the root hub when it connects. During this phase, the device reports its configuration using a set of data structures called "descriptors"—the device, configuration, interface, and endpoint descriptors.

The device descriptor contains the vendor and product IDs; this is how the Windows Plug-and-Play (PNP) manager knows which USB driver to load. The configuration, interface, and endpoint descriptors describe how the device wishes to be connected to the bus. The actual connections are called "endpoints" and represent unidirectional destinations or sources of data between devices and hosts. To send or receive data, you need two endpoints—one output to the device, and one input from the device. These descriptors are almost always predefined in the device's firmware (EEPROM or other).

Data and control messages are transferred in one or more individual transactions. Transactions are contained within a frame (low speed) or microframe (high speed); frames can contain multiple transactions. If the amount of data will not fit into one transaction, then it is broken up into multiple transactions over several frames. The frames are either 1 millisecond (low-speed bus) or 125 microseconds (high-speed bus) in duration. The host controller schedules all of the transactions and frames. One simple way to think about this process is that each frame is a train with a fixed number of cargo cars where each car contains a transaction; the host controller schedules these trains and fills the cars as necessary. This is a silly example, but gets the point across.

USB defines four types of transfers: Bulk, Isochronous, Interrupt, and Control. These transfer types provide USB device designers a nice selection of options; depending on the type of device, you can select the appropriate transfer type. For example, a USB keyboard does not need the bandwidth of a bulk transfer.

Table 1 summarizes each type of transfer and its characteristics. The use of descriptors, support of different transfer types, and configurable nature make USB very flexible; you can connect almost any conceivable device. Since the USB bus interface is flexible and can support a wide range of devices, you can think of it as a Swiss Army knife of sorts.

In short, the key items that relate directly to device drivers are:

The current USB specification is Version 2.0 and is available at http://www.usb.org/.

USB and Windows

Microsoft provides a full stack of drivers for USB bus support—you just have to write a driver for your own USB device. Microsoft also provides a set of class drivers for audio, network, storage, printer, and HID (Human Input Device, a fancy name for a mouse or keyboard). Support for these types of USB devices lets your device easily fit into a specific device class. For example, if you have a USB disk drive, the fact the drive is located on a USB bus is transparent to the system. The Microsoft-supplied usbstor.sys handles all of the details of presenting the disk driver to the SCSI Port driver. Depending on the type of device, you may not have to write a driver for your USB device, as is the case for audio devices (your USB device must be compliant with USB Audio spec 1.0).

USB driver stacks (that is, driver layers) are different for Windows 2000 and XP/2003. The main differences between the driver stacks are that Windows XP/2003 supports high-speed (USB 2.0) devices and includes a new generic parent driver (usbccgp.sys) to support composite devices. The new generic parent driver solves the problem of managing separate function drivers for composite USB devices. For example, if you designed a new digital camera that could both take pictures and capture real-time video, you may want a separate driver for each function—one driver to download pictures and another to display real-time video on the screen. Figure 2 shows the USB driver stack for Windows 2000 and Windows XP/2003.

USB devices are identified by a unique vendor and product ID, which are managed by the USB-Implementers Forum. The cost of a vendor ID is $1500 or $2500, depending on whether you become a member of the USB-IF organization (http://www.usb.org/developers/vendor). In addition to getting a vendor ID, you will probably want your device to be certified by USB-IF and pass Windows WHQL testing. The vendor ID string is identical to how PCI devices are identified; the format of the USB vendor ID string is: USB/VID_XXXX&PID_ZZZZ, where XXXX and ZZZZ are the vendor and product IDs, respectively. This information is sent to the host system as part of the device descriptor during the device setup phase. Windows searches the list of installed drivers looking for a matching vendor and product ID. Your driver's install .INF file should contain these numbers.

Descriptors Everywhere

One of the most confusing aspects of writing USB drivers is understanding all of the descriptors (device, configuration, interface, and endpoint), what they are used for, and how your driver manages them. (Unfortunately, the DDK documentation is a bit confusing.)

A descriptor is nothing more than a data structure, it's not a handle or some other unique object. When the USB Standard designers sat down and tried to figure out a flexible and extensible way of configuring USB devices, they defined a set of data structures (descriptors) that a USB device would present to describe itself and its capabilities. A USB device has one device descriptor that describes the device as a whole. This descriptor contains the vendor and product ID, and the number of configurations. Each configuration is described by a configuration descriptor. The USB Standard allows for multiple configurations but, in practice, only one is used (the Microsoft USB class drivers support only the first configuration). Each configuration contains a set of interface descriptors where each interface defines a set of endpoints. These are the endpoints that your driver will communicate with. Your driver code will ultimately use a pipe handle to send/receive data with a USB device; the pipe handle actually represents an endpoint. (For more information, see the accompanying text box entitled "Pipe Handles.")

When your driver gets the USB device configuration, these descriptors are returned to your driver (via a call to the Microsoft USB bus driver) in a chunk of memory containing a continuous list of descriptors. Here is one of the confusing parts of the DDK documentation: The DDK describes how to get this descriptor list, but doesn't do a good job in explaining exactly what you've got. Figure 3 shows what you have. The important pieces in all of this are the interface and endpoint descriptors. These are the descriptors that define how your driver talks to the USB device.

Interfaces and Endpoints

A USB device defines one or more interfaces where each interface describes one or more endpoints. What does this mean? Essentially, an interface is a logical grouping of endpoints; you should group similar endpoints into the same interface. In the driver I present in this article, the encryption and decryption functions are contained in separate interfaces. As another example, you could design a USB device to perform encryption/decryption and compression/decompression where the en(de)cryption and (de)compression are contained in separate interfaces. Figure 4 illustrates how interface and endpoints are logically organized.

An endpoint is a source or destination of data that your driver reads from or writes to. Endpoints are unidirectional, have unique addresses, and have a transfer type (bulk, isochronous, control, or interrupt). From a driver's perspective, an endpoint is represented by a USB pipe handle. Endpoint addresses are 8 bits, where the MSB (bit 7) defines whether the endpoint is an input or output. If the MSB is set, then this is an input endpoint; bits 6-4 are unused and bits 3-0 are the actual endpoint number. The terms "input" and "output" are from the host-system perspective—data read from a USB device is from an input (input to the host) endpoint. Data sent to a USB device is delivered to an output endpoint. For example, an endpoint address of 0x81 defines an input endpoint address of 0x01. An address for an input and output endpoint can be the same; they are distinguished by their direction.

So how are these addresses determined? By designers of the USB device. The USB host controller doesn't assign an endpoint address; this address is contained in the endpoint descriptor that the USB device returns. However, the USB device address is assigned by the host controller during the initial setup phase by the USB Set Address command.

When thinking about interfaces and endpoints, it is helpful to take the perspective of someone designing a USB device. What types of interfaces and endpoints will a USB device have? It depends on how it is designed. So when writing your USB driver, you need to collaborate with the engineer working on the USB device itself.

Sample USB Driver: USB Encrypt/Decryptor Device

This sample USB device I present here is an encryptor/decryptor device where the actual en(de)cryptor engine runs on the USB device itself. I use the TUSB3210KDBPDK development kit from Texas Instrument (see http://www.ti.com/usb) as the USB device. This kit is intended for keyboard devices, but for illustration, I ignore the keyboard features. The TI part, TUSB3210, is an 8052-based device intended for general-purpose USB peripheral applications. TI does offer other USB peripheral devices (TUSB3410 and TUSB6250) for different peripheral applications such as streaming audio. You can purchase this development kit online directly from TI for about $200.00. If you're thinking about developing a USB device, this kit (along with several others) offers an inexpensive way to start working with USB. The only catch is that you'll have to get a hold of a compiler for the USB device itself. Most vendors offer demo compilers with limited capability; however, a full-fledged compiler runs about $3000. There are several vendors that offer very good development environments for your USB device firmware. I used the Embedded Workbench Development from IAR Systems (http://www.iar.com/) with great success.

Again, the sample is an encryptor/decryptor USB device. The idea is to embed the actual encryption engine in the USB device itself, thus letting users easily disconnect the device and secure it as necessary. The de(en)cryptor device has two interfaces—the first interface is used for encrypting, and the second for decrypting. The encrypting interface has two endpoints—one to send data to be encrypted and one to read the encrypted data back. The decrypting interface uses three endpoints—the first two are used to send and receive encrypted data, the third endpoint is used by the device to send the number of bytes decrypted to the host. The en(de)crypt endpoints are configured to use bulk transfer, the third endpoint is configured to use interrupt transfer.

Figure 5 shows the main functional areas of the driver. The functions UsbCrypt_Write() and UsbCrypt_Read() are the main functions called when performing I/O with the USB device. Both of these functions use SendBulkIntTransfer(), which builds the USB and sends it to the lower USB bus driver (provided by Microsoft). A separate thread, GetProgress() (implemented using a work item—IoQueueWorkItem()) is used to get the number of bytes decrypted. GetProgress() returns the number of decrypted bytes, but any type of device status can be returned. Also, a read from the USB device blocks if the USB device hasn't sent anything to the host. Your driver should be able to handle this case, which means implementing a cancel routine for your pending IRPs.

The code used to get the device configuration and create the necessary USB pipes is contained in CrConfig.c. The entry point for configuring is the function ConfigureDriver(). The interfaces and pipes are created in the function SetupInterfaces(), and the pipe handles for each interface are saved in the device extension in the function SaveInterfaceInfo(). The pipe handles for the decrypt or encrypt interface are also saved in the FsContext for each open file handle (see UsbCrypt_Create()), so an open handle can only encrypt or decrypt. An application can open two file handles: one for encrypting and one for decrypting. Storing the pipe handles in the FsContext field is a nice technique to keep track of the individual pipes for each open handle.

You should use Windows Management Instrumentation (WMI) to expose any type of device statistics, status, and events to management applications. WMI can be a pain to set up, but it is necessary to do (along with WHQL) to provide your users with a quality driver. In the sample driver, I use WMI to provide the number of bytes decrypted. The sample application reads this information using the WMI COM interfaces.

User Application

The accompanying application sends data to the USB device to be encrypted or decrypted. In this sample, only 20 bytes are sent at one time; a real product should be able to handle an arbitrary large number of bytes. The radio buttons select the encrypt or decrypt function. If decrypting, the number of bytes decrypted by the USB device is also retrieved using the WMI COM interface. The functions ConnectToWMI() and GetDecryptBytes() handle the WMI COM details. On startup, the application opens a handle to the USB device. Reading and writing data is performed using the standard Win32 API ReadFile() and WriteFile() calls (which are used by the MFC CFile class).

Conclusion

You now have enough information to start writing a USB driver or, at a minimum, be able to explain what it will take to your manager. Just remember: Descriptors are just C-language structures used by the USB device to describe itself and its capabilities. The organization of interfaces and endpoints is up to you. Pipe handles represent endpoints. And WMI is a pain, but necessary to provide a professional, polished product.

DDJ