Kevin Cuzner's Personal Blog

Electronics, Embedded Systems, and Software are my breakfast, lunch, and dinner.


Writing reusable USB device descriptors (and other constant data) with C++ constexpr

Several years ago I wrote a post which introduced my method of declaring XML comments in my source code and scanning them with a Python script to produce a generated byte array. I've used this several times over the years and as tends to happen, I now hate it. My biggest pet peeve has turned out to be its lack of flexibility. Every time I want to do something crazy, like create HID reports or add extensive audio descriptors (with their relatively complicated cross-referencing scheme), I end up having to make big changes to my Python. It just isn't simple enough! The other thing is that it's not very portable either. If have some hardware that, for example, locks endpoint addresses to specific endpoint instances (a restriction that the STM32 USB peripheral doesn't have, but the SAMD21 does), it'll be yet another modification to the script.

I'd like to introduce in this post a fluent API written entirely using C++ constexpr which enables a syntax like this:

 1constexpr auto kHidEndpointIn = usb::EndpointDescriptor()
 2                                    .EndpointAddress(0x81)
 3                                    .Attributes(0x03)
 4                                    .MaxPacketSize(64)
 5                                    .Interval(1);
 6constexpr auto kHidEndpointOut = usb::EndpointDescriptor()
 7                                     .EndpointAddress(0x01)
 8                                     .EndpointAddress(0x01)
 9                                     .Attributes(0x03)
10                                     .MaxPacketSize(64)
11                                     .Interval(1);
12
13constexpr auto kConfigDescriptor =
14    usb::ConfigurationDescriptor(0)
15        .ConfigurationValue(1)
16        .Attributes(0x80)
17        .WithInterface(
18            usb::InterfaceDescriptor()
19                .InterfaceClass(0x03)
20                .InterfaceSubClass(0x00)
21                .WithEndpoint(kHidEndpointIn)
22                .WithEndpoint(kHidEndpointOut));

To produce something like this in the .rodata section of my executable:

 1000014e1 <_ZL17kConfigDescriptor>:
 2    14e1:   00290209        eoreq   r0, r9, r9, lsl #4
 3    14e5:   80000101        andhi   r0, r0, r1, lsl #2
 4    14e9:   00040900        andeq   r0, r4, r0, lsl #18
 5    14ed:   00030200        andeq   r0, r3, r0, lsl #4
 6    14f1:   21090000        mrscs   r0, (UNDEF: 9)
 7    14f5:   01000111        tsteq   r0, r1, lsl r1
 8    14f9:   07001922        streq   r1, [r0, -r2, lsr #18]
 9    14fd:   40038105        andmi   r8, r3, r5, lsl #2
10    1501:   05070100        streq   r0, [r7, #-256] @ 0xffffff00
11    1505:   00400301        subeq   r0, r0, r1, lsl #6
12    1509:   00000001        andeq   r0, r0, r1

Now, I'm not a C++ expert by any means. I'm almost certain I did things in a harder way than necessary. But my hope is that by telling my journey in getting to this point someone might find some benefit.

Continue on to read more!

Writing reusable USB device descriptors with some XML, Python, and C

A recent project required me to reuse (once again) my USB HID device driver. This is my third or fourth project using this and I had started to find it annoying to need to hand-modify a heavily-commented, self-referencing array of uint8_t's. I figured there must be a better way, so I decided to try something different.

In this post I will present a script that turns this madness, which lives in a separate file:

  1/**
  2 * Device descriptor
  3 */
  4static const USB_DATA_ALIGN uint8_t dev_descriptor[] = {
  5    18, //bLength
  6    1, //bDescriptorType
  7    0x00, 0x02, //bcdUSB
  8    0x00, //bDeviceClass (defined by interfaces)
  9    0x00, //bDeviceSubClass
 10    0x00, //bDeviceProtocl
 11    USB_CONTROL_ENDPOINT_SIZE, //bMaxPacketSize0
 12    0xc0, 0x16, //idVendor
 13    0xdc, 0x05, //idProduct
 14    0x11, 0x00, //bcdDevice
 15    1, //iManufacturer
 16    2, //iProduct
 17    0, //iSerialNumber,
 18    1, //bNumConfigurations
 19};
 20
 21static const USB_DATA_ALIGN uint8_t hid_report_descriptor[] = {
 22    HID_SHORT(0x04, 0x00, 0xFF), //USAGE_PAGE (Vendor Defined)
 23    HID_SHORT(0x08, 0x01), //USAGE (Vendor 1)
 24    HID_SHORT(0xa0, 0x01), //COLLECTION (Application)
 25    HID_SHORT(0x08, 0x01), //  USAGE (Vendor 1)
 26    HID_SHORT(0x14, 0x00), //  LOGICAL_MINIMUM (0)
 27    HID_SHORT(0x24, 0xFF, 0x00), //LOGICAL_MAXIMUM (0x00FF)
 28    HID_SHORT(0x74, 0x08), //  REPORT_SIZE (8)
 29    HID_SHORT(0x94, 64), //  REPORT_COUNT(64)
 30    HID_SHORT(0x80, 0x02), //  INPUT (Data, Var, Abs)
 31    HID_SHORT(0x08, 0x01), //  USAGE (Vendor 1)
 32    HID_SHORT(0x90, 0x02), //  OUTPUT (Data, Var, Abs)
 33    HID_SHORT(0xc0),       //END_COLLECTION
 34};
 35
 36/**
 37 * Configuration descriptor
 38 */
 39static const USB_DATA_ALIGN uint8_t cfg_descriptor[] = {
 40    9, //bLength
 41    2, //bDescriptorType
 42    9 + 9 + 9 + 7 + 7, 0x00, //wTotalLength
 43    1, //bNumInterfaces
 44    1, //bConfigurationValue
 45    0, //iConfiguration
 46    0x80, //bmAttributes
 47    250, //bMaxPower
 48    /* INTERFACE 0 BEGIN */
 49    9, //bLength
 50    4, //bDescriptorType
 51    0, //bInterfaceNumber
 52    0, //bAlternateSetting
 53    2, //bNumEndpoints
 54    0x03, //bInterfaceClass (HID)
 55    0x00, //bInterfaceSubClass (0: no boot)
 56    0x00, //bInterfaceProtocol (0: none)
 57    0, //iInterface
 58        /* HID Descriptor */
 59        9, //bLength
 60        0x21, //bDescriptorType (HID)
 61        0x11, 0x01, //bcdHID
 62        0x00, //bCountryCode
 63        1, //bNumDescriptors
 64        0x22, //bDescriptorType (Report)
 65        sizeof(hid_report_descriptor), 0x00,
 66        /* INTERFACE 0, ENDPOINT 1 BEGIN */
 67        7, //bLength
 68        5, //bDescriptorType
 69        0x81, //bEndpointAddress (endpoint 1 IN)
 70        0x03, //bmAttributes, interrupt endpoint
 71        USB_HID_ENDPOINT_SIZE, 0x00, //wMaxPacketSize,
 72        10, //bInterval (10 frames)
 73        /* INTERFACE 0, ENDPOINT 1 END */
 74        /* INTERFACE 0, ENDPOINT 2 BEGIN */
 75        7, //bLength
 76        5, //bDescriptorType
 77        0x02, //bEndpointAddress (endpoint 2 OUT)
 78        0x03, //bmAttributes, interrupt endpoint
 79        USB_HID_ENDPOINT_SIZE, 0x00, //wMaxPacketSize
 80        10, //bInterval (10 frames)
 81        /* INTERFACE 0, ENDPOINT 2 END */
 82    /* INTERFACE 0 END */
 83};
 84
 85static const USB_DATA_ALIGN uint8_t lang_descriptor[] = {
 86    4, //bLength
 87    3, //bDescriptorType
 88    0x09, 0x04 //wLANGID[0]
 89};
 90
 91static const USB_DATA_ALIGN uint8_t manuf_descriptor[] = {
 92    2 + 15 * 2, //bLength
 93    3, //bDescriptorType
 94    'k', 0x00, //wString
 95    'e', 0x00,
 96    'v', 0x00,
 97    'i', 0x00,
 98    'n', 0x00,
 99    'c', 0x00,
100    'u', 0x00,
101    'z', 0x00,
102    'n', 0x00,
103    'e', 0x00,
104    'r', 0x00,
105    '.', 0x00,
106    'c', 0x00,
107    'o', 0x00,
108    'm', 0x00
109};
110
111static const USB_DATA_ALIGN uint8_t product_descriptor[] = {
112    2 + 14 * 2, //bLength
113    3, //bDescriptorType
114    'L', 0x00,
115    'E', 0x00,
116    'D', 0x00,
117    ' ', 0x00,
118    'W', 0x00,
119    'r', 0x00,
120    'i', 0x00,
121    's', 0x00,
122    't', 0x00,
123    'w', 0x00,
124    'a', 0x00,
125    't', 0x00,
126    'c', 0x00,
127    'h', 0x00
128};
129
130const USBDescriptorEntry usb_descriptors[] = {
131    { 0x0100, 0x0000, sizeof(dev_descriptor), dev_descriptor },
132    { 0x0200, 0x0000, sizeof(cfg_descriptor), cfg_descriptor },
133    { 0x0300, 0x0000, sizeof(lang_descriptor), lang_descriptor },
134    { 0x0301, 0x0409, sizeof(manuf_descriptor), manuf_descriptor },
135    { 0x0302, 0x0409, sizeof(product_descriptor), product_descriptor },
136    { 0x2200, 0x0000, sizeof(hid_report_descriptor), hid_report_descriptor },
137    { 0x0000, 0x0000, 0x00, NULL }
138};

Into these comment blocks which can live anywhere in the source and are somewhat more readable:

  1/**
  2 * <descriptor id="device" type="0x01">
  3 *  <length name="bLength" size="1" />
  4 *  <type name="bDescriptorType" size="1" />
  5 *  <word name="bcdUSB">0x0200</word>
  6 *  <byte name="bDeviceClass">0</byte>
  7 *  <byte name="bDeviceSubClass">0</byte>
  8 *  <byte name="bDeviceProtocol">0</byte>
  9 *  <byte name="bMaxPacketSize0">USB_CONTROL_ENDPOINT_SIZE</byte>
 10 *  <word name="idVendor">0x16c0</word>
 11 *  <word name="idProduct">0x05dc</word>
 12 *  <word name="bcdDevice">0x0010</word>
 13 *  <ref name="iManufacturer" type="0x03" refid="manufacturer" size="1" />
 14 *  <ref name="iProduct" type="0x03" refid="product" size="1" />
 15 *  <byte name="iSerialNumber">0</byte>
 16 *  <count name="bNumConfigurations" type="0x02" size="1" />
 17 * </descriptor>
 18 * <descriptor id="lang" type="0x03" first="first">
 19 *  <length name="bLength" size="1" />
 20 *  <type name="bDescriptorType" size="1" />
 21 *  <foreach type="0x03" unique="unique">
 22 *    <echo name="wLang" />
 23 *  </foreach>
 24 * </descriptor>
 25 * <descriptor id="manufacturer" type="0x03" wIndex="0x0409">
 26 *  <property name="wLang" size="2">0x0409</property>
 27 *  <length name="bLength" size="1" />
 28 *  <type name="bDescriptorType" size="1" />
 29 *  <string name="wString">kevincuzner.com</string>
 30 * </descriptor>
 31 * <descriptor id="product" type="0x03" wIndex="0x0409">
 32 *  <property name="wLang" size="2">0x0409</property>
 33 *  <length name="bLength" size="1" />
 34 *  <type name="bDescriptorType" size="1" />
 35 *  <string name="wString">LED Wristwatch</string>
 36 * </descriptor>
 37 * <descriptor id="configuration" type="0x02">
 38 *  <length name="bLength" size="1" />
 39 *  <type name="bDescriptorType" size="1" />
 40 *  <length name="wTotalLength" size="2" all="all" />
 41 *  <count name="bNumInterfaces" type="0x04" associated="associated" size="1" />
 42 *  <byte name="bConfigurationValue">1</byte>
 43 *  <byte name="iConfiguration">0</byte>
 44 *  <byte name="bmAttributes">0x80</byte>
 45 *  <byte name="bMaxPower">250</byte>
 46 *  <children type="0x04" />
 47 * </descriptor>
 48 */
 49
 50/**
 51 * <include>usb_hid.h</include>
 52 * <descriptor id="hid_interface" type="0x04" childof="configuration">
 53 *  <length name="bLength" size="1" />
 54 *  <type name="bDescriptorType" size="1" />
 55 *  <index name="bInterfaceNumber" size="1" />
 56 *  <byte name="bAlternateSetting">0</byte>
 57 *  <count name="bNumEndpoints" type="0x05" associated="associated" size="1" />
 58 *  <byte name="bInterfaceClass">0x03</byte>
 59 *  <byte name="bInterfaceSubClass">0x00</byte>
 60 *  <byte name="bInterfaceProtocol">0x00</byte>
 61 *  <byte name="iInterface">0</byte>
 62 *  <children type="0x21" />
 63 *  <children type="0x05" />
 64 * </descriptor>
 65 * <descriptor id="hid" type="0x21" childof="hid_interface">
 66 *  <length name="bLength" size="1" />
 67 *  <type name="bDescriptorType" size="1" />
 68 *  <word name="bcdHID">0x0111</word>
 69 *  <byte name="bCountryCode">0x00</byte>
 70 *  <count name="bNumDescriptors" type="0x22" size="1" associated="associated" />
 71 *  <foreach type="0x22" associated="associated">
 72 *    <echo name="bDescriptorType" />
 73 *    <echo name="wLength" />
 74 *  </foreach>
 75 * </descriptor>
 76 * <descriptor id="hid_in_endpoint" type="0x05" childof="hid_interface">
 77 *  <length name="bLength" size="1" />
 78 *  <type name="bDescriptorType" size="1" />
 79 *  <inendpoint name="bEndpointAddress" define="HID_IN_ENDPOINT" />
 80 *  <byte name="bmAttributes">0x03</byte>
 81 *  <word name="wMaxPacketSize">USB_HID_ENDPOINT_SIZE</word>
 82 *  <byte name="bInterval">10</byte>
 83 * </descriptor>
 84 * <descriptor id="hid_out_endpoint" type="0x05" childof="hid_interface">
 85 *  <length name="bLength" size="1" />
 86 *  <type name="bDescriptorType" size="1" />
 87 *  <outendpoint name="bEndpointAddress" define="HID_OUT_ENDPOINT" />
 88 *  <byte name="bmAttributes">0x03</byte>
 89 *  <word name="wMaxPacketSize">USB_HID_ENDPOINT_SIZE</word>
 90 *  <byte name="bInterval">10</byte>
 91 * </descriptor>
 92 * <descriptor id="hid_report" childof="hid" top="top" type="0x22" order="1" wIndexType="0x04">
 93 *  <hidden name="bDescriptorType" size="1">0x22</hidden>
 94 *  <hidden name="wLength" size="2">sizeof(hid_report)</hidden>
 95 *  <raw>
 96 *  HID_SHORT(0x04, 0x00, 0xFF), //USAGE_PAGE (Vendor Defined)
 97 *  HID_SHORT(0x08, 0x01), //USAGE (Vendor 1)
 98 *  HID_SHORT(0xa0, 0x01), //COLLECTION (Application)
 99 *  HID_SHORT(0x08, 0x01), //  USAGE (Vendor 1)
100 *  HID_SHORT(0x14, 0x00), //  LOGICAL_MINIMUM (0)
101 *  HID_SHORT(0x24, 0xFF, 0x00), //LOGICAL_MAXIMUM (0x00FF)
102 *  HID_SHORT(0x74, 0x08), //  REPORT_SIZE (8)
103 *  HID_SHORT(0x94, 64), //  REPORT_COUNT(64)
104 *  HID_SHORT(0x80, 0x02), //  INPUT (Data, Var, Abs)
105 *  HID_SHORT(0x08, 0x01), //  USAGE (Vendor 1)
106 *  HID_SHORT(0x90, 0x02), //  OUTPUT (Data, Var, Abs)
107 *  HID_SHORT(0xc0),       //END_COLLECTION
108 *  </raw>
109 * </descriptor>
110 */

In most of my projects before this one I would have something like the first script shown above sitting in a file by itself, declaring a bunch of uint8_t arrays and a usb_descriptors[] table constant that would be consumed by my USB driver as it searched for USB descriptors. A header file that exposes the usb_descriptors[] table would also be found in the project. Any USB descriptor that had to be returned by the device would be found in this table. To make things more complex, descriptors like the configuration descriptor have to declare all of the device interfaces and so pieces and parts of each separate USB interface component would be interspersed inside of other descriptors.

I've been using this structure for some time after writing my first USB driver after reading through the Teensy driver. This is probably the only structural code that has made it all the way from the Teensy driver into all of my other code.

With this new script I've written there's no more need for manually computing how long a descriptor is or needing to modify the configuration descriptor every time a new interface has been added. All the parts of a descriptor are self-contained in the source file that defines a particular interface and can be easily moved around from project to project.

All the code for this post lives here:

`https://github.com/kcuzner/midi-fader <https://github.com/kcuzner/midi-fader>`__