
Table of Contents
When using the USB peripheral on STM32 MCU's, you can choose between the vendor-provided USB stack and a third-party one like TinyUSB. The vendor-provided is inconvenient to use because even though it provides a selection of predefined classes, the code generation always overwrites any changes one might need to make to the stack to customize it or even to fix bugs in the implementation. TinyUSB has the advantage to be hardware-independent, is easy-to-use and highly customizable. Its disadvantage is that its integration into STM32CubeIDE and the HAL is not straight-forward. Hence this guide.
STMCubeMX configuration
TinyUSB uses the abstractions of the lower-level HAL for STM32 MCU's. This is configured in CubeMX. First, the USB_OTG_FS
peripheral in the Connectivity
section needs to be enabled. Set it to Device Only
and most importantly check the global USB interrupt in NVIC Settings
. This generates the OTG_FS_IRQHandler()
interrupt callback in the stm32f<x>xx_it.c
file, it is needed later.
Make sure that the USB Device
in Middleware
is NOT enabled.
Then the clock solver needs to be run the in Clock configuration
. After that work is done in the CubeMX tool and the code can be generated.
STM32CubeIDE integration
First, the TinyUSB repository needs to be cloned into the project and enabled for linking and compilation.
In the project root directory, execute:
git clone https://github.com/hathach/tinyusb
# Or when the repository itself is managed with git
git submodule add https://github.com/hathach/tinyusb
TinyUSB itself uses some library submodules in lib
, which need to be initialized.
cd tinyusb
git submodule update --init --recursive lib
It is recommended to use a released version, so the repository needs to be checked out to a release tag (0.14.0
at the time of writing):
git fetch --all --tags
git checkout 0.14.0
Then the linker and compiler need to be configured. First, open the projects properties window under Project->Properties
and navigate to C/C++ Build->Settings
. In the MCU GCC Compiler->Include Paths
view you can add the path to the TinyUSB source directory to include its header files.
Make sure it is added for all configurations.
Then navigate to C/C++ General->Paths and Symbols
and in Source Location
add the TinyUSB source directory as well for all configurations.
At the time of writing with v0.14.0
there are two drivers for STM32 devices. Leaving them both enabled would result in Symbol already defined
compilation errors. To prevent this, one of them needs to be disabled. This can be done by excluding its file with a filter. Select the added path and click on Edit Filters
on the right side. Then add the portable/st/synopsys/dcd_synopsys.c
to the filter.
This disables the older driver, and the newer driver in portable/synopsys/dwc2/dcd_dwc2.c
gets used. By developer comment the older one will be removed at some point so this might not be necessary anymore in the future.
Code (CDC dual ports example)
To be able to use TinyUSB, it needs to be integrated in the existing code. For demonstration purposes the CDC dual ports example will get used.
tusb_config.h
TinyUSB expects the tusb_config.h
configuration file to be present. In there the library and the peripheral is configured. Create it in Core/Inc
.
The CFG_TUSB_MCU
and CFG_TUSB_OS
definitions are mandatory. Valid values for both definitions can be looked up in tusb_option.h
.
// tusb_config.h
#ifndef _TUSB_CONFIG_H_
#define _TUSB_CONFIG_H_
#ifdef __cplusplus
extern "C" {
#endif
//--------------------------------------------------------------------
// BOARD CONFIGURATION
//--------------------------------------------------------------------
// Specifying the used MCU. Valid values are defined in `tusb_option.h`
#define CFG_TUSB_MCU OPT_MCU_STM32F4
// Specifying the used OS. Valid values are defined in `tusb_option.h`
#define CFG_TUSB_OS OPT_OS_NONE
// This enables and configures the Root Hub (Or on some MCU's multiple).
// Here Root-Hub 0 is defined as device and with full-speed.
// Valid values are defined in `tusb_option.h`
#define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | OPT_MODE_FULL_SPEED)
#define BOARD_DEVICE_RHPORT_NUM 0
// Debugging
#define CFG_TUSB_DEBUG 3
// Enable Device stack
#define CFG_TUD_ENABLED 1
// Default is max speed that hardware controller could support with on-chip PHY
#define CFG_TUD_MAX_SPEED OPT_MODE_DEFAULT_SPEED
/* USB DMA on some MCUs can only access a specific SRAM region with restriction on alignment.
* Tinyusb use follows macros to declare transferring memory so that they can be put
* into those specific section.
* e.g
* - CFG_TUSB_MEM SECTION : __attribute__ (( section(".usb_ram") ))
* - CFG_TUSB_MEM_ALIGN : __attribute__ ((aligned(4)))
*/
#ifndef CFG_TUSB_MEM_SECTION
#define CFG_TUSB_MEM_SECTION
#endif
#ifndef CFG_TUSB_MEM_ALIGN
#define CFG_TUSB_MEM_ALIGN __attribute__ ((aligned(4)))
#endif
//--------------------------------------------------------------------
// DEVICE CONFIGURATION
//--------------------------------------------------------------------
// Valid values are dependent on the speed.
// for low-speed: 8
// for full-speed: 64
// for high-speed: either 8, 16, 32, 64
#ifndef CFG_TUD_ENDPOINT0_SIZE
#define CFG_TUD_ENDPOINT0_SIZE 64
#endif
//------------- CLASS -------------//
// Values > 0 enable a class and the number specifies
// how many interfaces of the class are enabled.
#define CFG_TUD_CDC 2
#define CFG_TUD_MSC 0
#define CFG_TUD_HID 0
#define CFG_TUD_MIDI 0
#define CFG_TUD_VENDOR 0
// CDC FIFO size of TX and RX
#define CFG_TUD_CDC_RX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64)
#define CFG_TUD_CDC_TX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64)
// CDC Endpoint transfer buffer size, more is faster
#define CFG_TUD_CDC_EP_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64)
#ifdef __cplusplus
}
#endif
#endif /* _TUSB_CONFIG_H_ */
usb_descriptor.c
Add usb_descriptors.c
in Core/Src/
. This file is for configuring the USB descriptors.
// usb_descriptor.c
#include "tusb.h"
/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug.
* Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC.
*
* Auto ProductID layout's Bitmap:
* [MSB] MIDI | HID | MSC | CDC [LSB]
*/
#define _PID_MAP(itf, n) ( (CFG_TUD_##itf) << (n) )
#define USB_PID (0x4000 | _PID_MAP(CDC, 0) | _PID_MAP(MSC, 1) | _PID_MAP(HID, 2) | \
_PID_MAP(MIDI, 3) | _PID_MAP(VENDOR, 4) )
#define USB_VID 0xCafe
#define USB_BCD 0x0200
//--------------------------------------------------------------------+
// Device Descriptors
//--------------------------------------------------------------------+
tusb_desc_device_t const desc_device =
{
.bLength = sizeof(tusb_desc_device_t),
.bDescriptorType = TUSB_DESC_DEVICE,
.bcdUSB = USB_BCD,
// Use Interface Association Descriptor (IAD) for CDC
// As required by USB Specs IAD's subclass must be common class (2) and protocol must be IAD (1)
.bDeviceClass = TUSB_CLASS_MISC,
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
.bDeviceProtocol = MISC_PROTOCOL_IAD,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.idVendor = USB_VID,
.idProduct = USB_PID,
.bcdDevice = 0x0100,
.iManufacturer = 0x01,
.iProduct = 0x02,
.iSerialNumber = 0x03,
.bNumConfigurations = 0x01
};
// Invoked when received GET DEVICE DESCRIPTOR
// Application return pointer to descriptor
uint8_t const * tud_descriptor_device_cb(void)
{
return (uint8_t const *) &desc_device;
}
//--------------------------------------------------------------------+
// Configuration Descriptor
//--------------------------------------------------------------------+
enum
{
ITF_NUM_CDC_0 = 0,
ITF_NUM_CDC_0_DATA,
ITF_NUM_CDC_1,
ITF_NUM_CDC_1_DATA,
ITF_NUM_TOTAL
};
#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + CFG_TUD_CDC * TUD_CDC_DESC_LEN)
#if CFG_TUSB_MCU == OPT_MCU_LPC175X_6X || CFG_TUSB_MCU == OPT_MCU_LPC177X_8X || CFG_TUSB_MCU == OPT_MCU_LPC40XX
// LPC 17xx and 40xx endpoint type (bulk/interrupt/iso) are fixed by its number
// 0 control, 1 In, 2 Bulk, 3 Iso, 4 In etc ...
#define EPNUM_CDC_0_NOTIF 0x81
#define EPNUM_CDC_0_OUT 0x02
#define EPNUM_CDC_0_IN 0x82
#define EPNUM_CDC_1_NOTIF 0x84
#define EPNUM_CDC_1_OUT 0x05
#define EPNUM_CDC_1_IN 0x85
#elif CFG_TUSB_MCU == OPT_MCU_SAMG || CFG_TUSB_MCU == OPT_MCU_SAMX7X
// SAMG & SAME70 don't support a same endpoint number with different direction IN and OUT
// e.g EP1 OUT & EP1 IN cannot exist together
#define EPNUM_CDC_0_NOTIF 0x81
#define EPNUM_CDC_0_OUT 0x02
#define EPNUM_CDC_0_IN 0x83
#define EPNUM_CDC_1_NOTIF 0x84
#define EPNUM_CDC_1_OUT 0x05
#define EPNUM_CDC_1_IN 0x86
#else
#define EPNUM_CDC_0_NOTIF 0x81
#define EPNUM_CDC_0_OUT 0x02
#define EPNUM_CDC_0_IN 0x82
#define EPNUM_CDC_1_NOTIF 0x83
#define EPNUM_CDC_1_OUT 0x04
#define EPNUM_CDC_1_IN 0x84
#endif
uint8_t const desc_fs_configuration[] =
{
// Config number, interface count, string index, total length, attribute, power in mA
TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100),
// 1st CDC: Interface number, string index, EP notification address and size, EP data address (out, in) and size.
TUD_CDC_DESCRIPTOR(ITF_NUM_CDC_0, 4, EPNUM_CDC_0_NOTIF, 8, EPNUM_CDC_0_OUT, EPNUM_CDC_0_IN, 64),
// 2nd CDC: Interface number, string index, EP notification address and size, EP data address (out, in) and size.
TUD_CDC_DESCRIPTOR(ITF_NUM_CDC_1, 4, EPNUM_CDC_1_NOTIF, 8, EPNUM_CDC_1_OUT, EPNUM_CDC_1_IN, 64),
};
#if TUD_OPT_HIGH_SPEED
// Per USB specs: high speed capable device must report device_qualifier and other_speed_configuration
uint8_t const desc_hs_configuration[] =
{
// Config number, interface count, string index, total length, attribute, power in mA
TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100),
// 1st CDC: Interface number, string index, EP notification address and size, EP data address (out, in) and size.
TUD_CDC_DESCRIPTOR(ITF_NUM_CDC_0, 4, EPNUM_CDC_0_NOTIF, 8, EPNUM_CDC_0_OUT, EPNUM_CDC_0_IN, 512),
// 2nd CDC: Interface number, string index, EP notification address and size, EP data address (out, in) and size.
TUD_CDC_DESCRIPTOR(ITF_NUM_CDC_1, 4, EPNUM_CDC_1_NOTIF, 8, EPNUM_CDC_1_OUT, EPNUM_CDC_1_IN, 512),
};
// device qualifier is mostly similar to device descriptor since we don't change configuration based on speed
tusb_desc_device_qualifier_t const desc_device_qualifier =
{
.bLength = sizeof(tusb_desc_device_t),
.bDescriptorType = TUSB_DESC_DEVICE,
.bcdUSB = USB_BCD,
.bDeviceClass = TUSB_CLASS_MISC,
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
.bDeviceProtocol = MISC_PROTOCOL_IAD,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.bNumConfigurations = 0x01,
.bReserved = 0x00
};
// Invoked when received GET DEVICE QUALIFIER DESCRIPTOR request
// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete.
// device_qualifier descriptor describes information about a high-speed capable device that would
// change if the device were operating at the other speed. If not highspeed capable stall this request.
uint8_t const* tud_descriptor_device_qualifier_cb(void)
{
return (uint8_t const*) &desc_device_qualifier;
}
// Invoked when received GET OTHER SEED CONFIGURATION DESCRIPTOR request
// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete
// Configuration descriptor in the other speed e.g if high speed then this is for full speed and vice versa
uint8_t const* tud_descriptor_other_speed_configuration_cb(uint8_t index)
{
(void) index; // for multiple configurations
// if link speed is high return fullspeed config, and vice versa
return (tud_speed_get() == TUSB_SPEED_HIGH) ? desc_fs_configuration : desc_hs_configuration;
}
#endif // highspeed
// Invoked when received GET CONFIGURATION DESCRIPTOR
// Application return pointer to descriptor
// Descriptor contents must exist long enough for transfer to complete
uint8_t const * tud_descriptor_configuration_cb(uint8_t index)
{
(void) index; // for multiple configurations
#if TUD_OPT_HIGH_SPEED
// Although we are highspeed, host may be fullspeed.
return (tud_speed_get() == TUSB_SPEED_HIGH) ? desc_hs_configuration : desc_fs_configuration;
#else
return desc_fs_configuration;
#endif
}
//--------------------------------------------------------------------+
// String Descriptors
//--------------------------------------------------------------------+
// array of pointer to string descriptors
char const* string_desc_arr [] =
{
(const char[]) { 0x09, 0x04 }, // 0: is supported language is English (0x0409)
"TinyUSB", // 1: Manufacturer
"TinyUSB Device", // 2: Product
"123456", // 3: Serials, should use chip ID
"TinyUSB CDC", // 4: CDC Interface
};
static uint16_t _desc_str[32];
// Invoked when received GET STRING DESCRIPTOR request
// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete
uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid)
{
(void) langid;
uint8_t chr_count;
if ( index == 0)
{
memcpy(&_desc_str[1], string_desc_arr[0], 2);
chr_count = 1;
}else
{
// Note: the 0xEE index string is a Microsoft OS 1.0 Descriptors.
// https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-defined-usb-descriptors
if ( !(index < sizeof(string_desc_arr)/sizeof(string_desc_arr[0])) ) return NULL;
const char* str = string_desc_arr[index];
// Cap at max char
chr_count = (uint8_t) strlen(str);
if ( chr_count > 31 ) chr_count = 31;
// Convert ASCII string into UTF-16
for(uint8_t i=0; i<chr_count; i++)
{
_desc_str[1+i] = str[i];
}
}
// first byte is length (including header), second byte is string type
_desc_str[0] = (uint16_t) ((TUSB_DESC_STRING << 8 ) | (2*chr_count + 2));
return _desc_str;
}
stm32fxxx_it.c
As mentioned in the beginning, the code inside the global interrupt handler needs to be modified. In there tud_int_handler()
is called to hand the handling of the interrupt to TinyUSB. By returning early, the "normal" interrupt handler code is prevented to be run which could interfere with TinyUSB.
// stm32fxxx_it.c
//...
void OTG_FS_IRQHandler(void)
{
/* USER CODE BEGIN OTG_FS_IRQn 0 */
return tud_int_handler(BOARD_DEVICE_RHPORT_NUM);
/* USER CODE END OTG_FS_IRQn 0 */
HAL_PCD_IRQHandler(&hpcd_USB_OTG_FS);
/* USER CODE BEGIN OTG_FS_IRQn 1 */
/* USER CODE END OTG_FS_IRQn 1 */
}
// ...
main.c
Then finally in main.c
the library is initialized with tusb_init()
AFTER the clock and peripheral initialization of the HAL. In the main loop tud_task()
is repeatedly called to handle all pending events in TinyUSB's internal event queue.
For the CDC example there is also some code to echo the received input from either CDC interface to both in either lower- and uppercase.
// main.c
// ...
#include "ctype.h"
#include "tusb_config.h"
#include "tusb.h"
// ...
static void echo_serial_port(uint8_t itf, uint8_t buf[], uint32_t count);
static void cdc_task(void);
int main(void) {
// HAL Initialization code
tusb_init();
while (1) {
tud_task(); // TinyUSB device task
cdc_task(); // CDC example
// ...
}
}
// ***
// echo to either Serial0 or Serial1
// with Serial0 as all lower case, Serial1 as all upper case
static void echo_serial_port(uint8_t itf, uint8_t buf[], uint32_t count) {
uint8_t const case_diff = 'a' - 'A';
for (uint32_t i = 0; i < count; i++) {
if (itf == 0) {
// echo back 1st port as lower case
if (isupper(buf[i]))
buf[i] += case_diff;
} else {
// echo back 2nd port as upper case
if (islower(buf[i]))
buf[i] -= case_diff;
}
tud_cdc_n_write_char(itf, buf[i]);
}
tud_cdc_n_write_flush(itf);
}
//--------------------------------------------------------------------+
// USB CDC
//--------------------------------------------------------------------+
static void cdc_task(void) {
uint8_t itf;
for (itf = 0; itf < CFG_TUD_CDC; itf++) {
// connected() check for DTR bit
// Most but not all terminal client set this when making connection
// if ( tud_cdc_n_connected(itf) )
{
if (tud_cdc_n_available(itf)) {
uint8_t buf[64];
uint32_t count = tud_cdc_n_read(itf, buf, sizeof(buf));
// echo back to both serial ports
echo_serial_port(0, buf, count);
echo_serial_port(1, buf, count);
}
}
}
}
// ...
Testing
Before running the code, it is advisable to enable debugging and logging at first by setting CFG_TUSB_DEBUG
to a value greater than zero in the configuration file. By default TinyUSB is logging with printf()
. To be able to inspect these entries over e.g. UART, _write()
can be overwritten with something like:
// main.c
// ...
int _write(int file, char *ptr, int len) {
return HAL_UART_Transmit(&huart2, (uint8_t*) ptr, len, HAL_MAX_DELAY);
}
// ...
This is it! The code should now compile and run on the MCU. Once the USB peripheral is connected to the host, there should appear a USB device with Vendor ID 0xCafe
and two CDC devices. They should appear as virtual-COM Ports in the "Device Manager" on Windows or by inspecting the output of lsusb
on Linux.
To connect to both CDC's a serial terminal tool can be used, for example tio
. On linux execute tio /dev/ttyACM<x>
for both devices.
Then, whenever something is typed into one of the terminals it should be echoed by both in lower- and uppercase.