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

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

Teensy 3.1 bare metal: Writing a USB driver

One of the things that has intrigued me for the past couple years is making embedded USB devices. It's an industry standard bus that just about any piece of computing hardware can connect with yet is complex enough that doing it yourself is a bit of a chore.

Traditionally I have used the work of others, mainly the V-USB driver for AVR, to get my devices connected. Lately I have been messing around more with the ARM processor on a Teensy 3.1 which has an integrated USB module. The last microcontrollers I used that had these were the PIC18F4550s that I used in my dot matrix project. Even with those, I used microchip's library and drivers.

Over the thanksgiving break I started cobbling together some software with the intent of writing a driver for the USB module in the Teensy myself. I started originally with my bare metal stuff, but I ended up going with something closer to Karl Lunt's solution. I configured code::blocks to use the arm-none-eabi compiler that I had installed and created a code blocks project for my code and used that to build it (with a post-compile event translating the generated elf file into a hex file).

This is a work in progress and the git repository will be updated as things progress since it's not a dedicated demonstration of the USB driver.

The github repository here will be eventually turned in to a really really rudimentary 500-800ksps oscilloscope.

The code:  https://github.com/kcuzner/teensy-oscilloscope

The code for this post was taken from the following commit:

https://github.com/kcuzner/teensy-oscilloscope/tree/9a5a4c9108717cfec0174709a72edeab93fcf2b8

At the end of this post, I will have outlined all of the pieces needed to have a simple USB device setup that responds with a descriptor on endpoint 0.

USB Basics

I will actually not be talking about these here as I am most definitely no expert. However, I will point to the page that I found most helpful when writing this: http://www.usbmadesimple.co.uk/index.html

This site explained very clearly exactly what was going on with USB. Coupled with my previous knowledge, it was almost all I needed in terms of getting the protocol.

The Freescale K20 Family and their USB module

The one thing that I don't like about all of these great microcontrollers that come out with USB support is that all of them have their very own special USB module which doesn't work like anyone else. Sure, there are similarities, but there are no two exactly alike. Since I have a Teensy and the K20 family of microcontrollers seem to be relatively popular, I don't feel bad about writing such specific software.

There are two documents I found to be essential to writing this driver:

  1. The family manual. Getting a correct version for the MK20DX256VLH7 (the processor on the Teensy) can be a pain. PJRC comes to the rescue here: http://www.pjrc.com/teensy/K20P64M72SF1RM.pdf (note, the Teensies based on the MK20DX128VLH5 use a different manual)
  2. The Kinetis Peripheral Module Quick Reference: http://cache.freescale.com/files/32bit/doc/quick_ref_guide/KQRUG.pdf. This specifies the initialization sequence and other things that will be needed for the module.

There are a few essential parts to understand about the USB module:

  • It needs a specific memory layout. Since it doesn't have any dedicated user-accessible memory, it requires that the user specify where things should be. There are specific valid locations for its Buffer Descriptor Table (more on that later) and the endpoint buffers. The last one bit me for several days until I figured it out.
  • It has several different clock inputs and all of them must be enabled. Identifying the different signals is the most difficult part. After that, its not hard.
  • The module only handles the electrical aspect of things. It doesn't handle sending descriptors or anything like that. The only real things it handles are the signaling levels, responding to USB packets in a valid manner, and routing data into buffers by endpoint. Other than that, its all user software.
  • The module can act as both a host (USB On-the-go (OTG)) and a device. We will be exclusively focusing on using it as a device here.

In writing this, I must confess that I looked quite a lot at the Teensyduino code along with the V-USB driver code (even though V-USB is for AVR and is pure software). Without these "references", this would have been a very difficult project. Much of the structure found in the last to parts of this document reflects the Teensyduino USB driver since they did it quite efficiently and I didn't spend a lot of time coming up with a "better" way to do it, given the scope of this project. I will likely make more changes as I customize it for my end use-case.

Part 1: The clocks

The K20 family of microcontrollers utilizes a miraculous hardware module which they call the "Multipurpose Clock Generator" (hereafter called the MCG). This is a module which basically allows the microcontroller to take any clock input between a few kilohertz and several megahertz and transform it into a higher frequency clock source that the microcontroller can actually use. This is how the Teensy can have a rated speed of 96Mhz but only use a 16Mhz crystal. The configuration that this project uses is the Phase Locked Loop (PLL) from the high speed crystal source. The exact setup of this configuration is done by the sysinit code.

The PLL operates by using a divider-multiplier setup where we give it a divisor to divide the input clock frequency by and then a multiplier to multiply that result by to give us the final clock speed. After that, it heads into the System Integration Module (SIM) which distributes the clock. Since the Teensy uses a 16Mhz crystal and we need a 96Mhz system clock (the reason will become apparent shortly), we set our divisor to 4 and our multiplier to 24 (see common.h). If the other type of Teensy 3 is being used (the one with the MK20DX128VLH5), the divisor would be 8 and the multiplier 36 to give us 72Mhz.

Every module on a K20 microcontroller has a gate on its clock. This saves power since there are many modules on the microcontroller that are not being used in any given application. Distributing the clock to each of these is expensive in terms of power and would be wasted if that module wasn't used. The SIM handles this gating in the SIM_SCGC* registers. Before using any module, its clock gate must be enabled. If this is not done, the microcontroller will "crash" and stop executing when it tries to talk to the module registers (I think a handler for this can be specified, but I'm not sure). I had this happen once or twice while messing with this. So, the first step is to "turn on" the USB module by setting the appropriate bit in SIM_SCGC4 (per the family manual mentioned above, page 252):

1SIM_SCGC4 |= SIM_SCGC4_USBOTG_MASK;

Now, the USB module is a bit different than the other modules. In addition to the module clock it needs a reference clock for USB. The USB module requires that this reference clock be at 48Mhz. There are two sources for this clock: an internal source generated by the MCG/SIM or an external source from a pin. We will use the internal source:

1SIM_SOPT2 |= SIM_SOPT2_USBSRC_MASK | SIM_SOPT2_PLLFLLSEL_MASK;
2SIM_CLKDIV2 = SIM_CLKDIV2_USBDIV(1);

The first line here selects that the USB reference clock will come from an internal source. It also specifies that the internal source will be using the output from the PLL in the MCG (the other option is the FLL (frequency lock loop), which we are not using). The second line sets the divider needed to give us 48Mhz from the PLL clock. Once again there are two values: The divider and the multiplier. The multiplier can only be 1 or 2 and the divider can be anywhere from 1 to 16. Since we have a 96Mhz clock, we simply divide by 2 (the value passed is a 1 since 0 = "divide by 1", 1 = "divide by 2", etc). If we were using the 72Mhz clock, we would first multiply by 2 before dividing by 3.

With that, the clock to the USB module has been activated and the module can now be initialized.

Part 2: The startup sequence

The Peripheral Module Quick Reference guide mentioned earlier contains a flowchart which outlines the exact sequence needed to initialize the USB module to act as a device. I don't know if I can copy it here (yay copyright!), but it can be found on page 134, figure 15-6. There is another flowchart specifying the initialization sequence for using the module as a host.

Our startup sequence goes as follows:

 1//1: Select clock source
 2SIM_SOPT2 |= SIM_SOPT2_USBSRC_MASK | SIM_SOPT2_PLLFLLSEL_MASK; //we use MCGPLLCLK divided by USB fractional divider
 3SIM_CLKDIV2 = SIM_CLKDIV2_USBDIV(1); //(USBFRAC + 0)/(USBDIV + 1) = (1 + 0)/(1 + 1) = 1/2 for 96Mhz clock
 4
 5//2: Gate USB clock
 6SIM_SCGC4 |= SIM_SCGC4_USBOTG_MASK;
 7
 8//3: Software USB module reset
 9USB0_USBTRC0 |= USB_USBTRC0_USBRESET_MASK;
10while (USB0_USBTRC0 & USB_USBTRC0_USBRESET_MASK);
11
12//4: Set BDT base registers
13USB0_BDTPAGE1 = ((uint32_t)table) >> 8;  //bits 15-9
14USB0_BDTPAGE2 = ((uint32_t)table) >> 16; //bits 23-16
15USB0_BDTPAGE3 = ((uint32_t)table) >> 24; //bits 31-24
16
17//5: Clear all ISR flags and enable weak pull downs
18USB0_ISTAT = 0xFF;
19USB0_ERRSTAT = 0xFF;
20USB0_OTGISTAT = 0xFF;
21USB0_USBTRC0 |= 0x40; //a hint was given that this is an undocumented interrupt bit
22
23//6: Enable USB reset interrupt
24USB0_CTL = USB_CTL_USBENSOFEN_MASK;
25USB0_USBCTRL = 0;
26
27USB0_INTEN |= USB_INTEN_USBRSTEN_MASK;
28//NVIC_SET_PRIORITY(IRQ(INT_USB0), 112);
29enable_irq(IRQ(INT_USB0));
30
31//7: Enable pull-up resistor on D+ (Full speed, 12Mbit/s)
32USB0_CONTROL = USB_CONTROL_DPPULLUPNONOTG_MASK;

The first two steps were covered in the last section. The next one is relatively straightfoward: We ask the module to perform a "reset" on itself. This places the module to its initial state which allows us to configure it as needed. I don't know if the while loop is necessary since the manual says that the reset bit always reads low and it only says we must "wait two USB clock cycles". In any case, enough of a wait seems to be executed by the above code to allow it to reset properly.

The next section (4: Set BDT base registers) requires some explanation. Since the USB module doesn't have a dedicated memory block, we have to provide it. The BDT is the "Buffer Descriptor Table" and contains 16 * 4 entries that look like so:

1typedef struct {
2    uint32_t desc;
3    void* addr;
4} bdt_t;

"desc" is a descriptor for the buffer and "addr" is the address of the buffer. The exact bits of the "desc" are explained in the manual (p. 971, Table 41-4), but they basically specify ownership of the buffer (user program or USB module) and the USB token that generated the data in the buffer (if applicable).

Each entry in the BDT corresponds to one of 4 buffers in one of the 16 USB endpoints: The RX even, RX odd, TX even, and TX odd. The RX and TX are pretty self explanatory...the module needs somewhere to read the data its going to send and somewhere to write the data it just received. The even and odd are a configuration that I have seen before in the PIC 18F4550 USB module: Ping-pong buffers. While one buffer is being sent/received by the module, the other can be in use by user code reading/writing (ping). When the user code is done with its buffers, it swaps buffers, giving the usb module control over the ones it was just using (pong). This allows seamless communication between the host and the device and minimizes the need for copying data between buffers. I have declared the BDT in my code as follows:

1#define BDT_INDEX(endpoint, tx, odd) ((endpoint << 2) | (tx << 1) | odd)
2__attribute__ ((section(".usbdescriptortable"), used))
3static bdt_t table[(USB_N_ENDPOINTS + 1)*4]; //max endpoints is 15 + 1 control

One caveat of the BDT is that it must be aligned with a 512-byte boundary in memory. Our code above showed that only 3 bytes of the 4 byte address of "table" are passed to the module. This is because the last byte is basically the index along the table (the specification of this is found in section 41.4.3, page 970 of the manual). The #define directly above the declaration is a helper macro for referencing entries in the table for specific endpoints (this is used later in the interrupt). Now, accomplishing this boundary alignment requires some modification of the linker script. Before this, I had never had any need to modify a linker script. We basically need to create a special area of memory (in the above, it is called ".usbdescriptortable" and the attribute declaration tells the compiler to place that variable's reference inside of it) which is aligned to a 512-byte boundary in RAM. I declared mine like so:

1.usbdescriptortable (NOLOAD) : {
2     . = ALIGN(512);
3     *(.usbdescriptortable*)
4} > sram

The position of this in the file is mildly important, so looking at the full linker script would probably be good. This particular declaration I more or less lifted from the Teensyduino linker script, with some changes to make it fit into my linker script.

Steps 5-6 set up the interrupts. There is only one USB interrupt, but there are two registers of flags. We first reset all of the flags. Interestingly, to reset a flag we write back a '1' to the particular flag bit. This has the effect of being able to set a flag register to itself to reset all of the flags since a flag bit is '1' when it is triggered. After resetting the flags, we enable the interrupt in the NVIC (Nested Vector Interrupt Controller). I won't discuss the NVIC much, but it is a fairly complex piece of hardware. It has support for lots and lots of interrupts (over 100) and separate priorities for each one. I don't have reliable code for setting interrupt priorities yet, but eventually I'll get around to messing with that. The "enable_irq()" call is a function that is provided in arm_cm4.c and all that it does is enable the interrupt specified by the passed vector number. These numbers are specified in the datasheet, but we have a #define specified in the mk20d7 header file (warning! 12000 lines ahead) which gives us the number.

The very last step in initialization is to set the internal pullup on D+. According to the USB specification, a pullup on D- specifies a low speed device (1.2Mbit/s) and a pullup on D+ specifies a full speed device (12Mbit/s). We want to use the higher speed grade. The Kinetis USB module does not support high speed (480Mbit/s) mode.

Part 3: The interrupt handler state machine

The USB protocol can be interpreted in the context of a state machine with each call to the interrupt being a "tick" in the machine. The interrupt handler must process all of the flags to determine what happened and where to go from there.

 1#define ENDP0_SIZE 64
 2
 3/**
 4 * Endpoint 0 receive buffers (2x64 bytes)
 5 */
 6static uint8_t endp0_rx[2][ENDP0_SIZE];
 7
 8//flags for endpoint 0 transmit buffers
 9static uint8_t endp0_odd, endp0_data = 0;
10
11/**
12 * Handler functions for when a token completes
13 * TODO: Determine if this structure really will work for all kinds of handlers
14 *
15 * I hope this looks like a dynamic jump table to the compiler
16 */
17static void (*handlers[USB_N_ENDPOINTS + 2]) (uint8_t);
18
19void USBOTG_IRQHandler(void)
20{
21    uint8_t status;
22    uint8_t stat, endpoint;
23
24    status = USB0_ISTAT;
25
26    if (status & USB_ISTAT_USBRST_MASK)
27    {
28        //handle USB reset
29
30        //initialize endpoint 0 ping-pong buffers
31        USB0_CTL |= USB_CTL_ODDRST_MASK;
32        endp0_odd = 0;
33        table[BDT_INDEX(0, RX, EVEN)].desc = BDT_DESC(ENDP0_SIZE, 0);
34        table[BDT_INDEX(0, RX, EVEN)].addr = endp0_rx[0];
35        table[BDT_INDEX(0, RX, ODD)].desc = BDT_DESC(ENDP0_SIZE, 0);
36        table[BDT_INDEX(0, RX, ODD)].addr = endp0_rx[1];
37        table[BDT_INDEX(0, TX, EVEN)].desc = 0;
38        table[BDT_INDEX(0, TX, ODD)].desc = 0;
39
40        //initialize endpoint0 to 0x0d (41.5.23)
41        //transmit, recieve, and handshake
42        USB0_ENDPT0 = USB_ENDPT_EPRXEN_MASK | USB_ENDPT_EPTXEN_MASK | USB_ENDPT_EPHSHK_MASK;
43
44        //clear all interrupts...this is a reset
45        USB0_ERRSTAT = 0xff;
46        USB0_ISTAT = 0xff;
47
48        //after reset, we are address 0, per USB spec
49        USB0_ADDR = 0;
50
51        //all necessary interrupts are now active
52        USB0_ERREN = 0xFF;
53        USB0_INTEN = USB_INTEN_USBRSTEN_MASK | USB_INTEN_ERROREN_MASK |
54            USB_INTEN_SOFTOKEN_MASK | USB_INTEN_TOKDNEEN_MASK |
55            USB_INTEN_SLEEPEN_MASK | USB_INTEN_STALLEN_MASK;
56
57        return;
58    }
59    if (status & USB_ISTAT_ERROR_MASK)
60    {
61        //handle error
62        USB0_ERRSTAT = USB0_ERRSTAT;
63        USB0_ISTAT = USB_ISTAT_ERROR_MASK;
64    }
65    if (status & USB_ISTAT_SOFTOK_MASK)
66    {
67        //handle start of frame token
68        USB0_ISTAT = USB_ISTAT_SOFTOK_MASK;
69    }
70    if (status & USB_ISTAT_TOKDNE_MASK)
71    {
72        //handle completion of current token being processed
73        stat = USB0_STAT;
74        endpoint = stat >> 4;
75        handlers[endpoint](stat);
76
77        USB0_ISTAT = USB_ISTAT_TOKDNE_MASK;
78    }
79    if (status & USB_ISTAT_SLEEP_MASK)
80    {
81        //handle USB sleep
82        USB0_ISTAT = USB_ISTAT_SLEEP_MASK;
83    }
84    if (status & USB_ISTAT_STALL_MASK)
85    {
86        //handle usb stall
87        USB0_ISTAT = USB_ISTAT_STALL_MASK;
88    }
89}

The above code will be executed whenever the IRQ for the USB module fires. This function is set up in the crt0.S file, but with a weak reference, allowing us to override it easily by simply defining a function called USBOTG_IRQHandler. We then proceed to handle all of the USB interrupt flags. If we don't handle all of the flags, the interrupt will execute again, giving us the opportunity to fully process all of them.

Reading through the code is should be obvious that I have not done much with many of the flags, including USB sleep, errors, and stall. For the purposes of this super simple driver, we really only care about USB resets and USB token decoding.

The very first interrupt that we care about which will be called when we connect the USB device to a host is the Reset. The host performs this by bringing both data lines low for a certain period of time (read the USB basics stuff for more information). When we do this, we need to reset our USB state into its initial and ready state. We do a couple things in sequence:

  1. Initialize the buffers for endpoint 0. We set the RX buffers to point to some static variables we have defined which are simply uint8_t arrays of length "ENDP0_SIZE". The TX buffers are reset to null since nothing is going to be transmitted. One thing to note is that the ODDRST bit is flipped on in the USB0_CTL register. This is very important since it "syncronizes" the USB module with our code in terms of knowing whether the even or odd buffer should be used next for transmitting. When we do ODDRST, it sets the next buffer to be used to be the even buffer. We have a "user-space" flag (endp0_odd) which we reset at the same time so that we stay in sync with the buffer that the USB module is going to use.
  2. We enable endpoint 0. Specifically, we say that it can transmit, receive, and handshake. Enabled endpoints always handshake, but endpoints can either send, receive, or both. Endpoint 0 is specified as a reading and writing endpoint in the USB specification. All of the other endpoints are device-specific.
  3. We clear all of the interrupts. If this is a reset we obviously won't be doing much else.
  4. Set our USB address to 0. Each device on the USB bus gets an address between 0 and 127. Endpoint 0 is reserved for devices that haven't been assigned an address yet (i.e. have been reset), so that becomes our address. We will receive an address later via a command sent to endpoint 0.
  5. Activate all necessary interrupts. In the previous part where we discussed the initialization sequence we only enabled the reset interrupt. After being reset, we get to enable all of the interrupts that we will need to be able to process USB events.

After a reset the USB module will begin decoding tokens. While there are a couple different types of tokens, the USB module has a single interrupt for all of them. When a token is decoded the module gives us information about what endpoint the token was for and what BDT entry should be used. This information is contained in the USB0_STAT register.

The exact method for processing these tokens is up to the individual developer. My choice for the moment was to make a dynamic jump table of sorts which stores 16 function pointers which will be called in order to process the tokens. Initially, these pointers point to dummy functions that do nothing. The code for the endpoint 0 handler will be discussed in the next section.

Our code here uses USB0_STAT to determine which endpoint the token was decoded for, finds the appropriate function pointer, and calls it with the value of USB0_STAT.

Part 4: Token processing & descriptors

This is one part of the driver that isn't something that must be done a certain way, but however it is done, it must accomplish the task correctly. My super-simple driver processes this in two stages: Processing the token type and processing the token itself.

As mentioned in the previous section, I had a handler for each endpoint that would be called after a token was decoded. The handler for endpoint 0 is as follows:

 1#define PID_OUT   0x1
 2#define PID_IN    0x9
 3#define PID_SOF   0x5
 4#define PID_SETUP 0xd
 5
 6typedef struct {
 7    union {
 8        struct {
 9            uint8_t bmRequestType;
10            uint8_t bRequest;
11        };
12        uint16_t wRequestAndType;
13    };
14    uint16_t wValue;
15    uint16_t wIndex;
16    uint16_t wLength;
17} setup_t;
18
19/**
20 * Endpoint 0 handler
21 */
22static void usb_endp0_handler(uint8_t stat)
23{
24    static setup_t last_setup;
25
26    //determine which bdt we are looking at here
27    bdt_t* bdt = &table[BDT_INDEX(0, (stat & USB_STAT_TX_MASK) >> USB_STAT_TX_SHIFT, (stat & USB_STAT_ODD_MASK) >> USB_STAT_ODD_SHIFT)];
28
29    switch (BDT_PID(bdt->desc))
30    {
31    case PID_SETUP:
32        //extract the setup token
33        last_setup = *((setup_t*)(bdt->addr));
34
35        //we are now done with the buffer
36        bdt->desc = BDT_DESC(ENDP0_SIZE, 1);
37
38        //clear any pending IN stuff
39        table[BDT_INDEX(0, TX, EVEN)].desc = 0;
40        table[BDT_INDEX(0, TX, ODD)].desc = 0;
41        endp0_data = 1;
42
43        //run the setup
44        usb_endp0_handle_setup(&last_setup);
45
46        //unfreeze this endpoint
47        USB0_CTL = USB_CTL_USBENSOFEN_MASK;
48        break;
49    case PID_IN:
50        if (last_setup.wRequestAndType == 0x0500)
51        {
52            USB0_ADDR = last_setup.wValue;
53        }
54        break;
55    case PID_OUT:
56        //nothing to do here..just give the buffer back
57        bdt->desc = BDT_DESC(ENDP0_SIZE, 1);
58        break;
59    case PID_SOF:
60        break;
61    }
62
63    USB0_CTL = USB_CTL_USBENSOFEN_MASK;
64}

The very first step in handling a token is determining the buffer which contains the data for the token transmitted. This is done by the first statement which finds the appropriate address for the buffer in the table using the BDT_INDEX macro which simply implements the addressing form found in Figure 41-3 in the family manual.

After determining where the data received is located, we need to determine which token exactly was decoded. We only do things with four of the tokens. Right now, if a token comes through that we don't understand, we don't really do anything. My thought is that I should be initiating an endpoint stall, but I haven't seen anywhere that specifies what exactly I should do for an unrecognized token.

The main token that we care about with endpoint 0 is the SETUP token. The data attached to this token will be in the format described by setup_t, so the first step is that we dereference and cast the buffer into which the data was loaded into a setup_t. This token will be stored statically since we need to look at it again for tokens that follow, especially in the case of the IN token following the request to be assigned an address.

One part of processing a setup token that tripped me up for a while was what the next DATA state should be. The USB standard specifies that the data in a frame is either marked DATA0 or DATA1 and it alternates by frame. This information is stored in a flag that the USB module will read from the first 4 bytes of the BDT (the "desc" field). Immediately following a SETUP token, the next DATA transmitted must be a DATA1.

After this, the setup function is run (more on that next) and as a final step, the USB module is "unfrozen". Whenever a token is being processed, the USB module "freezes" so that processing can occur. While I haven't yet read enough documentation on the subject, it seems to me that this is to give the user program some time to actually handle a token before the USB module decodes another one. I'm not sure what happens if the user program takes to long, but I imagine some error flag will go off.

The guts of handling a SETUP request are as follows:

  1typedef struct {
  2    uint8_t bLength;
  3    uint8_t bDescriptorType;
  4    uint16_t bcdUSB;
  5    uint8_t bDeviceClass;
  6    uint8_t bDeviceSubClass;
  7    uint8_t bDeviceProtocol;
  8    uint8_t bMaxPacketSize0;
  9    uint16_t idVendor;
 10    uint16_t idProduct;
 11    uint16_t bcdDevice;
 12    uint8_t iManufacturer;
 13    uint8_t iProduct;
 14    uint8_t iSerialNumber;
 15    uint8_t bNumConfigurations;
 16} dev_descriptor_t;
 17
 18typedef struct {
 19    uint8_t bLength;
 20    uint8_t bDescriptorType;
 21    uint8_t bInterfaceNumber;
 22    uint8_t bAlternateSetting;
 23    uint8_t bNumEndpoints;
 24    uint8_t bInterfaceClass;
 25    uint8_t bInterfaceSubClass;
 26    uint8_t bInterfaceProtocol;
 27    uint8_t iInterface;
 28} int_descriptor_t;
 29
 30typedef struct {
 31    uint8_t bLength;
 32    uint8_t bDescriptorType;
 33    uint16_t wTotalLength;
 34    uint8_t bNumInterfaces;
 35    uint8_t bConfigurationValue;
 36    uint8_t iConfiguration;
 37    uint8_t bmAttributes;
 38    uint8_t bMaxPower;
 39    int_descriptor_t interfaces[];
 40} cfg_descriptor_t;
 41
 42typedef struct {
 43    uint16_t wValue;
 44    uint16_t wIndex;
 45    const void* addr;
 46    uint8_t length;
 47} descriptor_entry_t;
 48
 49/**
 50 * Device descriptor
 51 * NOTE: This cannot be const because without additional attributes, it will
 52 * not be placed in a part of memory that the usb subsystem can access. I
 53 * have a suspicion that this location is somewhere in flash, but not copied
 54 * to RAM.
 55 */
 56static dev_descriptor_t dev_descriptor = {
 57    .bLength = 18,
 58    .bDescriptorType = 1,
 59    .bcdUSB = 0x0200,
 60    .bDeviceClass = 0xff,
 61    .bDeviceSubClass = 0x0,
 62    .bDeviceProtocol = 0x0,
 63    .bMaxPacketSize0 = ENDP0_SIZE,
 64    .idVendor = 0x16c0, //VOTI VID/PID for use with libusb
 65    .idProduct = 0x05dc,
 66    .bcdDevice = 0x0001,
 67    .iManufacturer = 0,
 68    .iProduct = 0,
 69    .iSerialNumber = 0,
 70    .bNumConfigurations = 1
 71};
 72
 73/**
 74 * Configuration descriptor
 75 * NOTE: Same thing about const applies here
 76 */
 77static cfg_descriptor_t cfg_descriptor = {
 78    .bLength = 9,
 79    .bDescriptorType = 2,
 80    .wTotalLength = 18,
 81    .bNumInterfaces = 1,
 82    .bConfigurationValue = 1,
 83    .iConfiguration = 0,
 84    .bmAttributes = 0x80,
 85    .bMaxPower = 250,
 86    .interfaces = {
 87        {
 88            .bLength = 9,
 89            .bDescriptorType = 4,
 90            .bInterfaceNumber = 0,
 91            .bAlternateSetting = 0,
 92            .bNumEndpoints = 0,
 93            .bInterfaceClass = 0xff,
 94            .bInterfaceSubClass = 0x0,
 95            .bInterfaceProtocol = 0x0,
 96            .iInterface = 0
 97        }
 98    }
 99};
100
101static const descriptor_entry_t descriptors[] = {
102    { 0x0100, 0x0000, &dev_descriptor, sizeof(dev_descriptor) },
103    { 0x0200, 0x0000, &cfg_descriptor, 18 },
104    { 0x0000, 0x0000, NULL, 0 }
105};
106
107static void usb_endp0_transmit(const void* data, uint8_t length)
108{
109    table[BDT_INDEX(0, TX, endp0_odd)].addr = (void *)data;
110    table[BDT_INDEX(0, TX, endp0_odd)].desc = BDT_DESC(length, endp0_data);
111    //toggle the odd and data bits
112    endp0_odd ^= 1;
113    endp0_data ^= 1;
114}
115
116/**
117 * Endpoint 0 setup handler
118 */
119static void usb_endp0_handle_setup(setup_t* packet)
120{
121    const descriptor_entry_t* entry;
122    const uint8_t* data = NULL;
123    uint8_t data_length = 0;
124
125
126    switch(packet->wRequestAndType)
127    {
128    case 0x0500: //set address (wait for IN packet)
129        break;
130    case 0x0900: //set configuration
131        //we only have one configuration at this time
132        break;
133    case 0x0680: //get descriptor
134    case 0x0681:
135        for (entry = descriptors; 1; entry++)
136        {
137            if (entry->addr == NULL)
138                break;
139
140            if (packet->wValue == entry->wValue && packet->wIndex == entry->wIndex)
141            {
142                //this is the descriptor to send
143                data = entry->addr;
144                data_length = entry->length;
145                goto send;
146            }
147        }
148        goto stall;
149        break;
150    default:
151        goto stall;
152    }
153
154    //if we are sent here, we need to send some data
155    send:
156        if (data_length > packet->wLength)
157            data_length = packet->wLength;
158        usb_endp0_transmit(data, data_length);
159        return;
160
161    //if we make it here, we are not able to send data and have stalled
162    stall:
163        USB0_ENDPT0 = USB_ENDPT_EPSTALL_MASK | USB_ENDPT_EPRXEN_MASK | USB_ENDPT_EPTXEN_MASK | USB_ENDPT_EPHSHK_MASK;
164}

This is the part that took me the longest once I managed to get the module talking. Handling of SETUP tokens on endpoint 0 must be done in a rather exact fashion and the slightest mistake gives some very cryptic errors.

This is a very very very minimalistic setup token handler and is not by any means complete. It does only what is necessary to get the computer to see the device successfully read its descriptors. There is no functionality for actually doing things with the USB device. Most of the space is devoted to actually returning the various descriptors. In this example, the descriptor is for a device with a single configuration and a single interface which uses no additional endpoints. In a real device, this would almost certainly not be the case (unless one uses V-USB...this is how V-USB sets up their device if no other endpoints are compiled in).

The SETUP packet comes with a "request" and a "type". We process these as one word for simplicity. The above shows only the necessary commands to actually get this thing to connect to a Linux machine running the standard USB drivers that come with the kernel. I have not tested it on Windows and it may require some modification to work since it doesn't implement all of the necessary functionality. A description of the functionality follows:

  • Set address (0x0500): This is a very simple command. All it does is wait for the next IN token. Upon receipt of this token, the address is considered "committed" and the USB module is told of its new address (see the endpoint 0 handler function above (not the setup handler)).
  • Set configuration (0x0900): This command can be complex, but I have stripped it down for the purposes of this example. Normally, during this command the USB module would be set up with all the requisite BDT entries for the endpoints described by the selected configuration. Since we only have one possible configuration and it doesn't use any additional endpoints, we basically do nothing. Once I start added other endpoints to this, all of the setup for those endpoints will go in here. This is the equivalent of the RESET handler for non-zero endpoints in terms of the operations that occur. If the Set Interface command was implemented, it would have similar functionality. More about this command can be read in the referenced USB basics website.
  • Get descriptor (0x0680, 0x0681): In reality, this is two commands: Get descriptor and get interface. However, due to the structure we have chosen in storing the descriptors, these two commands can be merged. This is the most complex part of this particular driver and is influenced heavily by the way things are done with the Teensyduino driver since I thought they had a very efficient pattern. Basically, it uses the wIndex and wValue to find a pointer to some data to return, whether that be the device descriptor, the configuration descriptor, a string, or something else. In our case, we have only the device descriptor and the configuration descriptor. Adding a string would be trivial, however, and the exact wIndex and wValue combination for that is described in the USB basics. The wIndex for strings matches with any of the several i* (iManufacturer, iProduct, etc) which may be specified.
  • default: When an unrecognized command is received, we enter a stall. This is basically the USB way of saying "uhh...I don't know what to do here" and requires the host to un-stall the endpoint before it can continue. From what I gather, there isn't really much the user code has to do other than declare that a stall has occurred. The USB module seems to take care of the rest of that.

After handling a command and determining that it isn't a stall, the transmission is set up. At the moment, I only have transmission set up for a maximum of 64 bytes. In reality, this is limited by the wLength transmitted with the setup packet (note the if statement before the call to usb_endp0_transmit), but as far as I have seen this is generally the same as the length of the endpoint (I could be very wrong here...so watch out for that one). However, it would be fairly straightfoward to allow it to transmit more bytes: Upon receipt of an IN token, just check if we have reached the end of what we are supposed to transmit. If not, point the next TX buffer to the correct starting point and subtract the endpoint size from the remaining length until we have transmitted all of the bytes. Although the endpoint size is 64 bytes, it is easy to transmit much more than that; it just takes multiple IN requests. The data length is given by the descriptors, so the host can determine when to stop sending IN requests.

During transmission, both the even and data flags are toggled. This ensures that we are always using the correct TX buffer (even/odd) and the DATA flag transmitted is valid.

The descriptors are the one part that can't really be screwed up here. Screwing up the descriptors causes interesting errors when the host tries to communicate. I did not like how the "reference" usb drivers I looked at generally defined descriptors: They used a char array. This works very well for the case where there are a variable number of entries in the descriptor, but for my purposes I decided to use named structs so that I could match the values I had specified on my device to values I read from the host machine without resorting to counting bytes in the array. It's simply for easier reading and doesn't really give much more than that. It may even be more error prone because I am relying on the compiler packing the struct into memory in the correct order for transmission and in later versions I may end up using the char array method.

I won't delve into a long and drawn out description of what the USB descriptor has in it, but I will give a few points:

  • In Linux, the device descriptor is requested first and then the configuration descriptor after that. They are two separate commands, hence the two separate descriptor entries in my descriptor table.
  • The device descriptor must NOT be "const". For my compiler at least, this causes it to be placed into flash which, while a perfectly valid memory address that in general can be read, is inaccessible to the USB module. I spent a long time banging my head on this one saying "but it should work! why doesn't it work???" Moral of the story: Anything that is pointed to by a BDT entry (transmit buffers, receive buffers) must be located in main RAM, not in the flash. It must not be const.
  • A device must have at least one configuration. Linux, at least, didn't seem to like it very much when there were zero configurations and would put lots of errors into my log.
  • The configuration needs to have at least one interface. Specifying no interfaces caused the same problems as not specifying any configurations.
  • The configuration indices (bConfigurationValue) are 1-based and the interface indices (bInterfaceNumber) are zero based. I haven't fooled around with these enough to test the veracity of this claim fully, but it was the only configuration that I managed to get things working in.
  • The length values are very important. If these are not correct, the host will have some serious troubles reading the descriptors. I spend a while troubleshooting these. The main one to make sure of is the wTotalLength value in the configuration descriptor. Most of the others are pretty much always going to be the same.

Where to go from here

The driver I have implemented leaves much to be desired. This isn't meant to be a fully featured driver. Instead, its meant to be something of an introduction to getting the USB module to work on the bare metal without the support of some external dependency. A few things that would definitely need to be implemented are:

  • The full set of commands for the endpoint 0 SETUP token processing
  • A more expansive configuration that allows for having some bulk endpoints for sending data. The 64-byte limitation of packet size for endpoint 0 can cause some issues when attempting to actually utilize the full 12Mbit/s bandwidth. The USB protocol does actually add overhead and the less times that a token has to be invoked, the better.
  • Strings in the configuration. Right now, the configuration is essentially "blank" because it uses a shared VID/PID and doesn't specify a manufacturer, product, or serial number. It would be rather hard to identify this device using libusb on a system with multiple devices using that VID/PID combination.
  • Real error handling. Right now, the interrupt basically ignores the errors. In a real application, these would need to be handled.
  • A better structure. I am not a real fan of how I have structured this, but my idea was to make it "expandable" without needing to recompile usb.c every time a change was made. It doesn't achieve that yet, but in future iterations I hope to have a relatively portable usb driver module that I can port to other projects without modification, placing the other device-specific things into another, mimimalistic, file.

Conclusion

I can only hope that this discussion has been helpful. I spent a long time reading documentation, writing code, smashing my keyboard, and figuring things out and I would like to see that someone else could benefit from this. I hope as I learn more about using the modules on my Teensy that I will become more competent in understanding how many of the systems I rely on on a daily basis function.

The code I have included above isn't always complete, so I would definitely recommend actually reading the code in the repository referenced at the beginning of this article.

If there are any mistakes in the above, please let me know in the comments or shoot me an email.

Extreme Attributed Metadata with Autofac

Introduction

If you are anything like me, you love reflection in any programming language. For the last two years or so I have been writing code for work almost exclusively in C# and have found its reflection system to be a pleasure to use. Its simple, can be fast, and can do so much.

I recently started using Autofac at work to help achieve Inversion of Control within our projects. It has honestly been the most life changing C# library (sorry Autofac, jQuery and Knockout still take the cake for "life-changing in all languages") I have ever used and has changed the way I decompose problems when writing programs.

This article will cover some very interesting features of the Autofac Attributed Metadata module. It is a little lengthy, so I have here what will be covered:

  • What is autofac?
  • Attributed Metadata: The Basics
  • The IMetadataProvider interface
  • IMetadataProvider: Making a set of objects
  • IMetadataProvider: Hierarchical Metadata

What is Autofac?

This post assumes that the reader is at least passingly familiar with Autofac. However, I will make a short introduction: Autofac allows you to "compose" your program structure by "registering" components and then "resolving" them at runtime. The idea is that you define an interface for some object that does "something" and create one or more classes that implement that interface, each accomplishing the "something" in their own way. Your parent class, which needs to have one of those objects for doing that "something" will ask the Autofac container to "resolve" the interface. Autofac will give back either one of your implementations or an IEnumerable of all of your implementations (depending on how you ask it to resolve). The "killer feature" of Autofac, IMO, is being able to use constructor arguments to recursively resolve the "dependencies" of an object. If you want an implementation of an interface passed into your object when it is resolved, just put the interface in the constructor arguments and when your object is resolved by Autofac, Autofac will resolve that interface for you and pass it in to your constructor. Now, this article isn't meant to introduce Autofac, so I would definitely recommend reading up on the subject.

Attributed Metadata: The Basics

One of my most favorite features has been Attributed Metadata. Autofac allows Metadata to be included with objects when they are resolved. Metadata allows one to specify some static parameters that are associated with a particular implementation of something registered with the container. This Metadata is normally created during registration of the particular class and, without this module, must be done "manually". The Attributed Metadata module allows one to use custom attributes to specify the Metadata for the class rather than needing to specify it when the class is registered. This is an absurdly powerful feature which allows for doing some pretty interesting things.

For my example I will use a "extendible" letter formatting program that adds some text to the content of a "letter". I define the following interface:

1interface ILetterFormatter
2{
3    string FormatLetter(string content);
4}

This interface is for something that can "format" a letter in some way. For starters, I will define two implementations:

 1class ImpersonalLetterFormatter : ILetterFormatter
 2{
 3    public string MakeLetter(string content)
 4    {
 5        return "To Whom It May Concern:nn" + content;
 6    }
 7}
 8
 9class PersonalLetterFormatter : ILetterFormatter
10{
11    public string MakeLetter(string content)
12    {
13        return "Dear Individual,nn" + content;
14    }
15}

Now, here is a simple program that will use these formatters:

 1class MainClass
 2{
 3    public static void Main (string[] args)
 4    {
 5        var builder = new ContainerBuilder();
 6
 7        //register all ILetterFormatters in this assembly
 8        builder.RegisterAssemblyTypes(typeof(MainClass).Assembly)
 9            .Where(c => c.IsAssignableTo<ILetterFormatter>())
10            .AsImplementedInterfaces();
11
12        var container = builder.Build();
13
14        using (var scope = container.BeginLifetimeScope())
15        {
16            //resolve all formatters
17            IEnumerable<ILetterFormatter> formatters = scope.Resolve<IEnumerable<ILetterFormatter>>();
18
19            //What do we do now??? So many formatters...which is which?
20        }
21    }
22}

Ok, so we have ran into a problem: We have a list of formatters, but we don't know which is which. There are a couple different solutions:

  • Use the "is" test or do a "soft cast" using the "as" operator to a specific type. This is bad because it requires that the resolver know about the specific implementations of the interface (which is what we are trying to avoid)
  • Just choose one based on order. This is bad because the resolution order is just as guaranteed as reflection order in C#...which is not guaranteed at all. We can't be sure they will be resolved in the same order each time.
  • Use metadata at registration time and resolve it with metadata. The issue here is that if we used RegisterAssemblyTyps like above, it makes registration difficult. Also, once we get any sizable number of things registered with metadata, it becomes unmanageable IMO.
  • Use attributed metadata! Example follows...

We define another class:

 1[MetadataAttribute]
 2sealed class LetterFormatterAttribute : Attribute
 3{
 4    public string Name { get; private set; }
 5
 6    public LetterFormatterAttribute(string name)
 7    {
 8        this.Name = name;
 9    }
10}

Marking it with System.ComponetModel.Composition.MetadataAttributeAttribute (no, that's not a typo) will make the Attributed Metadata module place the public properties of the Attribute into the metadata dictionary that is associated with the class at registration time.

We mark the classes as follows:

1[LetterFormatter("Impersonal")]
2class ImpersonalLetterFormatter : ILetterFormatter
3...
4
5[LetterFormatter("Personal")]
6class PersonalLetterFormatter : ILetterFormatter
7...

And then we change the builder to take into account the metadata by asking it to register the Autofac.Extras.Attributed.AttributedMetadataModule. This will cause the Attributed Metadata extensions to scan all of the registered types (past, present, and future) for MetadataAttribute-marked attributes and use the public properties as metadata:

1var builder = new ContainerBuilder();
2
3builder.RegisterModule<AttributedMetadataModule>();
4
5builder.RegisterAssemblyTypes(typeof(MainClass).Assembly)
6    .Where(c => c.IsAssignableTo<ILetterFormatter>())
7    .AsImplementedInterfaces();

Now, when we resolve the ILetterFormatter classes, we can either use Autofac.Features.Meta<TImplementation> or Autofac.Features.Meta<TImplementation, TMetadata>. I'm a personal fan of the "strong" metadata, or the latter. It causes the metadata dictionary to be "forced" into a class rather than just directly accessing the metadata dictionary. This removes any uncertainty about types and such. So, I will create a class that will hold the metadata when the implementations are resolved:

1class LetterMetadata
2{
3    public string Name { get; set; }
4}

It would worthwhile to note that the individual properties must have a value in the metadata dictionary unless the DefaultValue attribute is applied to the property. For example, if I had an integer property called Foo an exception would be thrown when metadata was resolved since I have no corresponding Foo metadata. However, if I put DefaultValue(6) on the Foo property, no exception would be thrown and Foo would be set to 6.

So, we now have the following inside our using statement that controls our scope in the main method:

 1//resolve all formatters
 2IEnumerable<Meta<ILetterFormatter, LetterMetadata>> formatters = scope.Resolve<IEnumerable<Meta<ILetterFormatter, LetterMetadata>>>();
 3
 4//we will ask how the letter should be formatted
 5Console.WriteLine("Formatters:");
 6foreach (var formatter in formatters)
 7{
 8    Console.Write("- ");
 9    Console.WriteLine(formatter.Metadata.Name);
10}
11
12ILetterFormatter chosen = null;
13while (chosen == null)
14{
15    Console.WriteLine("Choose a formatter:");
16    string name = Console.ReadLine();
17    chosen = formatters.Where(f => f.Metadata.Name == name).Select(f => f.Value).FirstOrDefault();
18
19    if (chosen == null)
20        Console.WriteLine(string.Format("Invalid formatter: {0}", name));
21}
22
23//just for kicks, we say the first argument  is our letter, so we format it and output it to the console
24Console.WriteLine(chosen.FormatLetter(args[0]));

The IMetadataProvider Interface

So, in the contrived example above, we were able to identify a class based solely on its metadata rather than doing type checking. What's more, we were able to define the metadata through attributes. However, this is old hat for Autofac. This feature has been around for a while.

When I was at work the other day, I needed to be able to handle putting sets of things into metadata (such as a list of strings). Autofac makes no prohibition on this in its metadata dictionary. The dictionary is of the type IDictionary<string, object>, so it can hold pretty much anything, including arbitrary objects. The problem is that the Attributed Metadata module had no way to do this easily. Attributes can only take certain types as constructor arguments and that seriously places a limit on what sort of things could be put into metadata via attributes easily.

I decided to remedy this and after submitting an idea for autofac via a pull request, having some discussion, changing the exact way to accomplish this goal, and fixing things up, my pull request was merged into autofac which resulted in a new feature: The IMetadataProvider interface. This interface provides a way for metadata attributes to control how exactly they produce metadata. By default, the attribute would just have its properties scanned. However, if the attribute implemented the IMetadataProvider interface, a method will be called to get the metadata dictionary rather than doing the property scan. When an IMetadataProvider is found, the GetMetadata(Type targetType) method will be called with the first argument set to the type that is being registered. This allows the IMetadataProvider the opportunity to know which class it is actually applied to; something normally not possible without explicitly passing the attribute a Type in a constructor argument.

To get an idea of what this would look like, here is a metadata attribute which implements this interface:

 1[MetadataAttribute]
 2class LetterFormatterAttribute : Attribute, IMetadataProvider
 3{
 4    public string Name { get; private set; }
 5
 6    public LetterFormatterAttribute(string name)
 7    {
 8        this.Name = name;
 9    }
10
11    #region IMetadataProvider implementation
12
13    public IDictionary<string, object> GetMetadata(Type targetType)
14    {
15        return new Dictionary<string, object>()
16        {
17            { "Name", this.Name }
18        };
19    }
20
21    #endregion
22}

This metadata doesn't do much more than the original. It actually returns exactly what would be created via property scanning. However, this allows much more flexibility in how MetadataAttributes can provide metadata. They can scan the type for other attributes, create arbitrary objects, and many other fun things that I can't even think of.

IMetadataProvider: Making a set of objects

Perhaps the simplest application of this new IMetadataProvider is having the metadata contain a list of objects. Building on our last example, we saw that the "personal" letter formatter just said "Dear Individual" every time. What if we could change that so that there was some way to pass in some "properties" or "options" provided by the caller of the formatting function? We can do this using an IMetadataProvider. We make the following changes:

 1class FormatOptionValue
 2{
 3    public string Name { get; set; }
 4    public object Value { get; set; }
 5}
 6
 7interface IFormatOption
 8{
 9    string Name { get; }
10    string Description { get; }
11}
12
13interface IFormatOptionProvider
14{
15    IFormatOption GetOption();
16}
17
18interface ILetterFormatter
19{
20    string FormatLetter(string content, IEnumerable<FormatOptionValue> options);
21}
22
23[MetadataAttribute]
24sealed class LetterFormatterAttribute : Attribute, IMetadataProvider
25{
26    public string Name { get; private set; }
27
28    public LetterFormatterAttribute(string name)
29    {
30        this.Name = name;
31    }
32
33    public IDictionary<string, object> GetMetadata(Type targetType)
34    {
35        var options = targetType.GetCustomAttributes(typeof(IFormatOptionProvider), true)
36            .Cast<IFormatOptionProvider>()
37            .Select(p => p.GetOption())
38            .ToList();
39
40        return new Dictionary<string, object>()
41        {
42            { "Name", this.Name },
43            { "Options", options }
44        };
45    }
46}
47
48//note the lack of the [MetadataAttribute] here. We don't want autofac to scan this for properties
49[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
50sealed class StringOptionAttribute : Attribute, IFormatOptionProvider
51{
52    public string Name { get; private set; }
53
54    public string Description { get; private set; }
55
56    public StringOptionAttribute(string name, string description)
57    {
58        this.Name = name;
59        this.Description = description;
60    }
61
62    public IFormatOption GetOption()
63    {
64        return new StringOption()
65        {
66            Name = this.Name,
67            Description = this.Description
68        };
69    }
70}
71
72public class StringOption : IFormatOption
73{
74    public string Name { get; set; }
75
76    public string Description { get; set; }
77
78    //note that we could easily define other properties that
79    //do not appear in the interface
80}
81
82class LetterMetadata
83{
84    public string Name { get; set; }
85
86    public IEnumerable<IFormatOption> Options { get; set; }
87}

Ok, so this is just a little bit more complicated. There are two changes to pay attention to: Firstly, the FormatLetter function now takes a list of FormatOptionValues. The second change is what enables the caller of FormatLetter to know which options to pass in. The LetterFormatterAttribute now scans the type in order to construct its metadata dictionary by looking for attributes that describe what options it needs. I feel like the usage of this is best illustrated by decorating our PersonalLetterFormatter for it to have some metadata describing the options that it requires:

 1[LetterFormatter("Personal")]
 2[StringOption(ToOptionName, "Name of the individual to address the letter to")]
 3class PersonalLetterFormatter : ILetterFormatter
 4{
 5    const string ToOptionName = "To";
 6
 7    public string FormatLetter(string content, IEnumerable<FormatOptionValue> options)
 8    {
 9        var toName = options.Where(o => o.Name == ToOptionName).Select(o => o.Value).FirstOrDefault() as string;
10        if (toName == null)
11            throw new ArgumentException("The " + ToOptionName + " string option is required");
12
13        return "Dear " + toName + ",nn" + content;
14    }
15}

When the metadata for the PersonalLetterFormatter is resolved, it will contain an IFormatOption which represents the To option. The resolver can attempt to cast the IFormatOption to a StringOption to find out what type it should pass in using the FormatOptionValue.

This can be extended quite easily for other IFormatOptionProviders and IFormatOption pairs, making for a very extensible way to easily declare metadata describing a set of options attached to a class.

IMetadataProvider: Hierarchical Metadata

The last example showed that the IMetadataProvider could be used to scan the class to provide metadata into a structure containing an IEnumerable of objects. It is a short leap to see that this could be used to create hierarchies of arbitrary objects.

For now, I won't provide a full example of how this could be done, but in the future I plan on having a gist or something showing arbitrary metadata hierarchy creation.

Conclusion

I probably use Metadata more than I should in Autofac. With the addition of the IMetadataProvider I feel like its quite easy to define complex metadata and use it with Autofac's natural constructor injection system. Overall, the usage of metadata & reflection in my programs has made them quite a bit more flexible and extendable and I feel like Autofac and its metadata system complement the built in reflection system of C# quite well.