on 2025-07-21 12:28 AM
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 board
You can directly clone the project from STM32 Hotspot GitHub or follow the steps below to set up your project.
Start a new project in STM32CubeIDE by selecting File → New → STM32 Project. Navigate to the [Board Selector] tab and choose [NUCLEO-U545RE-Q].
After clicking the [Next] button, tick the [USER B1] box in the Board project options.
Board 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.
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.
Afterwards, the instruction cache (ICACHE) must be disabled in our project.
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.
First, add new folders named Middlewares and USB DEVICE to the project workspace directory.
After that, refresh the project to update and include the newly added folders.
Added 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 Symbols, ensure to add related the file source under [Source Location].
Build configuration
Under [Includes], add the middleware header files.
Build configuration
After adding all the necessary files and including them in the compilation toolchain, Proceed to modify the necessary files for the application.
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.
Add the necessary configurations in theusbd_conf.h
file to enable the composite class, including both MSC and HID.
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
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;
}
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.
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.
After successfully building and flashing the application onto theNUCLEO-U545RE-Q board, connect the board to the PC using the USB user connector. The host PC recognizes the composite USB device as both a mass storage device and an HID mouse.
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.
As we can see, both USB classes operate simultaneously, demonstrating the successful implementation of the composite device (MSC+HID).
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.
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.
Good Job #Hamdi
Great work @Hamdi_TEYEB this article is really helpful