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 Name | Value | Bytes |
|---|---|---|
| bLength | 18 | 0x12 |
| bDescriptorType | DEVICE | 0x01 |
| bcdUSB | USB 2.0 | 0x00 0x02 |
| bDeviceClass | 0 | 0x00 |
| bDeviceSubClass | 0 | 0x00 |
| bDeviceProtocol | 0 | 0x00 |
| bMaxPacketSize | 64 | 0x40 |
| idVendor | TEST | 0xff 0xff |
| idProduct | TEST | 0xff 0xff |
| bcdDevice | 0.0.1 | 0x01 0x00 |
| iManufacturer | 1 | 0x01 |
| iProduct | 2 | 0x02 |
| iSerialNumber | 3 | 0x03 |
| bNumConfigurations | 1 | 0x01 |
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 Name | Value | Bytes |
|---|---|---|
| bLength | 7 | 0x07 |
| bDescriptorType | ENDPOINT | 0x05 |
| bEndpointAddress | EP2IN | 0x82 |
| bmAttributes | BULK | 0x02 |
| wMaxPacketSize | 64 | 0x40 0x00 |
| bInterval | IGNORED | 0x00 |
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 Name | Value | Bytes |
|---|---|---|
| bLength | 9 | 0x09 |
| bDescriptorType | INTERFACE | 0x04 |
| bInterfaceNumber | 1 | 0x01 |
| bAlternateSetting | 1 | 0x01 |
| bNumEndpoints | 1 | 0x01 |
| bInterfaceClass | 0 | 0x00 |
| bInterfaceSubClass | 0 | 0x00 |
| bInterfaceProtocol | 0 | 0x00 |
| iInterface | 5 | 0x05 |
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 Name | Value | Bytes |
|---|---|---|
| bLength | 9 | 0x09 |
| bDescriptorType | CONFIG | 0x02 |
| wTotalLength | 9+9+7 | 0x19 0x00 |
| bNumInterfaces | 1 | 0x01 |
| bConfigurationValue | 1 | 0x01 |
| iConfiguration | 4 | 0x04 |
| bmAttributes | Bus powered, no wake | 0x80 |
| bMaxPower | 500mA | 0xfa |
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 Name | Value | Bytes |
|---|---|---|
| bLength | 4 | 0x04 |
| bDescriptorType | STRING | 0x03 |
| wLangID[0] | en_GB | 0x09 0x08 |
0x04 0x03 0x09 0x08
...and...
| Field Name | Value | Bytes |
|---|---|---|
| bLength | 38 | 0x26 |
| bDescriptorType | STRING | 0x03 |
| 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.