iron's blog

The universal serial bus

When I recently tried to create a USB device from scratch, I discovered that the USB protocol is quite a bit more complex than I initlally expected. It is nothing like other simple and common protocols used by microcontrollers such as I²C or SPI.

USB 101

The following happens when a USB device is plugged into a host-capable machine:

  1. The host detects a device based on the data lines.
  2. The host detects what type of device is connected.
  3. The host assigns a correct driver to the device.
  4. The device is ready for use by software on the host.

This is a very high-level which leave out countless small details. In this article, I will describe my experience with the creation of USB devices. This means that the (much more complicated) host device will not be described at all.

After a USB device is plugged in and detected, the host will detect the type of device that is connected. This process is called Enumeration. The concept of enumeration is quite simple, as the host will ask the device to list all of its descriptors.

Mysterious descriptors

Descriptors are just lists which tell the host about information such as device configuration, endpoints and interfaces. Some example descriptors for a serial connection can look like this:

Standard Device Descriptor
USB spec 9.6.1, page 262-263, Table 9-8

Dec ValueHex Value¹TypeDescription
180x12bLengthSize of this descriptor in bytes
10x01bDescriptorTypeDEVICE descriptor type
0x0200bcdUSBUSB Specification number
20x02bDeviceClassClass code
00x00bDeviceSubClassSubclass code
00x00bDeviceProtocolProtocol code
160x10bMaxPacketSize0Max packet size for endpoint 0
0xC016idVendorVendor ID
0x7A04idProductProduct ID
0x0100bcdDeviceDevice release number
10x01iManufacturerIndex of string descriptor of manufacturer
20x02iProductIndex of string descriptor of product
30x03iSerialNumberIndex of string descriptor of serial number
10x01bNumConfigurationsNumber of possible configurations

Standard Configuraton Descriptor
USB spec 9.6.3, page 265-266, Table 9-10

Dec ValueHex Value¹TypeDescription
90x09bLengthSize of this descriptor in bytes
20x02bDescriptorTypeCONFIGURATION descriptor type
670x4300wTotalLengthThe total length for this configuration
20x02bNumInterfacesNumber of interfaces in this configuration
10x01bConfigurationValueValue to use to select this configuration
00x00iConfigurationIndex of string descriptor of configuration
0xC0bmAttributesDevice power charasteristics
500x32bMaxPowerMax power usage in units of 2mA; (50=100mA)

¹ As per USB specification, HEX values are written LSB first.

A list of all non class-specific descriptors can be found in the USB Specification. The USB 2.0 Specification can be found here: USB 2.0 Specification. Class specific descriptors are specified in their respective specifications, for example, the CDC definition can be found here: CDC definition.

Finding out which values to use in what fields and what descriptors to use is a real challenge. I suggest cheating, and taking inspiration from the descriptors used by a similar device. I got mine from Teensy’s USB serial interface. Getting a VenderID and ProductID is a different problem to which several free solutions exist, such as pid.codes.

AVR’s USB series

Atmel’s AVR series contain multiple part numbers with native support for USB. An example of a part with USB support is the ATmega8U2. This is a part which can be bought at distributors like mouser or digikey for around € 2,80 at the time of writing this. The ATmega8U2 is for example used in the Arduino UNO as a serial interface between the USB port and the ATmega328p chip.

Connecting the Atmega8U2 to a USB port is super simple, and is described in the datasheet. An example of this is shown below.²

USB wiring to the AVR

² The ATmegaXXu2 datasheet shows an UID pin on the AVR part, but this pin does not exist and can be ignored.

It is important that the MCU is clocked using a precise 8MHz or 16MHz crystal, because the full-speed USB interface requires a 48MHz clock which is derived from the main clock signal using an internal PLL. There should also be two 22Ω resistors on D+ and D-, and a 1uF capacitor connected to UCAP.

Several libraries exist to simplify the experience of using the MCU’s USB capabilities, such as LUFA and Teensy’s core implementation. Because LUFA is complex and has a very complicated build system, I will be using a derivative of Teensy’s code, which is quite simple.

Teensy uses the built-in interrupts USB_GEN_vect and USB_COM_vect to handle almost all USB related logic, such as device enumeration and handling of the CDC logic.

The inner workings of the library are straight forward:

  1. Initialise the USB
    1. Freeze the USB
    2. Configure the PLL for the MCU’s crystal
    3. Wait for the PLL to stabalise
    4. Unfreeze the USB
    5. Enable the internal USB connection resistor
    6. Enable USB interrupts
  2. In the USB_COM_vect interrupt, wait for the RXSTPI (Received SETUP) flag to be set and respond to all USB requests. such as:
    1. GET_DESCRIPTOR
    2. SET_ADDRESS
    3. SET_CONFIGURATION
    4. GET_CONFIGURATION
    5. GET_STATUS
    6. Other class specific requests, such as CDC_SET_CONTROL_LINE_STATE for the CDC class.
  3. Wait for the RWAL ( Read/Write Allowed) flag to be set.
  4. Send/Receive whatever data you please using UEDATX! (In the case of Teensy, sending text)

Ofcourse its also possible to create a USB device using microcontrollers without hardware USB support, using bit-banging libraries such as V-BUS. But those come at a significant cost of CPU power.

Using the Teensy USB library and my own Nori AVR library, the following hello-world application will work:

#include <Nori/AVR/AVR_Timer1.h>
#include <avr/sleep.h>
#include "usb_serial.h"

Nori::AVR_Timer1 clock;

int main(void) {
  sei();

  usb_init();

  while (!usb_configured())
    ;

  while (!(usb_serial_get_control() & USB_SERIAL_DTR))
    ;

  clock.SetMode(Nori::TimerMode::CTC)
      ->SetClockSource(Nori::ClockSource::ClkDiv256) // 8 MHz / 256 = 31250 Hz
      ->SetLimit((uint16_t)31250) // 31250 Hz / 31250 = 1 Hz
      ->AttachInterrupt([]() {
        uint8_t* message = (uint8_t*)"Hello world!\r\n";
        usb_serial_write(message, sizeof(message));
      });

  set_sleep_mode(SLEEP_MODE_IDLE);
  while (true) {
    sleep_mode();
    set_sleep_mode(SLEEP_MODE_IDLE);
  }
}

If I make a nice wrapper around Teensy’s library in the style of the Nori AVR library, using USB in my projects is no longer an issue.

Thank you for reading this article.
If you spot any mistakes or if you would like to contact me, visit the contact page for more details.