diff --git a/components/usb/include/usb/usb_host.h b/components/usb/include/usb/usb_host.h index ba44c584d1..823a42b158 100644 --- a/components/usb/include/usb/usb_host.h +++ b/components/usb/include/usb/usb_host.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -346,6 +346,35 @@ esp_err_t usb_host_get_device_descriptor(usb_device_handle_t dev_hdl, const usb_ */ esp_err_t usb_host_get_active_config_descriptor(usb_device_handle_t dev_hdl, const usb_config_desc_t **config_desc); +/** + * @brief Get get device's configuration descriptor + * + * - The USB Host library only caches a device's active configuration descriptor. + * - This function reads any configuration descriptor of a particular device (specified by bConfigurationValue). + * - This function will read the specified configuration descriptor via control transfers, and allocate memory to store that descriptor. + * - Users can call usb_host_get_config_desc_free() to free the descriptor's memory afterwards. + * + * @note This function can block + * @note A client must call usb_host_device_open() on the device first + * @param[in] client_hdl Client handle - usb_host_client_handle_events() should be called repeatedly in a separate task to handle client events + * @param[in] dev_hdl Device handle + * @param[out] config_desc_ret Returned configuration descriptor + * @param[in] bConfigurationValue Index of device's configuration descriptor to be read + * @note bConfigurationValue starts from index 1 + * @return esp_err_t + */ +esp_err_t usb_host_get_config_desc(usb_host_client_handle_t client_hdl, usb_device_handle_t dev_hdl, uint8_t bConfigurationValue, const usb_config_desc_t **config_desc_ret); + +/** + * @brief Free a configuration descriptor + * + * This function frees a configuration descriptor that was returned by usb_host_get_config_desc() + * + * @param[out] config_desc Configuration descriptor + * @return esp_err_t + */ +esp_err_t usb_host_get_config_desc_free(const usb_config_desc_t *config_desc); + // ----------------------------------------------- Interface Functions ------------------------------------------------- /** diff --git a/components/usb/test_apps/usb_host/main/multiconf_client.h b/components/usb/test_apps/usb_host/main/multiconf_client.h new file mode 100644 index 0000000000..b5af74db74 --- /dev/null +++ b/components/usb/test_apps/usb_host/main/multiconf_client.h @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +typedef struct { + SemaphoreHandle_t dev_open_smp; + uint8_t bConfigurationValue; +} multiconf_client_test_param_t; + +/** + * @brief Multiconfiguration client task + */ +void multiconf_client_async_task(void *arg); + +/** + * @brief Get configuration descriptor + */ +void multiconf_client_get_conf_desc(void); diff --git a/components/usb/test_apps/usb_host/main/multiconf_client_async.c b/components/usb/test_apps/usb_host/main/multiconf_client_async.c new file mode 100644 index 0000000000..26e67069a9 --- /dev/null +++ b/components/usb/test_apps/usb_host/main/multiconf_client_async.c @@ -0,0 +1,179 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "esp_err.h" +#include "esp_log.h" +#include "test_usb_common.h" +#include "multiconf_client.h" +#include "mock_msc.h" +#include "dev_msc.h" +#include "usb/usb_host.h" +#include "unity.h" + +/* +Implementation of a multi-configuration client used for USB Host Tests. + +- The multi-configuration client will: + - Register itself as a client + - Receive USB_HOST_CLIENT_EVENT_NEW_DEV event message, and open the device + - Get active configuration descriptor + - Start handling client events + - Wait for a request from main task to read a configuration descriptor + - Compare the obtained configuration descriptor with the active configuration descriptor + - Free the memory used for storing the configuration descriptor + - Close the device + - Deregister control client +*/ + +const char *MULTICONF_CLIENT_TAG = "Multi config Client"; + +#define CLIENT_NUM_EVENT_MSG 5 + +typedef enum { + TEST_STAGE_WAIT_CONN, + TEST_STAGE_DEV_OPEN, + TEST_STAGE_WAIT, + TEST_STAGE_CHECK_CONFIG_DESC, + TEST_STAGE_DEV_CLOSE, +} test_stage_t; + +typedef struct { + // Test parameters + multiconf_client_test_param_t test_param; + // device info + uint8_t dev_addr; + usb_speed_t dev_speed; + // Client variables + usb_host_client_handle_t client_hdl; + usb_device_handle_t dev_hdl; + // Test state + test_stage_t cur_stage; + test_stage_t next_stage; + const usb_config_desc_t *config_desc_cached; +} multiconf_client_obj_t; + +static multiconf_client_obj_t *s_multiconf_obj; + +static void multiconf_client_event_cb(const usb_host_client_event_msg_t *event_msg, void *arg) +{ + multiconf_client_obj_t *multiconf_obj = (multiconf_client_obj_t *)arg; + switch (event_msg->event) { + case USB_HOST_CLIENT_EVENT_NEW_DEV: + TEST_ASSERT_EQUAL(TEST_STAGE_WAIT_CONN, multiconf_obj->cur_stage); + multiconf_obj->next_stage = TEST_STAGE_DEV_OPEN; + multiconf_obj->dev_addr = event_msg->new_dev.address; + break; + default: + abort(); // Should never occur in this test + break; + } +} + +void multiconf_client_async_task(void *arg) +{ + multiconf_client_obj_t multiconf_obj; + // Initialize test params + memcpy(&multiconf_obj.test_param, arg, sizeof(multiconf_client_test_param_t)); + // Initialize client variables + multiconf_obj.client_hdl = NULL; + multiconf_obj.dev_hdl = NULL; + // Initialize test stage + multiconf_obj.cur_stage = TEST_STAGE_WAIT_CONN; + multiconf_obj.next_stage = TEST_STAGE_WAIT_CONN; + multiconf_obj.dev_addr = 0; + + // Register client + usb_host_client_config_t client_config = { + .is_synchronous = false, + .max_num_event_msg = CLIENT_NUM_EVENT_MSG, + .async = { + .client_event_callback = multiconf_client_event_cb, + .callback_arg = (void *) &multiconf_obj, + }, + }; + TEST_ASSERT_EQUAL(ESP_OK, usb_host_client_register(&client_config, &multiconf_obj.client_hdl)); + s_multiconf_obj = &multiconf_obj; + + // Wait to be started by main thread + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + ESP_LOGD(MULTICONF_CLIENT_TAG, "Starting"); + + bool exit_loop = false; + bool skip_event_handling = false; + while (!exit_loop) { + if (!skip_event_handling) { + TEST_ASSERT_EQUAL(ESP_OK, usb_host_client_handle_events(multiconf_obj.client_hdl, portMAX_DELAY)); + } + skip_event_handling = false; + if (multiconf_obj.cur_stage == multiconf_obj.next_stage) { + continue; + } + multiconf_obj.cur_stage = multiconf_obj.next_stage; + + switch (multiconf_obj.next_stage) { + case TEST_STAGE_DEV_OPEN: { + ESP_LOGD(MULTICONF_CLIENT_TAG, "Open"); + // Open the device + TEST_ASSERT_EQUAL_MESSAGE(ESP_OK, usb_host_device_open(multiconf_obj.client_hdl, multiconf_obj.dev_addr, &multiconf_obj.dev_hdl), "Failed to open the device"); + + // Get device info to get it's speed + usb_device_info_t dev_info; + TEST_ASSERT_EQUAL(ESP_OK, usb_host_device_info(multiconf_obj.dev_hdl, &dev_info)); + multiconf_obj.dev_speed = dev_info.speed; + + multiconf_obj.next_stage = TEST_STAGE_WAIT; + skip_event_handling = true; + break; + } + case TEST_STAGE_WAIT: { + // Give semaphore signalizing that the device has been opened + xSemaphoreGive(multiconf_obj.test_param.dev_open_smp); + break; + } + case TEST_STAGE_CHECK_CONFIG_DESC: { + ESP_LOGD(MULTICONF_CLIENT_TAG, "Check config descriptors"); + // Get mocked config descriptor + const usb_config_desc_t *config_desc_ref = dev_msc_get_config_desc(multiconf_obj.dev_speed); + TEST_ASSERT_EQUAL_MESSAGE(multiconf_obj.config_desc_cached->wTotalLength, config_desc_ref->wTotalLength, "Incorrect length of CFG descriptor"); + TEST_ASSERT_EQUAL_MEMORY_MESSAGE(config_desc_ref, multiconf_obj.config_desc_cached, sizeof(usb_config_desc_t), "Configuration descriptors do not match"); + + // Free the memory used to store the config descriptor + TEST_ASSERT_EQUAL(ESP_OK, usb_host_get_config_desc_free(multiconf_obj.config_desc_cached)); + multiconf_obj.next_stage = TEST_STAGE_DEV_CLOSE; + skip_event_handling = true; + break; + } + case TEST_STAGE_DEV_CLOSE: { + ESP_LOGD(MULTICONF_CLIENT_TAG, "Close"); + vTaskDelay(10); // Give USB Host Lib some time to process all transfers + TEST_ASSERT_EQUAL(ESP_OK, usb_host_device_close(multiconf_obj.client_hdl, multiconf_obj.dev_hdl)); + exit_loop = true; + break; + } + default: + abort(); + break; + } + } + TEST_ASSERT_EQUAL(ESP_OK, usb_host_client_deregister(multiconf_obj.client_hdl)); + ESP_LOGD(MULTICONF_CLIENT_TAG, "Done"); + vTaskDelete(NULL); +} + +void multiconf_client_get_conf_desc(void) +{ + // Get configuration descriptor, ctrl transfer is sent to the device to get the config descriptor + TEST_ASSERT_EQUAL(ESP_OK, usb_host_get_config_desc(s_multiconf_obj->client_hdl, s_multiconf_obj->dev_hdl, s_multiconf_obj->test_param.bConfigurationValue, &s_multiconf_obj->config_desc_cached)); + + // Go to next stage + s_multiconf_obj->next_stage = TEST_STAGE_CHECK_CONFIG_DESC; + ESP_ERROR_CHECK(usb_host_client_unblock(s_multiconf_obj->client_hdl)); +} diff --git a/components/usb/test_apps/usb_host/main/test_app_main.c b/components/usb/test_apps/usb_host/main/test_app_main.c index 93e2510374..de47f0b0f3 100644 --- a/components/usb/test_apps/usb_host/main/test_app_main.c +++ b/components/usb/test_apps/usb_host/main/test_app_main.c @@ -33,6 +33,8 @@ void tearDown(void) vTaskDelay(10); // Clean up USB Host ESP_ERROR_CHECK(usb_host_uninstall()); + // Short delay to allow task to be cleaned up after client uninstall + vTaskDelay(10); test_usb_deinit_phy(); // Deinitialize the internal USB PHY after testing unity_utils_evaluate_leaks(); } diff --git a/components/usb/test_apps/usb_host/main/test_usb_host_async.c b/components/usb/test_apps/usb_host/main/test_usb_host_async.c index c7329d91fa..0dac7076f5 100644 --- a/components/usb/test_apps/usb_host/main/test_usb_host_async.c +++ b/components/usb/test_apps/usb_host/main/test_usb_host_async.c @@ -14,6 +14,7 @@ #include "dev_msc.h" #include "msc_client.h" #include "ctrl_client.h" +#include "multiconf_client.h" #include "usb/usb_host.h" #include "unity.h" @@ -21,6 +22,7 @@ #define TEST_MSC_NUM_SECTORS_PER_XFER 2 #define TEST_MSC_SCSI_TAG 0xDEADBEEF #define TEST_CTRL_NUM_TRANSFERS 30 +#define B_CONFIGURATION_VALUE 1 // --------------------------------------------------- Test Cases ------------------------------------------------------ @@ -275,3 +277,66 @@ TEST_CASE("Test USB Host async API", "[usb_host][full_speed][low_speed]") vTaskDelay(10); } } + +/* +Test USB Host Asynchronous API single client + +Purpose: + - Test that client can read configuration descriptor by request + +Procedure: + - Install USB Host Library + - Create a task to run a multiconfig client + - Create a task to handle system events + - Start the MSC client task. It will open the device and start handling client events + - Wait for the main task requests client to read configuration descriptor + - Compare the requested configuration descriptor with the active configuration descriptor + - Wait for the host library event handler to report a USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS event + - Free all devices + - Uninstall USB Host Library +*/ +static void host_lib_task(void *arg) +{ + while (1) { + // Start handling system events + uint32_t event_flags; + usb_host_lib_handle_events(portMAX_DELAY, &event_flags); + if (event_flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS) { + printf("No more clients\n"); + TEST_ASSERT_EQUAL(ESP_ERR_NOT_FINISHED, usb_host_device_free_all()); + } + if (event_flags & USB_HOST_LIB_EVENT_FLAGS_ALL_FREE) { + break; + } + } + + printf("Deleting host_lib_task\n"); + vTaskDelete(NULL); +} + +TEST_CASE("Test USB Host multiconfig client (single client)", "[usb_host][full_speed][high_speed]") +{ + SemaphoreHandle_t dev_open_smp = xSemaphoreCreateBinary(); + TaskHandle_t client_task; + + multiconf_client_test_param_t multiconf_params = { + .dev_open_smp = dev_open_smp, + .bConfigurationValue = B_CONFIGURATION_VALUE, + }; + + xTaskCreatePinnedToCore(multiconf_client_async_task, "async client", 4096, (void*)&multiconf_params, 2, &client_task, 0); + TEST_ASSERT_NOT_NULL_MESSAGE(client_task, "Failed to create async client task"); + // Start the task + xTaskNotifyGive(client_task); + + TaskHandle_t host_lib_task_hdl; + xTaskCreatePinnedToCore(host_lib_task, "host lib", 4096, NULL, 2, &host_lib_task_hdl, 0); + TEST_ASSERT_NOT_NULL_MESSAGE(host_lib_task_hdl, "Failed to create host lib task"); + + // Wait for the device to be open + xSemaphoreTake(dev_open_smp, portMAX_DELAY); + multiconf_client_get_conf_desc(); + + // Cleanup + vSemaphoreDelete(dev_open_smp); +} diff --git a/components/usb/usb_host.c b/components/usb/usb_host.c index b2e5ed137a..18909d4ab2 100644 --- a/components/usb/usb_host.c +++ b/components/usb/usb_host.c @@ -10,6 +10,7 @@ Warning: The USB Host Library API is still a beta version and may be subject to #include #include +#include #include "sdkconfig.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -52,6 +53,9 @@ static portMUX_TYPE host_lock = portMUX_INITIALIZER_UNLOCKED; #define ENABLE_ENUM_FILTER_CALLBACK #endif // CONFIG_USB_HOST_ENABLE_ENUM_FILTER_CALLBACK +#define SHORT_DESC_REQ_LEN 8 +#define CTRL_TRANSFER_MAX_DATA_LEN CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE + typedef struct ep_wrapper_s ep_wrapper_t; typedef struct interface_s interface_t; typedef struct client_s client_t; @@ -349,6 +353,12 @@ static bool endpoint_callback(usbh_ep_handle_t ep_hdl, usbh_ep_event_t ep_event, return yield; } +static void get_config_desc_transfer_cb(usb_transfer_t *transfer) +{ + SemaphoreHandle_t transfer_done = (SemaphoreHandle_t)transfer->context; + xSemaphoreGive(transfer_done); +} + // ------------------------------------------------ Library Functions -------------------------------------------------- // ----------------------- Public -------------------------- @@ -945,6 +955,135 @@ esp_err_t usb_host_get_active_config_descriptor(usb_device_handle_t dev_hdl, con return usbh_dev_get_config_desc(dev_hdl, config_desc); } +// ----------------- Descriptors Transfer Requests -------------------- + +static usb_transfer_status_t wait_for_transmission_done(usb_transfer_t *transfer) +{ + SemaphoreHandle_t transfer_done = (SemaphoreHandle_t)transfer->context; + xSemaphoreTake(transfer_done, portMAX_DELAY); + usb_transfer_status_t status = transfer->status; + + // EP0 halt->flush->clear is managed by USBH and lower layers + return status; +} + +static esp_err_t get_config_desc_transfer(usb_host_client_handle_t client_hdl, usb_transfer_t *ctrl_transfer, const int bConfigurationValue, const int num_bytes) +{ + const usb_device_desc_t *dev_desc; + ESP_ERROR_CHECK(usbh_dev_get_desc(ctrl_transfer->device_handle, &dev_desc)); + + usb_setup_packet_t *setup_pkt = (usb_setup_packet_t *)ctrl_transfer->data_buffer; + USB_SETUP_PACKET_INIT_GET_CONFIG_DESC(setup_pkt, bConfigurationValue - 1, num_bytes); + ctrl_transfer->num_bytes = sizeof(usb_setup_packet_t) + usb_round_up_to_mps(num_bytes, dev_desc->bMaxPacketSize0); + + // IN data stage should return exactly num_bytes (SHORT_DESC_REQ_LEN or wTotalLength) bytes + const int expect_num_bytes = sizeof(usb_setup_packet_t) + num_bytes; + + // Submit control transfer + esp_err_t ret = usb_host_transfer_submit_control(client_hdl, ctrl_transfer); + if (ret != ESP_OK) { + ESP_LOGE(USB_HOST_TAG, "Submit ctrl transfer failed"); + return ret; + } + + // Wait for transfer to finish + const usb_transfer_status_t status_short_desc = wait_for_transmission_done(ctrl_transfer); + if (status_short_desc != USB_TRANSFER_STATUS_COMPLETED) { + ESP_LOGE(USB_HOST_TAG, "Get config descriptor transfer status: %d", status_short_desc); + ret = ESP_ERR_INVALID_STATE; + return ret; + } + + // Check IN transfer returned the expected correct number of bytes + if ((expect_num_bytes != 0) && (ctrl_transfer->actual_num_bytes != expect_num_bytes)) { + if (ctrl_transfer->actual_num_bytes > expect_num_bytes) { + // The device returned more bytes than requested. + // This violates the USB specs chapter 9.3.5, but we can continue + ESP_LOGW(USB_HOST_TAG, "Incorrect number of bytes returned %d", ctrl_transfer->actual_num_bytes); + return ESP_OK; + } else { + // The device returned less bytes than requested. We cannot continue. + ESP_LOGE(USB_HOST_TAG, "Incorrect number of bytes returned %d", ctrl_transfer->actual_num_bytes); + return ESP_ERR_INVALID_RESPONSE; + } + } + return ESP_OK; +} + +esp_err_t usb_host_get_config_desc(usb_host_client_handle_t client_hdl, usb_device_handle_t dev_hdl, uint8_t bConfigurationValue, const usb_config_desc_t **config_desc_ret) +{ + esp_err_t ret = ESP_OK; + HOST_CHECK(client_hdl != NULL && dev_hdl != NULL && config_desc_ret != NULL, ESP_ERR_INVALID_ARG); + + // Get number of configurations + const usb_device_desc_t *dev_desc; + ESP_ERROR_CHECK(usbh_dev_get_desc(dev_hdl, &dev_desc)); + + HOST_CHECK(bConfigurationValue != 0, ESP_ERR_INVALID_ARG); + HOST_CHECK(bConfigurationValue <= dev_desc->bNumConfigurations, ESP_ERR_NOT_SUPPORTED); + + // Initialize transfer + usb_transfer_t *ctrl_transfer; + if (usb_host_transfer_alloc(sizeof(usb_setup_packet_t) + CTRL_TRANSFER_MAX_DATA_LEN, 0, &ctrl_transfer)) { + return ESP_ERR_NO_MEM; + } + + SemaphoreHandle_t transfer_done = xSemaphoreCreateBinary(); + if (transfer_done == NULL) { + ret = ESP_ERR_NO_MEM; + goto exit; + } + + ctrl_transfer->device_handle = dev_hdl; + ctrl_transfer->bEndpointAddress = 0; + ctrl_transfer->callback = get_config_desc_transfer_cb; + ctrl_transfer->context = (void *)transfer_done; + + // Initiate control transfer for short config descriptor + ret = get_config_desc_transfer(client_hdl, ctrl_transfer, bConfigurationValue, SHORT_DESC_REQ_LEN); + if (ret != ESP_OK) { + goto exit; + } + + // Get length of full config descriptor + const usb_config_desc_t *config_desc_short = (usb_config_desc_t *)(ctrl_transfer->data_buffer + sizeof(usb_setup_packet_t)); + + // Initiate control transfer for full config descriptor + ret = get_config_desc_transfer(client_hdl, ctrl_transfer, bConfigurationValue, config_desc_short->wTotalLength); + if (ret != ESP_OK) { + goto exit; + } + + // Allocate memory to store the configuration descriptor + const usb_config_desc_t *config_desc_full = (usb_config_desc_t *)(ctrl_transfer->data_buffer + sizeof(usb_setup_packet_t)); + usb_config_desc_t *config_desc = heap_caps_malloc(config_desc_full->wTotalLength, MALLOC_CAP_DEFAULT); + if (config_desc == NULL) { + ret = ESP_ERR_NO_MEM; + goto exit; + } + + // Copy the configuration descriptor + memcpy(config_desc, config_desc_full, config_desc_full->wTotalLength); + *config_desc_ret = config_desc; + ret = ESP_OK; + +exit: + if (ctrl_transfer) { + usb_host_transfer_free(ctrl_transfer); + } + if (transfer_done != NULL) { + vSemaphoreDelete(transfer_done); + } + return ret; +} + +esp_err_t usb_host_get_config_desc_free(const usb_config_desc_t *config_desc) +{ + HOST_CHECK(config_desc != NULL, ESP_ERR_INVALID_ARG); + heap_caps_free((usb_config_desc_t*)config_desc); + return ESP_OK; +} + // ----------------------------------------------- Interface Functions ------------------------------------------------- // ----------------------- Private -------------------------