Building a USB descriptor table set

In order to proceed further on our USB/STM32 oddessy, we need to start to build a USB descriptor set for our first prototype piece of code. For this piece, we're going to put together a USB device which is vendor-specific class-wise and has a single configuration with a interface with a single endpoint which we're not going to actually implement anything of. What we're after is just to get the information presented to the computer so that lsusb can see it.

To get these built, let's refer to information we discovered and recorded in a previous post about how descriptors go together.

Device descriptor

Remembering that values which are > 1 byte in length are always stored little-endian, we can construct our device descriptor as:

Field NameValueBytes
bLength180x12
bDescriptorTypeDEVICE0x01
bcdUSBUSB 2.00x00 0x02
bDeviceClass00x00
bDeviceSubClass00x00
bDeviceProtocol00x00
bMaxPacketSize640x40
idVendorTEST0xff 0xff
idProductTEST0xff 0xff
bcdDevice0.0.10x01 0x00
iManufacturer10x01
iProduct20x02
iSerialNumber30x03
bNumConfigurations10x01

We're using the vendor ID and product id 0xffff because at this point we don't have any useful values for this (it costs $5,000 to register a vendor ID).

This gives us a final byte array of:

0x12 0x01 0x00 0x02 0x00 0x00 0x00 0x40 (Early descriptor)

0xff 0xff 0xff 0xff 0x01 0x00 0x01 0x02 0x03 0x01 (and the rest)

We're reserving string ids 1, 2, and 3, for the manufacturer string, product name string, and serial number string respectively. I'm deliberately including them all so that we can see it all come out later in lsusb.

If you feed the above hex sequence into a USB descriptor decoder then you can check my working.

Endpoint Descriptor

We want a single configuration, which covers our one interface, with one endpoint in it. Let's start with the endpoint...

Field NameValueBytes
bLength70x07
bDescriptorTypeENDPOINT0x05
bEndpointAddressEP2IN0x82
bmAttributesBULK0x02
wMaxPacketSize640x40 0x00
bIntervalIGNORED0x00

We're giving a single bulk IN endpoint, since that's the simplest thing to describe at this time. This endpoint will never be ready and so nothing will ever be read into the host.

All that gives us:

0x07 0x05 0x82 0x02 0x40 0x00 0x00

Interface Descriptor

The interface descriptor prefaces the endpoint set, and thanks to our simple single endpoint, and no plans for alternate interfaces, we can construct the interface simply as:

Field NameValueBytes
bLength90x09
bDescriptorTypeINTERFACE0x04
bInterfaceNumber10x01
bAlternateSetting10x01
bNumEndpoints10x01
bInterfaceClass00x00
bInterfaceSubClass00x00
bInterfaceProtocol00x00
iInterface50x05

All that gives us:

0x09 0x04 0x01 0x01 0x01 0x00 0x00 0x00 0x05

Configuration descriptor

Finally we can put it all together and get the configuration descriptor...

Field NameValueBytes
bLength90x09
bDescriptorTypeCONFIG0x02
wTotalLength9+9+70x19 0x00
bNumInterfaces10x01
bConfigurationValue10x01
iConfiguration40x04
bmAttributesBus powered, no wake0x80
bMaxPower500mA0xfa

The wTotalLength field is interesting. It contains the configuration length, the interface length, and the endpoint length, hence 9 plus 9 plus 7 is 25.

This gives:

0x09 0x02 0x19 0x00 0x01 0x01 0x04 0x80 0xfa

String descriptors

We allowed ourselves a total of five strings, they were iManufacturer, iProduct, iSerial (from the device descriptor), iConfiguration (from the configuration descriptor), and iInterface (from the interface descriptor) respectively.

Our string descriptors will therefore be:

Field NameValueBytes
bLength40x04
bDescriptorTypeSTRING0x03
wLangID[0]en_GB0x09 0x08

0x04 0x03 0x09 0x08

...and...

Field NameValueBytes
bLength380x26
bDescriptorTypeSTRING0x03
bString"Rusty Manufacturer"...

0x26 0x03 0x52 0x00 0x75 0x00 0x73 0x00

0x74 0x00 0x79 0x00 0x20 0x00 0x4d 0x00

0x61 0x00 0x6e 0x00 0x75 0x00 0x66 0x00

0x61 0x00 0x63 0x00 0x74 0x00 0x75 0x00

0x72 0x00 0x65 0x00 0x72 0x00

(You get the idea, there's no point me breaking down the rest of the string descriptors here, suffice it to say that the other strings are appropriate for the values they represent - namely product, serial, configuration, and interface.)

Putting it all together

Given all the above, we have a device descriptor which is standalone, then a configuration descriptor which encompasses the interface and endpoint descriptors too. Finally we have a string descriptor table with six entries, the first is the language sets available, and the rest are our strings. In total we have:


    // Device descriptor
    const DEV_DESC: [u8; 18] = {
        0x12, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x40,
        0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x01, 0x02,
        0x03, 0x01
    };

    // Configuration descriptor
    const CONF_DESC: [u8; 25] = {
        0x09, 0x02, 0x19, 0x00, 0x01, 0x01, 0x04, 0x80, 0xfa,
        0x09, 0x04, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x05,
        0x07, 0x05, 0x82, 0x02, 0x40, 0x00, 0x00
    };

    // String Descriptor zero
    const STR_DESC_0: [u8; 4] = {0x04, 0x03, 0x09, 0x08};

    // String Descriptor 1, "Rusty Manufacturer"
    const STR_DESC_1: [u8; 38] = {
        0x26, 0x03, 0x52, 0x00, 0x75, 0x00, 0x73, 0x00,
        0x74, 0x00, 0x79, 0x00, 0x20, 0x00, 0x4d, 0x00,
        0x61, 0x00, 0x6e, 0x00, 0x75, 0x00, 0x66, 0x00,
        0x61, 0x00, 0x63, 0x00, 0x74, 0x00, 0x75, 0x00,
        0x72, 0x00, 0x65, 0x00, 0x72, 0x00
    };

    // String Descriptor 2, "Rusty Product"
    const STR_DESC_2: [u8; 28] = {
        0x1c, 0x03, 0x52, 0x00, 0x75, 0x00, 0x73, 0x00,
        0x74, 0x00, 0x79, 0x00, 0x20, 0x00, 0x50, 0x00,
        0x72, 0x00, 0x6f, 0x00, 0x64, 0x00, 0x75, 0x00,
        0x63, 0x00, 0x74, 0x00
    };

    // String Descriptor 3, "123ABC"
    const STR_DESC_3: [u8; 14] = {
        0x0e, 0x03, 0x31, 0x00, 0x32, 0x00, 0x33, 0x00,
        0x41, 0x00, 0x42, 0x00, 0x43, 0x00
    };

    // String Descriptor 4, "Rusty Configuration"
    const STR_DESC_4: [u8; 40] = {
        0x28, 0x03, 0x52, 0x00, 0x75, 0x00, 0x73, 0x00,
        0x74, 0x00, 0x79, 0x00, 0x20, 0x00, 0x43, 0x00,
        0x6f, 0x00, 0x6e, 0x00, 0x66, 0x00, 0x69, 0x00,
        0x67, 0x00, 0x75, 0x00, 0x72, 0x00, 0x61, 0x00,
        0x74, 0x00, 0x69, 0x00, 0x6f, 0x00, 0x6e, 0x00
    };

    // String Descriptor 5, "Rusty Interface"
    const STR_DESC_5: [u8; 32] = {
        0x20, 0x03, 0x52, 0x00, 0x75, 0x00, 0x73, 0x00,
        0x74, 0x00, 0x79, 0x00, 0x20, 0x00, 0x49, 0x00,
        0x6e, 0x00, 0x74, 0x00, 0x65, 0x00, 0x72, 0x00,
        0x66, 0x00, 0x61, 0x00, 0x63, 0x00, 0x65, 0x00
    };

With the above, we're a step closer to our first prototype which will hopefully be enumerable. Next time we'll look at beginning our prototype low level USB device stack mock-up.