USBX Basics
USBX is the USB stack for the ThreadX RTOS. It supports multiple host drivers, device enumeration, and OTG.
The open-source repository is here:
The introduction is here:
If you use ThreadX RTOS, using this USB stack as either a host or a device by itself is very simple. However, if you want to use both host and device features (OTG) at the same time, it gets a bit harder, because the official documentation only mentions it in passing, and no forum has a complete, detailed example.
USBX's OTG does not mean hardware OTG; it means OTG at the protocol layer. In other words, the host and device features of the USBX stack can be used at the same time, so that host and device objects can both work properly. USBX does not care whether the hardware layer has one interface or multiple interfaces.
First, confirm that you have the following abilities:
- A basic understanding of how to use macros to configure a C library and control the behavior of code.
- The ability to read the official examples, that is, to understand what every line in the main flow of the official examples does.
- The ability to configure chip peripherals manually or with the help of tools. The peripheral driver part is not covered later.
Basic Usage of the Library
The following is the basic pattern for starting the host stack. Overall, you define a memory region as a byte pool, allocate a block from it for the USBX system, register the host classes you need, and allocate another block to start the host application thread. For what to do in the host application thread, refer to the official examples.
#define UX_HOST_APP_MEM_POOL_SIZE 1024 * 44
__ALIGN_BEGIN static UCHAR ux_host_byte_pool_buffer[UX_HOST_APP_MEM_POOL_SIZE] __ALIGN_END;
static TX_BYTE_POOL ux_host_app_byte_pool;
void txAppUSBXHostInit(void)
{
UINT status = TX_SUCCESS;
VOID *memory_ptr;
if(tx_byte_pool_create(&ux_host_app_byte_pool, "Ux App memory pool", ux_host_byte_pool_buffer, UX_HOST_APP_MEM_POOL_SIZE) != TX_SUCCESS)
{
}
else
{
memory_ptr = (VOID *)&ux_host_app_byte_pool;
status = MX_USBX_Host_Init(memory_ptr);
if(status != UX_SUCCESS)
{
while(1)
{
}
}
}
}
UINT MX_USBX_Host_Init(VOID *memory_ptr)
{
UINT ret = UX_SUCCESS;
UCHAR *pointer;
TX_BYTE_POOL *byte_pool = (TX_BYTE_POOL *)memory_ptr;
if(tx_byte_allocate(byte_pool, (VOID **)&pointer, USBX_HOST_MEMORY_STACK_SIZE, TX_NO_WAIT) != TX_SUCCESS)
{
return TX_POOL_ERROR;
}
if(ux_system_initialize(pointer, USBX_HOST_MEMORY_STACK_SIZE, UX_NULL, 0) != UX_SUCCESS)
{
return UX_ERROR;
}
if(ux_host_stack_initialize(ux_host_event_callback) != UX_SUCCESS)
{
return UX_ERROR;
}
ux_utility_error_callback_register(&ux_host_error_callback);
if(ux_host_stack_class_register(_ux_system_host_class_storage_name, ux_host_class_storage_entry) != UX_SUCCESS)
{
return UX_ERROR;
}
if(tx_byte_allocate(byte_pool, (VOID **)&pointer, UX_HOST_APP_THREAD_STACK_SIZE, TX_NO_WAIT) != TX_SUCCESS)
{
return TX_POOL_ERROR;
}
if(tx_thread_create(&ux_host_app_thread, UX_HOST_APP_THREAD_NAME, app_ux_host_thread_entry, 0, pointer, UX_HOST_APP_THREAD_STACK_SIZE, UX_HOST_APP_THREAD_PRIO, UX_HOST_APP_THREAD_PREEMPTION_THRESHOLD, UX_HOST_APP_THREAD_TIME_SLICE, UX_HOST_APP_THREAD_START_OPTION) != TX_SUCCESS)
{
return TX_THREAD_ERROR;
}
return ret;
}
The following is the basic pattern for starting the device stack. Overall, it is similar to the host operation: create a byte pool, allocate memory, initialize and register, then start the device application thread.
#define UX_DEVICE_APP_MEM_POOL_SIZE 1024 * 24
__ALIGN_BEGIN static UCHAR ux_device_byte_pool_buffer[UX_DEVICE_APP_MEM_POOL_SIZE] __ALIGN_END;
static TX_BYTE_POOL ux_device_app_byte_pool;
void txAppUSBXDrviceInit(void)
{
UINT status = TX_SUCCESS;
VOID *memory_ptr;
if(tx_byte_pool_create(&ux_device_app_byte_pool, "Ux App memory pool", ux_device_byte_pool_buffer, UX_DEVICE_APP_MEM_POOL_SIZE) != TX_SUCCESS)
{
}
else
{
memory_ptr = (VOID *)&ux_device_app_byte_pool;
status = MX_USBX_Device_Init(memory_ptr);
if(status != UX_SUCCESS)
{
while(1)
{
}
}
}
}
UINT MX_USBX_Device_Init(VOID *memory_ptr)
{
UINT ret = UX_SUCCESS;
UCHAR *device_framework_high_speed;
UCHAR *device_framework_full_speed;
ULONG device_framework_hs_length;
ULONG device_framework_fs_length;
ULONG string_framework_length;
ULONG language_id_framework_length;
UCHAR *string_framework;
UCHAR *language_id_framework;
UCHAR *pointer;
TX_BYTE_POOL *byte_pool = (TX_BYTE_POOL *)memory_ptr;
if(tx_byte_allocate(byte_pool, (VOID **)&pointer, USBX_DEVICE_MEMORY_STACK_SIZE, TX_NO_WAIT) != TX_SUCCESS)
{
return TX_POOL_ERROR;
}
if(ux_system_initialize(pointer, USBX_DEVICE_MEMORY_STACK_SIZE, UX_NULL, 0) != UX_SUCCESS)
{
return UX_ERROR;
}
device_framework_high_speed = USBD_Get_Device_Framework_Speed(USBD_HIGH_SPEED, &device_framework_hs_length);
device_framework_full_speed = USBD_Get_Device_Framework_Speed(USBD_FULL_SPEED, &device_framework_fs_length);
string_framework = USBD_Get_String_Framework(&string_framework_length);
language_id_framework = USBD_Get_Language_Id_Framework(&language_id_framework_length);
if(ux_device_stack_initialize(device_framework_high_speed, device_framework_hs_length, device_framework_full_speed, device_framework_fs_length, string_framework, string_framework_length, language_id_framework, language_id_framework_length, UX_NULL) != UX_SUCCESS)
{
return UX_ERROR;
}
cdc_acm_parameter.ux_slave_class_cdc_acm_instance_activate = USBD_CDC_ACM_Activate;
cdc_acm_parameter.ux_slave_class_cdc_acm_instance_deactivate = USBD_CDC_ACM_Deactivate;
cdc_acm_parameter.ux_slave_class_cdc_acm_parameter_change = USBD_CDC_ACM_ParameterChange;
cdc_acm_configuration_number = USBD_Get_Configuration_Number(CLASS_TYPE_CDC_ACM, 0);
cdc_acm_interface_number = USBD_Get_Interface_Number(CLASS_TYPE_CDC_ACM, 0);
if(ux_device_stack_class_register(_ux_system_slave_class_cdc_acm_name, ux_device_class_cdc_acm_entry, cdc_acm_configuration_number, cdc_acm_interface_number, &cdc_acm_parameter) != UX_SUCCESS)
{
return UX_ERROR;
}
if(tx_byte_allocate(byte_pool, (VOID **)&pointer, UX_DEVICE_APP_THREAD_STACK_SIZE, TX_NO_WAIT) != TX_SUCCESS)
{
return TX_POOL_ERROR;
}
if(tx_thread_create(&ux_device_app_thread, UX_DEVICE_APP_THREAD_NAME, app_ux_device_thread_entry, 0, pointer, UX_DEVICE_APP_THREAD_STACK_SIZE, UX_DEVICE_APP_THREAD_PRIO, UX_DEVICE_APP_THREAD_PREEMPTION_THRESHOLD, UX_DEVICE_APP_THREAD_TIME_SLICE, UX_DEVICE_APP_THREAD_START_OPTION) != TX_SUCCESS)
{
return TX_THREAD_ERROR;
}
if(tx_byte_allocate(byte_pool, (VOID **)&pointer, 1024, TX_NO_WAIT) != TX_SUCCESS)
{
return TX_POOL_ERROR;
}
if(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) != TX_SUCCESS)
{
return TX_THREAD_ERROR;
}
return ret;
}
Special Steps for Enabling USBX OTG
First, when using USBX, it is a good practice to add a global compile-time define: UX_INCLUDE_USER_DEFINE_FILE. You can use ux_user.h to trim and configure USBX features. In the library source there should be a file named ux_user_sample.h; copy it, rename it to ux_user.h, and add it to your project.
It should contain something similar to the following. This is the main source of confusion: in the original sample, the UX_OTG_SUPPORT define is commented out. In your own ux_user.h, you need to uncomment UX_OTG_SUPPORT and comment out the UX_HOST_SIDE_ONLY and UX_DEVICE_SIDE_ONLY defines. This enables OTG support in the library. The UX_DEVICE_BIDIRECTIONAL_ENDPOINT_SUPPORT and UX_DEVICE_CLASS_CDC_ACM_WRITE_AUTO_ZLP macros are also recommended.
#ifndef UX_HOST_SIDE_ONLY
#ifndef UX_DEVICE_SIDE_ONLY
/* #define UX_OTG_SUPPORT */
#endif
#endif
Note that in the basic patterns above, both the host and device init flows call ux_system_initialize. This is exactly why copying the examples directly, or using CubeMX to generate a project that includes both host and device, doesn't work. ux_system_initialize must not be called twice; if it is, one side will malfunction and get stuck in an internal processing thread. The correct pattern is to predefine three memory regions and use three byte pools. Take a block from the first pool and call ux_system_initialize first, then use the second and third byte pools for the host and device initialization. Do not call ux_system_initialize again during those flows. After that, everything works normally.
Additional Details
The driverless CDC_ACM serial port on Windows always uses endpoints 0x01 and 0x81 for IN/OUT. Do not change these endpoint numbers.
Some packets require the length to be 4-byte aligned. Here is an excellent zero-padding method:
uint8_t *data; // 原有报文buffer
uint32_t len = OriginalLen; // 原有长度
for(uint32_t i = len; i < ((len + 0x3) & ~0x3U); i++)
{
data[i] = 0x00;
}
_write(data, ((len + 0x3) & ~0x3U));
For composite devices, it is best to modify the descriptors based on the ux_device_descriptors.c generated by CubeMX. Actually, the descriptor-generation source code is really excellent.
Note that when an endpoint is used, the corresponding USB TxFIFO must be enabled correctly. Here is an example:
/* Set Rx FIFO */
HAL_PCDEx_SetRxFiFo(&hpcd_USB_OTG_FS, 0x200);
/* Set Tx FIFO 0 */
HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_FS, 0, 0x80); // 通用的 setup 端点
/* Set Tx FIFO 1 */
HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_FS, 1, 0x100); // 开启了 0x01 和 0x81 端点
/* Set Tx FIFO 3 */
HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_FS, 3, 0x100); // 开启了 0x03 和 0x83 端点