Electronics, Embedded Systems, and Software are my breakfast, lunch, and dinner.
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!
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>`__