on 2024-03-12 02:08 AM
This article presents a step-by-step tutorial on who to use the USBX package to develop a USB device composite application on an STM32. The tutorial is based on NUCLEO-H723ZG and can be easily tailored to any other STM32 that is compatible with the X-CUBE-AZRTOS expansion package.
Hello developer, and welcome to this article. Here we present a tutorial on how to implement the USB device composite in the STM32 using the Azure USBX package.
For this tutorial, we use the NUCLEO-H723ZG board, which has a USB OTG_FS connector interface to open two different classes. One being the HID device as a mouse and the other as CDC ACM, a typical VCOM port. This board has an STM32H723ZGT6 microcontroller, and the steps shown here can be easily adapted to any other STM32. The STM32CubeIDE 1.14.1, the STM32CubeH7 1.11.1, and the X-CUBE-AZRTOS-H7 3.1.0 releases are used to build this tutorial.
Start by creating a project for the STM32H723ZGT6 in the STM32CubeIDE and enabling the following:
Configure the PC13 (User button) pin as GPIO_Input to allow us to read the state of the user button available on the Nucleo board.
Navigate to the USB_OTG_HS menu under the "Connectivity" tab. Once opened, navigate to the "USB Peripheral" tab, select the “Device_Only” option in the “Internal FS Phy.” Afterwards, enable the “USB On the Go HS” global interrupt under the "NVIC Settings" tab. By doing so, the "Clock Settings" tab signalize an error. but don’t worry, we will correct it in the next step.
Go to the "Clock Configuration" tab, increase the frequency of HCLK frequency and the most important thing, change the USB clock mux source to the HSI48.
After doing that, go back to the “Pinout & Configuration” tab and under the "Middleware and Software Packs" menu, click on the X-CUBE-AZRTOS-H7. It opens the "Software Packs Component Selector." Then enable the following required components:
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, as covered in this other article How to implement USBX in standalone mode - STMicroelectronics Community.
In 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. Furthermore, the interface between the low and high layers. Lastly, the UX device controllers, low layer firmware to interface with the hardware USB peripheral.
Finally, add the class components of your application. For this example, we use the HID mouse and the CDC ACM classes:
Once selected the components, press OK and back to the X-CUBE-AZRTOS-H7 under the "Middleware and Software Packs" and enable both RTOS ThreadX and USB USBX components:
In the next step, we need to increase the amount of memory allocated for the USBX Device System Stack Size. To define this amount of data, refer to the information block to see the necessary memory for each class. Since we are using two classes in this example (the HID and CDC ACM), the amount of memory is the sum of those.
Then increase the USBX device memory pool size. This memory pool should be adjusted to fit the "USBX Device System Stack Size" and the thread stacks that are used in the USBX application. You can select whether the threads are in the ThreadX or in the USBX memory pool. In this example, we create three threads with 1 KB of stack. So, we use 10KB (USBX Device Stack Size) + 3KB (Threads stack) + 2KB (for other components) = 15 KB.
The next changes will be made in the USBX tab. There, increase the UX_MAX_SLAVE_CLASS_DRIVER to 2, since we are using 2 classes, then decrease the UX_SLAVE_REQUEST_DATA_MAX_LENGTH to 512.
Scroll the menu down and select the Endpoints for the HID and for the CDC ACM. Here, the 0x81 EP IN is used for the HID mouse. The 0x02 EP OUT for the CDC ACM out. The 0x82 EP IN for the CDC ACM in and finally the 0x83 EP IN for the CDC ACM command in.
Since we are using an RTOS, we cannot use the Sys_Tick for the HAL timebase, as this timer is used by it. Go to "System Core", then "SYS" and change the timebase for any basic timer, such as TIM6.
Now, go to the "Project Manager" tab and in the "Code Generator" section enable the “Generate peripheral initialization as pair of ‘.c/.h’ files per peripheral”. This facilitates the access to the peripheral resources in the code.
The last step before generating the code is to disable the USB_OTG_HS peripheral initialization code in the advanced settings also found in the "Project Manager" tab.
Now, that all the settings are done you can generate the code. For that you can use the alt + K shortcut or press the generate button.
Once the code is generated, we must start initializing the USB peripheral and linking the HAL driver with the USBX package using the UX device controller driver resource. For that, open the ../USBX/App/app_usbx_device.c file and add the following includes:
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "main.h"
#include "usb_otg.h"
#include "ux_dcd_stm32.h"
/* USER CODE END Includes */
Then, scroll down until the app_ux_device_thread_entry(…) function and add the code below. This code starts by setting the USB OTG_HS peripheral by calling the MX_USB_OTG_HS_PCD_Init() function. After that, an Rx FIFO is set for all EP OUTs (Control EP 0 and CDC Data EP2). Finally, four Tx FIFOSs are allocated for the EP INs (Control EP 0, Mouse EP 1, CDC Data EP2 and CDC Command EP3).
After setting the FIFOs, the code links the HAL USB peripheral driver. It calls the UX DCD function and passes the USB peripheral base address, then the address of the peripheral handler. The code finishes by starting the USB Peripheral by calling the HAL_PCD_Start(…) function.
static VOID app_ux_device_thread_entry(ULONG thread_input)
{
/* USER CODE BEGIN app_ux_device_thread_entry */
/* Initialize the USB OTG HS Peripheral */
MX_USB_OTG_HS_PCD_Init();
/* Allocate the RX and TX FIFOs */
HAL_PCDEx_SetRxFiFo(&hpcd_USB_OTG_HS, 0x200);
HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_HS, 0, 0x100);
HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_HS, 1, 0x100);
HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_HS, 2, 0x100);
HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_HS, 3, 0x100);
/* Link the drivers using to the USBX */
ux_dcd_stm32_initialize((ULONG)USB_OTG_HS, (ULONG)&hpcd_USB_OTG_HS);
/* Start the Peripheral */
HAL_PCD_Start(&hpcd_USB_OTG_HS);
/* USER CODE END app_ux_device_thread_entry */
}
Doing that you have all set for using the USBX. If you build and program this code, and then connect a USB cable in the board’s user USB connector, it should enumerate and present two classes: A CDC and an HID.
In the next steps, we will guide you through an example of how to use these two classes.
Start creating three threads, one for the HID and two for the CDC (read and write). For that, create the three thread stacks as follows:
/* USER CODE BEGIN PV */
static TX_THREAD ux_hid_mouse_thread;
static TX_THREAD ux_cdc_acm_read_thread;
static TX_THREAD ux_cdc_acm_write_thread;
/* USER CODE END PV */
Start creating 3 threads, one for the HID and two for the CDC (read and write). For that, create the three thread stacks within the MX_USBX_Device_Init function as follows:
/* USER CODE BEGIN MX_USBX_Device_Init1 */
if(tx_byte_allocate(byte_pool, (VOID **)&pointer, 1024, TX_NO_WAIT) != TX_SUCCESS)
{
HAL_GPIO_WritePin(LD3_RED_GPIO_Port, LD3_RED_Pin, GPIO_PIN_SET);
return TX_POOL_ERROR;
}
if(tx_thread_create(&ux_hid_mouse_thread, "HID Mouse Thread", usbx_hid_mouse_thread_entry, 1, pointer, 1024, 20, 20, 1, TX_AUTO_START) != TX_SUCCESS)
{
HAL_GPIO_WritePin(LD3_RED_GPIO_Port, LD3_RED_Pin, GPIO_PIN_SET);
return TX_THREAD_ERROR;
}
if(tx_byte_allocate(byte_pool, (VOID **)&pointer, 1024, TX_NO_WAIT) != TX_SUCCESS)
{
HAL_GPIO_WritePin(LD3_RED_GPIO_Port, LD3_RED_Pin, GPIO_PIN_SET);
return TX_POOL_ERROR;
}
if(tx_thread_create(&ux_cdc_acm_read_thread, "CDC Read Thread", usbx_cdc_read_thread_entry, 1, pointer, 1024, 20, 20, 1, TX_AUTO_START) != TX_SUCCESS)
{
HAL_GPIO_WritePin(LD3_RED_GPIO_Port, LD3_RED_Pin, GPIO_PIN_SET);
return TX_THREAD_ERROR;
}
if(tx_byte_allocate(byte_pool, (VOID **)&pointer, 1024, TX_NO_WAIT) != TX_SUCCESS)
{
HAL_GPIO_WritePin(LD3_RED_GPIO_Port, LD3_RED_Pin, GPIO_PIN_SET);
return TX_POOL_ERROR;
}
if(tx_thread_create(&ux_cdc_acm_write_thread, "CDC Write Thread", usbx_cdc_write_thread_entry, 1, pointer, 1024, 20, 20, 1, TX_AUTO_START) != TX_SUCCESS)
{
HAL_GPIO_WritePin(LD3_RED_GPIO_Port, LD3_RED_Pin, GPIO_PIN_SET);
return TX_THREAD_ERROR;
}
/* USER CODE END MX_USBX_Device_Init1 */
Now, open the ../USBX/App/ux_device_acm_.c file and include the main.h file to get access to the GPIO LED resources.
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "main.h"
/* USER CODE END Includes */
Then create the following variables:
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
UX_SLAVE_CLASS_CDC_ACM *cdc_acm;
uint8_t UserRxBuffer[16];
const uint8_t UserTxMessage[] = "MY CDC CLASS IS RUNNING!\r\n";
/* USER CODE END PV */
In the USBD_CDC_ACM_Activate, we need to assign the cdc_acm instance to be used to receive and transmit data through this class:
VOID USBD_CDC_ACM_Activate(VOID *cdc_acm_instance)
{
/* USER CODE BEGIN USBD_CDC_ACM_Activate */
cdc_acm = (UX_SLAVE_CLASS_CDC_ACM *)cdc_acm_instance;
HAL_GPIO_WritePin(LD1_GREEN_GPIO_Port, LD1_GREEN_Pin, GPIO_PIN_SET);
/* USER CODE END USBD_CDC_ACM_Activate */
return;
}
We should also clear the instance when the device is deactivated:
VOID USBD_CDC_ACM_Deactivate(VOID *cdc_acm_instance)
{
/* USER CODE BEGIN USBD_CDC_ACM_Deactivate */
cdc_acm = UX_NULL;
HAL_GPIO_WritePin(LD1_GREEN_GPIO_Port, LD1_GREEN_Pin, GPIO_PIN_RESET);
/* USER CODE END USBD_CDC_ACM_Deactivate */
return;
}
Finally, we can create our CDC ACM threads to transmit and receive data:
/* USER CODE BEGIN 1 */
VOID usbx_cdc_read_thread_entry(ULONG thread_input)
{
/* Local Variables */
ULONG actual_length;
UX_SLAVE_DEVICE *device;
device = &_ux_system_slave->ux_system_slave_device;
/* Infinite Loop */
while(1)
{
/* Check if device is configured */
if((device->ux_slave_device_state == UX_DEVICE_CONFIGURED) && (cdc_acm != UX_NULL))
{
ux_device_class_cdc_acm_read(cdc_acm, (UCHAR *)UserRxBuffer, 16, &actual_length);
for(uint8_t i = 0; i < actual_length; i++)
{
switch(UserRxBuffer[i])
{
case '1':
HAL_GPIO_WritePin(LD2_YELLOW_GPIO_Port, LD2_YELLOW_Pin, GPIO_PIN_SET);
break;
case '0':
HAL_GPIO_WritePin(LD2_YELLOW_GPIO_Port, LD2_YELLOW_Pin, GPIO_PIN_RESET);
break;
}
}
}
}
/* Sleep for 10ms */
tx_thread_sleep(1);
}
VOID usbx_cdc_write_thread_entry(ULONG thread_input)
{
/* Local Variables */
ULONG actual_length;
UX_SLAVE_DEVICE *device;
device = &_ux_system_slave->ux_system_slave_device;
/* Infinite Loop */
while(1)
{
/* Check if device is configured */
if((device->ux_slave_device_state == UX_DEVICE_CONFIGURED) && (cdc_acm != UX_NULL))
{
ux_device_class_cdc_acm_write(cdc_acm, (UCHAR *)UserTxMessage, sizeof(UserTxMessage), &actual_length);
/* Sleep for 1s */
tx_thread_sleep(100);
}
}
}
/* USER CODE END 1 */
In the next step, create the Threads function prototypes in the ../USBX/ux_cdc_acm.h file.
/* USER CODE BEGIN EFP */
VOID usbx_cdc_read_thread_entry(ULONG thread_input);
VOID usbx_cdc_write_thread_entry(ULONG thread_input);
/* USER CODE END EFP */
Once finished these parts, you can save and close both the ux_device_cdc_acm.h and .c files.
Open the ux_device_mouse.c file to create the HID Mouse threads and start including the main.h file to get access to the user button.
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "main.h"
/* USER CODE END Includes */
Then create the following variable to store the class instance address:
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
UX_SLAVE_CLASS_HID *hid_mouse;
/* USER CODE END PV */
After that, we need to store the instance address when the class is activated:
VOID USBD_HID_Mouse_Activate(VOID *hid_instance)
{
/* USER CODE BEGIN USBD_HID_Mouse_Activate */
hid_mouse = (UX_SLAVE_CLASS_HID *)hid_instance;
/* USER CODE END USBD_HID_Mouse_Activate */
return;
}
Clear that in the deactivate function:
VOID USBD_HID_Mouse_Deactivate(VOID *hid_instance)
{
/* USER CODE BEGIN USBD_HID_Mouse_Deactivate */
hid_mouse = UX_NULL;
/* USER CODE END USBD_HID_Mouse_Deactivate */
return;
}
Create the HID thread:
/* USER CODE BEGIN 1 */
VOID usbx_hid_mouse_thread_entry(ULONG thread_input)
{
/* Local Variables */
UX_SLAVE_DEVICE *device;
UX_SLAVE_CLASS_HID_EVENT hid_event;
device = &_ux_system_slave->ux_system_slave_device;
/* Infinite Loop */
while(1)
{
/* Check if the Device is configured */
if((device->ux_slave_device_state == UX_DEVICE_CONFIGURED) && (hid_mouse != UX_NULL))
{
/* Check if the User Button is pressed */
if(HAL_GPIO_ReadPin(USER_BUTTON_GPIO_Port, USER_BUTTON_Pin) == GPIO_PIN_SET)
{
/* Mouse event. Length is fixed to 4 */
hid_event.ux_device_class_hid_event_length = 4;
/* Set select position */
hid_event.ux_device_class_hid_event_buffer[0] = 0;
/* Set X position */
hid_event.ux_device_class_hid_event_buffer[1] = 5;
/* Set Y position */
hid_event.ux_device_class_hid_event_buffer[2] = 5;
/* Set wheel position */
hid_event.ux_device_class_hid_event_buffer[3] = 0;
/* Send an event to the hid */
ux_device_class_hid_event_set(hid_mouse, &hid_event);
}
}
/* Wait for 100ms */
tx_thread_sleep(10);
}
}
/* USER CODE END 1 */
Now, it is necessary to create the thread prototype in the ux_device_mouse.h file:
/* USER CODE BEGIN EFP */
VOID usbx_hid_mouse_thread_entry(ULONG thread_input);
/* USER CODE END EFP */
Save and close both ux_device_mouse.c and .h files. And to conclude the code back to the app_usbx_device.c and include the class headers:
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "main.h"
#include "usb_otg.h"
#include "ux_dcd_stm32.h"
#include "ux_device_cdc_acm.h"
#include "ux_device_mouse.h"
/* USER CODE END Includes */
Here all the code implementation is done! You can build and flash the code into your device and try it!
After flashing the code, connect a mini-USB cable into the user USB connector and connect it to the computer. Doing that, a USB composite device is enumerated with the two classes HID and CDC, as shown earlier.
Open a terminal to observe the message being transmitted every second via the VCOM port created. Additionally, sending the value 1 turns on the yellow LED, while sending the value 0 turns it off. Finally, pressing the user button moves the cursor diagonally to the bottom left of the screen.
Now, you have the needed knowledge to implement a composite class in the STM32 using the Azure USBX package. Here we presented the step-by-step to construct an HID + CDC, but the steps for opening other classes should be similar.
For details on how to use/implement the other device classes, refer to our GitHub page and the X-CUBE-AZRTOS package for your microcontroller family. Within, you may find a huge set of examples showing the usage of the most classes available.
Best wishes for your developments and hope you enjoyed this material!
Here are some related links that contain the material that was used to create this article, and can be helpful in your developments.
GitHub - X-CUBE-AZRTOS-H7 (Azure RTOS software expansion for STM32Cube)
STMicroelectronics - Introduction to USBX
STMicroelectronics - Azure RTOS
GitHub - STM32H7 USBX examples
Good article however bit more advanced articles would be preferable. Like CDC-ECM and CDC-ACM or CDC-ACM and storage. HID and CDC-ACM examples are already there.
See also https://youtu.be/wCRIpp-OHY4?si=568yHPHQMFHDTEYT and the whole related workshop.