diff --git a/components/esp_system/include/esp_private/esp_vfs_cdcacm_select.h b/components/esp_system/include/esp_private/esp_vfs_cdcacm_select.h new file mode 100644 index 0000000000..13c77bbdd6 --- /dev/null +++ b/components/esp_system/include/esp_private/esp_vfs_cdcacm_select.h @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "freertos/FreeRTOS.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + CDCACM_SELECT_READ_NOTIF, + CDCACM_SELECT_WRITE_NOTIF, + CDCACM_SELECT_ERROR_NOTIF, +} cdcacm_select_notif_t; + +typedef void (*cdcacm_select_notif_callback_t)(cdcacm_select_notif_t cdcacm_select_notif, BaseType_t *task_woken); + +/** + * @brief Set notification callback function for select() events + * @param cdcacm_select_notif_callback callback function + */ +void cdcacm_set_select_notif_callback(cdcacm_select_notif_callback_t cdcacm_select_notif_callback); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_system/include/esp_private/usb_console.h b/components/esp_system/include/esp_private/usb_console.h index c3877f245b..028b83a2f2 100644 --- a/components/esp_system/include/esp_private/usb_console.h +++ b/components/esp_system/include/esp_private/usb_console.h @@ -1,5 +1,6 @@ + /* - * SPDX-FileCopyrightText: 2019-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2019-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -91,6 +92,15 @@ bool esp_usb_console_write_available(void); */ esp_err_t esp_usb_console_set_cb(esp_usb_console_cb_t rx_cb, esp_usb_console_cb_t tx_cb, void* arg); +/** + * @brief Checks whether the USB console is installed or not + * + * @return + * - true USB console is installed + * - false USB console is not installed + */ +bool esp_usb_console_is_installed(void); + #ifdef __cplusplus } #endif diff --git a/components/esp_system/port/usb_console.c b/components/esp_system/port/usb_console.c index 07adfd55c2..b81b462652 100644 --- a/components/esp_system/port/usb_console.c +++ b/components/esp_system/port/usb_console.c @@ -46,6 +46,8 @@ #include "esp32s3/rom/usb/chip_usb_dw_wrapper.h" #endif +#include "esp_private/esp_vfs_cdcacm_select.h" + #define CDC_WORK_BUF_SIZE (ESP_ROM_CDC_ACM_WORK_BUF_MIN + CONFIG_ESP_CONSOLE_USB_CDC_RX_BUF_SIZE) typedef enum { @@ -55,6 +57,7 @@ typedef enum { REBOOT_BOOTLOADER_DFU, } reboot_type_t; +static bool s_usb_installed = false; static reboot_type_t s_queue_reboot = REBOOT_NONE; static int s_prev_rts_state; static intr_handle_t s_usb_int_handle; @@ -131,6 +134,18 @@ int esp_usb_console_osglue_wait_proc(int delay_us) } } +/* USB interrupt handler, forward the call to the ROM driver. + * Non-static to allow placement into IRAM by ldgen. + */ +static cdcacm_select_notif_callback_t s_cdcacm_select_notif_callback = NULL; + +void cdcacm_set_select_notif_callback(cdcacm_select_notif_callback_t cdcacm_select_notif_callback) +{ + if (esp_usb_console_is_installed()) { + s_cdcacm_select_notif_callback = cdcacm_select_notif_callback; + } +} + /* Called by ROM CDC ACM driver from interrupt context./ * Non-static to allow placement into IRAM by ldgen. */ @@ -168,6 +183,8 @@ void esp_usb_console_dfu_detach_cb(int timeout) */ void esp_usb_console_interrupt(void *arg) { + BaseType_t xTaskWoken = 0; + usb_dc_check_poll_for_interrupts(); /* Restart can be requested from esp_usb_console_cdc_acm_cb or esp_usb_console_dfu_detach_cb */ if (s_queue_reboot != REBOOT_NONE) { @@ -192,6 +209,21 @@ void esp_usb_console_interrupt(void *arg) esp_restart_noos(); } } + + if (esp_usb_console_available_for_read() > 0) { + if (s_cdcacm_select_notif_callback != NULL) { + s_cdcacm_select_notif_callback(CDCACM_SELECT_READ_NOTIF, &xTaskWoken); + } + } + if (esp_usb_console_write_available()) { + if (s_cdcacm_select_notif_callback != NULL) { + s_cdcacm_select_notif_callback(CDCACM_SELECT_WRITE_NOTIF, &xTaskWoken); + } + } + + if (xTaskWoken == pdTRUE) { + portYIELD_FROM_ISR(); + } } /* Called as esp_timer callback when the restart timeout expires. @@ -299,6 +331,7 @@ esp_err_t esp_usb_console_init(void) esp_rom_install_channel_putc(1, &esp_usb_console_write_char); #endif // CONFIG_ESP_CONSOLE_USB_CDC_SUPPORT_ETS_PRINTF + s_usb_installed = true; return ESP_OK; } @@ -422,6 +455,11 @@ esp_err_t esp_usb_console_set_cb(esp_usb_console_cb_t rx_cb, esp_usb_console_cb_ return ESP_OK; } +bool esp_usb_console_is_installed(void) +{ + return s_usb_installed; +} + ssize_t esp_usb_console_available_for_read(void) { if (s_cdc_acm_device == NULL) { diff --git a/components/esp_vfs_console/test_apps/.build-test-rules.yml b/components/esp_vfs_console/test_apps/.build-test-rules.yml new file mode 100644 index 0000000000..526b1ad3b0 --- /dev/null +++ b/components/esp_vfs_console/test_apps/.build-test-rules.yml @@ -0,0 +1,4 @@ +# Documentation: .gitlab/ci/README.md#manifest-file-to-control-the-buildtest-apps +components/esp_vfs_console/test_apps/usb_cdc_vfs: + enable: + - if: IDF_TARGET in ["esp32s3"] # reason: console components is only implemented on these targets. TODO P4: IDF-9120 diff --git a/components/esp_vfs_console/test_apps/usb_cdc_vfs/CMakeLists.txt b/components/esp_vfs_console/test_apps/usb_cdc_vfs/CMakeLists.txt new file mode 100644 index 0000000000..0f63d0caaa --- /dev/null +++ b/components/esp_vfs_console/test_apps/usb_cdc_vfs/CMakeLists.txt @@ -0,0 +1,9 @@ +# This is the project CMakeLists.txt file for the test subproject +cmake_minimum_required(VERSION 3.5) + +list(PREPEND SDKCONFIG_DEFAULTS "$ENV{IDF_PATH}/tools/test_apps/configs/sdkconfig.debug_helpers" "sdkconfig.defaults") + +set(COMPONENTS main) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(usb_cdc_vfs_test) diff --git a/components/esp_vfs_console/test_apps/usb_cdc_vfs/README.md b/components/esp_vfs_console/test_apps/usb_cdc_vfs/README.md new file mode 100644 index 0000000000..be0f068def --- /dev/null +++ b/components/esp_vfs_console/test_apps/usb_cdc_vfs/README.md @@ -0,0 +1,2 @@ +| Supported Targets | ESP32-S3 | +| ----------------- | -------- | diff --git a/components/esp_vfs_console/test_apps/usb_cdc_vfs/main/CMakeLists.txt b/components/esp_vfs_console/test_apps/usb_cdc_vfs/main/CMakeLists.txt new file mode 100644 index 0000000000..14d0b8955d --- /dev/null +++ b/components/esp_vfs_console/test_apps/usb_cdc_vfs/main/CMakeLists.txt @@ -0,0 +1,7 @@ +set(src "test_app_main.c") + +idf_component_register(SRCS ${src} + PRIV_INCLUDE_DIRS . + PRIV_REQUIRES esp_system esp_vfs_console unity + WHOLE_ARCHIVE + ) diff --git a/components/esp_vfs_console/test_apps/usb_cdc_vfs/main/test_app_main.c b/components/esp_vfs_console/test_apps/usb_cdc_vfs/main/test_app_main.c new file mode 100644 index 0000000000..c72692dc75 --- /dev/null +++ b/components/esp_vfs_console/test_apps/usb_cdc_vfs/main/test_app_main.c @@ -0,0 +1,96 @@ +/* + * SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include +#include +#include +#include "unity.h" +#include "esp_private/usb_console.h" +#include "esp_vfs_cdcacm.h" + +static int read_bytes_with_select(FILE *stream, void *buf, size_t buf_len, struct timeval tv) +{ + int fd = fileno(stream); + fd_set read_fds; + FD_ZERO(&read_fds); + FD_SET(fd, &read_fds); + + /* call select with to wait for either a read ready or timeout to happen */ + int nread = select(fd + 1, &read_fds, NULL, NULL, &tv); + if (nread < 0) { + return -1; + } else if (FD_ISSET(fd, &read_fds)) { + int read_count = 0; + int total_read = 0; + + do { + read_count = read(fileno(stream), buf + total_read, 1); + if (read_count < 0 && errno != EWOULDBLOCK) { + return -1; + } else if (read_count > 0) { + total_read += read_count; + if (total_read > buf_len) { + fflush(stream); + break; + } + } + } while (read_count > 0); + + return total_read; + } else { + return -2; + } +} + +void app_main(void) +{ + struct timeval tv; + tv.tv_sec = 1; + tv.tv_usec = 0; + char out_buffer[32]; + memset(out_buffer, 0, sizeof(out_buffer)); + size_t out_buffer_len = sizeof(out_buffer); + + // stdin needs to be non blocking to properly call read after select returns + // with read ready on stdin. + int fd = fileno(stdin); + int flags = fcntl(fd, F_GETFL); + flags |= O_NONBLOCK; + int res = fcntl(fd, F_SETFL, flags); + TEST_ASSERT(res == 0); + + // init driver + // ESP_ERROR_CHECK(esp_usb_console_init()); + // esp_vfs_dev_cdcacm_register(); + + // send the message from pytest environment and make sure it can be read + bool message_received = false; + size_t char_read = 0; + while (!message_received && out_buffer_len > char_read) { + int nread = read_bytes_with_select(stdin, out_buffer + char_read, out_buffer_len - char_read, tv); + if (nread > 0) { + char_read += nread; + if (out_buffer[char_read - 1] == '\n') { + message_received = true; + } + } else if (nread == -2) { + // time out occurred, send the expected message back to the testing + // environment to trigger the testing environment into sending the + // test message. don't update this message without updating the pytest + // function since the string is expected as is by the test environment + char timeout_msg[] = "select timed out\n"; + write(fileno(stdout), timeout_msg, sizeof(timeout_msg)); + } else { + break; + } + } + + // write the received message back to the test environment. The test + // environment will check that the message received matches the one sent + write(fileno(stdout), out_buffer, char_read); + + vTaskDelay(10); // wait for the string to send +} diff --git a/components/esp_vfs_console/test_apps/usb_cdc_vfs/pytest_usb_cdc_vfs.py b/components/esp_vfs_console/test_apps/usb_cdc_vfs/pytest_usb_cdc_vfs.py new file mode 100644 index 0000000000..9c3d626b9b --- /dev/null +++ b/components/esp_vfs_console/test_apps/usb_cdc_vfs/pytest_usb_cdc_vfs.py @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 +import pytest +from pytest_embedded import Dut + + +@pytest.mark.esp32s3 +@pytest.mark.usb_device +@pytest.mark.parametrize( + 'port, flash_port, config', + [ + pytest.param('/dev/serial_ports/ttyACM-esp32', '/dev/serial_ports/ttyUSB-esp32', 'release'), + ], + indirect=True,) +@pytest.mark.parametrize('test_message', ['test123456789!@#%^&*']) +def test_usb_cdc_vfs_default(dut: Dut, test_message: str) -> None: + dut.expect_exact('select timed out', timeout=2) + dut.write(test_message) + dut.expect_exact(test_message, timeout=2) diff --git a/components/esp_vfs_console/test_apps/usb_cdc_vfs/sdkconfig.ci b/components/esp_vfs_console/test_apps/usb_cdc_vfs/sdkconfig.ci new file mode 100644 index 0000000000..e69de29bb2 diff --git a/components/esp_vfs_console/test_apps/usb_cdc_vfs/sdkconfig.ci.release b/components/esp_vfs_console/test_apps/usb_cdc_vfs/sdkconfig.ci.release new file mode 100644 index 0000000000..673b6f8f74 --- /dev/null +++ b/components/esp_vfs_console/test_apps/usb_cdc_vfs/sdkconfig.ci.release @@ -0,0 +1,6 @@ +CONFIG_PM_ENABLE=y +CONFIG_FREERTOS_USE_TICKLESS_IDLE=y +CONFIG_COMPILER_OPTIMIZATION_SIZE=y +CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y +CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT=y +CONFIG_COMPILER_OPTIMIZATION_NONE=y diff --git a/components/esp_vfs_console/test_apps/usb_cdc_vfs/sdkconfig.defaults b/components/esp_vfs_console/test_apps/usb_cdc_vfs/sdkconfig.defaults new file mode 100644 index 0000000000..9f592f7463 --- /dev/null +++ b/components/esp_vfs_console/test_apps/usb_cdc_vfs/sdkconfig.defaults @@ -0,0 +1,8 @@ +# Enable Unity fixture support +CONFIG_UNITY_ENABLE_FIXTURE=n +CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=y + +# Custom partition table for this test app +CONFIG_ESP_TASK_WDT_INIT=n + +CONFIG_ESP_CONSOLE_USB_CDC=y diff --git a/components/esp_vfs_console/vfs_cdcacm.c b/components/esp_vfs_console/vfs_cdcacm.c index 98b3330d54..db30b1fcce 100644 --- a/components/esp_vfs_console/vfs_cdcacm.c +++ b/components/esp_vfs_console/vfs_cdcacm.c @@ -15,9 +15,13 @@ #include "esp_vfs_cdcacm.h" #include "esp_attr.h" #include "sdkconfig.h" +#include "esp_heap_caps.h" +#include "esp_private/esp_vfs_cdcacm_select.h" #include "esp_private/usb_console.h" +#define USB_CDC_LOCAL_FD 0 + // Newline conversion mode when transmitting static esp_line_endings_t s_tx_mode = #if CONFIG_NEWLIB_STDOUT_LINE_ENDING_CRLF @@ -38,6 +42,12 @@ static esp_line_endings_t s_rx_mode = ESP_LINE_ENDINGS_LF; #endif +#if CONFIG_VFS_SELECT_IN_RAM +#define CDCACM_VFS_MALLOC_FLAGS (MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT) +#else +#define CDCACM_VFS_MALLOC_FLAGS MALLOC_CAP_DEFAULT +#endif + #define NONE -1 //Read and write lock, lazily initialized @@ -48,6 +58,26 @@ static bool s_blocking; static SemaphoreHandle_t s_rx_semaphore; static SemaphoreHandle_t s_tx_semaphore; +#ifdef CONFIG_VFS_SUPPORT_SELECT + +typedef struct { + esp_vfs_select_sem_t select_sem; + fd_set *readfds; + fd_set *writefds; + fd_set *errorfds; + fd_set readfds_orig; + fd_set writefds_orig; + fd_set errorfds_orig; +} cdcacm_select_args_t; + +static cdcacm_select_args_t **s_registered_selects = NULL; +static int s_registered_select_num = 0; +static portMUX_TYPE s_registered_select_lock = portMUX_INITIALIZER_UNLOCKED; + +static esp_err_t cdcacm_end_select(void *end_select_args); + +#endif // CONFIG_VFS_SUPPORT_SELECT + static ssize_t cdcacm_write(int fd, const void *data, size_t size) { assert(fd == 0); @@ -82,7 +112,7 @@ static int cdcacm_fsync(int fd) static int cdcacm_open(const char *path, int flags, int mode) { - return 0; // fd 0 + return USB_CDC_LOCAL_FD; // fd 0 } static int cdcacm_fstat(int fd, struct stat *st) @@ -290,6 +320,167 @@ static int cdcacm_fcntl(int fd, int cmd, int arg) return result; } +#ifdef CONFIG_VFS_SUPPORT_SELECT + +static void select_notif_callback_isr(cdcacm_select_notif_t cdcacm_select_notif, BaseType_t *task_woken) +{ + portENTER_CRITICAL_ISR(&s_registered_select_lock); + for (int i = 0; i < s_registered_select_num; ++i) { + cdcacm_select_args_t *args = s_registered_selects[i]; + if (args) { + switch (cdcacm_select_notif) { + case CDCACM_SELECT_READ_NOTIF: + if (FD_ISSET(USB_CDC_LOCAL_FD, &args->readfds_orig)) { + FD_SET(USB_CDC_LOCAL_FD, args->readfds); + esp_vfs_select_triggered_isr(args->select_sem, task_woken); + } + break; + case CDCACM_SELECT_WRITE_NOTIF: + if (FD_ISSET(USB_CDC_LOCAL_FD, &args->writefds_orig)) { + FD_SET(USB_CDC_LOCAL_FD, args->writefds); + esp_vfs_select_triggered_isr(args->select_sem, task_woken); + } + break; + case CDCACM_SELECT_ERROR_NOTIF: + if (FD_ISSET(USB_CDC_LOCAL_FD, &args->errorfds_orig)) { + FD_SET(USB_CDC_LOCAL_FD, args->errorfds); + esp_vfs_select_triggered_isr(args->select_sem, task_woken); + } + break; + } + } + } + portEXIT_CRITICAL_ISR(&s_registered_select_lock); +} + +static esp_err_t register_select(cdcacm_select_args_t *args) +{ + esp_err_t ret = ESP_ERR_INVALID_ARG; + + if (args) { + portENTER_CRITICAL(&s_registered_select_lock); + const int new_size = s_registered_select_num + 1; + cdcacm_select_args_t **new_selects; + if ((new_selects = heap_caps_realloc(s_registered_selects, new_size * sizeof(cdcacm_select_args_t *), CDCACM_VFS_MALLOC_FLAGS)) == NULL) { + ret = ESP_ERR_NO_MEM; + } else { + /* on first select registration register the callback */ + if (s_registered_select_num == 0) { + cdcacm_set_select_notif_callback(select_notif_callback_isr); + } + + s_registered_selects = new_selects; + s_registered_selects[s_registered_select_num] = args; + s_registered_select_num = new_size; + ret = ESP_OK; + } + portEXIT_CRITICAL(&s_registered_select_lock); + } + + return ret; +} + +static esp_err_t unregister_select(cdcacm_select_args_t *args) +{ + esp_err_t ret = ESP_OK; + if (args) { + ret = ESP_ERR_INVALID_STATE; + portENTER_CRITICAL(&s_registered_select_lock); + for (int i = 0; i < s_registered_select_num; ++i) { + if (s_registered_selects[i] == args) { + const int new_size = s_registered_select_num - 1; + // The item is removed by overwriting it with the last item. The subsequent rellocation will drop the + // last item. + s_registered_selects[i] = s_registered_selects[new_size]; + s_registered_selects = heap_caps_realloc(s_registered_selects, new_size * sizeof(cdcacm_select_args_t *), CDCACM_VFS_MALLOC_FLAGS); + // Shrinking a buffer with realloc is guaranteed to succeed. + s_registered_select_num = new_size; + + /* when the last select is unregistered, also unregister the callback */ + if (s_registered_select_num == 0) { + cdcacm_set_select_notif_callback(NULL); + } + + ret = ESP_OK; + break; + } + } + portEXIT_CRITICAL(&s_registered_select_lock); + } + return ret; +} + +static esp_err_t cdcacm_start_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, + esp_vfs_select_sem_t select_sem, void **end_select_args) +{ + (void)nfds; /* Since there is only 1 USB OTG, this parameter is useless */ + *end_select_args = NULL; + + if (!esp_usb_console_is_installed()) { + return ESP_ERR_INVALID_STATE; + } + + cdcacm_select_args_t *args = heap_caps_malloc(sizeof(cdcacm_select_args_t), CDCACM_VFS_MALLOC_FLAGS); + + if (args == NULL) { + return ESP_ERR_NO_MEM; + } + + args->select_sem = select_sem; + args->readfds = readfds; + args->writefds = writefds; + args->errorfds = exceptfds; + args->readfds_orig = *readfds; // store the original values because they will be set to zero + args->writefds_orig = *writefds; + args->errorfds_orig = *exceptfds; + FD_ZERO(readfds); + FD_ZERO(writefds); + FD_ZERO(exceptfds); + + esp_err_t ret = register_select(args); + if (ret != ESP_OK) { + free(args); + return ret; + } + + bool trigger_select = false; + if (FD_ISSET(USB_CDC_LOCAL_FD, &args->readfds_orig) && + esp_usb_console_available_for_read() > 0) { + + // signalize immediately when read is ready + FD_SET(USB_CDC_LOCAL_FD, readfds); + trigger_select = true; + } + + if (FD_ISSET(USB_CDC_LOCAL_FD, &args->writefds_orig) && + esp_usb_console_write_available()) { + + // signalize immediately when write is ready + FD_SET(USB_CDC_LOCAL_FD, writefds); + trigger_select = true; + } + + if (trigger_select) { + esp_vfs_select_triggered(args->select_sem); + } + + *end_select_args = args; + return ESP_OK; +} + +static esp_err_t cdcacm_end_select(void *end_select_args) +{ + cdcacm_select_args_t *args = end_select_args; + esp_err_t ret = unregister_select(args); + if (args) { + free(args); + } + + return ret; +} + +#endif // CONFIG_VFS_SUPPORT_SELECT + void esp_vfs_dev_cdcacm_set_tx_line_endings(esp_line_endings_t mode) { s_tx_mode = mode; @@ -300,6 +491,13 @@ void esp_vfs_dev_cdcacm_set_rx_line_endings(esp_line_endings_t mode) s_rx_mode = mode; } +#ifdef CONFIG_VFS_SUPPORT_SELECT +static const esp_vfs_select_ops_t s_cdcacm_vfs_select = { + .start_select = &cdcacm_start_select, + .end_select = &cdcacm_end_select, +}; +#endif // CONFIG_VFS_SUPPORT_SELECT + static const esp_vfs_fs_ops_t s_cdcacm_vfs = { .write = &cdcacm_write, .open = &cdcacm_open, @@ -307,7 +505,10 @@ static const esp_vfs_fs_ops_t s_cdcacm_vfs = { .close = &cdcacm_close, .read = &cdcacm_read, .fcntl = &cdcacm_fcntl, - .fsync = &cdcacm_fsync + .fsync = &cdcacm_fsync, +#ifdef CONFIG_VFS_SUPPORT_SELECT + .select = &s_cdcacm_vfs_select, +#endif // CONFIG_VFS_SUPPORT_SELECT }; const esp_vfs_fs_ops_t *esp_vfs_cdcacm_get_vfs(void)