From fc8cd4048d0089dd9f1df01c17f992e71d419553 Mon Sep 17 00:00:00 2001 From: Ivan Grokhotkov Date: Thu, 30 Apr 2020 16:36:20 +0200 Subject: [PATCH] vfs: add vfs_cdcacm driver (aka USB CDC) --- components/vfs/CMakeLists.txt | 4 + components/vfs/component.mk | 8 +- components/vfs/include/esp_vfs_cdcacm.h | 66 +++++ components/vfs/vfs_cdcacm.c | 324 ++++++++++++++++++++++++ 4 files changed, 397 insertions(+), 5 deletions(-) create mode 100644 components/vfs/include/esp_vfs_cdcacm.h create mode 100644 components/vfs/vfs_cdcacm.c diff --git a/components/vfs/CMakeLists.txt b/components/vfs/CMakeLists.txt index d62c9e039..e2e3e1ec5 100644 --- a/components/vfs/CMakeLists.txt +++ b/components/vfs/CMakeLists.txt @@ -3,6 +3,10 @@ idf_component_register(SRCS "vfs.c" "vfs_semihost.c" INCLUDE_DIRS include) +if(CONFIG_ESP_CONSOLE_USB_CDC) + target_sources(${COMPONENT_LIB} PRIVATE "vfs_cdcacm.c") +endif() + # Some newlib syscalls are implemented in vfs.c, make sure these are always # seen by the linker target_link_libraries(${COMPONENT_LIB} INTERFACE "-u vfs_include_syscalls_impl") diff --git a/components/vfs/component.mk b/components/vfs/component.mk index c2c4c03a1..f1fca0764 100644 --- a/components/vfs/component.mk +++ b/components/vfs/component.mk @@ -1,5 +1,3 @@ -# -# Component Makefile -# -# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) - +ifndef CONFIG_ESP_CONSOLE_USB_CDC + COMPONENT_OBJEXCLUDE := vfs_cdcacm.o +endif diff --git a/components/vfs/include/esp_vfs_cdcacm.h b/components/vfs/include/esp_vfs_cdcacm.h new file mode 100644 index 000000000..b8dd03d92 --- /dev/null +++ b/components/vfs/include/esp_vfs_cdcacm.h @@ -0,0 +1,66 @@ +// Copyright 2015-2017 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "esp_err.h" +#include "esp_vfs.h" +#include "esp_vfs_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief add /dev/cdcacm virtual filesystem driver + * + * This function is called from startup code to enable console output + */ +esp_err_t esp_vfs_dev_cdcacm_register(void); + +/** + * @brief Set the line endings expected to be received + * + * This specifies the conversion between line endings received and + * newlines ('\n', LF) passed into stdin: + * + * - ESP_LINE_ENDINGS_CRLF: convert CRLF to LF + * - ESP_LINE_ENDINGS_CR: convert CR to LF + * - ESP_LINE_ENDINGS_LF: no modification + * + * @note this function is not thread safe w.r.t. reading + * + * @param mode line endings expected + */ +void esp_vfs_dev_cdcacm_set_rx_line_endings(esp_line_endings_t mode); + +/** + * @brief Set the line endings to sent + * + * This specifies the conversion between newlines ('\n', LF) on stdout and line + * endings sent: + * + * - ESP_LINE_ENDINGS_CRLF: convert LF to CRLF + * - ESP_LINE_ENDINGS_CR: convert LF to CR + * - ESP_LINE_ENDINGS_LF: no modification + * + * @note this function is not thread safe w.r.t. writing + * + * @param mode line endings to send + */ +void esp_vfs_dev_cdcacm_set_tx_line_endings(esp_line_endings_t mode); + +#ifdef __cplusplus +} +#endif diff --git a/components/vfs/vfs_cdcacm.c b/components/vfs/vfs_cdcacm.c new file mode 100644 index 000000000..02cce7e68 --- /dev/null +++ b/components/vfs/vfs_cdcacm.c @@ -0,0 +1,324 @@ +// Copyright 2015-2020 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include +#include "esp_vfs.h" +#include "esp_vfs_cdcacm.h" +#include "esp_attr.h" +#include "sdkconfig.h" + +#include "esp_private/usb_console.h" + +// Newline conversion mode when transmitting +static esp_line_endings_t s_tx_mode = +#if CONFIG_NEWLIB_STDOUT_LINE_ENDING_CRLF + ESP_LINE_ENDINGS_CRLF; +#elif CONFIG_NEWLIB_STDOUT_LINE_ENDING_CR + ESP_LINE_ENDINGS_CR; +#else + ESP_LINE_ENDINGS_LF; +#endif + +// Newline conversion mode when receiving +static esp_line_endings_t s_rx_mode = +#if CONFIG_NEWLIB_STDIN_LINE_ENDING_CRLF + ESP_LINE_ENDINGS_CRLF; +#elif CONFIG_NEWLIB_STDIN_LINE_ENDING_CR + ESP_LINE_ENDINGS_CR; +#else + ESP_LINE_ENDINGS_LF; +#endif + +#define NONE -1 + +//Read and write lock, lazily initialized +static _lock_t s_write_lock; +static _lock_t s_read_lock; + +static bool s_blocking; +static SemaphoreHandle_t s_rx_semaphore; +static SemaphoreHandle_t s_tx_semaphore; + +static ssize_t cdcacm_write(int fd, const void *data, size_t size) +{ + assert(fd == 0); + const char *cdata = (const char *)data; + _lock_acquire_recursive(&s_write_lock); + for (size_t i = 0; i < size; i++) { + if (cdata[i] != '\n') { + esp_usb_console_write_buf(&cdata[i], 1); + } else { + if (s_tx_mode == ESP_LINE_ENDINGS_CRLF || s_tx_mode == ESP_LINE_ENDINGS_CR) { + char cr = '\r'; + esp_usb_console_write_buf(&cr, 1); + } + if (s_tx_mode == ESP_LINE_ENDINGS_CRLF || s_tx_mode == ESP_LINE_ENDINGS_LF) { + char lf = '\n'; + esp_usb_console_write_buf(&lf, 1); + } + } + } + _lock_release_recursive(&s_write_lock); + return size; +} + +static int cdcacm_fsync(int fd) +{ + assert(fd == 0); + _lock_acquire_recursive(&s_write_lock); + ssize_t written = esp_usb_console_flush(); + _lock_release_recursive(&s_write_lock); + return (written < 0) ? -1 : 0; +} + +static int cdcacm_open(const char *path, int flags, int mode) +{ + return 0; // fd 0 +} + +static int cdcacm_fstat(int fd, struct stat *st) +{ + assert(fd == 0); + st->st_mode = S_IFCHR; + return 0; +} + +static int cdcacm_close(int fd) +{ + assert(fd == 0); + return 0; +} + +static int s_peek_char = NONE; + +/* Helper function which returns a previous character or reads a new one from + * CDC-ACM driver. Previous character can be returned ("pushed back") using + * cdcacm_return_char function. Returns NONE if no character is available. Note + * the cdcacm driver maintains its own RX buffer and a receive call does not + * invoke an USB operation, so there's no penalty to reading data char-by-char. + */ +static int cdcacm_read_char(void) +{ + /* return character from peek buffer, if it is there */ + if (s_peek_char != NONE) { + int c = s_peek_char; + s_peek_char = NONE; + return c; + } + /* Peek buffer is empty; try to read from cdcacm driver. */ + uint8_t c; + ssize_t read = esp_usb_console_read_buf((char *) &c, 1); + if (read <= 0) { + return NONE; + } else { + return c; + } +} + +static bool cdcacm_data_in_buffer(void) +{ + if (s_peek_char != NONE) { + return true; + } + if (esp_usb_console_read_available()) { + return true; + } + return false; +} + +/* Push back a character; it will be returned by next call to cdcacm_read_char */ +static void cdcacm_return_char(int c) +{ + assert(s_peek_char == NONE); + s_peek_char = c; +} + +static ssize_t cdcacm_read(int fd, void *data, size_t size) +{ + assert(fd == 0); + char *data_c = (char *) data; + ssize_t received = 0; + _lock_acquire_recursive(&s_read_lock); + + while (!cdcacm_data_in_buffer()) { + if (!s_blocking) { + errno = EWOULDBLOCK; + _lock_release_recursive(&s_read_lock); + return -1; + } + xSemaphoreTake(s_rx_semaphore, portMAX_DELAY); + } + + + if (s_rx_mode == ESP_LINE_ENDINGS_CR || s_rx_mode == ESP_LINE_ENDINGS_LF) { + /* This is easy. Just receive, and if needed replace \r by \n. */ + received = esp_usb_console_read_buf(data_c, size); + if (s_rx_mode == ESP_LINE_ENDINGS_CR) { + /* Change CRs to newlines */ + for (ssize_t i = 0; i < received; i++) { + if (data_c[i] == '\r') { + data_c[i] = '\n'; + } + } + } + } else { + while (received < size) { + int c = cdcacm_read_char(); + if (c == '\r') { + /* look ahead */ + int c2 = cdcacm_read_char(); + if (c2 == NONE) { + /* could not look ahead, put the current character back */ + cdcacm_return_char(c); + break; + } + if (c2 == '\n') { + /* this was \r\n sequence. discard \r, return \n */ + c = '\n'; + } else { + /* \r followed by something else. put the second char back, + * it will be processed on next iteration. return \r now. + */ + cdcacm_return_char(c2); + } + } else if (c == NONE) { + break; + } + data_c[received++] = (char) c; + if (c == '\n') { + break; + } + } + } + _lock_release_recursive(&s_read_lock); + if (received > 0) { + return received; + } + errno = EWOULDBLOCK; + return -1; +} + +/* Non-static, to be able to place into IRAM by ldgen */ +void cdcacm_rx_cb(void* arg) +{ + assert(s_blocking); + xSemaphoreGive(s_rx_semaphore); +} + +/* Non-static, to be able to place into IRAM by ldgen */ +void cdcacm_tx_cb(void* arg) +{ + assert(s_blocking); + xSemaphoreGive(s_tx_semaphore); +} + +static int cdcacm_enable_blocking(void) +{ + s_rx_semaphore = xSemaphoreCreateBinary(); + if (!s_rx_semaphore) { + errno = ENOMEM; + goto fail; + } + s_tx_semaphore = xSemaphoreCreateBinary(); + if (!s_tx_semaphore) { + errno = ENOMEM; + goto fail; + } + esp_err_t err = esp_usb_console_set_cb(&cdcacm_rx_cb, &cdcacm_tx_cb, NULL); + if (err != ESP_OK) { + errno = ENODEV; + goto fail; + } + s_blocking = true; + return 0; + +fail: + if (s_rx_semaphore) { + vSemaphoreDelete(s_rx_semaphore); + s_rx_semaphore = NULL; + } + if (s_tx_semaphore) { + vSemaphoreDelete(s_tx_semaphore); + s_tx_semaphore = NULL; + } + return -1; +} + +static int cdcacm_disable_blocking(void) +{ + esp_usb_console_set_cb(NULL, NULL, NULL); /* ignore any errors */ + vSemaphoreDelete(s_rx_semaphore); + s_rx_semaphore = NULL; + vSemaphoreDelete(s_tx_semaphore); + s_tx_semaphore = NULL; + s_blocking = false; + return 0; +} + + +static int cdcacm_fcntl(int fd, int cmd, int arg) +{ + assert(fd == 0); + int result; + if (cmd == F_GETFL) { + result = 0; + if (!s_blocking) { + result |= O_NONBLOCK; + } + } else if (cmd == F_SETFL) { + bool blocking = (arg & O_NONBLOCK) == 0; + result = 0; + if (blocking && !s_blocking) { + result = cdcacm_enable_blocking(); + } else if (!blocking && s_blocking) { + result = cdcacm_disable_blocking(); + } + } else { + /* unsupported operation */ + result = -1; + errno = ENOSYS; + } + return result; +} + +void esp_vfs_dev_cdcacm_set_tx_line_endings(esp_line_endings_t mode) +{ + s_tx_mode = mode; +} + +void esp_vfs_dev_cdcacm_set_rx_line_endings(esp_line_endings_t mode) +{ + s_rx_mode = mode; +} + +esp_err_t esp_vfs_dev_cdcacm_register(void) +{ + const esp_vfs_t vfs = { + .flags = ESP_VFS_FLAG_DEFAULT, + .write = &cdcacm_write, + .open = &cdcacm_open, + .fstat = &cdcacm_fstat, + .close = &cdcacm_close, + .read = &cdcacm_read, + .fcntl = &cdcacm_fcntl, + .fsync = &cdcacm_fsync + }; + return esp_vfs_register("/dev/cdcacm", &vfs, NULL); +}