Kevin Cuzner's Personal Blog

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


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

Dec 27, 2019

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>`__

The Script

I have continued to write my descriptors using the "Teensy method" for a few reasons:

  • They are compile-time constants and therefore don't take up valuable RAM (which consumes both .data and .rodata segments). I've seen other implementations that initialize a writable array in RAM with the descriptor and that just doesn't work well with memory-constrained embedded systems. It just makes the USB driver stack footprint too large for my comfort.
  • It is easy to figure out what is going on. There is very little "macro magic" here. Even the part where I look up descriptors in the table is really straightforward and beyond that, everything is just an opaque byte array that is copied out over USB. Real simple.

Writing descriptors like this has some problems, however:

  • It requires me to manually edit the binary contents of the descriptors, keep multiple fields in sync (i.e. length fields vs actual length), and handle endianness manually.
  • Making a new project requires me to copy-paste pieces from another project's descriptor file into my configuration descriptor and hope that I updated the lengths correctly.
  • Adding a new interface to my configuration again required editing this blob and hoping that I got it right.
  • Without generous comments, it is impossible to interpret and read. Finding a bug in the descriptor is very much a "stare at it until something moves" sort of process.

So, I decided to improve this a bit with some scripting. Here were my goals:

  • Fully automatic computation of the wLength fields in descriptors.
  • Ad-hoc descriptor definition (i.e. I can specify descriptors throughout the code in many places).
  • Portable to all my machines without any dependencies other than Python. In general I use arch with python installed, so requesting that python be available isn't a big deal for me.
  • Fully compatible with my existing USB driver structure (i.e. use the same usb_descriptors table format).
  • Fairly agnostic of the actual USB driver used. The idea is that this can be used by other people who don't want to be stuck with my USB driver implementation.

The way my script works, block comments in any source file can contain XML which is interpreted by the script which in turn generates a C file that declares the usb_descriptors[] table and contains the generated byte arrays containing all descriptors declared in the program. In addition, I have a static "USBApplication" object which handles each USB interface in a modular manner. I can how have my HID interface completely self-contained in a single file, my audio device interface in another single file, and some other custom interface in its own file. If I want to move the HID interface to another project, all I have to do is copy-paste the single HID source file (and header) and everything (source, descriptors, USB interface declaration) comes along with it. Nice and easy!

For example, here is the "main.c" file of my midi-fader device:

 1/**
 2 * USB Midi-Fader
 3 *
 4 * Kevin Cuzner
 5 *
 6 * Main Application
 7 */
 8
 9#include "usb.h"
10#include "usb_app.h"
11#include "usb_hid.h"
12#include "usb_midi.h"
13#include "osc.h"
14#include "error.h"
15#include "storage.h"
16#include "fader.h"
17#include "buttons.h"
18#include "systick.h"
19#include "mackie.h"
20
21#include "stm32f0xx.h"
22
23#include "_gen_usb_desc.h"
24
25/**
26 * <descriptor id="device" type="0x01">
27 *  <length name="bLength" size="1" />
28 *  <type name="bDescriptorType" size="1" />
29 *  <word name="bcdUSB">0x0200</word>
30 *  <byte name="bDeviceClass">0</byte>
31 *  <byte name="bDeviceSubClass">0</byte>
32 *  <byte name="bDeviceProtocol">0</byte>
33 *  <byte name="bMaxPacketSize0">USB_CONTROL_ENDPOINT_SIZE</byte>
34 *  <word name="idVendor">0x16c0</word>
35 *  <word name="idProduct">0x05dc</word>
36 *  <word name="bcdDevice">0x0010</word>
37 *  <ref name="iManufacturer" type="0x03" refid="manufacturer" size="1" />
38 *  <ref name="iProduct" type="0x03" refid="product" size="1" />
39 *  <byte name="iSerialNumber">0</byte>
40 *  <count name="bNumConfigurations" type="0x02" size="1" />
41 * </descriptor>
42 * <descriptor id="lang" type="0x03" first="first">
43 *  <length name="bLength" size="1" />
44 *  <type name="bDescriptorType" size="1" />
45 *  <foreach type="0x03" unique="unique">
46 *    <echo name="wLang" />
47 *  </foreach>
48 * </descriptor>
49 * <descriptor id="manufacturer" type="0x03" wIndex="0x0409">
50 *  <property name="wLang" size="2">0x0409</property>
51 *  <length name="bLength" size="1" />
52 *  <type name="bDescriptorType" size="1" />
53 *  <string name="wString">kevincuzner.com</string>
54 * </descriptor>
55 * <descriptor id="product" type="0x03" wIndex="0x0409">
56 *  <property name="wLang" size="2">0x0409</property>
57 *  <length name="bLength" size="1" />
58 *  <type name="bDescriptorType" size="1" />
59 *  <string name="wString">Midi-Fader</string>
60 * </descriptor>
61 * <descriptor id="configuration" type="0x02">
62 *  <length name="bLength" size="1" />
63 *  <type name="bDescriptorType" size="1" />
64 *  <length name="wTotalLength" size="2" all="all" />
65 *  <count name="bNumInterfaces" type="0x04" associated="associated" size="1" />
66 *  <byte name="bConfigurationValue">1</byte>
67 *  <byte name="iConfiguration">0</byte>
68 *  <byte name="bmAttributes">0x80</byte>
69 *  <byte name="bMaxPower">250</byte>
70 *  <children type="0x04" />
71 * </descriptor>
72 */
73
74#include <stddef.h>
75
76static const USBInterfaceListNode midi_interface_node = {
77    .interface = &midi_interface,
78    .next = NULL,
79};
80
81static const USBInterfaceListNode hid_interface_node = {
82    .interface = &hid_interface,
83    .next = &midi_interface_node,
84};
85
86const USBApplicationSetup setup = {
87    .interface_list = &hid_interface_node,
88};
89
90const USBApplicationSetup *usb_app_setup = &setup;
91
92uint8_t buf[16];
93int main()
94{
95...
96    return 0;
97}

It only needs to declare the main device descriptor with the manufacturer and model strings. I have two other interfaces (usb_hid and usb_midi) in this project, but there's no trace of them here except for the bits where I hook them into the overall application. I'll talk a little more about that at the end, but the main point of this post is to show my new method for handling USB descriptors.

Makefile changes

The script consists of a 800-ish line python script (current version: https://github.com/kcuzner/midi-fader/blob/master/firmware/scripts/descriptorgen.py) which takes as its arguments every source file in the project that could have some block comments. It then does the following:

  1. Find all block comments (/* ... */) in the source and extract them, stripping off leading "*" characters from each line. The blocks are retained as individual continuous pieces and are each parsed separately.
  2. If the block doesn't contain text matching the regex "<descriptor+.>", it is discarded. Otherwise, the contents of the block comment are wrapped in an arbitrary element and then parsed using elementtree.
  3. Each parsed comment block is assumed to declare one or more "descriptors". The parsed XML is run through an interpreter which begins assembling objects which will generate the binary descriptor.
  4. After every block has been parsed, the script will generate all the descriptors into a C file, automatically tracking endpoint numbers, addresses, and descriptor lengths.

The C file that this generates is placed in the obj folder during compilation and treated as a non-source-controlled component. It is regenerated every time the makefile is run. Here is a snippet of how my makefile invokes this script. I hope this makes some sense. My makefile style has changed somewhat for this project enable multiple targets, but hopefully this communicates the gist of how I made the Makefile execute the python script before compiling any other objects.

 1# These are spread out among several files, but are concatenated here for easy
 2# reading
 3
 4#
 5# These are declared in a Makefile meant as a header:
 6#
 7
 8# Project structure
 9SRCDIRS = src
10GENSRCDIRS = src
11BINDIR = bin
12OBJDIR = obj
13GENDIR = obj/gen
14CSRCDIRS = $(SRCDIRS)
15SSRCDIRS = $(SRCDIRS)
16
17# Sources
18GENERATE =
19SRC = $(foreach DIR,$(CSRCDIRS),$(wildcard $(DIR)/*.c))
20GENSRC = $(foreach DIR,$(GENSRCDIRS),$(wildcard $(DIR)/*.c))
21STORAGESRC = $(foreach DIR,$(CSRCDIRS),$(wildcard $(DIR)/*.storage.xml))
22ASM = $(foreach DIR,$(SSRCDIRS),$(wildcard $(DIR)/*.s))
23
24#
25# These are declared in the per-project makefile that configures the build
26# process:
27#
28
29SRCDIRS = src
30GENSRCDIRS = src
31
32# This will cause the USB descriptor to be generated
33GENERATE = USB_DESCRIPTOR
34
35#
36# These are declared in a Makefile meant as a footer that declares all recipes:
37#
38
39GENERATE_USB_DESCRIPTOR=USB_DESCRIPTOR
40GENERATE_USB_DESCRIPTOR_SRC=_gen_usb_desc.c
41GENERATE_USB_DESCRIPTOR_HDR=_gen_usb_desc.h
42
43OBJ := $(addprefix $(OBJDIR)/,$(notdir $(SRC:.c=.o)))
44OBJ += $(addprefix $(OBJDIR)/,$(notdir $(ASM:.s=.o)))
45
46# If the USB descriptor generation is requested, add it to the list of targets
47# which will run during code generation
48ifneq ($(filter $(GENERATE), $(GENERATE_USB_DESCRIPTOR)),)
49     GEN_OBJ += $(GENDIR)/$(GENERATE_USB_DESCRIPTOR_SRC:.c=.o)
50     GEN_TARGETS += $(GENERATE_USB_DESCRIPTOR)
51endif
52
53ALL_OBJ := $(OBJ) $(GEN_OBJ)
54
55# Invoke the python script to generate the USB descriptor
56$(GENERATE_USB_DESCRIPTOR):
57     @mkdir -p $(GENDIR)
58     $(DESCRIPTORGEN) -os $(GENDIR)/$(GENERATE_USB_DESCRIPTOR_SRC) \
59             -oh $(GENDIR)/$(GENERATE_USB_DESCRIPTOR_HDR) \
60             $(GENSRC)
61
62# Ensure generated objects get run first
63$(OBJ): | $(GEN_TARGETS)
64
65#
66# Later, the $(ALL_OBJ) variable is used in the linking step to include the
67# generated C source files.
68#

It's not the most straightforward method, but it works well for my multi-target project structure that I've been using lately. Perhaps I'll write a post about that someday.

This works like so:

  1. The GENERATE variable is set to contain the phrase "USB_DESCRIPTOR" which will trigger evaluation of the variables that will cause the USB descriptor to be generated.
  2. The ifneq statement adds $(GENERATE_USB_DESCRIPTOR) to the GEN_TARGETS variable if GENERATE contains the phrase "USB_DESCRIPTOR". The targets in this variable will have their recipes evaluated as a dependency for all the object files in $(OBJ) which doesn't include the generated object files.
  3. During makefile evaluation, the $(OBJ) list is created from all the source and is depended on by targets like "all" (not shown). This triggers evaluation of $(GEN_TARGETS) which is just set to $(GENERATE_USB_DESCRIPTOR).
  4. The $(GENERATE_USB_DESCRIPTOR) target's recipe is invoked. The python script is run with all source files as its argument. It creates the generated C files whose objects are captured in $(GEN_OBJ).
  5. Compilation will continue, compiling the C files for $(OBJ) and the C files for $(GEN_OBJ). This isn't shown in the snippet.
  6. Finally all the resulting objects (both source and generated files) are linked into the executable. Again, this isn't shown in the snippet.

USB Descriptor XML

As the python script is run, it searches the source files for XML which describes the USB descriptors. To demonstrate the XML format, here is the simplest USB descriptor. This will just declare a device, add product and model strings, and declare a simple configuration that requires maximum USB power:

 1<descriptor id="device" type="0x01">
 2  <length name="bLength" size="1" />
 3  <type name="bDescriptorType" size="1" />
 4  <word name="bcdUSB">0x0200</word>
 5  <byte name="bDeviceClass">0</byte>
 6  <byte name="bDeviceSubClass">0</byte>
 7  <byte name="bDeviceProtocol">0</byte>
 8  <byte name="bMaxPacketSize0">USB_CONTROL_ENDPOINT_SIZE</byte>
 9  <word name="idVendor">0x16c0</word>
10  <word name="idProduct">0x05dc</word>
11  <word name="bcdDevice">0x0010</word>
12  <ref name="iManufacturer" type="0x03" refid="manufacturer" size="1" />
13  <ref name="iProduct" type="0x03" refid="product" size="1" />
14  <byte name="iSerialNumber">0</byte>
15  <count name="bNumConfigurations" type="0x02" size="1" />
16</descriptor>
17<descriptor id="lang" type="0x03" first="first">
18  <length name="bLength" size="1" />
19  <type name="bDescriptorType" size="1" />
20  <foreach type="0x03" unique="unique">
21    <echo name="wLang" />
22  </foreach>
23</descriptor>
24<descriptor id="manufacturer" type="0x03" wIndex="0x0409">
25  <property name="wLang" size="2">0x0409</property>
26  <length name="bLength" size="1" />
27  <type name="bDescriptorType" size="1" />
28  <string name="wString">kevincuzner.com</string>
29</descriptor>
30<descriptor id="product" type="0x03" wIndex="0x0409">
31  <property name="wLang" size="2">0x0409</property>
32  <length name="bLength" size="1" />
33  <type name="bDescriptorType" size="1" />
34  <string name="wString">Midi-Fader</string>
35</descriptor>
36<descriptor id="configuration" type="0x02">
37  <length name="bLength" size="1" />
38  <type name="bDescriptorType" size="1" />
39  <length name="wTotalLength" size="2" all="all" />
40  <count name="bNumInterfaces" type="0x04" associated="associated" size="1" />
41  <byte name="bConfigurationValue">1</byte>
42  <byte name="iConfiguration">0</byte>
43  <byte name="bmAttributes">0x80</byte>
44  <byte name="bMaxPower">250</byte>
45  <children type="0x04" />
46</descriptor>

The syntax is as follows:

  • Every USB descriptor is declared using a <descriptor> element. This element has an "id" and a "type" attribute. The "id" is just a string which can be used to refer to the descriptor later inside of other descriptors. The "type" is a number which is exactly the same as the USB descriptor type as declared in the USB specification. For example, a device descriptor is type "1", a configuration descriptor is type "2", a string descriptor is type "3", and an interface descriptor is type "4".
    • I added the "type" as a <descriptor> -level attribute because elements like <children> require that we have indexed descriptors by type.
    • The <descriptor> can optionally declare the "childof" attribute. This attribute should be set to the "id" of another descriptor in which this discriptor will appear. If the "childof" attribute isn't specified, then the descriptor will appear in the global "usb_descriptors" table.
  • The order of the children inside the <descriptor> element defines the structure of the USB descriptor. Each element may create 0 or more bytes in the resulting output byte array:
    • Most child elements have a "name" attribute. This allows them to be referenced by other child elements in the same descriptor.
    • The <length> element will output the length of the descriptor in bytes. It has a "size" attribute which says how many bytes to take up. Note that in a configuration descriptor, this is used twice: Once for the bDescriptorLength (which is always 9) and once for the wTotalLength (which varies depending on the number of interfaces). By default, bytes created by the <children> element are not counted in the bytes generated by the <length> tag unless the "all" attribute is present.
    • The <type> element just echoes the type of the parent <descriptor> in the number of bytes specified by "size". This allows us to single-source the descriptor type number only in the <descriptor> element.
    • The <count> element outputs the number of descriptors of some type specified by the "type" attribute. This is the same "type" as declared in <descriptor> .
      • There is the concept of "associated" descriptors. An associated descriptor is one that declares this descriptor as its parent. If we don't specify the "associated" attribute, then <count> will count all descriptors found of the specified "type". Otherwise, it will only count descriptors who have explicitly declared that they are children of this descriptor.
    • The <string> element generates the bytes for a USB wchar string based on the text contained in the element.
      • This was one of the things about manual descriptors that annoyed me the most. I've never had to use the upper byte of wchars and so reading or modifying the strings was always a pain with the extra null bytes between each character.
    • The <byte> element generates a single byte based on interpeting the text in this element as a number.
    • The <word> element generates two bytes based on interpreting the text in this element as a number.
    • The <property> element declares non-outputting binary content that is associated with this descriptor by interpreting the text in this element as a number. The content can be outputted in other ways, such as through the <foreach> element in another descriptor. Its "size" argument declares how many bytes this will produce.
    • The <children> element will echo the entire binary contents of descriptors which declare their "childof" attribute to have the id of this descriptor. It has a "type" attribute which specifies which type of descriptor to echo.
    • The <foreach> element will output binary content based on the content of other descriptors. It has a "type" argument which specifies the descriptor type to enumerate. It examines all descriptors declared.
      • This element can have one child: <echo> . The <echo> element will take the binary content of the element whose name matches this element's "name" attribute in each descriptor matched by the <foreach> element.
      • The "unique" attribute of the <foreach> element will ensure that there are no duplicate <echo> values.
      • This is pretty much only used to output the "wLang" attribute of the string descriptors in the 0th string descriptor.

There's a couple other child tags that a descriptor can have, but they aren't part of this code snippet and are meant for facilitating HID report descriptors or more complex descriptors. See usb_hid.c and usb_midi.c for details. You can also read the source and while I consider it somewhat readable, I hacked it together in about 2 days and it definitely shows. There are inconsistencies in the "API" and badly named things (like "<hidden> " which I didn't mention above. I really should have spent more time on that one...I'm not even sure about all the ways it's different from "<property> " reading it now).

To summarize, this descriptor generating script allows me to do some pretty convenient things:

  • I can define a descriptor for an interface in the same file as the source file that handles it.
  • The descriptor moves around with the source, so I can simply copy-paste to another project without needing to make any changes.
  • Adding a descriptor to a project requires no modification of the makefile to get it included. So long as my makefile finds the source, the descriptor gets included.

USB Application Object

This section can be ignored if you're just here for generating descriptors. That is pretty generic and everyone needs to do it. This is more specific to hooking this into my USB driver and ensuring that I can simply copy-paste files around between my projects and they "just work" without needing to modify other source (within reason)

The next step to having something fully portable is to have an easy way to hook into the entire application. In general, my drivers have functions that start with hook_ which are called at certain points. Here are a few examples of hooks that I typically define:

  • hook_usb_handle_setup_request : Called whenever a setup request is received. Passes the setup request as its argument. It is only called when a setup request arrives that can't be processed by the default handler (which only processes SET_ADDRESS and GET_DESCRIPTOR requests).
  • hook_usb_reset : This is called whenever the USB peripheral receives a reset condition.
  • hook_usb_sof : This is called whenever the USB peripheral receives an SOF packet. Useful for periodic events.
  • hook_usb_endpoint_sent : This is called whenever a packet queued for sending on an interface is successfully sent. Passes the endpoint and transmit buffer as arguments.
  • hook_usb_endpoint_received : This is called whenever a packet is fully received from the peripheral. Passes the endpoing and receive buffer as arguments.

These are usually defined like this in the calling module:

 1USBControlResult __attribute__ ((weak)) hook_usb_handle_setup_request(USBSetupPacket const *setup, USBTransferData *nextTransfer)
 2{
 3    return USB_CTL_STALL; //default: Stall on an unhandled request
 4}
 5void __attribute__ ((weak)) hook_usb_control_complete(USBSetupPacket const *setup) { }
 6void __attribute__ ((weak)) hook_usb_reset(void) { }
 7void __attribute__ ((weak)) hook_usb_sof(void) { }
 8void __attribute__ ((weak)) hook_usb_set_configuration(uint16_t configuration) { }
 9void __attribute__ ((weak)) hook_usb_set_interface(uint16_t interface) { }
10void __attribute__ ((weak)) hook_usb_endpoint_setup(uint8_t endpoint, USBSetupPacket const *setup) { }
11void __attribute__ ((weak)) hook_usb_endpoint_received(uint8_t endpoint, void *buf, uint16_t len) { }
12void __attribute__ ((weak)) hook_usb_endpoint_sent(uint8_t endpoint, void *buf, uint16_t len) { }

Application code can then interface to these hooks like so (example from my HID driver):

 1void hook_usb_endpoint_sent(uint8_t endpoint, void *buf, uint16_t len)
 2{
 3    USBTransferData report = { buf, len };
 4    if (endpoint == HID_IN_ENDPOINT)
 5    {
 6        hook_usb_hid_in_report_sent(&report);
 7    }
 8}
 9
10void hook_usb_endpoint_received(uint8_t endpoint, void *buf, uint16_t len)
11{
12    USBTransferData report = { buf, len };
13    if (endpoint == HID_OUT_ENDPOINT)
14    {
15        hook_usb_hid_out_report_received(&report);
16    }
17}

The problem with this is that since the hook_ function can only be defined in a single place, every time I add an interface that needs to know when an endpoint receives a packet I need to modify the function. For composite devices (such as the midi-fader I'm using as an example here), this is really problematic and annoying for porting things between projects.

To remedy this, I created a "usb_app" layer which implements these hook_ functions and then dispatches them to handlers. I define these handlers by way of some structs (which are const, so they get stored in flash rather than RAM):

 1/**
 2 * Structure instantiated by each interface
 3 *
 4 * This is intended to usually be a static constant, but it could also
 5 * be created on the fly.
 6 */
 7typedef struct {
 8    /**
 9     * Hook function called when a USB reset occurs
10     */
11    USBNoParameterHook hook_usb_reset;
12    /**
13     * Hook function called when a setup request is received
14     */
15    USBHandleControlSetupHook hook_usb_handle_setup_request;
16    /**
17     * Hook function called when the status stage of a setup request is
18     * completed on endpoint zero.
19     */
20    USBHandleControlCompleteHook hook_usb_control_complete;
21    /**
22     * Hook function called when a SOF is received
23     */
24    USBNoParameterHook hook_usb_sof;
25    /**
26     * Hook function called when a SET_CONFIGURATION is received
27     */
28    USBSetConfigurationHook hook_usb_set_configuration;
29    /**
30     * Hook function called when a SET_INTERFACE is received
31     */
32    USBSetInterfaceHook hook_usb_set_interface;
33    /**
34     * Hook function called when data is received on a USB endpoint
35     */
36    USBEndpointReceivedHook hook_usb_endpoint_received;
37    /**
38     * Hook function called when data is sent on a USB endpoint
39     */
40    USBEndpointSentHook hook_usb_endpoint_sent;
41} USBInterface;
42
43/**
44 * Node structure for interfaces attached to the USB device
45 */
46typedef struct USBInterfaceListNode {
47    const USBInterface *interface;
48    const struct USBInterfaceListNode *next;
49} USBInterfaceListNode;
50
51typedef struct {
52    /**
53     * Hook function called when the USB peripheral is reset
54     */
55    USBNoParameterHook hook_usb_reset;
56    /**
57     * Hook function called when a SOF is received.
58     */
59    USBNoParameterHook hook_usb_sof;
60    /**
61     * Head of the interface list. This node will be visited first
62     */
63    const USBInterfaceListNode *interface_list;
64} USBApplicationSetup;
65
66/**
67 * USB setup constant
68 *
69 * Define this elsewhere, such as main
70 */
71extern const USBApplicationSetup *usb_app_setup;

Every module that has a USB descriptor and some interface can then declare an extern const USBInterface in its header. The application using the module can then just attach it to the usb_app_setup for the project. For example, my HID interface declares this in its header:

1/**
2 * USB interface object for the app
3 */
4extern const USBInterface hid_interface;

And then in my main.c, I link it (along with any other interfaces) into the rest of my application like so (using the usb_app framework):

 1static const USBInterfaceListNode midi_interface_node = {
 2    .interface = &midi_interface,
 3    .next = NULL,
 4};
 5
 6static const USBInterfaceListNode hid_interface_node = {
 7    .interface = &hid_interface, //this comes from usb_hid.h
 8    .next = &midi_interface_node,
 9};
10
11const USBApplicationSetup setup = {
12    .interface_list = &hid_interface_node,
13};
14
15const USBApplicationSetup *usb_app_setup = &setup;

Meanwhile, in my usb_hid.c I have defined hid_interface to look like this (all the referenced functions are also pretty short, but I haven't included them for brevity). If a hook is unused, I just leave it null:

1const USBInterface hid_interface = {
2    .hook_usb_handle_setup_request = &hid_usb_handle_setup_request,
3    .hook_usb_set_configuration = &hid_usb_set_configuration,
4    .hook_usb_endpoint_sent = &hid_usb_endpoint_sent,
5    .hook_usb_endpoint_received = &hid_usb_endpoint_received,
6};

Aside from the runtime overhead of now needing to walk a linked list to handle hooks, I now have a pretty low-resource method for making my modules portable. I can now take my self-contained module C file and header, drop them into a project (simply dropping them in tends to make the descriptor be generated), and then hook them up in main.c to the usb_app_setup object. Nice and easy.

Conclusion

I've presented here a couple code structure methods for making more portable embedded applications that use USB device desriptors (and their associated interface). My objective when I originally wrote these was to make it easier on myself when I wanted to build a project atop progress I had made on another project (since my home projects tend to go unfinished after they've achieved their goals for what I wanted to learn).

I expect the most useful thing here for others is probably the USB device descriptor generation, but perhaps my usb_app architecture can inspire someone to make an even better method for writing maintainable embedded code that has low runtime overhead.


arm-programming c hardware python stm32 usb usb-descriptor