Introduction
Most Microcontrollers have a built in Serial USB functionality that can not be changed (e.g. ESP32, ESP8266, Arduino Nano etc). Some Microcontrollers however allow to (re-)progam the USB functionality (e.g. Raspberry Pico, ESP32-S2, seeeduino xiao etc) and
TinyUSB is the project of choice for this.
A Microcontroller can either act as USB host or as device. In this blog I will concentrate on the USB device functionality, because this is where the library shines.
A TinyUSB device can acts as
- CDC Device (e.g. for Serial communication)
- Audio Device (microphone, music player)
- HID device (e.g. mouse or keyboard)
- MIDI device (to generate or play MIDI music)
- WebUSB device
- Network device (providing TCP/IP functionality over USB)
In the examples folder you can find the code that demonstrates how to use the functionality.
A short introduction into USB
USB is quite a daunting beast and tremendously complex. In order to understand the library you need to understand the concept of descriptors. BeyondLogic has an excellent introduction that I can recommend. In a nutshell you usually have:
- One Device Descriptor
- Usually one Configuration Descriptor
- One or many Interface Descriptors for each configuration
- One or many Endpoint Descriptors for each interface
On top of these standard descriptors each devices might describe their additional device specific descriptors. And finally there is the possibility to define strings and use them with their index position.
Anatomy of a TinyUSB Example
The examples are usually structured into 3 files:
- tusb_config.h – which contains the configuration
- usb_descriptors.c – which contains the descriptors and their related callbacks
- main.c – which contains the main logic and the implementation of callbacks
However, you have quite some flexibility how to structure this in your own projects:
- You could use C++ so you have .cpp instead of .c implementation files
- You could combine the content of usb_descriptors.c and the main.c into one single implementation file (e.g. myproject.c).
- If you want to avoid the tusb_config.h, you can can use the CFG_TUSB_CONFIG_FILE define to point to your own header file name. You can do this e.g. in your CMakeList.txt with
add_compile_options(-CFG_TUSB_CONFIG_FILE=myproject.h ...
The TinyUSB Configuration – tusb_config.h
The examples have quite some complexity because they need to support all types of environments. We can try to boil it down to the bare minimum which should work in most of the cases:
#pragma once
//--------------------------------------------------------------------
// DEVICE CONFIGURATION
//--------------------------------------------------------------------
//#define BOARD_DEVICE_RHPORT_NUM 0
#define BOARD_DEVICE_RHPORT_SPEED OPT_MODE_HIGH_SPEED
#define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | BOARD_DEVICE_RHPORT_SPEED)
#define CFG_TUD_ENDPOINT0_SIZE 64
//------------- CLASS -------------//
#define CFG_TUD_CDC 0
#define CFG_TUD_MSC 0
#define CFG_TUD_HID 0
#define CFG_TUD_MIDI 1
#define CFG_TUD_VENDOR 0
// MIDI FIFO size of TX and RX
#define CFG_TUD_MIDI_RX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64)
#define CFG_TUD_MIDI_TX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64)
We define the maximum size of the endpoint and activate the classes that we want to use in our project. The number indicates the number of Interfaces that we want to define. In the example above we define that we use two CDC and one MIDI device.
The Descriptors – usb_descriptors.c
The descriptors need to be provided by implementing the following 3 callback methods:
uint8_t const * tud_descriptor_device_cb(void)
uint8_t const * tud_descriptor_configuration_cb(uint8_t index)
uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid)
The tud_descriptor_device_cb provides the device profile and the tud_descriptor_configuration_cb provides the concatenated Configuration, Interface and Endpoint profiles.
The tud_descriptor_string_cb is responsible to provide the Strings in UTF8.
All the complexity in the usb_descriptors.c is related to the definition of the descriptors!
The Program Logic – main.c
The basic anatomy of the TinyUSB logic is quite simple:
- You need to setup the board by calling board_init()
- You need to setup the usb functionality by calling tusb_init()
- You need to call tud_task() – as often as possible to make the ‘midi engine’ do it’s work.
The remaining parts are application specific: you might want to do some actual midi generation or send some messages via cdc and blink your leds.
int main(void)
{
board_init();
tusb_init();
while (1)
{
tud_task(); // tinyusb device task
midi_task();
led_blinking_task();
}
return 0;
}
In order to be able to react to some events, the following callbacks are provided by the framework:
void tud_mount_cb(void) // Invoked when device is mounted
void tud_umount_cb(void) // Invoked when device is unmounted
void tud_suspend_cb(bool remote_wakeup_en) // Invoked when usb bus is suspended
void tud_resume_cb(void) // Invoked when usb bus is resumed
In the examples, these callback methods are used to change the LED blinking speed.
1 Comment
luliyt · 13. September 2021 at 16:53
In section – tusb_config.h, you’ve defined CFG_TUD_CDC as 0 instead of 2 as mentioned in the paragraph below the tusb_config.h.
We define the maximum size of the endpoint and activate the classes that we want to use in our project. The number indicates the number of Interfaces that we want to define. In the example above we define that we use “two CDC” and one MIDI device.