Skip to main content
B.Montanari
ST Technical Moderator
October 26, 2023

How to implement the USB Device CDC (VCOM) in STM32 using the Azure USBX package

  • October 26, 2023
  • 26 replies
  • 31614 views

Summary

This article presents a tutorial on how to implement the USB Device CDC in the STM32 using the Azure USBX package. Azure USBX is an RTOS USB embedded stack developed by Microsoft® that offers a wide range of classes to be implemented both for host and device applications.   

For this tutorial, we use the B-G474E-DPOW1 board, which has a USB Type-C® connector interface. However, the demonstration does not use the USB Type-C® additional features. This board has an STM32G474RET6 microcontroller, and the steps shown here can be easily adapted to any other STM32Gx family. For other families, we will present the necessary changes in the development of this article. The STM32CubeIDE 1.13.1, STM32CubeG4 1.5.1, and the X-CUBE-AZRTOS-G4 2.0.0 releases were used to build this tutorial.

BMontanari_0-1697561737297.png

 

1. Development

Let us start creating a project for the STM32G474RET6 in the STM32CubeIDE and enabling the PA15 (LD2 Blue LED), PB1 (LD3 Orange LED), PB7 (LD4 Green LED) and PB5 (LD5 Red LED) as GPIO Output Push-Pull to get access to the LEDs available on the board.

In the next step, we need to enable the USB Peripheral in Device (FS) mode. For this tutorial, the default Parameter Settings are used. We just need to enable the USB low-priority interrupt remap. The high priority interrupt will not work with the Azure ThreadX since it blocks the kernel to run due to the high priority interrupt.

BMontanari_1-1697561737303.png

 

Now, navigate until the X-CUBE-AZRTOS-G4 under the Middleware and Software Packs.

BMontanari_2-1697561737307.png

 

Once clicked on the package, the Software Packs Component Selector menu is opened. There we need to enable the following packs:

 

  • RTOS ThreadX -> ThreadX -> Core
  • USB USBX -> USBX -> CoreSystem
  • USB USBX -> USBX -> UX Device CoreStack
  • USB USBX -> USBX -> UX Device Controllers
  • USB USBX -> USBX -> UX Device

After that, press OK.

BMontanari_3-1697561737314.png

 

We enabled the RTOS ThreadX since the USBX was developed to run with the Azure RTOS. It is possible to run the firmware in standalone mode, but we will cover this in another article.

To use the USBX we enabled the CoreSystem and UX Device CoreStack, which contains the middle layer firmware for the USB in device mode that ensures the USB stack processing and the interface between the low and high layers. The UX Device Controllers, low layer firmware to interface with the hardware USB peripheral. And finally, the UX Device Class CDC ACM, the higher layer which includes the class components.

Going back to the STM32CubeIDE, open the X-CUBE-AZRTOS-G4 and enable both RTOS ThreadX and USB USBX. Then increase the amount of memory allocated for the USBX Device System Stack Size and USBX Device memory pool size.

BMontanari_4-1697561737324.png

 

To define the amount of memory for the USBX Device System Stack Size, refer to the table below:

 

CLASS

NEEDED MEMORY

TOTAL

HID

4 * 1024

4 KB

MASS STORAGE

5 * 1024

5 KB

DFU

4 * 1024

4 KB

CDC ACM

6 * 1024

6 KB

CDC ECM

34 * 1024

34 KB

VIDEO

6 * 1024

6 KB

RNDIS

34 * 1024

34 KB

MTP

12 * 1024

12 KB

PRINTER

3 * 1024

3 KB

CCID

8 * 1024

8 KB

Composite

Sum of the used classes

To define the amount of memory for the USBX Device memory pool size, you must take the following into account: USBX Device System Stack Size, the USBX Device Application Thread stack size, the thread stack you are planning to create for the USB application, and the RX and TX buffer.

BMontanari_5-1697561737325.png

 

 

Navigate to the USBX tab, and decrease the UX_SLAVE_REQUEST_DATA_MAX_LENGTH to save space in RAM, and change the USBD _CDCACM_ENDPOINT_IN_CMD_ADDR to 2.

BMontanari_6-1697561737336.png

 

Here, we use the endpoint 0 IN/OUT as the standard control endpoint. The endpoint 1 IN/OUT for the CDC data. And finally, the endpoint 2 IN for the CDC commands. You can change these addresses as you want, just ensure to respect the USB IP capabilities.

It is necessary to change the Timebase Source, since the SysTick is used the RTOS kernel. To do that, go to SYS and select a timer for the timebase.

BMontanari_7-1697561737342.png

 

 

The next step in the configuration tool is to adjust the Clock Configuration. So, go to this tab, select the HSI48 for the CK48 Clock Mux source, and se the HCLK to 170MHz to extract the maximum MCU performance.

BMontanari_8-1697561737347.png

 

 

After that, navigate until Project Manager tab, then Code Generator. In this menu, enable the Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral. This helps us to get access to the USB resources.

BMontanari_9-1697561737349.png

 

The last step is to disable the USB Initialization in the Advanced Settings menu.

BMontanari_10-1697561737352.png

 

Doing that, we are all set to generate the code and start coding our application.

 

BMontanari_11-1697561737354.png

 

Once the code is generated, we have the following structure:

BMontanari_12-1697561737359.png

 

The first steps in the code development are to link the ST HAL USB driver with the USBX firmware and then initialize the USB peripheral. These steps are done in the app_usbx_device.c file. So, open that and start adding the following includes:

/* Private includes ----------------------------------------------------------*/

/* USER CODE BEGIN Includes */

#include "main.h"

#include "usb.h"

#include "ux_dcd_stm32.h"

/* USER CODE END Includes */

Then, scroll down until the app_ux_device_thread_entry function. Into this function, add the code to link the driver and initialize the USB peripheral.

static VOID app_ux_device_thread_entry(ULONG thread_input)
{
 /* USER CODE BEGIN app_ux_device_thread_entry */
 MX_USB_PCD_Init();
 HAL_PCDEx_PMAConfig(&hpcd_USB_FS, 0x00 , PCD_SNG_BUF, 0x40);
 HAL_PCDEx_PMAConfig(&hpcd_USB_FS, 0x80 , PCD_SNG_BUF, 0x80);
 HAL_PCDEx_PMAConfig(&hpcd_USB_FS, 0x01, PCD_SNG_BUF, 0xC0);
 HAL_PCDEx_PMAConfig(&hpcd_USB_FS, 0x81, PCD_SNG_BUF, 0x100);
 HAL_PCDEx_PMAConfig(&hpcd_USB_FS, 0x82, PCD_SNG_BUF, 0x140);
 ux_dcd_stm32_initialize((ULONG)USB, (ULONG)&hpcd_USB_FS);
 HAL_PCD_Start(&hpcd_USB_FS);
 /* USER CODE END app_ux_device_thread_entry */
}

The code above starts calling the MX_USB_PCD_Init() to initialize the USB peripheral.

Then the HAL_PCDEx_PMAConfig(…) function is called 5 times. This function configures the PMA (packet memory area) within the dedicated USB RAM memory for the STM32Gx families. It should be done for every endpoint used in the application. In this case for the endpoints 0 IN/OUT (0x00 and 0x80), 1 IN/OUT (0x01, 0x81) and 2 IN (0x82). The PCD_SNG_BUF parameter means that we use a single buffer for the endpoint, this is necessary since we are using the endpoint in bidirectional mode. Finally, the last parameter of this function is the PMA address, which should be selected according to the following table:

 

REGION NAME

MEMORY ADDRESS

SIZE

BTABLE

0x00

64 B

TX Ep 0 Buffer

0x40

64 B

RX Ep 0 Buffer

0x80

64 B

TX Ep 1 Buffer

0xC0

64 B

RX Ep 1 Buffer

0x100

64 B

RX Ep 2 Buffer

0x140

64 B

As you may have noticed, we used the first address of the memory to store the BTABLE, which is a list of addresses of the endpoint buffers. The BTABLE stores 8 bytes for each endpoint. Since the STM32G474 has 8 endpoints, it can consume at maximum of 64 bytes.

In this example, we are using the endpoints from 0 until 2, so the rest of the table is not used. We could decrease the offset of the TX Ep 0 Buffer, optimizing the usage of the memory. However, following this table you can allocate all the 8 endpoints buffers without problems. For further details, refer to section 45.5 from the STM32G474 Reference Manual.

The next function called is ux_dcd_stm32_initialize(…) which is responsible to link the HAL USB drivers to the USBX application. And finally, we call the HAL_PCD_Start(..) to start the USB PCD peripheral.

The steps for filling this function may differ depending on the MCU family you are using. In this case, we recommend you to refer to the USBX examples available in the X-CUBE-AZRTOS-XX package, to see the correct way to set up your USB peripheral.

The endpoint addresses are defined according to the selected address in the graphical configuration tool, as presented before. To verify the generated addresses, you can double check the addresses in the ux_device_descriptors.h.

BMontanari_13-1697561737367.png

The last step is to open the ux_device_cdc_acm.c file and include the main.h header:

 

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "main.h"
/* USER CODE END Includes */

Then create the following variable:

/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
UX_SLAVE_CLASS_CDC_ACM *cdc_acm;
/* USER CODE END PV */

Populate the activate function as follows:

VOID USBD_CDC_ACM_Activate(VOID *cdc_acm_instance)
{
 /* USER CODE BEGIN USBD_CDC_ACM_Activate */
 /* Save the instance */
 cdc_acm = (UX_SLAVE_CLASS_CDC_ACM *)cdc_acm_instance;
 /* USER CODE END USBD_CDC_ACM_Activate */
 return;
}

 

Doing that, we already have our USB application functional. To class resources are available in the ux_device_cdc_acm.c file. The next steps will cover a usage example of the USBX CDC class.

Let us go back to the app_usbx_device.c, in the last user code section of the MX_USBX_Device_Init(…) function and create two threads for handling the write and read features.


/* USER CODE BEGIN MX_USBX_Device_Init1 */
/* Allocate memory for the UX RX thread */
tx_byte_allocate(byte_pool, (VOID **)&pointer, 1024, TX_NO_WAIT);
/* Create the UX RX thread */
tx_thread_create(&ux_cdc_read_thread, "cdc_acm_read_usbx_app_thread_entry", usbx_cdc_acm_read_thread_entry, 1, pointer, 1024, 20, 20, TX_NO_TIME_SLICE, TX_AUTO_START);
/* Allocate memory for the UX TX thread */
tx_byte_allocate(byte_pool, (VOID **)&pointer, 1024, TX_NO_WAIT);
/* Create the UX TX thread */
tx_thread_create(&ux_cdc_write_thread, "cdc_acm_write_usbx_app_thread_entry", usbx_cdc_acm_write_thread_entry, 1, pointer, 1025, 20, 20, TX_NO_TIME_SLICE, TX_AUTO_START);
/* USER CODE END MX_USBX_Device_Init1 */

          Create the threads in the private variables section:

/* USER CODE BEGIN PV */
static TX_THREAD ux_cdc_read_thread;
static TX_THREAD ux_cdc_write_thread;
/* USER CODE END PV */

          Finally, add the include of the ux_device_cdc_acm.h file in the app_usbx_device.c:

 

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "main.h"
#include "usb.h"
#include "ux_dcd_stm32.h"
#include "ux_device_cdc_acm.h"
/* USER CODE END Includes */

Now, open the ux_device_cdc_acm.h and add the thread prototypes:

/* USER CODE BEGIN EFP */
VOID usbx_cdc_acm_write_thread_entry(ULONG thread_input);
VOID usbx_cdc_acm_read_thread_entry(ULONG thread_input);
/* USER CODE END EFP */

The last modifications are done to the ux_device_cdc_acm.c file, where we add the thread codes:

/* USER CODE BEGIN 1 */
VOID usbx_cdc_acm_write_thread_entry(ULONG thread_input)
{
 /* Private Variables */
 ULONG tx_actual_length;
 const uint8_t message[] = "USBX Application Running!\r\n";
 while(1)
 {
 ux_device_class_cdc_acm_write(cdc_acm, (UCHAR *)(message), sizeof(message), &tx_actual_length);
 tx_thread_sleep(100);
 }
}

VOID usbx_cdc_acm_read_thread_entry(ULONG thread_input)
{
 /* Private Variables */
 ULONG rx_actual_length;
 uint8_t UserRxBuffer[64];
 /* Infinite Loop */
 while(1)
 {
 if(cdc_acm != UX_NULL)
 {
 ux_device_class_cdc_acm_read(cdc_acm, (UCHAR *)UserRxBuffer, 64, &rx_actual_length);
 switch(UserRxBuffer[rx_actual_length-1])
 {
 case '1':
 HAL_GPIO_WritePin(LD3_GPIO_Port, LD3_Pin, GPIO_PIN_SET);
 break;
 case '0':
 HAL_GPIO_WritePin(LD3_GPIO_Port, LD3_Pin, GPIO_PIN_RESET);
 break;
 }
 }
 }
}

/* USER CODE END 1 */

And that concludes our example code. Connect the USB of the STLINK in your board, then the USB Type-C®, start a debug session and see the application running!

2. Results

Once you have opened the debug session, let the code run and open a virtual COM port terminal in your computer. The “USBX Application Running!” message should be printed on every second. If you send the character ‘1’ the Orange LED should be turned ON, and it should be turned OFF when the character ‘0’ is sent.

 

BMontanari_14-1697561737371.png

3. Conclusion

That concludes our article. Now you have the needed information to start developing with the USBX Device stack. The steps for implementing other classes are similar up to the class layer code. In these cases, our GitHub contains a vast set of demonstration projects that includes most of the available classes both for Host and Device applications.

Our best wishes for your projects, and we hope you enjoyed the material!

Useful links 

Here are some useful links for you that is developing with the USBX package:

STMicroelectronics GitHub - X-CUBE-AZRTOS-G4

STMicroelectronics - Azure(R) RTOS Middleware

Introduction to USBX

Microsoft Azure RTOS documentation hub | Microsoft Learn

Understand Azure RTOS USBX | Microsoft Learn

B-G474E-DPOW1 - Discovery kit with STM32G474RE MCU - STMicroelectronics

STM32G474RE - Mainstream Arm Cortex-M4 MCU 170 MHz with 512 Kbytes of Flash memory, Math Accelerator, HR Timer, High Analog level integration - STMicroelectronics

STM32G474 Reference Manual

 

 

 

26 replies

Senior
August 14, 2025

Hello, @B.Montanari @D.Botelho ,

I am using STM32G474VET6, and I want to use USBX to get a USB VCP for serial debug port. I just did the config and modified the code as above like your article.

I found some issues:

1. when compile the files, I got an error of usb.h cannot found in app_usbx_device.c. I did not know what is a usb.h and why we need it here. so I comment out usb.h. it passed compiler.

if the usb.h is not needed, it is better that you delete usb.h.

2. after complied ok, I thecked the output of USB port, actually there is a serial port on my PC, the message printed is as below

dqsh06_0-1755157435026.png

there is a \0 on the message printed. but my code is the same as you, why?

dqsh06_1-1755157532403.png

3. I want to use this VCP port to print debug information, just as a printf. how can i change this VOID usbx_cdc_acm_write_thread_entry() to get a function, just like printf(fmt...)?

 

Thanks.

dqsh06

 

 

D.Botelho
ST Employee
August 15, 2025

Hello @dqsh06 ,

 

The usb.h file will only be part of your project if you enable the Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral option during the code generation settings phase. If you don't enable this, you will not need to include this file.

 

The \0 character is put by GCC compiler in the end of every string that you declare in your code. So, when we declare:

const uint8_t message[] = "USBX Application Running!\r\n";

We should expect that the last byte of the array is equal to '\0', to delimit the end of the string.

 

To avoid the behavior in this code, you should modify the following line:

ux_device_class_cdc_acm_write(cdc_acm, (UCHAR *)(message), sizeof(message), &tx_actual_length);

 

To:

ux_device_class_cdc_acm_write(cdc_acm, (UCHAR *)(message), sizeof(message) - 1, &tx_actual_length);

 

Then the \0 delimiter will not be transmitted.

 

Finally, to redirect the printf to the USB, you can adapt the implementation presented in the following article to change the UART to the USB port:

https://community.st.com/s/article/how-to-redirect-the-printf-function-to-a-uart-for-debug-messages

 

 

Hope it helps!

Best Regards,

Dan

Associate II
August 26, 2025

Hi @B.Montanari @D.Botelho 

I’m working on the NUCLEO-H563ZI. ux_device_class_cdc_acm_write() works as expected, but ux_device_class_cdc_acm_read() blocks indefinitely inside _ux_device_stack_transfer_request() and never returns, even when the PC sends data. As a result, I can’t receive data via ux_device_class_cdc_acm_read(). The echo test below (read from PC, then write back) does not work.

ULONG USBD_CDC_ACM_Write(UCHAR *txBuf, ULONG len)
{
	ULONG actual_length;
	ux_device_class_cdc_acm_write(cdc_acm, txBuf, len, &actual_length);

	return actual_length;
}

VOID usbx_cdc_acm_read_thread_entry(ULONG thread_input)
{
	ULONG rx_actual_length;
	uint8_t rxBuf[64];

	while(1) {
		if(cdc_acm != UX_NULL) {
			ux_device_class_cdc_acm_read(cdc_acm, (UCHAR *)rxBuf, 64, &rx_actual_length);
			USBD_CDC_ACM_Write(rxBuf, rx_actual_length);
		}
	}
}

I follow the STM32H5 workshop - 09 USB - USBX-CDC lab (experts) and this post for reference.

Is there any possible issue I missed? 

Senior
September 30, 2025

hello @D.Botelho @B.Montanari,

I have another issue to comfirm, I modified the send message interval to 10ms. code as below:

VOID usbx_cdc_acm_write_thread_entry(ULONG thread_input)
{
 /* Private Variables */
 ULONG tx_actual_length;
 const uint8_t message[] = "USBX Application Running!\r\n";

 while(1)
 {
 if(cdc_acm != UX_NULL)
 {
 ux_device_class_cdc_acm_write(cdc_acm, (UCHAR *)message, sizeof(message), &tx_actual_length);
 tx_thread_sleep(10);
 }
 }
}

and then, I made a simple program the read the message on linux.

the result was as below:

dqsh06_0-1759210980182.png

acctually, I read the message on 10ms, but if look insight the timing, the interval is 10.085ms. why there is a 0.085ms interval? it is for USBX thread swithoff? could eliminate this 0.085ms?

Best Regards,

dqsh06

D.Botelho
ST Employee
October 1, 2025

Hi @Cooper_WJH ,

 

The behavior you are seeing seems to be a problem related to the endpoint allocation for your application.

Since you are working with the NUCLEO-H563ZI, I recommend you trying the following example from the STM32CubeH5 repository:

 

STM32CubeH5/Projects/NUCLEO-H563ZI/Applications/USBX/Ux_Device_HID_CDC_ACM at main · STMicroelectronics/STM32CubeH5 · GitHub

 

 

Best Regards,

Dan

D.Botelho
ST Employee
October 1, 2025

Hello @dqsh06 ,

 

The USB communication is ever driven by the Host side. It means that the Device will never send a packet if the Host don't ask for it. 

When calling the function below you are storing the message into the MCU USB EP IN buffer, and the message will be pending until the Host ask for the packet.

ux_device_class_cdc_acm_write(cdc_acm, (UCHAR *)message, sizeof(message), &tx_actual_length);

 

We may also consider the latency of the USBX to prepare the out buffer to be transmitted.

The main point here is that the USB transfer doesn`t happen instantaneously after calling the write function, we may consider a small latency between them.

 

Best Regards,

Dan