Beyond Description

Last week I showed you an example USB peripheral using the Microchip 18F2550 and the Microchip USB stack. However, I put off explaining too much of it until the future. The future is now. Well, at least part of the future.

I am going to put off talking about the actual working code until next time, because for now I want to focus on something that is at least as important: the USB descriptors. These data structures describe the device to the host operating system. I found that Linux is fairly lenient if you mess up the descriptors, but Windows is very picky indeed. If you have the choice, you might consider getting your system up on Linux first and then try Windows, since you can clobber some of the simpler bugs in the Linux environment.

I started with the Microchip usb_descriptors.c file, which has quite a few comments to get you started. A device descriptor is the root data structure and may contain one or more configuration descriptions. Each configuration can have multiple interface descriptors and the interfaces can have multiple endpoints. If you recall, endpoints are how the host and device communicate. Each endpoint only carries data in a single direction, so for two-way communication, you need at least two endpoints.

Here’s the top level of the descriptor:

    0x12,    // Size of this descriptor in bytes
    USB_DESCRIPTOR_DEVICE,                // DEVICE descriptor type
    0x0200,                 // USB Spec Release Number in BCD format
    0x00,                   // Class Code
    0x00,                   // Subclass code
    0x00,                   // Protocol code
    USB_EP0_BUFF_SIZE,          // Max packet size for EP0, see usb_config.h
    0x04D8,                 // Vendor ID
    0xF83F,                 // Product ID: AWC
    0x0002,                 // Device release number in BCD format
    0x01,                   // Manufacturer string index
    0x02,                   // Product string index
    0x00,                   // Device serial number string index
    0x01                    // Number of possible configurations

The first few bits are more or less boilerplate. The size and type are present in all descriptors (although, of course, the actual size and type value differ between types of descriptors). The version number (2.0) determines what is in the rest of the structure.

The class, subclass, and protocol codes are assigned by the USB organization. The zero value in the class field means that each interface specifies its own class code. The maximum packet size applies to endpoint zero and can be 8, 16, 32, or 64 bytes.

The next two fields identify the device type uniquely. The vendor ID is from the USB organization and identifies your company. These IDs are pretty pricey (and renewable each year). Small companies (and poor bloggers) won’t want to shell out that kind of money, so luckily Microchip (along with other similar vendors) have sublicensing programs. The idea is that if you agree to use their product, they will let you use their vendor ID and restrict you to using one product ID. If you have your own vendor ID, the product ID is just a unique number for each of your devices. When using a sublicense ID, there is often a restriction on how many units you can distribute under the sublicense.

The remainder of the structure is the release number (0.02), indices into the string table for things like the printable name for the vendor, product, and a serial number. The final field identifies how many configurations are available in this device.

The string table is comprised of string descriptors. These are optional – in the above example, there is no serial number, so the string index of zero means ignore that entry. In fact, string descriptor zero is reserved and describes what languages are available in other string descriptors.

Here’s my string descriptors (these appear near the end of the file):

//Language code string descriptor
ROM struct{BYTE bLength;BYTE bDscType;WORD string[1];}sd000={

//Manufacturer string descriptor
ROM struct{BYTE bLength;BYTE bDscType;WORD string[25];}sd001={
{'M','i','c','r','o','c','h','i','p',' ',
'T','e','c','h','n','o','l','o','g','y',' ','I','n','c','.'

//Product string descriptor
ROM struct{BYTE bLength;BYTE bDscType;WORD string[22];}sd002={
{'S','i','m','p','l','e',' ','D','e','v','i',
'c','e',' ','a','w','c','e','.','c','o','m'

Like all descriptors, the first string starts with a length and a type code. The rest of the descriptor has language IDs (0x0409 is US English, for example). The USB specification calls out the country codes, but they are similar to the locale numbers found in Microsoft Locale Identifiers (LCID).

The remaining string descriptors are simply a length, a descriptor type, and Unicode characters for the string in question (remember, Unicode characters are 16 bits long).

Skipping back up in the usb_descriptors.c file, you’ll find the configuration descriptors. In this case there is only one and, as usual, it starts with the length and the type. Some devices will have multiple configurations (for example, one when on bus power and another when self powered).

The configuration descriptor is sort of a header. The same block of data contains not only the configuration descriptor, but all the related interface and endpoint descriptors as well. The second length field (0x29, 0x00) is the total data length for the entire set of descriptors. Here’s the code:

/* Configuration 1 Descriptor */
ROM BYTE configDescriptor1[]={
    /* Configuration Descriptor */
    0x09,//sizeof(USB_CFG_DSC),    // Size of this descriptor in bytes
    0x29,0x00,            // Total length of data for this cfg
    1,                      // Number of interfaces in this cfg
    1,                      // Index value of this configuration
    0,                      // Configuration string index
    _DEFAULT /* |  _SELF */,               // Attributes, see usb_device.h
    50,                     // Max power consumption (2X mA)
    /* Interface Descriptor */
    0x09,//sizeof(USB_INTF_DSC),   // Size of this descriptor in bytes
    USB_DESCRIPTOR_INTERFACE,               // INTERFACE descriptor type
    0,                      // Interface Number
    0,                      // Alternate Setting Number
    2,                      // Number of endpoints in this intf
    HID_INTF,               // Class code
    0,     // Subclass code
    0,     // Protocol code
    0,                      // Interface string index

    /* HID Class-Specific Descriptor */
   0x09,//sizeof(USB_HID_DSC)+3,    // Size of this descriptor in bytes
    DSC_HID,                // HID descriptor type
    0x11,0x01,                 // HID Spec Release Number in BCD format (1.11)
    0x00,                   // Country Code (0x00 for Not supported)
    HID_NUM_OF_DSC,         // Number of class descriptors, see usbcfg.h
    DSC_RPT,                // Report descriptor type
    HID_RPT01_SIZE,0x00,//sizeof(hid_rpt01),      // Size of the report descriptor
   /* Endpoint Descriptor */
    USB_DESCRIPTOR_ENDPOINT,    //Endpoint Descriptor
    HID_EP | _EP_IN,                   //EndpointAddress
    _INTERRUPT,                       //Attributes
    0x40,0x00,                  //size
    0x01,                        //Interval
    /* Endpoint Descriptor */
    USB_DESCRIPTOR_ENDPOINT,    //Endpoint Descriptor
    HID_EP | _EP_OUT,                   //EndpointAddress
    _INTERRUPT,                       //Attributes
    0x40,0x00,                  //size
    0x01                        //Interval

This tells the host that the configuration has one interface and draws 100mA (maximum) and is not self-powered (note _SELF is commented out).

The interface has two endpoints and is a HID device (HID_INTF). There is a class-specific descriptor (DSC_HID) that describes a single report (more on that in a future blog).

The two endpoints do input and output using the USB interrupt transfer. Both transfer a maximum of 64 bytes and should be polled every frame (this interval has to do with the way USB handles polling endpoint transfers).

That’s a lot of bookkeeping, but not much code. Unfortunately, if you get anything even a little bit off, the host computer is likely to turn its nose up at your device. There’s actually one more important piece of paperwork left in usb_decriptors.c: the HID-specific descriptor that defines the reports. But I’ll save that for next time.