Electronics, Embedded Systems, and Software are my breakfast, lunch, and dinner.
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>`__
I have continued to write my descriptors using the "Teensy method" for a few reasons:
Writing descriptors like this has some problems, however:
So, I decided to improve this a bit with some scripting. Here were my goals:
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.
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:
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:
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:
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:
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:
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.
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.