diff --git a/host/usb/src/usb_host.c b/host/usb/src/usb_host.c index d973a599..f7024874 100644 --- a/host/usb/src/usb_host.c +++ b/host/usb/src/usb_host.c @@ -39,6 +39,10 @@ Warning: The USB Host Library API is still a beta version and may be subject to #endif #endif // SOC_USB_OTG_SUPPORTED +#ifdef CONFIG_ESP_SLEEP_EVENT_CALLBACKS +#include "esp_private/sleep_event.h" +#endif // CONFIG_ESP_SLEEP_EVENT_CALLBACKS + DEFINE_CRIT_SECTION_LOCK_STATIC(host_lock); #define HOST_ENTER_CRITICAL_ISR() esp_os_enter_critical_isr(&host_lock) #define HOST_EXIT_CRITICAL_ISR() esp_os_exit_critical_isr(&host_lock) @@ -492,6 +496,8 @@ static void get_config_desc_transfer_cb(usb_transfer_t *transfer) xSemaphoreGive(transfer_done); } +// ------------------- Power management Related ---------------------- + /** * @brief Automatic suspend timer timer callback * @@ -525,6 +531,104 @@ static void auto_suspend_timer_cb(TimerHandle_t xTimer) HOST_EXIT_CRITICAL(); } +#ifdef CONFIG_ESP_SLEEP_EVENT_CALLBACKS + +/** Max time to wait for async root-port suspend (EP idle → HCD suspend) before light sleep */ +#define USB_HOST_LIGHT_SLEEP_SUSPEND_WAIT_MS 500 + +/** + * @brief Block until hub reports root port suspended, yielding so USB host/hub processing can run. + * + * usb_host_lib_root_port_suspend() only starts the suspend pipeline; root_port_suspended becomes + * true after HCD_PORT_CMD_SUSPEND completes (see hub root_port_req PORT_REQ_SUSPEND). + * + * @param[in] timeout_ticks Max time to wait for root port to suspend, in FreeRTOS ticks + * + * @return + * - ESP_OK: Root port is suspended + * - ESP_ERR_TIMEOUT: Timed out waiting for root port to suspend + */ +static esp_err_t wait_until_root_port_suspended(TickType_t timeout_ticks) +{ + const TickType_t start = xTaskGetTickCount(); + + while (1) { + if (hub_root_is_suspended()) { + return ESP_OK; + } + if ((xTaskGetTickCount() - start) >= timeout_ticks) { + return ESP_ERR_TIMEOUT; + } + vTaskDelay(10); + } +} + +/** + * @brief Callback function to enter light sleep event + * + * - The function is called when the system is about to enter light sleep + * - The function suspends the root port and the devices connected to it to lower power consumption during light sleep + * - The function waits until the root port is suspended before allowing the system to enter light sleep + * + * @param[in] user_arg User argument, not used + * @param[in] ext_arg External argument, not used + * @return + * - ESP_OK: Root port successfully suspended, can enter light sleep + * - ESP_ERR_TIMEOUT: Timed out waiting for root port to suspend + * - Other error codes from usb_host_lib_root_port_suspend() + */ +static esp_err_t enter_light_sleep(void *user_arg, void *ext_arg) +{ + ESP_EARLY_LOGD(USB_HOST_TAG, "Enter light sleep cb"); + ESP_RETURN_ON_ERROR(usb_host_lib_root_port_suspend(), USB_HOST_TAG, "Failed to suspend root port before light sleep"); + ESP_RETURN_ON_ERROR( + wait_until_root_port_suspended(pdMS_TO_TICKS(USB_HOST_LIGHT_SLEEP_SUSPEND_WAIT_MS)), + USB_HOST_TAG, "Timed out waiting for root port suspend before light sleep"); + return ESP_OK; +} + +/** + * @brief Callback function for exit light sleep event + * + * - The function is called when the system wakes up from light sleep + * - The function resumes the root port and the devices connected to it + * to restore normal operation after waking up from light sleep + * + * @param[in] user_arg User argument + * @param[in] ext_arg External argument + * @return + * - ESP_OK: Root port successfully resumed, normal operation restored after light sleep + * - Other error codes from usb_host_lib_root_port_resume() + */ +static esp_err_t exit_light_sleep(void *user_arg, void *ext_arg) +{ + ESP_EARLY_LOGD(USB_HOST_TAG, "Exit light sleep cb"); + ESP_RETURN_ON_ERROR(usb_host_lib_root_port_resume(), USB_HOST_TAG, "Failed to resume root port after light sleep"); + return ESP_OK; +} + +/** + * @brief Callback configuration for enter light sleep event + */ +static const esp_sleep_event_cb_config_t enter_light_sleep_cb = { + .cb = enter_light_sleep, + .user_arg = NULL, + .prior = 2, + .next = NULL, +}; + +/** + * @brief Callback configuration for exit light sleep event + */ +static const esp_sleep_event_cb_config_t exit_light_sleep_cb = { + .cb = exit_light_sleep, + .user_arg = NULL, + .prior = 2, + .next = NULL, +}; + +#endif // CONFIG_ESP_SLEEP_EVENT_CALLBACKS + // ------------------------------------------------ Library Functions -------------------------------------------------- // ----------------------- Public -------------------------- @@ -686,6 +790,12 @@ esp_err_t usb_host_install(const usb_host_config_t *config) ESP_ERROR_CHECK(hub_root_start()); } + // Register light sleep callbacks to automatically suspend/resume the root hub when light sleep is entered/exited +#ifdef CONFIG_ESP_SLEEP_EVENT_CALLBACKS + ESP_ERROR_CHECK(esp_sleep_register_event_callback(SLEEP_EVENT_SW_GOTO_SLEEP, &enter_light_sleep_cb)); + ESP_ERROR_CHECK(esp_sleep_register_event_callback(SLEEP_EVENT_SW_EXIT_SLEEP, &exit_light_sleep_cb)); +#endif // CONFIG_ESP_SLEEP_EVENT_CALLBACKS + ret = ESP_OK; return ret; @@ -727,6 +837,11 @@ esp_err_t usb_host_uninstall(void) ESP_ERR_INVALID_STATE); HOST_EXIT_CRITICAL(); +#ifdef CONFIG_ESP_SLEEP_EVENT_CALLBACKS + ESP_ERROR_CHECK(esp_sleep_unregister_event_callback(SLEEP_EVENT_SW_GOTO_SLEEP, &enter_light_sleep)); + ESP_ERROR_CHECK(esp_sleep_unregister_event_callback(SLEEP_EVENT_SW_EXIT_SLEEP, &exit_light_sleep)); +#endif // CONFIG_ESP_SLEEP_EVENT_CALLBACKS + // Stop the root hub ESP_ERROR_CHECK(hub_root_stop()); @@ -936,6 +1051,8 @@ esp_err_t usb_host_lib_root_port_resume(void) return ret; } + + esp_err_t usb_host_lib_set_auto_suspend(usb_host_lib_auto_suspend_tmr_t timer_type, size_t timer_interval_ms) { HOST_ENTER_CRITICAL(); diff --git a/host/usb/test/target_test/usb_sleep_modes/CMakeLists.txt b/host/usb/test/target_test/usb_sleep_modes/CMakeLists.txt new file mode 100644 index 00000000..52acd73e --- /dev/null +++ b/host/usb/test/target_test/usb_sleep_modes/CMakeLists.txt @@ -0,0 +1,11 @@ +# This is the project CMakeLists.txt file for the test subproject +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +set(EXTRA_COMPONENT_DIRS "../common") + +# "Trim" the build. Include the minimal set of components, main, and anything it depends on. +idf_build_set_property(MINIMAL_BUILD ON) + +project(test_app_usb_sleep_modes) diff --git a/host/usb/test/target_test/usb_sleep_modes/README.md b/host/usb/test/target_test/usb_sleep_modes/README.md new file mode 100644 index 00000000..ea31628a --- /dev/null +++ b/host/usb/test/target_test/usb_sleep_modes/README.md @@ -0,0 +1,7 @@ +| Supported Targets | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | -------- | -------- | -------- | + +# USB: Sleep modes test application + +- USB Host usage with Light sleep +- USB Host usage with Deep sleep diff --git a/host/usb/test/target_test/usb_sleep_modes/main/CMakeLists.txt b/host/usb/test/target_test/usb_sleep_modes/main/CMakeLists.txt new file mode 100644 index 00000000..9f0f4ff2 --- /dev/null +++ b/host/usb/test/target_test/usb_sleep_modes/main/CMakeLists.txt @@ -0,0 +1,6 @@ +# In order for the cases defined by `TEST_CASE` to be linked into the final elf, +# the component can be registered as WHOLE_ARCHIVE +idf_component_register(SRC_DIRS "." + PRIV_INCLUDE_DIRS "." + REQUIRES usb unity common esp_timer + WHOLE_ARCHIVE) diff --git a/host/usb/test/target_test/usb_sleep_modes/main/ctrl_client.c b/host/usb/test/target_test/usb_sleep_modes/main/ctrl_client.c new file mode 100644 index 00000000..7cb63a68 --- /dev/null +++ b/host/usb/test/target_test/usb_sleep_modes/main/ctrl_client.c @@ -0,0 +1,214 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/event_groups.h" +#include "esp_err.h" +#include "esp_log.h" +#include "dev_msc.h" +#include "ctrl_client.h" +#include "usb/usb_host.h" +#include "unity.h" + +/* +Implementation of a control transfer client used for USB Host Tests. + +- The control transfer client will: + - Register itself as a client + - Receive USB_HOST_CLIENT_EVENT_NEW_DEV event message, and open the device + - Allocate a control transfer object and submit a control transfer to get the active configuration descriptor of the device + - Waits for client events and either schedules a new control transfer or closes the device based on the events received + - Free transfer objects + - Close the device + - Deregister control client +*/ + +#define CTRL_CLIENT_MAX_EVENT_MSGS 5 +#define MAX_TRANSFER_BYTES 256 +#define NEW_DEV_EVENT_MS 1000 // Delay to wait for a device to be connected + +const char *CTRL_CLIENT_TAG = "Ctrl Client"; + +typedef enum { + TEST_STAGE_WAIT_CONN, + TEST_STAGE_DEV_OPEN, + TEST_STAGE_CTRL_XFER, + TEST_STAGE_CTRL_XFER_WAIT, + TEST_STAGE_DEV_CLOSE, +} test_stage_t; + +typedef struct { + uint8_t dev_addr; /**< Device address */ + usb_speed_t dev_speed; /**< Device speed */ + usb_host_client_handle_t client_hdl; /**< Client handle */ + usb_device_handle_t dev_hdl; /**< Device handle */ + test_stage_t cur_stage; /**< Current stage of the test */ + test_stage_t next_stage; /**< Next stage of the test */ + const usb_config_desc_t *config_desc_cached; /**< Cached active configuration descriptor for comparison */ + EventGroupHandle_t test_event_group; /**< Event group handle for synchronizing with main test task */ + esp_sleep_mode_t sleep_mode; /**< Sleep mode to be tested */ +} ctrl_client_obj_t; + +static void ctrl_transfer_cb(usb_transfer_t *transfer) +{ + ctrl_client_obj_t *ctrl_obj = (ctrl_client_obj_t *)transfer->context; + // Check the completed control transfer + TEST_ASSERT_EQUAL_MESSAGE(USB_TRANSFER_STATUS_COMPLETED, transfer->status, "Transfer NOT completed"); + + // Get and check device configuration number + uint8_t *data = transfer->data_buffer + sizeof(usb_setup_packet_t); + TEST_ASSERT_EQUAL_MESSAGE(data[0], 1, "Device configuration value should be 1"); + + if (ctrl_obj->sleep_mode == ESP_SLEEP_MODE_LIGHT_SLEEP) { + ctrl_obj->next_stage = TEST_STAGE_CTRL_XFER_WAIT; + } else { + ctrl_obj->next_stage = TEST_STAGE_DEV_CLOSE; + } + + // CTRL transfer is done, signalize sleep task to enter either light sleep or deep sleep + xEventGroupSetBits(ctrl_obj->test_event_group, EVT_ALLOW_SLEEP); +} + +static void ctrl_client_event_cb(const usb_host_client_event_msg_t *event_msg, void *arg) +{ + ctrl_client_obj_t *ctrl_obj = (ctrl_client_obj_t *)arg; + switch (event_msg->event) { + case USB_HOST_CLIENT_EVENT_NEW_DEV: + ESP_LOGI(CTRL_CLIENT_TAG, "Client event -> New device"); + TEST_ASSERT_EQUAL(TEST_STAGE_WAIT_CONN, ctrl_obj->cur_stage); + ctrl_obj->next_stage = TEST_STAGE_DEV_OPEN; + ctrl_obj->dev_addr = event_msg->new_dev.address; + break; + case USB_HOST_CLIENT_EVENT_DEV_GONE: + ESP_LOGI(CTRL_CLIENT_TAG, "Client event -> Device gone"); + break; + case USB_HOST_CLIENT_EVENT_DEV_SUSPENDED: + ESP_LOGI(CTRL_CLIENT_TAG, "Client event -> Device suspended"); + break; + case USB_HOST_CLIENT_EVENT_DEV_RESUMED: + ESP_LOGI(CTRL_CLIENT_TAG, "Client event -> Device resumed"); + + // Only relevant to light sleep test, check if the device is resumed from light sleep and schedule next stage accordingly + EventBits_t bits = xEventGroupGetBits(ctrl_obj->test_event_group); + if (bits & EVT_TEST_FINISH) { + ctrl_obj->next_stage = TEST_STAGE_DEV_CLOSE; + } else { + ctrl_obj->next_stage = TEST_STAGE_CTRL_XFER; + } + break; + default: + abort(); // Should never occur in this test + break; + } +} + +void ctrl_client_task(void *arg) +{ + ctrl_client_obj_t ctrl_obj = {0}; + ctrl_obj.cur_stage = TEST_STAGE_WAIT_CONN; + ctrl_obj.next_stage = TEST_STAGE_WAIT_CONN; + ctrl_obj.sleep_mode = ((test_context_t *)arg)->sleep_mode; + ctrl_obj.test_event_group = ((test_context_t *)arg)->test_event_group; + + // Register client + usb_host_client_config_t client_config = { + .is_synchronous = false, + .max_num_event_msg = CTRL_CLIENT_MAX_EVENT_MSGS, + .async = { + .client_event_callback = ctrl_client_event_cb, + .callback_arg = (void *) &ctrl_obj, + }, + }; + TEST_ASSERT_EQUAL(ESP_OK, usb_host_client_register(&client_config, &ctrl_obj.client_hdl)); + + // Allocate transfers + usb_transfer_t *ctrl_xfer = NULL; + TEST_ASSERT_EQUAL(ESP_OK, usb_host_transfer_alloc(sizeof(usb_setup_packet_t) + MAX_TRANSFER_BYTES, 0, &ctrl_xfer)); + ctrl_xfer->callback = ctrl_transfer_cb; + ctrl_xfer->context = (void *)&ctrl_obj; + + // Wait to be started by main thread + TEST_ASSERT_EQUAL_MESSAGE(pdTRUE, ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(2000)), "Ctrl client not started from main thread"); + ESP_LOGI(CTRL_CLIENT_TAG, "Starting"); + + // Handle device enumeration separately, wait for 1000ms for the device to be enumerated + // Catch an error in case the device is not enumerated correctly + esp_err_t enum_ret = usb_host_client_handle_events(ctrl_obj.client_hdl, pdMS_TO_TICKS(NEW_DEV_EVENT_MS)); + TEST_ASSERT_EQUAL_MESSAGE(TEST_STAGE_DEV_OPEN, ctrl_obj.next_stage, "USB_HOST_CLIENT_EVENT_NEW_DEV not generated on time"); + TEST_ASSERT_EQUAL_MESSAGE(ESP_OK, enum_ret, "Client handle events timed out"); + + bool exit_loop = false; + bool skip_event_handling = true; // Skip first event handling (we have handled the new device event separately) + while (!exit_loop) { + if (!skip_event_handling) { + TEST_ASSERT_EQUAL(ESP_OK, usb_host_client_handle_events(ctrl_obj.client_hdl, portMAX_DELAY)); + } + skip_event_handling = false; + if (ctrl_obj.cur_stage == ctrl_obj.next_stage) { + continue; + } + ctrl_obj.cur_stage = ctrl_obj.next_stage; + + switch (ctrl_obj.next_stage) { + case TEST_STAGE_DEV_OPEN: { + ESP_LOGI(CTRL_CLIENT_TAG, "Open"); + // Open the device + TEST_ASSERT_EQUAL_MESSAGE(ESP_OK, usb_host_device_open(ctrl_obj.client_hdl, ctrl_obj.dev_addr, &ctrl_obj.dev_hdl), "Failed to open the device"); + // Get device info to get device speed + usb_device_info_t dev_info; + TEST_ASSERT_EQUAL(ESP_OK, usb_host_device_info(ctrl_obj.dev_hdl, &dev_info)); + ctrl_obj.dev_speed = dev_info.speed; + ctrl_xfer->device_handle = ctrl_obj.dev_hdl; + + // Check that the device descriptor matches our expected MSC device + const usb_device_desc_t *device_desc; + const usb_device_desc_t *device_desc_ref = dev_msc_get_dev_desc(ctrl_obj.dev_speed); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_get_device_descriptor(ctrl_obj.dev_hdl, &device_desc)); + TEST_ASSERT_EQUAL(device_desc_ref->bLength, device_desc->bLength); + TEST_ASSERT_EQUAL_MEMORY_MESSAGE(device_desc_ref, device_desc, sizeof(usb_device_desc_t), "Device descriptors do not match."); + // Cache the active configuration descriptor for later comparison + TEST_ASSERT_EQUAL(ESP_OK, usb_host_get_active_config_descriptor(ctrl_obj.dev_hdl, &ctrl_obj.config_desc_cached)); + ctrl_obj.next_stage = TEST_STAGE_CTRL_XFER; + skip_event_handling = true; + break; + } + case TEST_STAGE_CTRL_XFER: { + ESP_LOGI(CTRL_CLIENT_TAG, "Transfer"); + // Submit a ctrl transfer to get a configuration value of the device + // It's expected that the config value is 1, thus the device did not undergo a reset during light sleep + USB_SETUP_PACKET_INIT_GET_CONFIG((usb_setup_packet_t *)ctrl_xfer->data_buffer); + ctrl_xfer->num_bytes = sizeof(usb_setup_packet_t) + MAX_TRANSFER_BYTES; + ctrl_xfer->bEndpointAddress = 0x80; // IN transfer + TEST_ASSERT_EQUAL(ESP_OK, usb_host_transfer_submit_control(ctrl_obj.client_hdl, ctrl_xfer)); + ctrl_obj.next_stage = TEST_STAGE_CTRL_XFER_WAIT; + skip_event_handling = false; + break; + } + case TEST_STAGE_CTRL_XFER_WAIT: { + break; + } + case TEST_STAGE_DEV_CLOSE: { + ESP_LOGI(CTRL_CLIENT_TAG, "Close"); + vTaskDelay(10); // Give USB Host Lib some time to process all transfers + TEST_ASSERT_EQUAL(ESP_OK, usb_host_device_close(ctrl_obj.client_hdl, ctrl_obj.dev_hdl)); + exit_loop = true; + break; + } + default: + abort(); + break; + } + } + // Free transfers and deregister client + TEST_ASSERT_EQUAL(ESP_OK, usb_host_transfer_free(ctrl_xfer)); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_client_deregister(ctrl_obj.client_hdl)); + ESP_LOGI(CTRL_CLIENT_TAG, "Done"); + xEventGroupSetBits(ctrl_obj.test_event_group, EVT_CLIENT_CLOSE); + vTaskDelete(NULL); +} diff --git a/host/usb/test/target_test/usb_sleep_modes/main/ctrl_client.h b/host/usb/test/target_test/usb_sleep_modes/main/ctrl_client.h new file mode 100644 index 00000000..1ed44679 --- /dev/null +++ b/host/usb/test/target_test/usb_sleep_modes/main/ctrl_client.h @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "esp_sleep.h" +#include "freertos/event_groups.h" + +#define EVT_ALLOW_SLEEP BIT0 +#define EVT_TEST_FINISH BIT1 +#define EVT_CLIENT_CLOSE BIT2 + +typedef struct { + EventGroupHandle_t test_event_group; /**< Event group handle for synchronizing with main test task */ + esp_sleep_mode_t sleep_mode; /**< Sleep mode to be tested */ +} test_context_t; + +/** + * @brief CTRL client task + */ +void ctrl_client_task(void *arg); diff --git a/host/usb/test/target_test/usb_sleep_modes/main/test_app_main.c b/host/usb/test/target_test/usb_sleep_modes/main/test_app_main.c new file mode 100644 index 00000000..c1403bf0 --- /dev/null +++ b/host/usb/test/target_test/usb_sleep_modes/main/test_app_main.c @@ -0,0 +1,84 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include "unity.h" +#include "unity_test_runner.h" +#include "unity_test_utils_memory.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_newlib.h" +#include "dev_msc.h" +#include "phy_common.h" +#include "usb/usb_host.h" + +// ----------------------------------------------------- Macros -------------------------------------------------------- + +#define TEST_P4_OTG11 0 // Change this to 1 to test on OTG1.1 peripheral - only for ESP32-P4 + +// --------------------- Constants ------------------------- + +#if TEST_P4_OTG11 +#define TEST_PHY USB_PHY_TARGET_INT +#define TEST_PERIPHERAL_MAP BIT1 +#else +#if CONFIG_IDF_TARGET_ESP32P4 +#define TEST_PHY USB_PHY_TARGET_UTMI +#else +#define TEST_PHY USB_PHY_TARGET_INT +#endif +#define TEST_PERIPHERAL_MAP BIT0 +#endif // TEST_P4_OTG11 + +void setUp(void) +{ + unity_utils_record_free_mem(); + dev_msc_init(); + // Install PHY separately + test_setup_usb_phy(TEST_PHY); + // Install USB Host + usb_host_config_t host_config = { + .skip_phy_setup = true, + .root_port_unpowered = false, + .intr_flags = ESP_INTR_FLAG_LOWMED, + .peripheral_map = TEST_PERIPHERAL_MAP, + }; + ESP_ERROR_CHECK(usb_host_install(&host_config)); + printf("USB Host installed\n"); +} + +void tearDown(void) +{ + // Short delay to allow task to be cleaned up + vTaskDelay(10); + // Clean up USB Host + printf("USB Host uninstall\n"); + ESP_ERROR_CHECK(usb_host_uninstall()); + test_delete_usb_phy(); + // Short delay to allow task to be cleaned up after client uninstall + vTaskDelay(10); + esp_reent_cleanup(); //clean up some of the newlib's lazy allocations + unity_utils_evaluate_leaks(); +} + +void app_main(void) +{ + // ____ ___ ___________________ __ __ + // | | \/ _____/\______ \ _/ |_ ____ _______/ |_ + // | | /\_____ \ | | _/ \ __\/ __ \ / ___/\ __\. + // | | / / \ | | \ | | \ ___/ \___ \ | | + // |______/ /_______ / |______ / |__| \___ >____ > |__| + // \/ \/ \/ \/ + printf(" ____ ___ ___________________ __ __ \r\n"); + printf("| | \\/ _____/\\______ \\ _/ |_ ____ _______/ |_ \r\n"); + printf("| | /\\_____ \\ | | _/ \\ __\\/ __ \\ / ___/\\ __\\\r\n"); + printf("| | / / \\ | | \\ | | \\ ___/ \\___ \\ | | \r\n"); + printf("|______/ /_______ / |______ / |__| \\___ >____ > |__| \r\n"); + printf(" \\/ \\/ \\/ \\/ \r\n"); + + unity_utils_setup_heap_record(80); + unity_utils_set_leak_level(128); + unity_run_menu(); +} diff --git a/host/usb/test/target_test/usb_sleep_modes/main/test_usb_host_esp_sleep.c b/host/usb/test/target_test/usb_sleep_modes/main/test_usb_host_esp_sleep.c new file mode 100644 index 00000000..1ec81ee9 --- /dev/null +++ b/host/usb/test/target_test/usb_sleep_modes/main/test_usb_host_esp_sleep.c @@ -0,0 +1,325 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/event_groups.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_intr_alloc.h" +#include "ctrl_client.h" +#include "usb/usb_host.h" +#include "unity.h" +#include "esp_sleep.h" +#include "esp_timer.h" +#include "esp_private/sleep_event.h" +#include "soc/soc_caps.h" + +#define TIMER_LIGHT_SLEEP_WAKEUP_TIME_US (3 * 1000 * 1000) // 3 seconds +#define TIMER_DEEP_SLEEPWAKEUP_TIME_US (5 * 1000 * 1000) // 5 seconds +#define EVT_WAIT_TIME_MS (5 * 1000) // 5 seconds +#define LIGHT_SLEEP_NUM_CYCLES (5) + +const char *USB_SLEEP_TAG = "USB sleep"; + +/* + Light sleep task + + - Sets timer as a wakeup source + - Waits for the EVT_ALLOW_SLEEP event from the CTRL client + - Enters light sleep -> woken up by a timer + - Runs LIGHT_SLEEP_NUM_CYCLES times +*/ +static void light_sleep_task(void *args) +{ + EventGroupHandle_t test_event_group = (EventGroupHandle_t) args; + // Configure timer wakeup source + TEST_ASSERT_EQUAL(ESP_OK, esp_sleep_enable_timer_wakeup(TIMER_LIGHT_SLEEP_WAKEUP_TIME_US)); + ESP_LOGI(USB_SLEEP_TAG, "Light sleep timer wakeup source is ready"); + + for (int i = 0; i < LIGHT_SLEEP_NUM_CYCLES; i++) { + TEST_ASSERT_EQUAL_MESSAGE( + pdTRUE, + xEventGroupWaitBits(test_event_group, EVT_ALLOW_SLEEP, pdTRUE, pdFALSE, pdMS_TO_TICKS(EVT_WAIT_TIME_MS)), + "CTRL client did not set event on time to allow light sleep"); + ESP_LOGD(USB_SLEEP_TAG, "Entering light sleep"); + + // Get timestamp before entering sleep + const int64_t t_before_us = esp_timer_get_time(); + + // Enter light sleep mode + TEST_ASSERT_EQUAL(ESP_OK, esp_light_sleep_start()); + + // Get timestamp after waking up from sleep + const int64_t t_after_us = esp_timer_get_time(); + + // Determine wake up reason + const uint32_t wakeup_cause = esp_sleep_get_wakeup_causes(); + TEST_ASSERT(wakeup_cause & BIT(ESP_SLEEP_WAKEUP_TIMER)); + TEST_ASSERT_INT_WITHIN_MESSAGE( + TIMER_LIGHT_SLEEP_WAKEUP_TIME_US / 10, + TIMER_LIGHT_SLEEP_WAKEUP_TIME_US, + t_after_us - t_before_us, + "Unexpected light sleep duration"); + printf("Returned from light sleep, reason: timer, t=%lld ms, slept for %lld ms\n", t_after_us / 1000, (t_after_us - t_before_us) / 1000); + } + + xEventGroupSetBits(test_event_group, EVT_TEST_FINISH); + vTaskDelete(NULL); +} + +/* + Deep sleep task + + - Sets timer as a wakeup source + - Waits for the EVT_ALLOW_SLEEP or EVT_CLIENT_CLOSE event from the CTRL client + - Enters deep sleep -> woken up by a timer + - Another test stage is run to check the functionality + +*/ +static void deep_sleep_task(void *args) +{ + EventGroupHandle_t test_event_group = (EventGroupHandle_t) args; + // Configure timer wakeup source + TEST_ASSERT_EQUAL(ESP_OK, esp_sleep_enable_timer_wakeup(TIMER_DEEP_SLEEPWAKEUP_TIME_US)); + ESP_LOGI(USB_SLEEP_TAG, "Deep sleep timer wakeup source is ready"); + + TEST_ASSERT_EQUAL_MESSAGE( + pdTRUE, + xEventGroupWaitBits(test_event_group, EVT_ALLOW_SLEEP | EVT_CLIENT_CLOSE, pdTRUE, pdFALSE, pdMS_TO_TICKS(EVT_WAIT_TIME_MS)), + "CTRL client did not set event on time to allow deep sleep"); + ESP_LOGI(USB_SLEEP_TAG, "Entering deep sleep"); + + /* Enter sleep mode */ + esp_deep_sleep_start(); +} + +/** + * @brief Common sleep function for both, light and deep sleep test cases + * + * @param[in] sleep_mode: Type of sleep mode to be tested (Deep/Light) + */ +static void usb_host_sleep_common(const esp_sleep_mode_t sleep_mode) +{ + TaskHandle_t ctrl_task_hdl = NULL; + TaskHandle_t sleep_task_hdl = NULL; + EventGroupHandle_t test_event_group = NULL; + + test_event_group = xEventGroupCreate(); + TEST_ASSERT_NOT_NULL(test_event_group); + + const test_context_t test_ctx = { + .test_event_group = test_event_group, + .sleep_mode = sleep_mode, + }; + + if (sleep_mode == ESP_SLEEP_MODE_LIGHT_SLEEP) { + TEST_ASSERT_EQUAL(pdTRUE, xTaskCreate(light_sleep_task, "sleep_task", 4096, (void *)test_event_group, 2, &sleep_task_hdl)); + } else { + TEST_ASSERT_EQUAL(pdTRUE, xTaskCreate(deep_sleep_task, "sleep_task", 4096, (void *)test_event_group, 2, &sleep_task_hdl)); + } + TEST_ASSERT_EQUAL(pdTRUE, xTaskCreate(ctrl_client_task, "ctrl_task", 4096, (void *)&test_ctx, 2, &ctrl_task_hdl)); + TEST_ASSERT_NOT_NULL(ctrl_task_hdl); + TEST_ASSERT_NOT_NULL(sleep_task_hdl); + + xTaskNotifyGive(ctrl_task_hdl); + + while (1) { + 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) { + printf("All devices free\n"); + break; + } + } + vEventGroupDelete(test_event_group); +} + +/* +Test USB Host with light sleep + +Purpose: +- Test usage of light sleep mode with USB Host. + +Procedure: + - CTRL client does the USB Host functionality verification + - SoC enters light sleep + - Root port suspend is called before entering the light sleep + - SoC is woken up by a timer + - Root port resume is called after exiting the light sleep + - All steps are repeated LIGHT_SLEEP_NUM_CYCLES times + - Device is expected not to be reset (not to loose address) during light sleep +*/ +TEST_CASE("Test USB Host light sleep", "[usb_sleep_modes][light_sleep]") +{ + // Call common function with light sleep mode + usb_host_sleep_common(ESP_SLEEP_MODE_LIGHT_SLEEP); +} + + +static void light_sleep_enter_task_error(void *args) +{ + // Configure timer wakeup source + TEST_ASSERT_EQUAL(ESP_OK, esp_sleep_enable_timer_wakeup(TIMER_LIGHT_SLEEP_WAKEUP_TIME_US)); + ESP_LOGI(USB_SLEEP_TAG, "Light sleep timer wakeup source is ready"); + + // Wait for the device to be connected + TEST_ASSERT_EQUAL_MESSAGE(pdTRUE, ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(2000)), "Device was not connected on time"); + + // Device is connected, wait for the enumeration + vTaskDelay(10); + + // Manually suspend the root port before entering light sleep to test error handing of the light sleep callbacks + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + + // Wait for the root port suspend procedure to complete before entering light sleep + vTaskDelay(100); + + // Get timestamp before entering sleep + const int64_t t_before_us = esp_timer_get_time(); + + // Enter light sleep mode + TEST_ASSERT_EQUAL(ESP_OK, esp_light_sleep_start()); + + // Get timestamp after waking up from sleep + const int64_t t_after_us = esp_timer_get_time(); + + // Determine wake up reason + const uint32_t wakeup_cause = esp_sleep_get_wakeup_causes(); + TEST_ASSERT(wakeup_cause & BIT(ESP_SLEEP_WAKEUP_TIMER)); + TEST_ASSERT_INT_WITHIN_MESSAGE( + TIMER_LIGHT_SLEEP_WAKEUP_TIME_US / 10, + TIMER_LIGHT_SLEEP_WAKEUP_TIME_US, + t_after_us - t_before_us, + "Unexpected light sleep duration"); + printf("Returned from light sleep, reason: timer, t=%lld ms, slept for %lld ms\n", t_after_us / 1000, (t_after_us - t_before_us) / 1000); + + // Wait for the resume procedure to complete before freeing the device + vTaskDelay(100); + // Free all devices and finish the test + TEST_ASSERT_EQUAL(ESP_ERR_NOT_FINISHED, usb_host_device_free_all()); + vTaskDelete(NULL); +} + +/* +Test USB Host light sleep error handling + +Purpose: +- Test light sleep callback error behavior + +Procedure: + - Manually suspend the root port before entering light sleep + - SoC enters light sleep + - Root port suspend is called before entering the light sleep, but root port is already suspended + - Callback function returns error, but light sleep should still proceed as normal + - SoC is woken up by a timer + - Root port resume is called after exiting the light sleep +*/ +TEST_CASE("Test USB Host enter light sleep error handling", "[usb_sleep_modes][light_sleep]") +{ + TaskHandle_t sleep_task_hdl = NULL; + + TEST_ASSERT_EQUAL(pdTRUE, xTaskCreate(light_sleep_enter_task_error, "light_sleep_enter_task_error", 4096, NULL, 2, &sleep_task_hdl)); + TEST_ASSERT_NOT_NULL(sleep_task_hdl); + + // Handle device connection separately + uint32_t event_flags_device_conn; + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_handle_events(pdMS_TO_TICKS(2000), &event_flags_device_conn)); + + xTaskNotifyGive(sleep_task_hdl); + + while (1) { + uint32_t event_flags; + usb_host_lib_handle_events(portMAX_DELAY, &event_flags); + + if (event_flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS) { + TEST_FAIL_MESSAGE("No clients for this test"); + } + if (event_flags & USB_HOST_LIB_EVENT_FLAGS_ALL_FREE) { + printf("All devices free\n"); + break; + } + } +} + + +/** + * @brief Deep sleep test case stage 1 + * + * Just calls the common sleep function, which does the USB Host functional test and enters deep sleep + */ +static void usb_host_deep_sleep_1(void) +{ + // Call common test function with deep sleep mode + usb_host_sleep_common(ESP_SLEEP_MODE_DEEP_SLEEP); +} + +/** + * @brief Deep sleep test case stage 2 + * + * Checks reset reason + * Calls the common sleep function, which does the USB Host functional test and enters deep sleep + */ +static void usb_host_deep_sleep_2(void) +{ + // Get reset reason and check if it's deep sleep reset + soc_reset_reason_t reason = esp_rom_get_reset_reason(0); + TEST_ASSERT(reason == RESET_REASON_CORE_DEEP_SLEEP); + // Call common test function with deep sleep mode + usb_host_sleep_common(ESP_SLEEP_MODE_DEEP_SLEEP); +} + +/** + * @brief Deep sleep test case stage 3 + * + * Checks reset reason + * Handles new device connection and disconnection, does not enter deep sleep anymore + */ +static void usb_host_deep_sleep_3(void) +{ + // Get reset reason and check if it's deep sleep reset + soc_reset_reason_t reason = esp_rom_get_reset_reason(0); + TEST_ASSERT(reason == RESET_REASON_CORE_DEEP_SLEEP); + + // End of last stage of the test: + // Wait for the device to be connected + vTaskDelay(100); + // Handle device connection + usb_host_lib_handle_events(0, NULL); + // Free the device + TEST_ASSERT_EQUAL(ESP_ERR_NOT_FINISHED, usb_host_device_free_all()); + + while (1) { + uint32_t event_flags; + usb_host_lib_handle_events(portMAX_DELAY, &event_flags); + + if (event_flags & USB_HOST_LIB_EVENT_FLAGS_ALL_FREE) { + printf("All devices free\n"); + break; + } + } +} + +/* +Test USB Host with deep sleep + +Purpose: +- Test usage of deep sleep mode with USB Host. + +Procedure: + - CTRL client does the USB Host functionality verification + - SoC enters deep sleep + - SoC is woken up by a timer + - All steps are repeated 2 times + - Device is expected to be disconnected during deep sleep +*/ +TEST_CASE_MULTIPLE_STAGES("Test USB Host deep sleep", "[usb_sleep_modes][deep_sleep]", usb_host_deep_sleep_1, usb_host_deep_sleep_2, usb_host_deep_sleep_3); diff --git a/host/usb/test/target_test/usb_sleep_modes/pytest_usb_sleep_modes.py b/host/usb/test/target_test/usb_sleep_modes/pytest_usb_sleep_modes.py new file mode 100644 index 00000000..598fe55f --- /dev/null +++ b/host/usb/test/target_test/usb_sleep_modes/pytest_usb_sleep_modes.py @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest +from pytest_embedded_idf.dut import IdfDut + + +@pytest.mark.usb_host_flash_disk +@pytest.mark.parametrize( + 'config, target', + [ + pytest.param('default', 'esp32s2'), + pytest.param('default', 'esp32s3'), + pytest.param('default', 'esp32p4', marks=[pytest.mark.eco_default]), + pytest.param('esp32p4_eco4', 'esp32p4', marks=[pytest.mark.esp32p4_eco4]), + ], + indirect=['target'], +) +def test_usb_light_sleep(dut: IdfDut) -> None: + '''Light and deep sleep test cases''' + dut.run_all_single_board_cases(group='usb_sleep_modes') diff --git a/host/usb/test/target_test/usb_sleep_modes/sdkconfig.ci b/host/usb/test/target_test/usb_sleep_modes/sdkconfig.ci new file mode 100644 index 00000000..e69de29b diff --git a/host/usb/test/target_test/usb_sleep_modes/sdkconfig.ci.esp32p4_eco4 b/host/usb/test/target_test/usb_sleep_modes/sdkconfig.ci.esp32p4_eco4 new file mode 100644 index 00000000..2c6c907f --- /dev/null +++ b/host/usb/test/target_test/usb_sleep_modes/sdkconfig.ci.esp32p4_eco4 @@ -0,0 +1,2 @@ +CONFIG_IDF_TARGET="esp32p4" +CONFIG_ESP32P4_SELECTS_REV_LESS_V3=y diff --git a/host/usb/test/target_test/usb_sleep_modes/sdkconfig.defaults b/host/usb/test/target_test/usb_sleep_modes/sdkconfig.defaults new file mode 100644 index 00000000..c5a40848 --- /dev/null +++ b/host/usb/test/target_test/usb_sleep_modes/sdkconfig.defaults @@ -0,0 +1,14 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +# CONFIG_ESP_TASK_WDT_INIT is not set +CONFIG_HEAP_POISONING_COMPREHENSIVE=y +# CONFIG_UNITY_ENABLE_FLOAT is not set +# CONFIG_UNITY_ENABLE_DOUBLE is not set +CONFIG_UNITY_ENABLE_BACKTRACE_ON_FAIL=y +CONFIG_USB_HOST_HUBS_SUPPORTED=y + +# Power management and sleep +CONFIG_PM_ENABLE=y +CONFIG_FREERTOS_USE_TICKLESS_IDLE=y +CONFIG_ESP_SLEEP_EVENT_CALLBACKS=y diff --git a/host/usb/test/target_test/usb_sleep_modes/sdkconfig.defaults.esp32p4 b/host/usb/test/target_test/usb_sleep_modes/sdkconfig.defaults.esp32p4 new file mode 100644 index 00000000..f532ffd7 --- /dev/null +++ b/host/usb/test/target_test/usb_sleep_modes/sdkconfig.defaults.esp32p4 @@ -0,0 +1,2 @@ +CONFIG_ESP32P4_SELECTS_REV_LESS_V3=n +CONFIG_ESP32P4_REV_MIN_301=y