How to Implement a USB Composite Device on STM32 (CDC + MSC)
If you're building advanced embedded systems with STM32, chances are you’ve needed multiple USB interfaces on a single device—for example:
- A virtual COM port (CDC) for debug/logging or CLI
- A USB Mass Storage device (MSC) for file system access (SD or eMMC) or firmware updates
This guide walks through how to combine CDC and MSC into a single USB composite device using the USB Device Middleware and the HAL stack. This guide assumes you already have a project setup and running with a recent version of the STM32_USB_Device_Library (traditionally found in Middlewares/ST). In the GitHub Cube repositories, this directory is now linked to stm32-mw-usb-device.
This guide is applicable to any STM32 MCU using the prevalent USB_OTG_FS and USB_OTG_HS cores, including most of Lx, Fx, Hx and Ux families.
Why Use a USB Composite Device?
A composite device allows your STM32 to expose multiple USB classes simultaneously over a single cable, without having to switch modes.
Common Use Cases
- Embedded data loggers: stream data + store logs on local SD or eMMC hosting FAT32
- Firmware update tools: drag-and-drop update + serial diagnostics
- Developer boards: CLI access + file system interface
System Overview
In this setup, your STM32 device will present:
| Interface | USB Class | Purpose |
|---|---|---|
| CDC | Communication Device Class | Serial communication |
| MSC | Mass Storage Class | File system access |
A quick summary of what needs to be done over top of what you already have for your one-class setup:
- Modify usbd_conf.h
- Modify usbd_conf.c
- Modify usbd_desc.c
- Include class files in your build for: CDC, MSC and Composite Builder
- Modify USBD initialization and startup routine
Modify usbd_conf.h
#define CDC_IN_EP 0x81
#define CDC_CMD_EP 0x82
#define CDC_OUT_EP 0x01
#define MSC_EPIN_ADDR 0x83
#define MSC_EPOUT_ADDR 0x03
/* Activate the composite builder */
#define USE_USBD_COMPOSITE/* Activate CDC and MSC classes in composite builder */
#define USBD_CMPSIT_ACTIVATE_CDC 1U
#define USBD_CMPSIT_ACTIVATE_MSC 1U/* Ensure to enable IAD descriptor for proper CDC enumeration */
#define USBD_COMPOSITE_USE_IAD 1U
Modify usbd_conf.c
Next up is to change usbd_conf.c to align the FIFO setup with the new EP assignments, and to allocate the necessary class memory.
For USB_OTG_FS cores, FIFO layout is a 320 word (320*4=1280 byte) region that is divvied up amongst a shared RX (OUT) endpoint and all TX (IN) endpoints.
STM32F4 FIFO layout concept
+------------------------+
| Rx FIFO |
+------------------------+
| Tx FIFO EP0 IN |
+------------------------+
| Tx FIFO EP1 IN |
+------------------------+
| Tx FIFO EP2 IN |
+------------------------+
| Tx FIFO EP3 IN |
+------------------------+
total ≤ 320 words
For USB_OTG_HS cores, the FIFO is larger (4096 bytes) but otherwise treated the same. For USB_HS, the packet sizes for bulk endpoints (CDC and MSC IN, and RX) are now 512 bytes, so the FIFO regions for those should be increased accordingly.
What we need to do is ensure each endpoint defined in usbd_conf.h gets an appropriate allocation. Please note that allocations must be a minimum of 16 words (64 bytes) and are specified in units of words. The allocation is specified in function USBD_LL_Init(). Here’s a working configuration:
// FIFO START: 1280
HAL_PCDEx_SetRxFiFo(&hpcd, 512>>2); // Shared RX
HAL_PCDEx_SetTxFiFo(&hpcd, 0, 64>>2); // EP0
HAL_PCDEx_SetTxFiFo(&hpcd, 1, 128>>2); // CDC IN
HAL_PCDEx_SetTxFiFo(&hpcd, 2, 64>>2); // CDC CMD
HAL_PCDEx_SetTxFiFo(&hpcd, 3, 512>>2); // MSC IN
Now for the class memory allocation, ensure that USBD_static_malloc() is implemented to assign a separate region to each of the CDC and MSC classes. One way to do this is shown below:
#include "usbd_cdc.h"
void *USBD_static_malloc(uint32_t size)
{
/* Each class, CDC and MSC, requires its own persistent memory region.
We can determine the region to return based on the requested size. */static uint32_t mem_cdc[(sizeof(USBD_CDC_HandleTypeDef) / 4) + 1]; /* On 32-bit boundary */
static uint32_t mem_msc[(sizeof(USBD_MSC_BOT_HandleTypeDef) / 4) + 1]; /* On 32-bit boundary */
if (size == sizeof(USBD_CDC_HandleTypeDef)) {
return mem_cdc;
}
else if (size == sizeof(USBD_MSC_BOT_HandleTypeDef)) {
return mem_msc;
}
else {
return NULL;
}
}
Modify usbd_desc.c
Traditionally, each USBD class (CDC or MSC for example) has their own usbd_desc_xxx.c file in the STM32Cube reference designs. What we must do is have one usbd_desc.c file that represents both. In reality, this file does not have any specific reference to either class but instead to a "composite" class. A subtle change must be made to the main standard device descriptor. Namely, the following field values are set:
0xEF, /* bDeviceClass was 0xEF*/
0x02, /* bDeviceSubClass was 0x2*/
0x01, /* bDeviceProtocol was 0x1*/
It is suggested to just copy and use this usbd_desc.c in your project.
Include Class Files in Build
In your project's preprocessor configuration, ensure to add paths for
- Class/CDC/Inc
- Class/MSC/Inc
- Class/CompositeBuilder/Inc
Similarly, ensure that the project compiles files in:
- Class/CDC/Src
- Class/MSC/Src
- Class/CompositeBuilder/Src
Modify USBD Initialization and Startup
For the CDC+MSC case, you’ll also need the definition of the endpoint lists supplied to the USBD_RegisterClassComposite. The order is important.
/* NOTE: The endpoints must be listed as: IN, OUT, IN */
static uint8_t MSC_EpAdd_Inst[2]={MSC_EPIN_ADDR, MSC_EPOUT_ADDR};
static uint8_t CDC_EpAdd_Inst[3]={CDC_IN_EP, CDC_OUT_EP, CDC_CMD_EP};...
/* Init Device Library */USBD_Init(&USBD_Device, &Class_Desc, 0);/* Add Class CDC FIRST */USBD_CDC_RegisterInterface(&USBD_Device, &USBD_CDC_fops);USBD_RegisterClassComposite(&USBD_Device, USBD_CDC_CLASS, CLASS_TYPE_CDC, CDC_EpAdd_Inst);/* Add Class MSC SECOND */USBD_MSC_RegisterStorage(&USBD_Device, &USBD_DISK_fops);USBD_RegisterClassComposite(&USBD_Device, USBD_MSC_CLASS, CLASS_TYPE_MSC, MSC_EpAdd_Inst);/* Store CDC Instance Class ID - this is the index into the list of classes (CDC registered first, sothe class ID would be 0, etc.)*/g_cdc_class_id = USBD_CMPSIT_GetClassID(&USBD_Device, CLASS_TYPE_CDC, 0 /* first instance of CDC (we only have one) */);/* Start Device Process */if (USBD_Start(&USBD_Device) != USBD_OK) {Error_Handler();}