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!