STM32 AzureRTOS USBX MTP: Hierarchical folder structure not working (Folders appear empty)
- April 28, 2026
- 3 replies
- 215 views
Environment:
Board: STM32H7S78-DK
- MCU: STM32H7S7L8H6
- Stack: AzureRTOS USBX (PIMA MTP + CDC ACM) + FileX.
Storage: microSD Card.
Host OS: Windows 11
Preliminary Note:
A significant portion of this code is adapted from the official ST example:
Current Status:
I have successfully implemented the MTP class. The device is correctly recognized by Windows, the storage is mounted, and I can view and modify files located in the root directory.
To manage MTP objects, I use a custom function SD_Scan, which traverses the FileX filesystem and builds a local object handle table (ObjectHandleTypeDef).
VOID SD_Scan(VOID)
{
/* Estructura estática para no usar el STACK */
static FolderList_t folder_list[SD_MAX_HANDLES];
/* Variables de control de la cola con nombres claros */
UINT NextFolderToScan = 0;
UINT TotalFoldersInList = 0;
/* Variables de estado y buffers (Declaradas arriba por limpieza) */
UINT status;
CHAR file_name[FX_MAX_LONG_NAME_LEN];
UINT attributes;
ULONG size;
/* Variables temporales para la carpeta que estamos procesando ahora */
CHAR *current_path;
ULONG current_parent;
ULONG new_folder_handle;
/* 1. Empezamos añadiendo la raíz a la cola */
strcpy(folder_list[TotalFoldersInList].path, "/");
folder_list[TotalFoldersInList].parent_handle = 0;
TotalFoldersInList++;
/* 2. Bucle principal: mientras queden carpetas por explorar */
while (NextFolderToScan < TotalFoldersInList)
{
/* Sacamos los datos de la carpeta que toca escanear ahora */
current_path = folder_list[NextFolderToScan].path;
current_parent = folder_list[NextFolderToScan].parent_handle;
NextFolderToScan++;
/* Informamos a FileX de que queremos trabajar en esta carpeta */
fx_directory_default_set(&sdmcc_sd1, current_path);
/* Buscamos el primer elemento */
status = fx_directory_first_full_entry_find(&sdmcc_sd1, file_name, &attributes, &size,
FX_NULL, FX_NULL, FX_NULL, FX_NULL, FX_NULL, FX_NULL);
while (status == FX_SUCCESS)
{
/* Ignorar las entradas "." y ".." */
if (file_name[0] != '.')
{
if (attributes & FX_DIRECTORY)
{
/* --- ENCONTRAMOS UNA SUB-CARPETA --- */
SD_object_handles_counter++;
/* La registramos en nuestra tabla global de objetos MTP */
SD_Object_SetHandleInfo(&SD_object_handle_index_num, file_name, 0, current_parent, FORMAT_DIRECTORY);
/* Guardamos el Handle que acaba de recibir esta carpeta para sus futuros hijos */
new_folder_handle = SD_object_handle_index_num;
/* Si hay hueco en la cola, la guardamos para explorarla luego */
if (TotalFoldersInList < SD_MAX_HANDLES)
{
/* Construimos la ruta absoluta (Path) */
if (strcmp(current_path, "/") == 0)
sprintf(folder_list[TotalFoldersInList].path, "/%s", file_name);
else
sprintf(folder_list[TotalFoldersInList].path, "%s/%s", current_path, file_name);
/* El padre de lo que hay dentro de esta carpeta es el Handle que acabamos de crear */
folder_list[TotalFoldersInList].parent_handle = new_folder_handle;
TotalFoldersInList++;
}
}
else
{
/* --- ENCONTRAMOS UN ARCHIVO --- */
SD_object_handles_counter++;
ULONG format = SD_Object_GetFormatFromName(file_name);
SD_Object_SetHandleInfo(&SD_object_handle_index_num, file_name, size, current_parent, format);
}
}
/* Siguiente entrada en la carpeta actual */
status = fx_directory_next_full_entry_find(&sdmcc_sd1, file_name, &attributes, &size,
FX_NULL, FX_NULL, FX_NULL, FX_NULL, FX_NULL, FX_NULL);
}
}
}For simplicity, I am currently working only with:
- Text files
- Folder associations
I have debugged this function, and its behavior is as expected, so I do not believe it is the source of the issue.
I am also aware that the implementation of USBD_MTP_SendObjectInfo is currently incomplete. However, my immediate priority is to correctly display the file and folder hierarchy in the host. Until this is working properly, I do not plan to proceed further with additional MTP features.
Problem:
In the original ST example, the following function is used to return all object handles to the host:
/**
* @brief Object_GetHandlesIndex
* Get all object handle.
* @PAram Param3: current object handle
* @PAram obj_handle: all objects handle files in current object
* @retval number of object handle in current object
*/
ULONG Object_GetHandlesIndex(ULONG Param3, ULONG *obj_handle)
{
UINT index;
ULONG object_handle_num = 0;
if (Param3 == 0xFFFFFFFF)
{
for (index = 0; index < MTP_MAX_HANDLES; index++)
{
if (ObjectHandleInfo[index].object_handle_index != 0)
{
obj_handle[index] = ObjectHandleInfo[index].object_handle_index;
}
}
/* Return number of object in Root folder */
object_handle_num = object_handles_counter;
}
return object_handle_num;
}This implementation returns all objects when Param3 == 0xFFFFFFFF (root).
However, the issue is that Windows displays all files as if they were located in the root directory, ignoring the folder hierarchy.
Attempted Solution:
To address this, I modified the function as follows:
/**
* @brief SD_Object_GetHandlesIndex
* Get all object handle.
* @PAram Param3: current object handle
* @PAram obj_handle: all objects handle files in current object
* @retval number of object handle in current object
*/
ULONG SD_Object_GetHandlesIndex(ULONG Param3, ULONG *obj_handle)
{
UINT index;
ULONG object_handle_num = 0;
/* MTP usa 0 o 0xFFFFFFFF para referirse a la raíz (Root) */
ULONG target_parent = (Param3 == 0xFFFFFFFF) ? 0 : Param3;
for (index = 0; index < SD_MAX_HANDLES; index++)
{
/* Filtro: El objeto debe existir Y su padre debe ser el solicitado por el PC */
if (SD_ObjectHandleInfo[index].object_handle_index != 0 &&
SD_ObjectHandleInfo[index].object_property.object_parent_object == target_parent)
{
obj_handle[object_handle_num] = SD_ObjectHandleInfo[index].object_handle_index;
object_handle_num++;
}
}
return object_handle_num;
}The idea is to:
- Filter objects based on their parent (object_parent_object)
- Return only the handles that belong to the directory requested by the host
I also take into account that MTP uses:
- 0 or 0xFFFFFFFF → root
I implemented this approach based on the TinyUSB MTP example, where object handles are also filtered by parent to represent the directory hierarchy.
Although TinyUSB is a different middleware, both implementations rely on the same MTP protocol. Therefore, the underlying logic for handling object hierarchy should, in principle, be the same.
Initial Assumption (Incorrect):
I initially assumed that when navigating into a folder from the Windows Explorer:
USBD_MTP_GetObjectHandles would be called again
The host would request the handles corresponding to that specific folder
However, based on my observations, this is not how it actually works.
Current Behavior:
- If I return all handles in the first call (Param3 == 0xFFFFFFFF):
→ All files are visible, but without hierarchy - If I filter by parent (current implementation):
→ Only root files/foldes are visible
Main Question
How should GetObjectHandles be properly implemented in MTP so that Windows correctly displays a hierarchical folder structure?
I tried to read the Media Transfer Protocol v1.1, but I couldn’t find anything clearly useful for my case, probably due to my inexperience with this topic.
I will attach the full project to the post, but I’ll also include the modifications I’ve made to the ST example. I’ll try to list all of them, though I might miss some.
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define USBD_MTP_SUPPORTED_PROP \
UX_DEVICE_CLASS_PIMA_OBJECT_PROP_STORAGEID, \
UX_DEVICE_CLASS_PIMA_OBJECT_PROP_OBJECT_FORMAT, \
UX_DEVICE_CLASS_PIMA_OBJECT_PROP_PROTECTION_STATUS, \
UX_DEVICE_CLASS_PIMA_OBJECT_PROP_OBJECT_SIZE, \
UX_DEVICE_CLASS_PIMA_OBJECT_PROP_OBJECT_FILE_NAME, \
UX_DEVICE_CLASS_PIMA_OBJECT_PROP_PARENT_OBJECT, \
UX_DEVICE_CLASS_PIMA_OBJECT_PROP_PERSISTENT_UNIQUE_OBJECT_IDENTIFIER, \
UX_DEVICE_CLASS_PIMA_OBJECT_PROP_NAME
#define USBD_MTP_SUPPORTED_ASSOC_PROP \
UX_DEVICE_CLASS_PIMA_OBJECT_PROP_STORAGEID, \
UX_DEVICE_CLASS_PIMA_OBJECT_PROP_OBJECT_FORMAT, \
UX_DEVICE_CLASS_PIMA_OBJECT_PROP_PROTECTION_STATUS, \
UX_DEVICE_CLASS_PIMA_OBJECT_PROP_OBJECT_SIZE, \
UX_DEVICE_CLASS_PIMA_OBJECT_PROP_ASSOCIATION_TYPE, \
UX_DEVICE_CLASS_PIMA_OBJECT_PROP_ASSOCIATION_DESC, \
UX_DEVICE_CLASS_PIMA_OBJECT_PROP_OBJECT_FILE_NAME, \
UX_DEVICE_CLASS_PIMA_OBJECT_PROP_PARENT_OBJECT, \
UX_DEVICE_CLASS_PIMA_OBJECT_PROP_PERSISTENT_UNIQUE_OBJECT_IDENTIFIER, \
UX_DEVICE_CLASS_PIMA_OBJECT_PROP_NAME
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* Define PIMA supported device properties */
USHORT USBD_MTP_DevicePropSupported[] = {
/* USER CODE BEGIN USBD_MTP_DevicePropSupported */
UX_DEVICE_CLASS_PIMA_DEV_PROP_DEVICE_FRIENDLY_NAME,
/* USER CODE END USBD_MTP_DevicePropSupported */
0
};
/* Define PIMA supported capture formats */
USHORT USBD_MTP_DeviceSupportedCaptureFormats[] = {
/* USER CODE BEGIN USBD_MTP_DeviceSupportedCaptureFormats */
UX_DEVICE_CLASS_PIMA_OFC_ASSOCIATION,
UX_DEVICE_CLASS_PIMA_OFC_TEXT,
/* USER CODE END USBD_MTP_DeviceSupportedCaptureFormats */
0
};
/* Define PIMA supported image formats */
USHORT USBD_MTP_DeviceSupportedImageFormats[] = {
/* USER CODE BEGIN USBD_MTP_DeviceSupportedImageFormats */
UX_DEVICE_CLASS_PIMA_OFC_ASSOCIATION,
UX_DEVICE_CLASS_PIMA_OFC_TEXT,
/* USER CODE END USBD_MTP_DeviceSupportedImageFormats */
0
};
/* Object property supported
WORD 0 : Object Format Code
WORD 1 : Number of Prop codes for this Object format
WORD n : Prop Codes
WORD n+2 : Next Object Format code ....
*/
USHORT USBD_MTP_ObjectPropSupported[] = {
/* USER CODE BEGIN USBD_MTP_ObjectPropSupported */
/* Object format code : Text */
UX_DEVICE_CLASS_PIMA_OFC_TEXT,
/* NUmber of objects supported for this format */
8,
/* Mandatory objects for all formats */
USBD_MTP_SUPPORTED_PROP,
/* Object format code : Text */
UX_DEVICE_CLASS_PIMA_OFC_ASSOCIATION,
/* NUmber of objects supported for this format */
10,
/* Mandatory objects for all formats */
USBD_MTP_SUPPORTED_ASSOC_PROP,
};
/**
* @brief USBD_MTP_SetObjectPropValue
* This function is invoked when host requested to set object
* prop value.
* @PAram pima_instance : Pointer to the pima class instance.
* @PAram object_handle : Handle of the object.
* @PAram object_prop_code : Object property code.
* @PAram object_prop_value: Object property value.
* @PAram object_prop_value_length: Object property value length.
* @retval status
*/
UINT USBD_MTP_SetObjectPropValue(struct UX_SLAVE_CLASS_PIMA_STRUCT *pima_instance,
ULONG object_handle,
ULONG object_prop_code,
UCHAR *object_prop_value,
ULONG object_prop_value_length)
{
UINT status = UX_SUCCESS;
/* USER CODE BEGIN USBD_MTP_SetObjectPropValue */
UX_PARAMETER_NOT_USED(pima_instance);
UX_PARAMETER_NOT_USED(object_prop_value_length);
ULONG handle_index;
ObjectPropertyDataSetTypeDef *object_info;
/* Check the object handle exist */
status = SD_Object_HandleCheck(object_handle, &handle_index);
if (status == UX_SUCCESS)
{
/* Get object Property info */
SD_Object_GetHandleInfo(handle_index, (VOID**) &object_info);
/* switch object property code */
switch (object_prop_code)
{
case UX_DEVICE_CLASS_PIMA_OBJECT_PROP_OBJECT_FILE_NAME :
/* Copy the file name after translate from Unicode to ASCIIZ */
ux_utility_unicode_to_string(object_prop_value, object_info->object_file_full_name);
status = UX_SUCCESS;
break;
case UX_DEVICE_CLASS_PIMA_OBJECT_PROP_NAME :
/* Copy the name after translate from Unicode to ASCIIZ */
ux_utility_unicode_to_string(object_prop_value, object_info->object_file_name);
status = UX_SUCCESS;
break;
case UX_DEVICE_CLASS_PIMA_OBJECT_PROP_STORAGEID :
case UX_DEVICE_CLASS_PIMA_OBJECT_PROP_OBJECT_FORMAT :
case UX_DEVICE_CLASS_PIMA_OBJECT_PROP_OBJECT_SIZE :
case UX_DEVICE_CLASS_PIMA_OBJECT_PROP_ASSOCIATION_TYPE :
case UX_DEVICE_CLASS_PIMA_OBJECT_PROP_ASSOCIATION_DESC :
case UX_DEVICE_CLASS_PIMA_OBJECT_PROP_PARENT_OBJECT :
/* Object is write protected */
status = UX_DEVICE_CLASS_PIMA_RC_OBJECT_WRITE_PROTECTED;
break;
default :
status = UX_DEVICE_CLASS_PIMA_RC_INVALID_OBJECT_PROP_CODE;
break;
}
}
else
{
/* Invalid object handle */
status = UX_DEVICE_CLASS_PIMA_RC_INVALID_OBJECT_HANDLE;
}
/* USER CODE END USBD_MTP_SetObjectPropValue */
return status;
}
/**
* @brief MTP_GetObjectHandle
* This function is called to get pima mtp object struct.
* @PAram object_handle: object handle.
* @PAram object: pima mtp object struct.
* @retval status
*/
UINT MTP_GetObjectHandle(ULONG object_handle, UX_SLAVE_CLASS_PIMA_OBJECT **object)
{
UINT status, index;
for (index = 0; index < SD_object_handles_counter; index++)
{
if (SD_ObjectHandleInfo[index].object_handle_index == object_handle)
{
ux_utility_memory_set(&MTP_Object, 0, sizeof(MTP_Object));
/* Fill MTP object struct */
MTP_Object.ux_device_class_pima_object_storage_id = SD_ObjectHandleInfo[index].object_property.object_storage_id;
MTP_Object.ux_device_class_pima_object_format = SD_ObjectHandleInfo[index].object_property.object_format;
MTP_Object.ux_device_class_pima_object_compressed_size = SD_ObjectHandleInfo[index].object_property.object_size;
MTP_Object.ux_device_class_pima_object_protection_status = SD_ObjectHandleInfo[index].object_property.object_protection_status;
MTP_Object.ux_device_class_pima_object_thumb_format = UX_DEVICE_CLASS_PIMA_OFC_UNDEFINED;
MTP_Object.ux_device_class_pima_object_thumb_compressed_size = 0U;
MTP_Object.ux_device_class_pima_object_thumb_pix_height = 0U;
MTP_Object.ux_device_class_pima_object_thumb_pix_width = 0U;
MTP_Object.ux_device_class_pima_object_image_pix_height = 0U;
MTP_Object.ux_device_class_pima_object_image_pix_width = 0U;
MTP_Object.ux_device_class_pima_object_image_bit_depth = 0U;
MTP_Object.ux_device_class_pima_object_parent_object = SD_ObjectHandleInfo[index].object_property.object_parent_object;
if (MTP_Object.ux_device_class_pima_object_format == UX_DEVICE_CLASS_PIMA_OFC_ASSOCIATION)
{
/* Generic folder association type (MTP/PTP value 0x0001). */
MTP_Object.ux_device_class_pima_object_association_type = 1U;
}
else
{
MTP_Object.ux_device_class_pima_object_association_type = 0U;
}
MTP_Object.ux_device_class_pima_object_association_desc = 0U;
MTP_Object.ux_device_class_pima_object_sequence_number = 0U;
ux_utility_string_to_unicode(SD_ObjectHandleInfo[index].object_property.object_file_full_name,
MTP_Object.ux_device_class_pima_object_filename);
*object = &MTP_Object;
status = UX_SUCCESS;
}
}
return status;
}Conclusion
The SD card scanning works correctly, and the object table appears consistent.
The issue lies in how object handles are managed and returned to the host, particularly regarding hierarchy handling.
I would appreciate guidance from someone with more experience in MTP or USBX regarding:
- The correct host-device interaction flow
- How GetObjectHandles should be implemented to support hierarchical browsing in Windows
