Apple Developer Connection
Advanced Search
Member Login Log In | Not a Member? Contact ADC

Building Customized User Client Drivers for USB Devices

Conventionally, when we talk about "drivers," we refer to software integrated with the kernel; for instance, the KEXT (kernel extension) files that a Mac OS X kernel loads at runtime. However, in some cases, a user space program, without special privileges, may be able to control a device using generic drivers already loaded in the kernel.

An obvious historical example of this is serial modems, where a generic driver protocol, the serial port, was used to control specific devices. In fact, thousands of different devices have communicated over serial ports. The system kernel provided a generic driver for "serial ports," and then user applications handled the details of communications. On modern systems, the USB specification, in particular the USB Human Interface Device (HID) specification, has come to fill a similar role.

This article walks through the process of developing customized user client drivers for USB HID devices. The "drivers" can be simple command-line applications which communicate with the devices; the code could easily be incorporated into a larger project, or used standalone.

The HID specification helps simplify interaction with generic devices. Vendors generally provide documentation which is good enough that you can provide the needed support yourself. While the most common HID devices are keyboards, mice, tablets, and similar devices, specialized HID devices can use the same underlying protocol for card readers, bill validators, and a broad variety of other devices. Some of these devices don't use the HID specification, depending instead on raw USB requests.

NOTE: Devices which use isochronous endpoints, such as video or audio devices (both microphones and speakers) are beyond the scope of this article.

A quick note about terminology is probably a good starting point. The primary mechanism of interaction with HID devices (which many special-purpose devices are implemented as) is sending data packets called reports. A report is a chunk of data in a format specified by the device, within limits specified by the USB HID specification. Sending data to the device is called "setting" the report. Receiving data from the device is called "getting" the report. Data moving from the host to the device is moving "downstream," data moving from the device to the host is moving "upstream." The report framework can simplify communications protocols; it's not necessary to parse messages to determine the boundaries between messages. HID devices can be queried about their reports, and the resulting report descriptions give at least some idea of what data will be exchanged; in an emergency, they might well be enough to let you begin debugging a driver for a simple device even without documentation.

Vendors will often be willing to give out documentation on USB messages which their devices handle. This documentation is wonderful once you have a framework for communicating with a device, but it won't help you at all in setting up that framework. In this article, we focus on the setup code needed to exchange data with special-purpose USB devices. While the specific devices used are HID devices, the essential issues are similar for non-HID devices. For more information on generic USB devices, see the ADC Reference Library article, Accessing a USB device.

The first device example in this article is a USB card reader which scans magnetic strips, such as those on credit cards or driver's licenses. The device's protocol is exceptionally simple, making it a good starting point.

Scanning the USB Bus

The first stage in developing a driver for a USB device is to detect the device. Mac OS X automatically detects USB devices. A HID device which is not detected by a more specific driver is simply attached to the system's device tree using the system's generic USB driver. The essential element in probing a device is an IO object iterator. USB devices, much like PCI devices, are identified by 16-bit vendor and product codes. To check for a device, scan through the device list (using the IO object iterator) obtaining vendor/product pairs, and check for matches. The values to check for are generally found in a device's documentation.

For a starting point, here's a simple program which scans through the system just printing the vendor/product pairs. The program is presented over several code listings. It needs to be linked with the CoreFoundation and IOKit frameworks.

Listing 1 Headers and intro

#include <stdio.h>
#include <IOKit/IOCFPlugIn.h>
#include <IOKit/hid/IOHIDKeys.h>
#include <CoreFoundation/CoreFoundation.h>

int
main(void) {
	IOReturn result = kIOReturnSuccess;
	io_iterator_t hidObjectIterator = 0;
	io_object_t hidDevice = IO_OBJECT_NULL;
	CFMutableDictionaryRef hidMatchDictionary = 0;
	CFMutableDictionaryRef hidProperties = 0;

This first section of the program is fairly self-explanatory. The IOKit headers provide access to the IOKit function declarations needed to do driver work in general, and the CoreFoundation header provides the CoreFoundation classes. More prosaically, stdio.h will be used shortly for displaying diagnostic messages.

The main function (this program doesn't need any arguments) begins with a number of declarations. The result variable is used to hold the results of a variety of IO operations. The other variables will be explained as they get used.

Listing 2 Creating the IO iterator

hidMatchDictionary = IOServiceMatching(kIOHIDDeviceKey);
result = IOServiceGetMatchingServices(kIOMasterPortDefault,
		hidMatchDictionary,
		&hidObjectIterator);
if ((result != kIOReturnSuccess) || (hidObjectIterator == 0)) {
	printf("Can't obtain an IO iterator\n");
	exit(1);
}

The IOServiceMatching function is a convenience function to create a matching dictionary usable by an iterator to identify devices and services matching the criteria specified by the dictionary parameter; in this case, HID devices. HID devices are identified by the constant kIOHIDDeviceKey, which is really just the string "IOHIDDevice". The IOServiceGetMatchingServices function creates an iterator, of type io_iterator_t, which iterates through IO devices using the provided dictionary.

Listing 3 Using the IO iterator

while ((hidDevice = IOIteratorNext(hidObjectIterator))) {
	hidProperties = 0;
	int vendor = 0, product = 0;
	result = IORegistryEntryCreateCFProperties(hidDevice, &hidProperties,
					kCFAllocatorDefault, kNilOptions);
	if ((result == KERN_SUCCESS) && hidProperties) {
		CFNumberRef vendorRef, productRef;

		vendorRef = CFDictionaryGetValue(hidProperties, CFSTR(kIOHIDVendorIDKey));
		productRef = CFDictionaryGetValue(hidProperties, CFSTR(kIOHIDProductIDKey));
		if (vendorRef) {
			CFNumberGetValue(vendorRef, kCFNumberIntType, &vendor);	   CFRelease(vendorRef);
		}
		if (productRef) {
			CFNumberGetValue(productRef, kCFNumberIntType, &product);
			CFRelease(productRef);
		}
	}
	printf("Got a device: vendor %04x, product %04x\n", vendor, product);
	IOObjectRelease(hidDevice);
}

This listing is where the actual work happens. The primary loop is on the IOIteratorNext routine, which iterates through the hidObjectIterator generated by the previous step. Each time through the loop, a new device is obtained, until there are no more matching devices. For each device, its vendor and product are obtained, if available. If for some reason (this should never happen), the dictionary lacks a vendor or product key, the default zero values are used; as long as your device doesn't have zero for either key, this will prevent false positives. After each device is checked, it is released.

After this loop, all that's needed is to free up some resources and exit.

Listing 4 Releasing resources and exiting

	IOObjectRelease(hidObjectIterator);
	return 0;
}

This concludes the simple scanner, which is able to iterate over available USB HID devices, print their vendor and product codes, and exit cleanly. So far, so good; this program lets a developer manually verify that a particular device is attached.

This program simply lists all attached USB devices; it doesn't talk to them. Sample output might look like this:

Figure 1: Sample Output

$ ./scan
Got a device: vendor 0557, product 2205
Got a device: vendor 0557, product 2205
Got a device: vendor 0801, product 0002

The next stage of development is to do something when the desired device is found. The easiest thing to do is exit the loop when a given device's product and vendor codes match. It's also possible to specify product and vendor codes in the matching dictionary.

Talking to a Device

The next stage in talking to a device is to receive messages from it. In the particular case of this card reader, messages are sent over the interrupt endpoint. Obtaining this data involves setting up a callback function to be called whenever new reports come in on the interrupt endpoint. The callback handler is attached to a new runloop (if you're not familiar with these, think of them as event-handler threads which can be explicitly started and stopped). In a more developed application, it might be added to an existing runloop, or something similar.

The callback routine is supposed to process the data from the report. The data from the report are copied into the provided buffer before the callback routine is called; if all you need to do is accept the data, your callback routine doesn't have to do anything, although you still have to provide a routine or the data won't be copied. The central interface feature is setInterruptReportHandlerCallback(), part in the IOHIDDeviceInterface122 class. (This class, a subclass of the more general IOHIDDeviceInterface class, offers additional features added since the initial release of Mac OS X.)

The following declarations are used to set up the communications channel; the struct reader type is used not only for the communications, but to free resources when the program completes. The size of the buffer is a feature of this particular device, and is the size of the reports it generates on a card swipe.

Listing 5 Declarations and types

struct reader {
	io_object_t ioObject;
	IOHIDDeviceInterface122 **interface;
	int gotdata;
	unsigned char buffer[338];
};

SInt32 score;
IOCFPlugInInterface **plugInInterface;
CFRunLoopSourceRef eventSource;
mach_port_t port;
struct reader *r;

These declarations are then used to create an event source to be attached to the desired runloop; in this case, the default runloop for the application.

Listing 6 Setting up the run loop

r = malloc(sizeof(*r));
r->ioObject = hidDevice;
IOCreatePlugInInterfaceForService(hidDevice, kIOHIDDeviceUserClientTypeID,
	kIOCFPlugInInterfaceID, &plugInInterface, &score);
(*plugInInterface)->QueryInterface(plugInInterface,
	CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID), (LPVOID) &(r->interface));
(*plugInInterface)->Release(plugInInterface);
(*(r->interface))->open(r->interface, 0);
(*(r->interface))->createAsyncPort(r->interface, &port);
(*(r->interface))->createAsyncEventSource(r->interface, &eventSource);
(*(r->interface))->setInterruptReportHandlerCallback(r->interface,
	r->buffer, 338, ReaderReportCallback, r, NULL);
(*(r->interface))->startAllQueues(r->interface);
CFRunLoopAddSource(CFRunLoopGetCurrent(), eventSource, kCFRunLoopDefaultMode);

In the interests of brevity, the error checking has been omitted from this code. The most significant probable error would be that the open of the device interface ((*(r->interface))->open(r->interface, 0);) could fail if another program has the device open already; the kIOHIDOptionsTypeSeizeDevice flag could be used to steal access to the device, assuming the program has sufficient privileges to do so; the line would become (*(r->interface))->open(r->interface, kIOHIDOptionsTypeSeizeDevice);. The sequence of events is as follows:

  1. Allocate memory for a structure (r) which will hold the device objects and associated data needed both to process callbacks and to release resources at the end of execution.
  2. Create a plugin interface to hidDevice, used to obtain a device interface.
  3. Obtain the interface from the plugin interface.
  4. Release the plugin interface.
  5. Open the device interface.
  6. Create asynchronous port and an event source for this interface.
  7. Set a report callback handler on the device, to handle interrupt reports coming from the device.
  8. Activate any queues for the device.
  9. Add the associated event source to the current runloop.

After this code has been run, the runloop calls the ReaderReportCallback function whenever interrupts arrive. Before each call to the callback function, the first 338 bytes of data from the report are copied into r->buffer. Since there's only one buffer, multiple calls to the callback will overwrite that buffer. In some applications, this would require you to complete the processing of the data within the callback, or copy it out to other storage. In this particular application, it's physically impossible to generate input events fast enough to overwrite anything, as long as the event handler isn't run for very long at a time. With this setup code completed, it's trivial to extract new messages when they come in:

Listing 7 Getting data

r->gotdata = 0;
while (!r->gotdata) {
	reason = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, false);
}

The callback function is used to indicate that a report has been obtained; the report data are then processed. The loop is only run for a tenth of a second; since the card swiper isn't that fast, there's no way to get more than one report from it, so there's no need (in this example) to worry about overwriting. In fact, since the system libraries copy the report data automatically, setting the flag is the only function the callback routine needs to perform:

Listing 8 A callback function

static void ReaderReportCallback(void *target, IOReturn result,
	void *refcon, void *sender, UInt32 size) {

	reader *r = target;
	r->gotdata = 1;
}

With all of this in place, it's now possible to obtain the raw messages provided by the device.

Reading Reports

The next stage is to go from obtaining reports to interpreting them. The details of this are highly device-specific; luckily, nearly all vendors provide solid documentation for the interpretation of their messages. Report data are transmitted as blocks of octets in a predictable order. The exact layout varies from one device to another. On the card reader, the data format is three status bytes (one for each of three possible tracks), three length bytes (one for each track), a byte indicating the encoding that was used on the card, the data from each track, and finally a status indicator for whether the card was inserted or removed. The initial status bytes can be used to determine which of the track data sections have meaningful data.

When interpreting data returned by USB devices, always be cautious. If the vendor says a value is reserved, do not make any assumptions about its contents. When sending data, if the vendor says a bit is reserved and should not be set, don't set it. Data packets produced by USB devices often omit string terminators, relying instead on other ways of determining the length of a string, such as a length encoded in the report. Don't assume that a string will be null-terminated. In cases where a value is more than a single byte, be careful about endian issues; shift and mask values, rather than just copying them directly into host words.

For the card reader, the contents of each track can be printed fairly simply, assuming that the report contents have been stored in a character array. The first three bytes are status bytes for each of the three possible tracks on a magnetic strip. Bit 0 of each byte is set if the track contains invalid data; the other bits are reserved. So, to check for data, and print data if there are any, the code looks like this:

Listing 9 Printing track 1

if (buffer[0] & 1) {
	fprintf(stderr, "track 1: invalid\n");
} else {
	printf("track 1: "%.*s"\n", buffer[3], buffer + 7);
}

The ".*" precision specifier to printf consumes an integer value from the argument list, and limits printing to no more than that many characters. The fourth byte in the report (offset 3) contains the length of the actual data decoded for the first track, if any; the track data starts at the eighth byte (offset 7). This is just the simple version you start with for debugging; after you swipe a few cards, or read some documentation about credit cards, you can start getting a feel for what data might show up in the report, and how to parse it more specifically... But at this point, the driver's done. You're no longer writing a USB driver; you're writing a program that understands credit cards or whatever other magnetic strips are passed to you.

More Complicated Devices

Some devices require bidirectional communication; this can usually be handled with the setReport and getReport functions of an IOHIDDeviceInterface. The setReport function is used to transmit data to a device; the getReport function requests specific data from a device. Not all devices support getReport, and in fact, the sample devices used for this article don't support it. The getReport feature is more likely used for devices where the device is not reporting specific events as they happen, but rather reporting initial status, or requesting snapshots. A game controller might be queried for current state, or might be used in a mode where it sends interrupts on state changes. As another example, a programmable keyboard might use getReport to query the contents of a macro, while the interrupt stream would be used to report keypresses.

Sending a report to a device is conveniently trivial. The device documentation will tell you the format and contents of the message, which will generally be a small string of bytes. To send it, you just use the setReport function:

Listing 10 Using setReport

(*(r->interface))->setReport(
	r->interface,		// self
	kIOHIDReportTypeOutput,	// report type
	0,			// report ID
	buffer,			// buffer
	sizeof(buffer),		// size
	100,			// timeout (in ms)
	0,			// callback function
	0,			// ... and arguments
	0);

The full description of this function is found in the IOHIDDeviceInterface class documentation. The key elements are the report ID (some devices can accept more than one type of report; 0 is the default for most devices that only take one), a buffer, a size, and a timeout. The last three arguments specify an optional callback function to be called upon completion, and arguments to pass to it; if no callback is provided, setReport blocks until completion or timeout, indicating success or failure through its return value, which is of type IOReturn.

Once again, once you have this function, the development task is no longer communicating over USB, but learning to interact with the device. USB devices commonly use combinations of bitmasks and numeric values in their reports, both for upstream and downstream messages.

Some devices will impose interesting high-level protocols. For instance, a device might require that a report be sent to it repeatedly (using setReport) until it is acknowledged by the device.

Non-HID devices will require you to build USB messages and send them directly. A sample of this is found in the ADC Reference Library chapter Accessing a USB Device. Here's a code sample to look at:

Listing 11 Writing to a device directly

IOReturn WriteToDevice(IOUSBDeviceInterface **dev, UInt16 deviceAddress,
			UInt16 length, UInt8 writeBuffer[])
{
    IOUSBDevRequest     request;
 
    request.bmRequestType = USBmakebmRequestType(kUSBOut, kUSBVendor,
						kUSBDevice);
    request.bRequest = 0xa0;
    request.wValue = deviceAddress;
    request.wIndex = 0;
    request.wLength = length;
    request.pData = writeBuffer;
 
    return (*dev)->DeviceRequest(dev, &request);
}

This is similar in effect to the HID-specific setReport, and sends a chunk of data directly to a device. Devices often accept multiple different request types, reflected in the bRequest field of the IOUSBDevRequest.

Representing Report Data

Once you've gotten past the basics of sending packets to and from a device, you need to develop the code to parse and create the packets. It's possible, though inadvisable, to communicate directly with a device using explicit constants. For instance, the following code is not completely self-explanatory, but might well work:

Listing 12 Underdocumented code

buffer[2] = 4;  // does something

A more useful way to write this might be to give some hint of what's being set, and what the value it's being set to means; here's the same code with symbolic constants to clarify its function:

Listing 13 Setting the LED pulsing speed on a PowerMate

buffer[POWERMATE_COMMAND] = LED_PULSE_SPEED;

In general, devices will tend to have patterns in their reports that let you assign meaningful names to the various bytes and bits within them. Using symbolic names like this makes it a lot easier to maintain your code. Convenience functions and tables help you focus on your algorithm and design rather than on bit-banging.

In general, a user client driver won't require you to do anything special for testing; you don't have to power-cycle a device, or unplug it and plug it back in, to "reconnect" it, because the kernel driver isn't being altered in any way. However, some devices may have internal state which is affected by messages sent; check the documentation.

Universal Binary Issues

While there are very few issues when adapting USB devices to universal binaries, there are a few. When USB devices use multi-byte values, they are little-endian. If your application talks directly with a device, bypassing the API presented here entirely, you need to byte-swap on PowerPC systems, but not on Intel systems. The best choice is usually to use the standard system API. If you need to bypass this, use the HostToUSB and USBToHost macro families to handle byte-swapping correctly on both Intel and PowerPC systems. If you use the standard API, all the byte-swapping you need is generally done in system libraries. The sample code used in this article makes no reference to endian issues, and works without modification on both PowerPC and Intel systems.

For More Information

Posted: 2006-06-26