I finished porting USB transfers from PIC C to Arduino C++. There is now a USB class in the repository. Two main types of USB transfers – control and bulk, are implemented. If you want to know more about USB transfer mechanics, take a look at this article. The code was written to work on Microchip PIC, however, the Arduino code is almost identical.
Since we now have tools to talk to USB device, let’s start using them by looking at USB descriptors. In order for sketches from this article to work you need to copy current repository contents ( *.cpp and *.h files ) to a sub directory in your Arduino hardware libraries directory. In addition, don’t forget to supply 5V to Vbus and connect a USB device, such as flash drive or mouse to USB connector of a breakout board.
First sketch demonstrates access to device descriptor. Device descriptor is the first descriptor that the host retrieves from the device during enumeration. The information which host needs is device endpoint zero maximum packet size. In this example, maximum packet size is already set by a function in USB class, called “Task”. In addition to maximum packet size device descriptor contains other information, used during device configuration.
/* MAX3421E USB Host controller get device descriptor */ #include <Spi.h> #include <Max3421e.h> #include <Usb.h> void setup(); void loop(); MAX3421E Max; USB Usb; void setup() { byte tmpdata = 0; Serial.begin( 9600 ); Serial.println("Start"); Max.powerOn(); delay( 200 ); } void loop() { byte rcode; Max.Task(); Usb.Task(); if( Usb.getUsbTaskState() >= 0x80 ) { //state configuring or higher rcode = getdevdescr( 1 ); //hardcoded device address if( rcode ) { Serial.print("\r\nRequest error. Error code:\t"); print_hex( rcode, 8 ); } while( 1 ); //stop } } byte getdevdescr( byte addr ) { USB_DEVICE_DESCRIPTOR buf; byte rcode; rcode = Usb.getDevDescr( addr, 0, 0x12, ( char *)&buf ); if( rcode ) { return( rcode ); } Serial.println("Device descriptor: "); Serial.print("Descriptor Length:\t"); print_hex( buf.bLength, 8 ); Serial.print("\r\nDescriptor type:\t"); print_hex( buf.bDescriptorType, 8 ); Serial.print("\r\nUSB version:\t"); print_hex( buf.bcdUSB, 16 ); Serial.print("\r\nDevice class:\t"); print_hex( buf.bDeviceClass, 8 ); Serial.print("\r\nDevice Subclass:\t"); print_hex( buf.bDeviceSubClass, 8 ); Serial.print("\r\nDevice Protocol:\t"); print_hex( buf.bDeviceProtocol, 8 ); Serial.print("\r\nMax.packet size:\t"); print_hex( buf.bMaxPacketSize0, 8 ); Serial.print("\r\nVendor ID:\t"); print_hex( buf.idVendor, 16 ); Serial.print("\r\nProduct ID:\t"); print_hex( buf.idProduct, 16 ); Serial.print("\r\nRevision ID:\t"); print_hex( buf.bcdDevice, 16 ); Serial.print("\r\nMfg.string index:\t"); print_hex( buf.iManufacturer, 8 ); Serial.print("\r\nProd.string index:\t"); print_hex( buf.iProduct, 8 ); Serial.print("\r\nSerial number index:\t"); print_hex( buf.iSerialNumber, 8 ); Serial.print("\r\nNumber of conf.:\t"); print_hex( buf.bNumConfigurations, 8 ); return( 0 ); } /* prints hex numbers with leading zeroes */ // copyright, Peter H Anderson, Baltimore, MD, Nov, '07 // source: http://www.phanderson.com/arduino/arduino_display.html void print_hex(int v, int num_places) { int mask=0, n, num_nibbles, digit; for (n=1; n<=num_places; n++) { mask = (mask << 1) | 0x0001; } v = v & mask; // truncate v to specified number of places num_nibbles = num_places / 4; if ((num_places % 4) != 0) { ++num_nibbles; } do { digit = ((v >> (num_nibbles-1) * 4)) & 0x0f; Serial.print(digit, HEX); } while(--num_nibbles); }
Load the sketch, compile, and run. Don’t forget to connect a device, otherwise program will print “Start” and pause waiting for the device to be connected. If everything is OK, you will see output similar to one below.
Start Device descriptor: Descriptor Length: 12 Descriptor type: 01 USB version: 0100 Device class: 00 Device Subclass: 00 Device Protocol: 00 Max.packet size: 40 Vendor ID: 0CF2 Product ID: 6220 Revision ID: 0100 Mfg.string index: 01 Prod.string index: 02 Serial number index: 04 Number of conf.: 01
Let’s take a look. The first line shows descriptor size – 18 bytes ( 12 Hex ). Next line shows descriptor type (01 – device descriptor). USB version shows 0100, that’s version 1.0, the very first one. Even though USB version 3.0 is here, many low speed devices are still manufactured. I have seen web cameras compliant to USB version 1.1 and low-speed mice and keyboards. Device used in this example is pretty new flash drive, by the way.
Device class, subclass, and protocol are set to zero. This usually means that device is supported as a class and class properties are defined on interface level. Maximum packet size is 64 bytes ( 40 hex ). Vendor ID and Product ID form unique device identifier.
Strings are human-readable descriptors. Device descriptor contains index values to some of the strings. If index of a string is zero, the corresponding string is not implemented. Also, since string descriptors are optional and, with very few exceptions, are not used during normal operation, many manufacturers implement strings incorrectly.
Last byte shows number of configurations supported by the device.
Next sketch shows how to get string descriptor. Unlike device descriptor, string descriptor length is variable. In addition, strings can be coded in several languages. Therefore, the sequence of actions to get a string is more complicated than one used to get device descriptor. First, we find out length of string descriptor with index zero, which contains Language IDs. Then, we ask for the same descriptor again, this time using the correct length. Next step is to extract first Language ID. After that we find out the length of the descriptor that we want, and finally get the whole descriptor and print it. Take a look at the code:
/* MAX3421E USB Host controller get string descriptor */ #include <Spi.h> #include <Max3421e.h> #include <Usb.h> #define LOBYTE(x) ((char*)(&(x)))[0] #define HIBYTE(x) ((char*)(&(x)))[1] void setup(); void loop(); MAX3421E Max; USB Usb; void setup() { byte tmpdata = 0; Serial.begin( 9600 ); Serial.println("Start"); Max.powerOn(); delay( 200 ); } void loop() { byte rcode; Max.Task(); Usb.Task(); if( Usb.getUsbTaskState() >= 0x80 ) { //state configuring or higher rcode = getstrdescr( 1, 1 ); //get string descriptor if( rcode ) { Serial.println( rcode, HEX ); } while( 1 ); //stop } } byte getstrdescr( byte addr, byte idx ) { char buf[ 66 ]; byte rcode; byte length; byte i; unsigned int langid; rcode = Usb.getStrDescr( addr, 0, 1, 0, 0, buf ); //get language table length if( rcode ) { Serial.println("Error retrieving LangID table length"); return( rcode ); } length = buf[ 0 ]; //length is the first byte rcode = Usb.getStrDescr( addr, 0, length, 0, 0, buf ); //get language table if( rcode ) { Serial.println("Error retrieving LangID table"); return( rcode ); } HIBYTE( langid ) = buf[ 3 ]; //get first langid LOBYTE( langid ) = buf[ 2 ]; rcode = Usb.getStrDescr( addr, 0, 1, idx, langid, buf ); if( rcode ) { Serial.println("Error retrieving string length"); return( rcode ); } length = buf[ 0 ]; rcode = Usb.getStrDescr( addr, 0, length, idx, langid, buf ); if( rcode ) { Serial.println("Error retrieving string"); return( rcode ); } for( i = 2; i < length; i+=2 ) { Serial.print( buf[ i ] ); } return( rcode ); }
The “getstrdescr” function takes string index as a second argument. It is set to 1 in the code, because almost any USB device has this string defined. However, if you want to see other strings of your device, just change the index number in “getstrdescr” call from the loop(), re-complile the sketch and run it.
The last sketch in this article demonstrates getting configuration descriptor. Configuration descriptor contains a wealth of information about the peripheral configuration, such as interfaces, their power consumption, endpoints and their maximum packet sizes, polling intervals, etc. The structure of configuration descriptor is even more complicated. Quoting USB 2.0 specification, “When the host requests the configuration descriptor, all related interface and endpoint descriptors are returned”. It means that not only length of the descriptor is variable, but data structure needs to be analyzed during retrieval since we don’t know in advance which descriptors would follow configuration.
In order to operate an USB device, configuration descriptor and subsequent descriptors has to be parsed. We need configuration value to switch USB device to the running state (called “configured” in USB spec), endpoint addresses, directions, and maximum packet sizes, as well as values from class-specific descriptors, if any. The sketch determines configuration length, retrieves it, an then goes through the buffer, printing configuration, interface, endpoint, or unknown descriptors as it encounters them.
/* MAX3421E USB Host controller get configuration descriptor */ #include <Spi.h> #include <Max3421e.h> #include <Usb.h> #define LOBYTE(x) ((char*)(&(x)))[0] #define HIBYTE(x) ((char*)(&(x)))[1] #define BUFSIZE 256 //buffer size void setup(); void loop(); MAX3421E Max; USB Usb; void setup() { byte tmpdata = 0; Serial.begin( 9600 ); Serial.println("Start"); Max.powerOn(); delay( 200 ); } void loop() { byte rcode; Max.Task(); Usb.Task(); if( Usb.getUsbTaskState() >= 0x80 ) { //state configuring or higher rcode = getconfdescr( 1, 0 ); //get configuration descriptor if( rcode ) { Serial.println( rcode, HEX ); } while( 1 ); //stop } } byte getconfdescr( byte addr, byte conf ) { char buf[ BUFSIZE ]; char* buf_ptr = buf; byte rcode; byte descr_length; byte descr_type; unsigned int total_length; rcode = Usb.getConfDescr( addr, 0, 4, conf, buf ); //get total length LOBYTE( total_length ) = buf[ 2 ]; HIBYTE( total_length ) = buf[ 3 ]; if( total_length > 256 ) { //check if total length is larger than buffer Serial.println("Total length truncated to 256 bytes"); total_length = 256; } rcode = Usb.getConfDescr( addr, 0, total_length, conf, buf ); //get the whole descriptor while( buf_ptr < buf + total_length ) { //parsing descriptors descr_length = *( buf_ptr ); descr_type = *( buf_ptr + 1 ); switch( descr_type ) { case( USB_DESCRIPTOR_CONFIGURATION ): printconfdescr( buf_ptr ); break; case( USB_DESCRIPTOR_INTERFACE ): printintfdescr( buf_ptr ); break; case( USB_DESCRIPTOR_ENDPOINT ): printepdescr( buf_ptr ); break; default: printunkdescr( buf_ptr ); break; }//switch( descr_type buf_ptr = ( buf_ptr + descr_length ); //advance buffer pointer }//while( buf_ptr <=... return( 0 ); } /* prints hex numbers with leading zeroes */ // copyright, Peter H Anderson, Baltimore, MD, Nov, '07 // source: http://www.phanderson.com/arduino/arduino_display.html void print_hex(int v, int num_places) { int mask=0, n, num_nibbles, digit; for (n=1; n<=num_places; n++) { mask = (mask << 1) | 0x0001; } v = v & mask; // truncate v to specified number of places num_nibbles = num_places / 4; if ((num_places % 4) != 0) { ++num_nibbles; } do { digit = ((v >> (num_nibbles-1) * 4)) & 0x0f; Serial.print(digit, HEX); } while(--num_nibbles); } /* function to print configuration descriptor */ void printconfdescr( char* descr_ptr ) { USB_CONFIGURATION_DESCRIPTOR* conf_ptr = ( USB_CONFIGURATION_DESCRIPTOR* )descr_ptr; Serial.println("Configuration descriptor:"); Serial.print("Total length:\t"); print_hex( conf_ptr->wTotalLength, 16 ); Serial.print("\r\nNum.intf:\t\t"); print_hex( conf_ptr->bNumInterfaces, 8 ); Serial.print("\r\nConf.value:\t"); print_hex( conf_ptr->bConfigurationValue, 8 ); Serial.print("\r\nConf.string:\t"); print_hex( conf_ptr->iConfiguration, 8 ); Serial.print("\r\nAttr.:\t\t"); print_hex( conf_ptr->bmAttributes, 8 ); Serial.print("\r\nMax.pwr:\t\t"); print_hex( conf_ptr->bMaxPower, 8 ); return; } /* function to print interface descriptor */ void printintfdescr( char* descr_ptr ) { USB_INTERFACE_DESCRIPTOR* intf_ptr = ( USB_INTERFACE_DESCRIPTOR* )descr_ptr; Serial.println("\r\nInterface descriptor:"); Serial.print("Intf.number:\t"); print_hex( intf_ptr->bInterfaceNumber, 8 ); Serial.print("\r\nAlt.:\t\t"); print_hex( intf_ptr->bAlternateSetting, 8 ); Serial.print("\r\nEndpoints:\t\t"); print_hex( intf_ptr->bNumEndpoints, 8 ); Serial.print("\r\nClass:\t\t"); print_hex( intf_ptr->bInterfaceClass, 8 ); Serial.print("\r\nSubclass:\t\t"); print_hex( intf_ptr->bInterfaceSubClass, 8 ); Serial.print("\r\nProtocol:\t\t"); print_hex( intf_ptr->bInterfaceProtocol, 8 ); Serial.print("\r\nIntf.string:\t"); print_hex( intf_ptr->iInterface, 8 ); return; } /* function to print endpoint descriptor */ void printepdescr( char* descr_ptr ) { USB_ENDPOINT_DESCRIPTOR* ep_ptr = ( USB_ENDPOINT_DESCRIPTOR* )descr_ptr; Serial.println("\r\nEndpoint descriptor:"); Serial.print("Endpoint address:\t"); print_hex( ep_ptr->bEndpointAddress, 8 ); Serial.print("\r\nAttr.:\t\t"); print_hex( ep_ptr->bmAttributes, 8 ); Serial.print("\r\nMax.pkt size:\t"); print_hex( ep_ptr->wMaxPacketSize, 16 ); Serial.print("\r\nPolling interval:\t"); print_hex( ep_ptr->bInterval, 8 ); return; } /*function to print unknown descriptor */ void printunkdescr( char* descr_ptr ) { byte length = *descr_ptr; byte i; Serial.println("\r\nUnknown descriptor:"); Serial. print("Length:\t\t"); print_hex( *descr_ptr, 8 ); Serial.print("\r\nType:\t\t"); print_hex( *(descr_ptr + 1 ), 8 ); Serial.print("\r\nContents:\t"); descr_ptr += 2; for( i = 0; i < length; i++ ) { print_hex( *descr_ptr, 8 ); descr_ptr++; } }
Let’s look at the output. First example shows configuration of USB flash drive. The configuration is very simple – one interface, two bulk endpoints (in addition to endpoint zero). We can see that total length of the configuration is 32 bytes (20 hex ), has one interface, the configuration value to use in “Set configuration” request to activate this configuration is “1″, string is not defined, all attributes are zero (by spec, MSB of attributes field is always one and is not an attribute ), and power consumption is 100mA.
Start Configuration descriptor: Total length: 0020 Num.intf: 01 Conf.value: 01 Conf.string: 00 Attr.: 80 Max.pwr: 32 Interface descriptor: Intf.number: 00 Alt.: 00 Endpoints: 02 Class: 08 Subclass: 06 Protocol: 50 Intf.string: 00 Endpoint descriptor: Endpoint address: 81 Attr.: 02 Max.pkt size: 0040 Polling interval: 00 Endpoint descriptor: Endpoint address: 01 Attr.: 02 Max.pkt size: 0040 Polling interval: 00
Interface descriptor shows interface number and two endpoints. Class, subclass, and protocol combination states that this device supports so-called “Mass-storage bulk-only transport” protocol – the most popular way to communicate with USB flash drives. Endpoints show address (which is called number in USB spec ) and direction (81 means “number 01, direction IN”), attributes (02 means “bulk”), maximum packet size 64 bytes, and polling interval zero, which, by the way, doesn’t make sense for bulk endpoints anyway.
Next example shows configuration of USB keyboard. This is a ptetty modern keyboard with extra buttons for web browsing and media player control. We can see two interfaces and two unknown descriptors. They are HID descriptors – per spec, any class-specific descriptors must follow interface descriptor. The first interface shows class 03 (HID), subclass 01 (keyboard), and protocol 01 (boot protocol). Boot protocol is a simplified protocol, which allows using USB keyboards during PC boot.
Second interface is just a plain HID interface. To use it, application needs to retrieve and parse HID reports for the interface.
Start Configuration descriptor: Total length: 003B Num.intf: 02 Conf.value: 01 Conf.string: 03 Attr.: A0 Max.pwr: 32 Interface descriptor: Intf.number: 00 Alt.: 00 Endpoints: 01 Class: 03 Subclass: 01 Protocol: 01 Intf.string: 00 Unknown descriptor: Length: 09 Type: 21 Contents: 10010001223E000705 Endpoint descriptor: Endpoint address: 81 Attr.: 03 Max.pkt size: 0008 Polling interval: 0A Interface descriptor: Intf.number: 01 Alt.: 00 Endpoints: 01 Class: 03 Subclass: 00 Protocol: 00 Intf.string: 00 Unknown descriptor: Length: 09 Type: 21 Contents: 1001000122AB000705 Endpoint descriptor: Endpoint address: 82 Attr.: 03 Max.pkt size: 0008 Polling interval: 0A
The last example is taken from my favorite no-name USB Bluetooth device. I like it for the implementation – it is phenomenally buggy. Strings are messy, descriptors don’t make sense (look at max.power, for example, or the last interface with no endpoints), plus, the configuration is quite long. It goes without explanation, just take a look to get an idea of how complex USB devices can really be.
Start Configuration descriptor: Total length: 00BA Num.intf: 03 Conf.value: 01 Conf.string: 00 Attr.: C0 Max.pwr: 00 Interface descriptor: Intf.number: 00 Alt.: 00 Endpoints: 03 Class: E0 Subclass: 01 Protocol: 01 Intf.string: 00 Endpoint descriptor: Endpoint address: 81 Attr.: 03 Max.pkt size: 0010 Polling interval: 01 Endpoint descriptor: Endpoint address: 02 Attr.: 02 Max.pkt size: 0040 Polling interval: 01 Endpoint descriptor: Endpoint address: 82 Attr.: 02 Max.pkt size: 0040 Polling interval: 01 Interface descriptor: Intf.number: 01 Alt.: 00 Endpoints: 02 Class: E0 Subclass: 01 Protocol: 01 Intf.string: 00 Endpoint descriptor: Endpoint address: 03 Attr.: 01 Max.pkt size: 0000 Polling interval: 01 Endpoint descriptor: Endpoint address: 83 Attr.: 01 Max.pkt size: 0000 Polling interval: 01 Interface descriptor: Intf.number: 01 Alt.: 01 Endpoints: 02 Class: E0 Subclass: 01 Protocol: 01 Intf.string: 00 Endpoint descriptor: Endpoint address: 03 Attr.: 01 Max.pkt size: 0009 Polling interval: 01 Endpoint descriptor: Endpoint address: 83 Attr.: 01 Max.pkt size: 0009 Polling interval: 01 Interface descriptor: Intf.number: 01 Alt.: 02 Endpoints: 02 Class: E0 Subclass: 01 Protocol: 01 Intf.string: 00 Endpoint descriptor: Endpoint address: 03 Attr.: 01 Max.pkt size: 0011 Polling interval: 01 Endpoint descriptor: Endpoint address: 83 Attr.: 01 Max.pkt size: 0011 Polling interval: 01 Interface descriptor: Intf.number: 01 Alt.: 03 Endpoints: 02 Class: E0 Subclass: 01 Protocol: 01 Intf.string: 00 Endpoint descriptor: Endpoint address: 03 Attr.: 01 Max.pkt size: 0019 Polling interval: 01 Endpoint descriptor: Endpoint address: 83 Attr.: 01 Max.pkt size: 0019 Polling interval: 01 Interface descriptor: Intf.number: 01 Alt.: 04 Endpoints: 02 Class: E0 Subclass: 01 Protocol: 01 Intf.string: 00 Endpoint descriptor: Endpoint address: 03 Attr.: 01 Max.pkt size: 0021 Polling interval: 01 Endpoint descriptor: Endpoint address: 83 Attr.: 01 Max.pkt size: 0021 Polling interval: 01 Interface descriptor: Intf.number: 01 Alt.: 05 Endpoints: 02 Class: E0 Subclass: 01 Protocol: 01 Intf.string: 00 Endpoint descriptor: Endpoint address: 03 Attr.: 01 Max.pkt size: 0031 Polling interval: 01 Endpoint descriptor: Endpoint address: 83 Attr.: 01 Max.pkt size: 0031 Polling interval: 01 Interface descriptor: Intf.number: 02 Alt.: 00 Endpoints: 00 Class: FE Subclass: 01 Protocol: 00 Intf.string: 00
In the next article, I will show how to use “Set” requests, as well as how to actually use USB device for something more interesting than looking at its descriptors. Stay tuned!
Oleg.
Related posts:
- Towards an FT232 Driver for the USB Host Shield- Part 0
- Digital camera control using Arduino USB Host Shield. Part 1 – basics.
- PS3 and Wiimote Game Controllers on the Arduino Host Shield: Part 3
- PS3 and Wiimote Game Controllers on the Arduino Host Shield: Part 2
- PS3 and Wiimote Game Controllers on the Arduino Host Shield: Part 1
- How to drive USB keyboard from Arduino
- Arduino USB Host – Peripherals.
- Arduino USB Host Shield build log. Part 4.
- Arduino USB host – First programs.
- Arduino USB host – Pre-prototyping.

