feat(usb_cdc): Add select functionality

This commit is contained in:
Guillaume Souchere
2024-05-28 08:56:36 +02:00
parent 4ace94342a
commit 181c903b0b
13 changed files with 434 additions and 3 deletions

View File

@@ -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

View File

@@ -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 * 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); 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 #ifdef __cplusplus
} }
#endif #endif

View File

@@ -46,6 +46,8 @@
#include "esp32s3/rom/usb/chip_usb_dw_wrapper.h" #include "esp32s3/rom/usb/chip_usb_dw_wrapper.h"
#endif #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) #define CDC_WORK_BUF_SIZE (ESP_ROM_CDC_ACM_WORK_BUF_MIN + CONFIG_ESP_CONSOLE_USB_CDC_RX_BUF_SIZE)
typedef enum { typedef enum {
@@ -55,6 +57,7 @@ typedef enum {
REBOOT_BOOTLOADER_DFU, REBOOT_BOOTLOADER_DFU,
} reboot_type_t; } reboot_type_t;
static bool s_usb_installed = false;
static reboot_type_t s_queue_reboot = REBOOT_NONE; static reboot_type_t s_queue_reboot = REBOOT_NONE;
static int s_prev_rts_state; static int s_prev_rts_state;
static intr_handle_t s_usb_int_handle; 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./ /* Called by ROM CDC ACM driver from interrupt context./
* Non-static to allow placement into IRAM by ldgen. * 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) void esp_usb_console_interrupt(void *arg)
{ {
BaseType_t xTaskWoken = 0;
usb_dc_check_poll_for_interrupts(); usb_dc_check_poll_for_interrupts();
/* Restart can be requested from esp_usb_console_cdc_acm_cb or esp_usb_console_dfu_detach_cb */ /* Restart can be requested from esp_usb_console_cdc_acm_cb or esp_usb_console_dfu_detach_cb */
if (s_queue_reboot != REBOOT_NONE) { if (s_queue_reboot != REBOOT_NONE) {
@@ -192,6 +209,21 @@ void esp_usb_console_interrupt(void *arg)
esp_restart_noos(); 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. /* 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); esp_rom_install_channel_putc(1, &esp_usb_console_write_char);
#endif // CONFIG_ESP_CONSOLE_USB_CDC_SUPPORT_ETS_PRINTF #endif // CONFIG_ESP_CONSOLE_USB_CDC_SUPPORT_ETS_PRINTF
s_usb_installed = true;
return ESP_OK; 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; return ESP_OK;
} }
bool esp_usb_console_is_installed(void)
{
return s_usb_installed;
}
ssize_t esp_usb_console_available_for_read(void) ssize_t esp_usb_console_available_for_read(void)
{ {
if (s_cdc_acm_device == NULL) { if (s_cdc_acm_device == NULL) {

View File

@@ -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

View File

@@ -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)

View File

@@ -0,0 +1,2 @@
| Supported Targets | ESP32-S3 |
| ----------------- | -------- |

View File

@@ -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
)

View File

@@ -0,0 +1,96 @@
/*
* SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/errno.h>
#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
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -15,9 +15,13 @@
#include "esp_vfs_cdcacm.h" #include "esp_vfs_cdcacm.h"
#include "esp_attr.h" #include "esp_attr.h"
#include "sdkconfig.h" #include "sdkconfig.h"
#include "esp_heap_caps.h"
#include "esp_private/esp_vfs_cdcacm_select.h"
#include "esp_private/usb_console.h" #include "esp_private/usb_console.h"
#define USB_CDC_LOCAL_FD 0
// Newline conversion mode when transmitting // Newline conversion mode when transmitting
static esp_line_endings_t s_tx_mode = static esp_line_endings_t s_tx_mode =
#if CONFIG_NEWLIB_STDOUT_LINE_ENDING_CRLF #if CONFIG_NEWLIB_STDOUT_LINE_ENDING_CRLF
@@ -38,6 +42,12 @@ static esp_line_endings_t s_rx_mode =
ESP_LINE_ENDINGS_LF; ESP_LINE_ENDINGS_LF;
#endif #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 #define NONE -1
//Read and write lock, lazily initialized //Read and write lock, lazily initialized
@@ -48,6 +58,26 @@ static bool s_blocking;
static SemaphoreHandle_t s_rx_semaphore; static SemaphoreHandle_t s_rx_semaphore;
static SemaphoreHandle_t s_tx_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) static ssize_t cdcacm_write(int fd, const void *data, size_t size)
{ {
assert(fd == 0); assert(fd == 0);
@@ -82,7 +112,7 @@ static int cdcacm_fsync(int fd)
static int cdcacm_open(const char *path, int flags, int mode) 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) 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; 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) void esp_vfs_dev_cdcacm_set_tx_line_endings(esp_line_endings_t mode)
{ {
s_tx_mode = 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; 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 = { static const esp_vfs_fs_ops_t s_cdcacm_vfs = {
.write = &cdcacm_write, .write = &cdcacm_write,
.open = &cdcacm_open, .open = &cdcacm_open,
@@ -307,7 +505,10 @@ static const esp_vfs_fs_ops_t s_cdcacm_vfs = {
.close = &cdcacm_close, .close = &cdcacm_close,
.read = &cdcacm_read, .read = &cdcacm_read,
.fcntl = &cdcacm_fcntl, .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) const esp_vfs_fs_ops_t *esp_vfs_cdcacm_get_vfs(void)