vfs: add vfs_cdcacm driver (aka USB CDC)

This commit is contained in:
Ivan Grokhotkov 2020-04-30 16:36:20 +02:00
parent 34f441249b
commit fc8cd4048d
4 changed files with 397 additions and 5 deletions

View file

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

View file

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

View file

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

324
components/vfs/vfs_cdcacm.c Normal file
View file

@ -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 <string.h>
#include <stdbool.h>
#include <stdarg.h>
#include <sys/errno.h>
#include <sys/lock.h>
#include <sys/fcntl.h>
#include <sys/param.h>
#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);
}