diff --git a/.github/workflows/build_and_run_test_app_usb.yml b/.github/workflows/build_and_run_test_app_usb.yml index f6ce2f7..ed65b44 100644 --- a/.github/workflows/build_and_run_test_app_usb.yml +++ b/.github/workflows/build_and_run_test_app_usb.yml @@ -12,7 +12,8 @@ jobs: strategy: fail-fast: false matrix: - idf_ver: ["release-v5.0", "release-v5.1", "release-v5.2", "release-v5.3", "release-v5.4", "latest"] + # idf_ver: ["release-v5.0", "release-v5.1", "release-v5.2", "release-v5.3", "release-v5.4", "latest"] + idf_ver: ["latest"] runs-on: ubuntu-20.04 container: espressif/idf:${{ matrix.idf_ver }} steps: @@ -48,7 +49,8 @@ jobs: strategy: fail-fast: false matrix: - idf_ver: ["release-v5.0", "release-v5.1", "release-v5.2", "release-v5.3", "release-v5.4", "latest"] + # idf_ver: ["release-v5.0", "release-v5.1", "release-v5.2", "release-v5.3", "release-v5.4", "latest"] + idf_ver: ["latest"] idf_target: ["esp32s2", "esp32p4"] runner_tag: ["usb_host", "usb_device"] exclude: @@ -69,6 +71,10 @@ jobs: - uses: actions/download-artifact@v4 with: name: usb_test_app_bin_${{ matrix.idf_ver }} + - name: ⚙️ Install System tools + run: | + apt update + apt install -y usbutils - name: Install Python packages env: PIP_EXTRA_INDEX_URL: "https://dl.espressif.com/pypi/" diff --git a/device/esp_tinyusb/include/tinyusb.h b/device/esp_tinyusb/include/tinyusb.h index 484c86e..887c63f 100644 --- a/device/esp_tinyusb/include/tinyusb.h +++ b/device/esp_tinyusb/include/tinyusb.h @@ -68,6 +68,16 @@ typedef struct { */ esp_err_t tinyusb_driver_install(const tinyusb_config_t *config); +/** + * @brief This is an all-in-one helper function, including: + * 1. Stops the task to handle usb events + * 2. TinyUSB stack tearing down + * 2. Freeing resources after descriptors preparation + * 3. Deletes USB PHY + * + * @retval ESP_FAIL Uninstall driver or tinyusb stack failed because of internal error + * @retval ESP_OK Uninstall driver, tinyusb stack and USB PHY successfully + */ esp_err_t tinyusb_driver_uninstall(void); #ifdef __cplusplus diff --git a/device/esp_tinyusb/test_apps/cdc_and_usb_device/main/test_bvalid_sig.c b/device/esp_tinyusb/test_apps/cdc_and_usb_device/main/test_bvalid_sig.c index 1bf865a..9bce381 100644 --- a/device/esp_tinyusb/test_apps/cdc_and_usb_device/main/test_bvalid_sig.c +++ b/device/esp_tinyusb/test_apps/cdc_and_usb_device/main/test_bvalid_sig.c @@ -121,6 +121,5 @@ TEST_CASE("bvalid_signal", "[esp_tinyusb][usb_device]") // Cleanup TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_uninstall()); - TEST_ASSERT_EQUAL(ESP_OK, tusb_stop_task()); } #endif // SOC_USB_OTG_SUPPORTED diff --git a/device/esp_tinyusb/test_apps/cdc_and_usb_device/main/test_descriptors_config.c b/device/esp_tinyusb/test_apps/cdc_and_usb_device/main/test_descriptors_config.c index a7c74bd..673761d 100644 --- a/device/esp_tinyusb/test_apps/cdc_and_usb_device/main/test_descriptors_config.c +++ b/device/esp_tinyusb/test_apps/cdc_and_usb_device/main/test_descriptors_config.c @@ -140,7 +140,6 @@ TEST_CASE("descriptors_config_all_default", "[esp_tinyusb][usb_device]") TEST_ASSERT_EQUAL(ESP_OK, __test_wait_conn()); // Cleanup TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_uninstall()); - TEST_ASSERT_EQUAL(ESP_OK, tusb_stop_task()); __test_free(); } @@ -163,7 +162,6 @@ TEST_CASE("descriptors_config_device", "[esp_tinyusb][usb_device]") TEST_ASSERT_EQUAL(ESP_OK, __test_wait_conn()); // Cleanup TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_uninstall()); - TEST_ASSERT_EQUAL(ESP_OK, tusb_stop_task()); __test_free(); } @@ -186,7 +184,6 @@ TEST_CASE("descriptors_config_device_and_config", "[esp_tinyusb][usb_device]") TEST_ASSERT_EQUAL(ESP_OK, __test_wait_conn()); // Cleanup TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_uninstall()); - TEST_ASSERT_EQUAL(ESP_OK, tusb_stop_task()); __test_free(); } @@ -208,7 +205,6 @@ TEST_CASE("descriptors_config_device_and_fs_config_only", "[esp_tinyusb][usb_dev TEST_ASSERT_EQUAL(ESP_OK, __test_wait_conn()); // Cleanup TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_uninstall()); - TEST_ASSERT_EQUAL(ESP_OK, tusb_stop_task()); __test_free(); } @@ -229,7 +225,6 @@ TEST_CASE("descriptors_config_device_and_hs_config_only", "[esp_tinyusb][usb_dev TEST_ASSERT_EQUAL(ESP_OK, __test_wait_conn()); // Cleanup TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_uninstall()); - TEST_ASSERT_EQUAL(ESP_OK, tusb_stop_task()); __test_free(); } @@ -250,7 +245,6 @@ TEST_CASE("descriptors_config_all_configured", "[esp_tinyusb][usb_device]") TEST_ASSERT_EQUAL(ESP_OK, __test_wait_conn()); // Cleanup TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_uninstall()); - TEST_ASSERT_EQUAL(ESP_OK, tusb_stop_task()); __test_free(); } #endif // TUD_OPT_HIGH_SPEED diff --git a/device/esp_tinyusb/test_apps/teardown_device/CMakeLists.txt b/device/esp_tinyusb/test_apps/teardown_device/CMakeLists.txt new file mode 100644 index 0000000..d63d14e --- /dev/null +++ b/device/esp_tinyusb/test_apps/teardown_device/CMakeLists.txt @@ -0,0 +1,9 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# "Trim" the build. Include the minimal set of components, main, and anything it depends on. +set(COMPONENTS main) + +project(test_app_teardown_device) diff --git a/device/esp_tinyusb/test_apps/teardown_device/main/CMakeLists.txt b/device/esp_tinyusb/test_apps/teardown_device/main/CMakeLists.txt new file mode 100644 index 0000000..e81e627 --- /dev/null +++ b/device/esp_tinyusb/test_apps/teardown_device/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRC_DIRS . + INCLUDE_DIRS . + REQUIRES unity + WHOLE_ARCHIVE) diff --git a/device/esp_tinyusb/test_apps/teardown_device/main/idf_component.yml b/device/esp_tinyusb/test_apps/teardown_device/main/idf_component.yml new file mode 100644 index 0000000..b1cb5b5 --- /dev/null +++ b/device/esp_tinyusb/test_apps/teardown_device/main/idf_component.yml @@ -0,0 +1,5 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/esp_tinyusb: + version: "*" + override_path: "../../../" diff --git a/device/esp_tinyusb/test_apps/teardown_device/main/test_app_main.c b/device/esp_tinyusb/test_apps/teardown_device/main/test_app_main.c new file mode 100644 index 0000000..b1321ed --- /dev/null +++ b/device/esp_tinyusb/test_apps/teardown_device/main/test_app_main.c @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "unity.h" +#include "unity_test_runner.h" +#include "unity_test_utils_memory.h" + +void app_main(void) +{ + /* + _ _ _ + | | (_) | | + ___ ___ _ __ | |_ _ _ __ _ _ _ _ ___| |__ + / _ \/ __| '_ \| __| | '_ \| | | | | | / __| '_ \ + | __/\__ \ |_) | |_| | | | | |_| | |_| \__ \ |_) | + \___||___/ .__/ \__|_|_| |_|\__, |\__,_|___/_.__/ + | |______ __/ | + |_|______| |___/ + _____ _____ _____ _____ + |_ _| ___/ ___|_ _| + | | | |__ \ `--. | | + | | | __| `--. \ | | + | | | |___/\__/ / | | + \_/ \____/\____/ \_/ + */ + + printf(" _ _ _ \n"); + printf(" | | (_) | | \n"); + printf(" ___ ___ _ __ | |_ _ _ __ _ _ _ _ ___| |__ \n"); + printf(" / _ \\/ __| '_ \\| __| | '_ \\| | | | | | / __| '_ \\ \n"); + printf("| __/\\__ \\ |_) | |_| | | | | |_| | |_| \\__ \\ |_) |\n"); + printf(" \\___||___/ .__/ \\__|_|_| |_|\\__, |\\__,_|___/_.__/ \n"); + printf(" | |______ __/ | \n"); + printf(" |_|______| |___/ \n"); + printf(" _____ _____ _____ _____ \n"); + printf("|_ _| ___/ ___|_ _| \n"); + printf(" | | | |__ \\ `--. | | \n"); + printf(" | | | __| `--. \\ | | \n"); + printf(" | | | |___/\\__/ / | | \n"); + printf(" \\_/ \\____/\\____/ \\_/ \n"); + + unity_utils_setup_heap_record(80); + unity_utils_set_leak_level(128); + unity_run_menu(); +} + +/* setUp runs before every test */ +void setUp(void) +{ + unity_utils_record_free_mem(); +} + +/* tearDown runs after every test */ +void tearDown(void) +{ + unity_utils_evaluate_leaks(); +} diff --git a/device/esp_tinyusb/test_apps/teardown_device/main/test_teardown.c b/device/esp_tinyusb/test_apps/teardown_device/main/test_teardown.c new file mode 100644 index 0000000..ee56290 --- /dev/null +++ b/device/esp_tinyusb/test_apps/teardown_device/main/test_teardown.c @@ -0,0 +1,145 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "soc/soc_caps.h" +#if SOC_USB_OTG_SUPPORTED + +// +#include +#include +// +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +// +#include "esp_system.h" +#include "esp_log.h" +#include "esp_err.h" +// +#include "unity.h" +#include "tinyusb.h" +#include "tusb_cdc_acm.h" + +static const char *TAG = "teardown"; + +SemaphoreHandle_t wait_mount = NULL; + +#define TEARDOWN_DEVICE_INIT_DELAY_MS 1000 +#define TEARDOWN_DEVICE_ATTACH_TIMEOUT_MS 1000 +#define TEARDOWN_DEVICE_DETACH_DELAY_MS 1000 + +#define TEARDOWN_AMOUNT 10 + +// #define TUSB_DESC_TOTAL_LEN (TUD_CONFIG_DESC_LEN) +static const uint16_t cdc_desc_config_len = TUD_CONFIG_DESC_LEN + CFG_TUD_CDC * TUD_CDC_DESC_LEN; +static uint8_t const test_configuration_descriptor[] = { + // Config number, interface count, string index, total length, attribute, power in mA + // TUD_CONFIG_DESCRIPTOR(1, 0, 0, TUSB_DESC_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_SELF_POWERED | TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100), + TUD_CONFIG_DESCRIPTOR(1, 2, 0, cdc_desc_config_len, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100), + TUD_CDC_DESCRIPTOR(0, 4, 0x81, 8, 0x02, 0x82, (TUD_OPT_HIGH_SPEED ? 512 : 64)), +}; + +static const tusb_desc_device_t test_device_descriptor = { + .bLength = sizeof(test_device_descriptor), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .idVendor = 0x303A, // This is Espressif VID. This needs to be changed according to Users / Customers + .idProduct = 0x4002, + .bcdDevice = 0x100, + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + .bNumConfigurations = 0x01 +}; + +#if (TUD_OPT_HIGH_SPEED) +static const tusb_desc_device_qualifier_t device_qualifier = { + .bLength = sizeof(tusb_desc_device_qualifier_t), + .bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER, + .bcdUSB = 0x0200, + .bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .bNumConfigurations = 0x01, + .bReserved = 0 +}; +#endif // TUD_OPT_HIGH_SPEED + +// Invoked when device is mounted +void tud_mount_cb(void) +{ + xSemaphoreGive(wait_mount); +} + +/** + * @brief TinyUSB Teardown specific testcase + * + * Scenario: + * 1. Install TinyUSB device without any class + * 2. Wait SetConfiguration() (tud_mount_cb) + * 3. If attempts == 0 goto step 8 + * 4. Wait TEARDOWN_DEVICE_DETACH_DELAY_MS + * 5. Uninstall TinyUSB device + * 6. Wait TEARDOWN_DEVICE_INIT_DELAY_MS + * 7. Decrease attempts by 1, goto step 3 + * 8. Wait TEARDOWN_DEVICE_DETACH_DELAY_MS + * 9. Uninstall TinyUSB device + */ +TEST_CASE("tinyusb_teardown", "[esp_tinyusb][teardown]") +{ + wait_mount = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_EQUAL(NULL, wait_mount); + + // TinyUSB driver configuration + const tinyusb_config_t tusb_cfg = { + .device_descriptor = &test_device_descriptor, + .string_descriptor = NULL, + .string_descriptor_count = 0, + .external_phy = false, +#if (TUD_OPT_HIGH_SPEED) + .fs_configuration_descriptor = test_configuration_descriptor, + .hs_configuration_descriptor = test_configuration_descriptor, + .qualifier_descriptor = &device_qualifier, +#else + .configuration_descriptor = test_configuration_descriptor, +#endif // TUD_OPT_HIGH_SPEED + }; + + TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_install(&tusb_cfg)); + // Wait for the usb event + ESP_LOGD(TAG, "wait mount..."); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(wait_mount, pdMS_TO_TICKS(TEARDOWN_DEVICE_ATTACH_TIMEOUT_MS))); + ESP_LOGD(TAG, "mounted"); + + // Teardown routine + int attempts = TEARDOWN_AMOUNT; + while (attempts--) { + // Keep device attached + vTaskDelay(pdMS_TO_TICKS(TEARDOWN_DEVICE_DETACH_DELAY_MS)); + TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_uninstall()); + // Teardown + vTaskDelay(pdMS_TO_TICKS(TEARDOWN_DEVICE_INIT_DELAY_MS)); + // Reconnect + TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_install(&tusb_cfg)); + // Wait for the usb event + ESP_LOGD(TAG, "wait mount..."); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(wait_mount, pdMS_TO_TICKS(TEARDOWN_DEVICE_ATTACH_TIMEOUT_MS))); + ESP_LOGD(TAG, "mounted"); + } + + // Teardown + vTaskDelay(pdMS_TO_TICKS(TEARDOWN_DEVICE_DETACH_DELAY_MS)); + TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_uninstall()); + // Remove primitives + vSemaphoreDelete(wait_mount); +} + +#endif diff --git a/device/esp_tinyusb/test_apps/teardown_device/pytest_teardown_device.py b/device/esp_tinyusb/test_apps/teardown_device/pytest_teardown_device.py new file mode 100644 index 0000000..65b6479 --- /dev/null +++ b/device/esp_tinyusb/test_apps/teardown_device/pytest_teardown_device.py @@ -0,0 +1,75 @@ +# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from pytest_embedded_idf.dut import IdfDut +import subprocess +from time import sleep, time + +class DeviceNotFoundError(Exception): + """Custom exception for device not found within the timeout period.""" + pass + +def tusb_dev_in_list(vid, pid): + try: + output = subprocess.check_output(["lsusb"], text=True) + search_string = f"{vid}:{pid}" + return search_string in output + except Exception as e: + print(f"Error while executing lsusb: {e}") + raise + +def wait_tusb_dev_appeared(vid, pid, timeout): + start_time = time() + while True: + if tusb_dev_in_list(vid, pid): + return True + if time() - start_time > timeout: + raise DeviceNotFoundError(f"Device with VID: 0x{vid:04x}, PID: 0x{pid:04x} not found within {timeout} seconds.") + sleep(0.5) + +def wait_tusb_dev_removed(vid, pid, timeout): + start_time = time() + while True: + if not tusb_dev_in_list(vid, pid): + return True + if time() - start_time > timeout: + raise DeviceNotFoundError(f"Device with VID: 0x{vid:04x}, PID: 0x{pid:04x} wasn't removed within {timeout} seconds.") + sleep(0.5) + +def tusb_device_teardown(iterations, timeout): + TUSB_VID = "303a" # Espressif TinyUSB VID + TUSB_PID = "4002" # Espressif TinyUSB VID + + for i in range(iterations): + # Wait until the device is present + print(f"Waiting for device ...") + wait_tusb_dev_appeared(TUSB_VID, TUSB_PID, timeout) + print("Device detected.") + + # Wait until the device is removed + print("Waiting for the device to be removed...") + wait_tusb_dev_removed(TUSB_VID, TUSB_PID, timeout) + print("Device removed.") + print("Monitoring completed.") + +@pytest.mark.esp32s2 +@pytest.mark.esp32s3 +@pytest.mark.esp32p4 +@pytest.mark.usb_device +def test_usb_teardown_device(dut) -> None: + dut.expect_exact('Press ENTER to see the list of tests.') + dut.write('[teardown]') + dut.expect_exact('TinyUSB: TinyUSB Driver installed') + sleep(2) # Some time for the OS to enumerate our USB device + + try: + tusb_device_teardown(10, 10) # Teardown tusb device: amount, timeout + + except DeviceNotFoundError as e: + print(f"Error: {e}") + raise + + except Exception as e: + print(f"An unexpected error occurred: {e}") + raise diff --git a/device/esp_tinyusb/test_apps/teardown_device/sdkconfig.defaults b/device/esp_tinyusb/test_apps/teardown_device/sdkconfig.defaults new file mode 100644 index 0000000..e437709 --- /dev/null +++ b/device/esp_tinyusb/test_apps/teardown_device/sdkconfig.defaults @@ -0,0 +1,16 @@ +# Configure TinyUSB, it will be used to mock USB devices +CONFIG_TINYUSB_CDC_ENABLED=y +CONFIG_TINYUSB_CDC_COUNT=1 + +# Disable watchdogs, they'd get triggered during unity interactive menu +CONFIG_ESP_INT_WDT=n +CONFIG_ESP_TASK_WDT=n + +# Run-time checks of Heap and Stack +CONFIG_HEAP_POISONING_COMPREHENSIVE=y +CONFIG_COMPILER_STACK_CHECK_MODE_STRONG=y +CONFIG_COMPILER_STACK_CHECK=y + +CONFIG_UNITY_ENABLE_BACKTRACE_ON_FAIL=y + +CONFIG_COMPILER_CXX_EXCEPTIONS=y diff --git a/device/esp_tinyusb/tinyusb.c b/device/esp_tinyusb/tinyusb.c index 52bc2ae..2315ab5 100644 --- a/device/esp_tinyusb/tinyusb.c +++ b/device/esp_tinyusb/tinyusb.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2020-2023 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2020-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -19,6 +19,11 @@ const static char *TAG = "TinyUSB"; static usb_phy_handle_t phy_hdl; +// For the tinyusb component without tusb_teardown() implementation +#ifndef tusb_teardown +# define tusb_teardown() (true) +#endif // tusb_teardown + esp_err_t tinyusb_driver_install(const tinyusb_config_t *config) { ESP_RETURN_ON_FALSE(config, ESP_ERR_INVALID_ARG, TAG, "Config can't be NULL"); @@ -66,8 +71,18 @@ esp_err_t tinyusb_driver_install(const tinyusb_config_t *config) return ESP_OK; } -esp_err_t tinyusb_driver_uninstall() +esp_err_t tinyusb_driver_uninstall(void) { + esp_err_t ret = tusb_stop_task(); + + if (ret != ESP_OK) { + return ret; + } + + if (!tusb_teardown()) { + return ESP_ERR_NOT_FINISHED; + } + tinyusb_free_descriptors(); return usb_del_phy(phy_hdl); }