Skip to main content
Hamdi_TEYEB
ST Employee
July 21, 2025

How to implement a USB composite device application with MSC and HID classes

  • July 21, 2025
  • 8 replies
  • 2970 views

Introduction

This article provides a step-by-step guide to implementing a composite USB device that combines mass storage class (MSC) and human interface device (HID) functionalities on an STM32 platform. Such a device is ideal for applications requiring both user interaction (for example, a mouse) and data storage (using SRAM). Using STM32CubeIDE, STM32CubeMX, and USB middleware, this tutorial demonstrates how to configure and implement this composite device.

For this article, the NUCLEO-U545RE-Q board is used to implement this application and can be easily tailored to any other STM32. This board features an STM32U545RET6Q microcontroller. For further details about the board, refer to the user manual.

STM32U5 Nucleo-64 boardSTM32U5 Nucleo-64 board                                                

1. Project setup

You can directly clone the project from STM32 Hotspot GitHub or follow the steps below to set up your project.

1.1 Create a project

Start a new project in STM32CubeIDE by selecting File  New STM32 Project. Navigate to the [Board Selector] tab and choose [NUCLEO-U545RE-Q].

2.png

After clicking the [Next] button, tick the [USER B1] box in the Board project options.

Board project optionsBoard project options

After creating the project, navigate to the [Connectivity] section, enable the USB peripheral [USB_DRD_FS] in [Device Only mode], and activate the USB global interrupt in the NVIC settings.

4.png

The final step before generating the code is to update the system clock frequency. According to the reference manual, the USB peripheral requires a minimum clock frequency of 48 MHz to avoid data overrun and underrun issues. For this example, the clock frequency is set to the maximum allowable value to achieve optimal performance.

5.png

Afterwards, the instruction cache (ICACHE) must be disabled in our project.

TEYEB_5-1751274330122.png

After generating the code, you must manually add the composite builder class, MSC class, and HID class source files to the project. Since the STM32U5 product is native to AzureRTOS, CubeMX does not automatically generate the middleware layer.

1.2 Add the middleware layer

First, add new folders named Middlewares and USB DEVICE to the project workspace directory.

6.png

After that, refresh the project to update and include the newly added folders.

Added foldersAdded folders

Once the project architecture is complete, if the legacy USB middleware is not available on your PC, download it from STMicroelectronics - GitHub /stm32_mw_usb_device.

The table below lists the files from the USB middleware that must be added to the project:

USB middleware File name  Source of files   Destination 
USB Core  usbd_core.c Core/Src  Middlewares/Core/Src 
usbd_ctlreq.c
usbd_ioreq.c
usbd_core.h Core/Inc  Middlewares/Core/Inc 
usbd_ctlreq.h
usbd_ioreq.h
Class HID usbd_hid.h Class/HID Middlewares/Class/HID/Inc 
usbd_hid.c Middlewares/Class/HID/Src 
Class MSC usbd_msc.c Class/MSC  Middlewares/Class/MSC/Inc 
usbd_msc_bot.c
usbd_msc_data.c
usbd_msc_scsi.c
usbd_msc.h Middlewares/Class/MSC/Src 
usbd_msc_bot.h
usbd_msc_data.h
usbd_msc_scsi.h
Class CompositeBuilder  usbd_composite_builder.c Class/CompositeBuilder Middlewares/Class/Compositebuilder/Src  
usbd_composite_builder.h Middlewares/Class/Compositebuilder/Inc   

And these are the source files modified for my specific application. You can directly clone them from your application.

File name  Source of files  Destination 
usbd_def.h usbd_def.h  USB Device/App/Inc 
usbd_desc.h usbd_desc.h 
usbd_desc.c
usbd_desc.c  USB Device/App/Src 
usbd_conf.h
usbd_conf.h  USB Device/Target/Inc 
usbd_conf.c
usbd_conf.c  USB Device/Target/Src 
usbd_storage.h 
usbd_storage.h  Core/Inc 
usbd_storage.c  usbd_storage.c  Core/Src 

After adding all the files, Under Properties → C/C++ General → Paths and Symbolsensure to add related the file source under [Source Location].

Build configurationBuild configuration

Under [Includes], add the middleware header files.

Build configurationBuild configuration

After adding all the necessary files and including them in the compilation toolchain, Proceed to modify the necessary files for the application.

2. Develop your own project

2.1 Interface file

8.png

The files usbd_storage.h and usbd_storage.c are essential for implementing the USB mass storage class functionality. In these files, the size of the memory space is fixed, and the functions for writing and reading are defined.

#define STORAGE_LUN_NBR 1
#define STORAGE_BLK_NBR 300
#define STORAGE_BLK_SIZ 0x200
/* Private macro -------------------------------------------------------------*/
/* Private variables ---------------------------------------------------------*/
uint8_t buffer[STORAGE_BLK_NBR*STORAGE_BLK_SIZ];

The buffer size, defined as buffer[STORAGE_BLK_NBR * STORAGE_BLK_SIZ], directly affects the USB mass storage class memory usage in SRAM. In our application, it is set to approximately 150 KB out of the total 274 KB available SRAM.

Using the STORAGE_Read function, you can efficiently read data from the internal storage buffer and transfer it to the USB host.

int8_t STORAGE_Read(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len)
{
 /* Prevent unused argument(s) compilation warning */
 UNUSED(lun);
 memcpy(buf, &buffer[blk_addr*STORAGE_BLK_SIZ], blk_len*STORAGE_BLK_SIZ);
 return (USBD_OK);
}

Using the STORAGE_Write function, the data received from the USB host is written to the internal storage buffer.

int8_t STORAGE_Write(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len)
{
 /* Prevent unused argument(s) compilation warning */
 UNUSED(lun);
 memcpy(&buffer[blk_addr*STORAGE_BLK_SIZ], buf, blk_len*STORAGE_BLK_SIZ);
 return (USBD_OK);
}

These are the essential configurations for memory manipulation in the mass storage application. You can download the complete file from the table or directly through this GitHub link.

2.2 USB configuration

Add the necessary configurations in theusbd_conf.hfile to enable the composite class, including both MSC and HID.

  1. Activate the classes

/* Activate the composite builder */
#define USE_USBD_COMPOSITE

/* Activate HID and MSC classes in composite builder */
#define USBD_CMPSIT_ACTIVATE_HID					1U
#define USBD_CMPSIT_ACTIVATE_MSC					1U

    2. Set HID and MSC endpoints

  • For HID, configure only the IN endpoint address.
  • For MSC, configure both the IN and OUT endpoint addresses.

Ensure that the endpoint addresses for HID and MSC are different to prevent conflicts.

/*Set HID and MSC endpoints*/
#define HID_EPIN_ADDR 0x81U

#define MSC_EPIN_ADDR 0x82U
#define MSC_EPOUT_ADDR 0x01U

    3. Set the media packet for MSC 

Setup of this media packet ensures efficient reading and writing of data blocks during USB communication

/* MSC Class Config */
#define MSC_MEDIA_PACKET 512U

    4. Configure the USBD_LL_Init function 

In the file usbd_conf.c, scroll down to the definition of the function USBD_LL_Init and add the following code

USBD_StatusTypeDef USBD_LL_Init(USBD_HandleTypeDef *pdev)
{
	hpcd_USB_DRD_FS.Instance = USB_DRD_FS;
	hpcd_USB_DRD_FS.Init.dev_endpoints = 8;
	hpcd_USB_DRD_FS.Init.speed = PCD_SPEED_FULL;
	hpcd_USB_DRD_FS.Init.phy_itface = PCD_PHY_EMBEDDED;
	hpcd_USB_DRD_FS.Init.Sof_enable = DISABLE;
	hpcd_USB_DRD_FS.Init.low_power_enable = DISABLE;
	hpcd_USB_DRD_FS.Init.lpm_enable = DISABLE;
	hpcd_USB_DRD_FS.Init.battery_charging_enable = DISABLE;
	hpcd_USB_DRD_FS.Init.vbus_sensing_enable = DISABLE;
	hpcd_USB_DRD_FS.Init.bulk_doublebuffer_enable = DISABLE;
	hpcd_USB_DRD_FS.Init.iso_singlebuffer_enable = DISABLE;
	HAL_PCD_Init(&hpcd_USB_DRD_FS) ;
	/* Link the driver to the stack */
	pdev->pData = &hpcd_USB_DRD_FS;
	hpcd_USB_DRD_FS.pData = pdev;


	/* Control Endpoints */
	HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x00 , PCD_SNG_BUF, 0x20);
	HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x80 , PCD_SNG_BUF, 0x40);

	/* HID Endpoints */
	HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , HID_EPIN_ADDR , PCD_SNG_BUF, 0x50);

	/* MSC Endpoints */
	HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , MSC_EPIN_ADDR , PCD_SNG_BUF, 0x80);
 HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , MSC_EPOUT_ADDR , PCD_SNG_BUF, 0x80);
	return USBD_OK;
}

2.3 Main application

Firstly, in the main.h file, add the following Includes:

/* Includes ------------------------------------------------------------------*/
#include "stm32u5xx_hal.h"
#include "stm32u5xx_nucleo.h"
#include <stdio.h>
#include "usbd_core.h"
#include "usbd_desc.h"
#include "usbd_composite_builder.h"
#include "usbd_msc.h"
#include "usbd_hid.h"
#include "usbd_storage.h"

Second, in the main.c file, add the following declarations for the endpoint addresses:

uint8_t MSC_EpAdd_Inst[2]={MSC_EPIN_ADDR,MSC_EPOUT_ADDR};
uint8_t HID_EpAdd_Inst = HID_EPIN_ADDR;
uint8_t hid_InstID= 0;

After that, between /* USER CODE BEGIN 2 */ and /* USER CODE END 2 */, insert the following code to initialize the user button and USB communication.

 /* USER CODE BEGIN 2 */

	/* Initialize USER push-button, will be used to trigger an interrupt each time it's pressed.*/
	BSP_PB_Init(BUTTON_USER, BUTTON_MODE_EXTI);

	/* Init Device Library */
	USBD_Init(&USBD_Device, &Class_Desc, 0);

	/* Add Class MSC */
	USBD_RegisterClassComposite(&USBD_Device ,USBD_MSC_CLASS ,CLASS_TYPE_MSC, MSC_EpAdd_Inst);

	/* Store HID Instance Class ID */
	hid_InstID = USBD_Device.classId;

	/* Add Class HID */
	USBD_RegisterClassComposite(&USBD_Device,USBD_HID_CLASS,CLASS_TYPE_HID, &HID_EpAdd_Inst);

	/* Add MSC Media interface */
	USBD_CMPSIT_SetClassID(&USBD_Device, CLASS_TYPE_MSC,0);

	/* Add Storage callbacks for MSC Class */
	USBD_MSC_RegisterStorage(&USBD_Device,&USBD_DISK_fops);

	/* Start Device Process */
	USBD_Start(&USBD_Device);

 /* USER CODE END 2 */

In a composite USB device with multiple classes, such as HID and MSC, a unique interface number must be assigned to each class to prevent conflicts during enumeration. In this application, the MSC class is assigned interface number 0, and the HID class is assigned the next available interface number by default.

Now, open the file stm32u5xx_it.c to configure the interrupt mode function. This mode is used to send HID reports that move the mouse pointer.

2.4 Interrupt mode

Between /* USER CODE BEGIN 0 */ and /* USER CODE END 0 */, insert the following line of code.

/* USER CODE BEGIN 0 */
extern USBD_HandleTypeDef USBD_Device;
#define CURSOR_STEP 20
uint8_t HID_Buffer[4];
extern uint8_t hid_InstID;
static void GetPointerData(uint8_t *pbuf);
/* USER CODE END 0 */

Between /* USER CODE BEGIN 1*/ and /* USER CODE END 1 */, insert the definition of the function GetPointerData. This function is used to obtain the coordinates for mouse movement.

static void GetPointerData(uint8_t *pbuf)
{
 static int8_t cnt = 0;
 int8_t x = 0, y = 0 ;

 if(cnt++ > 0)
 {
 x = CURSOR_STEP;
 }
 else
 {
 x = -CURSOR_STEP;
 }

 pbuf[0] = 0;
 pbuf[1] = x;
 pbuf[2] = y;
 pbuf[3] = 0;
}

After that, add the following callback function to handle the user button press and to send the HID report.

void BSP_PB_Callback(Button_TypeDef Button)
{
	 if (Button == BUTTON_USER)
	 {
	 GetPointerData(HID_Buffer);
	 USBD_HID_SendReport(&USBD_Device,HID_Buffer,sizeof(HID_Buffer), hid_InstID);
	 }
}

The application is now ready. Build the project and flash it to the target.

3. Results

After successfully building and flashing the application onto theNUCLEO-U545RE-Q board, connect the board to the PC using the USB user connectorThe host PC recognizes the composite USB device as both a mass storage device and an HID mouse.

devmgr.png

Now, you can press the blue USER button on the board to see the mouse pointer move on the screen. When opening the file explorer, notice the new USB drive that represents the mass storage device.

´final.png

As we can see, both USB classes operate simultaneously, demonstrating the successful implementation of the composite device (MSC+HID).

Conclusion

This guide provides a detailed approach to developing an STM32 application that integrates human interface device (HID) and mass storage class (MSC) functionalities into a composite USB device. By following these steps, you can create a USB device that enables both user interaction and mass storage capabilities, utilizing SRAM efficiently.

Perspective

This application currently uses internal SRAM as the storage medium for the mass storage class interface. However, it is designed with flexibility in mind and can support various external memory types, such as SD cards, NOR flash, or external SRAM. To enable and customize support for these external memories, modify the usbd_storage.c file accordingly. Adapt the storage interface functions to match your chosen memory hardware.

Related links 

8 replies

Enissay0
Explorer
July 21, 2025

Good Job #Hamdi 

Explorer
July 21, 2025

Great work @Hamdi_TEYEB this article is really helpful

Explorer
July 22, 2025

 

Excellent job @Hamdi_TEYEB, I found this article very valuable

 

Associate
July 30, 2025

Great Job @Hamdi_TEYEB, this article is so helpful 

firmwareguru
Graduate
June 16, 2026

For those looking to get CDC + MSC working, rather than HID + MSC, as well as with the STM32F4 series (and similar “legacy” parts), I have the procedure outlined below.

First, we need to get the usbd_conf.h configured correctly.  The trick is to adjust the endpoint assignments for CDC and MSC so that there are non-overlapping (by default they overlap). CDC is assigned first, and the two CDC “IN” endpoints must be assigned the first two slots.  The order is important because the composite builder is expecting this arrangement.

 

#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

 

The following additional defines must be present in usbd_conf.h.  Note that without the “IAD” descriptor present (constructed for you by the composite builder), it won’t work on Windows. 

 

/* 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

 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.

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

 

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 what I chose:

 

  //                                      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;
}
}

 

Lastly, initializing and starting the CDC and MSC classes requires this specific call sequence:

	/* 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, so
the 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();
}

The key takeaway is that the class interface functions need to be registered before the associated RegisterClassComposite().  As well, the class ID assigned to the CDC class (which will be 0, but is explicitly obtained through USBD_CMPSIT_GetClassID()), must be made available to the CDC interface functions  USBD_CDC_SetTxBuffer() and USBD_CDC_TransmitPacket().  You could skip this step (storing the CDC class ID) and just add a “0” argument to the function calls to make this work.

 

And that should do it.

Cheers

firmwareguru
Graduate
June 17, 2026

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};

 

firmwareguru
Graduate
June 17, 2026

Also note, the “DRD” USB IP is, as I understand it, only present on a small subset of the U5 parts (e.g. U545 as used in original post).  I was able to get CDC+MSC working on an STM32F4 (USB FS) and STM32U5G9J (USB HS with built-in PHY), with almost the same configuration.  The STM32U5G9 has nearly the same USB IP as other STM32 families (F4, F7, etc.) and thus offers the same PCD APIs (HAL_PCDEx_SetTxFiFo).  The only difference with the high speed core from a CDC+MSC configuration standpoint is that the FIFO is larger (4096 vs 1280 bytes).

Andrew Neil
Super User
June 17, 2026

@Hamdi_TEYEB 

Start a new project in STM32CubeIDE by selecting File  New  STM32 Project

 

You need to update that for the new STM32CubeIDE 2.0.0 workflow !

A complex system that works is invariably found to have evolved from a simple system that worked.A complex system designed from scratch never works and cannot be patched up to make it work.