From 34f441249bbb900018bdf9988f63e16eb5c532bd Mon Sep 17 00:00:00 2001 From: Ivan Grokhotkov Date: Thu, 30 Apr 2020 16:35:22 +0200 Subject: [PATCH] esp32s2: add internal usb_console API --- .../include/esp_private/usb_console.h | 73 +++ .../esp_system/port/esp32s2/CMakeLists.txt | 3 + .../esp_system/port/esp32s2/usb_console.c | 425 ++++++++++++++++++ 3 files changed, 501 insertions(+) create mode 100644 components/esp_system/include/esp_private/usb_console.h create mode 100644 components/esp_system/port/esp32s2/usb_console.c diff --git a/components/esp_system/include/esp_private/usb_console.h b/components/esp_system/include/esp_private/usb_console.h new file mode 100644 index 000000000..1e9536f45 --- /dev/null +++ b/components/esp_system/include/esp_private/usb_console.h @@ -0,0 +1,73 @@ +// Copyright 2019-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. + +#pragma once + +#include +#include +#include +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file usb_console.h + * This file contains definitions of low-level USB console functions. + * These functions are not considered to be a public interface and + * should not be called by applications directly. + * Application interface to the USB console is provided either by + * "cdcacm" VFS driver, or by the USB CDC driver in TinyUSB. + */ + + +/** + * RX/TX callback function type + * @param arg callback-specific context pointer + */ +typedef void (*esp_usb_console_cb_t)(void* arg); + +/** + * Initialize USB console output using ROM USB CDC driver. + * This function is called by the early startup code if USB CDC is + * selected as the console output option. + * @return + * - ESP_OK on success + * - ESP_ERR_NO_MEM + * - other error codes from the interrupt allocator + */ +esp_err_t esp_usb_console_init(void); + +/** + * Write a buffer to USB CDC + * @param buf data to write + * @param size size of the data, in bytes + * @return -1 on error, otherwise the number of bytes + */ +ssize_t esp_usb_console_write_buf(const char* buf, size_t size); + +ssize_t esp_usb_console_flush(void); + +ssize_t esp_usb_console_read_buf(char* buf, size_t buf_size); + +bool esp_usb_console_read_available(void); + +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); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_system/port/esp32s2/CMakeLists.txt b/components/esp_system/port/esp32s2/CMakeLists.txt index e07df9ed2..4c2e7c85e 100644 --- a/components/esp_system/port/esp32s2/CMakeLists.txt +++ b/components/esp_system/port/esp32s2/CMakeLists.txt @@ -2,3 +2,6 @@ set(srcs "dport_panic_highint_hdl.S" "clk.c" "reset_reason.c") add_prefix(srcs "${CMAKE_CURRENT_LIST_DIR}/" ${srcs}) target_sources(${COMPONENT_LIB} PRIVATE ${srcs}) +if(CONFIG_ESP_CONSOLE_USB_CDC) + target_sources(${COMPONENT_LIB} PRIVATE "${CMAKE_CURRENT_LIST_DIR}/usb_console.c") +endif() diff --git a/components/esp_system/port/esp32s2/usb_console.c b/components/esp_system/port/esp32s2/usb_console.c new file mode 100644 index 000000000..16c62d464 --- /dev/null +++ b/components/esp_system/port/esp32s2/usb_console.c @@ -0,0 +1,425 @@ +// Copyright 2019-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 "sdkconfig.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "esp_system.h" +#include "esp_intr_alloc.h" +#include "esp_private/usb_console.h" +#include "soc/periph_defs.h" +#include "soc/rtc_cntl_reg.h" +#include "soc/usb_struct.h" +#include "soc/usb_reg.h" +#include "soc/spinlock.h" +#include "hal/soc_hal.h" +#include "esp32s2/rom/uart.h" +#include "esp32s2/rom/usb/usb_dc.h" +#include "esp32s2/rom/usb/cdc_acm.h" +#include "esp32s2/rom/usb/usb_dfu.h" +#include "esp32s2/rom/usb/usb_device.h" +#include "esp32s2/rom/usb/usb_os_glue.h" +#include "esp32s2/rom/usb/usb_persist.h" +#include "esp32s2/rom/usb/chip_usb_dw_wrapper.h" + + +#define CDC_WORK_BUF_SIZE (CDC_ACM_WORK_BUF_MIN + CONFIG_ESP_CONSOLE_USB_CDC_RX_BUF_SIZE) + +typedef enum { + REBOOT_NONE, + REBOOT_NORMAL, + REBOOT_BOOTLOADER, + REBOOT_BOOTLOADER_DFU, +} reboot_type_t; + + +static reboot_type_t s_queue_reboot = REBOOT_NONE; +static int s_prev_rts_state; +static intr_handle_t s_usb_int_handle; +static cdc_acm_device *s_cdc_acm_device; +static char s_usb_tx_buf[ACM_BYTES_PER_TX]; +static size_t s_usb_tx_buf_pos; +static uint8_t cdcmem[CDC_WORK_BUF_SIZE]; +static esp_usb_console_cb_t s_rx_cb; +static esp_usb_console_cb_t s_tx_cb; +static void *s_cb_arg; + +#ifdef CONFIG_ESP_CONSOLE_USB_CDC_SUPPORT_ETS_PRINTF +static spinlock_t s_write_lock = SPINLOCK_INITIALIZER; +void esp_usb_console_write_char(char c); +#define ISR_FLAG ESP_INTR_FLAG_IRAM +#else +#define ISR_FLAG 0 +#endif // CONFIG_ESP_CONSOLE_USB_CDC_SUPPORT_ETS_PRINTF + + +/* Optional write lock routines; used only if ets_printf output via CDC is enabled */ +static inline void write_lock_acquire(void); +static inline void write_lock_release(void); + +/* The two functions below need to be revisited in the multicore case */ +_Static_assert(SOC_CPU_CORES_NUM == 1, "usb_osglue_*_int is not multicore capable"); + +/* Called by ROM to disable the interrupts + * Non-static to allow placement into IRAM by ldgen. + */ +void esp_usb_console_osglue_dis_int(void) +{ + if (s_usb_int_handle) { + esp_intr_disable(s_usb_int_handle); + } +} + +/* Called by ROM to enable the interrupts + * Non-static to allow placement into IRAM by ldgen. + */ +void esp_usb_console_osglue_ena_int(void) +{ + if (s_usb_int_handle) { + esp_intr_enable(s_usb_int_handle); + } +} + +/* Delay function called by ROM USB driver. + * Non-static to allow placement into IRAM by ldgen. + */ +int esp_usb_console_osglue_wait_proc(int delay_us) +{ + if (xTaskGetSchedulerState() != taskSCHEDULER_RUNNING || + !xPortCanYield()) { + ets_delay_us(delay_us); + return delay_us; + } + if (delay_us == 0) { + /* We should effectively yield */ + vPortYield(); + return 1; + } else { + /* Just delay */ + int ticks = MAX(delay_us / (portTICK_PERIOD_MS * 1000), 1); + vTaskDelay(ticks); + return ticks * portTICK_PERIOD_MS * 1000; + } +} + +/* Called by ROM CDC ACM driver from interrupt context./ + * Non-static to allow placement into IRAM by ldgen. + */ +void esp_usb_console_cdc_acm_cb(cdc_acm_device *dev, int status) +{ + if (status == USB_DC_RESET || status == USB_DC_CONNECTED) { + s_prev_rts_state = 0; + } else if (status == ACM_STATUS_LINESTATE_CHANGED) { + uint32_t rts, dtr; + cdc_acm_line_ctrl_get(dev, LINE_CTRL_RTS, &rts); + cdc_acm_line_ctrl_get(dev, LINE_CTRL_DTR, &dtr); + if (!rts && s_prev_rts_state) { + if (dtr) { + s_queue_reboot = REBOOT_BOOTLOADER; + } else { + s_queue_reboot = REBOOT_NORMAL; + } + } + s_prev_rts_state = rts; + } else if (status == ACM_STATUS_RX && s_rx_cb) { + (*s_rx_cb)(s_cb_arg); + } else if (status == ACM_STATUS_TX && s_tx_cb) { + (*s_tx_cb)(s_cb_arg); + } +} + +/* Non-static to allow placement into IRAM by ldgen. */ +void esp_usb_console_dfu_detach_cb(int timeout) +{ + s_queue_reboot = REBOOT_BOOTLOADER_DFU; +} + +/* USB interrupt handler, forward the call to the ROM driver. + * Non-static to allow placement into IRAM by ldgen. + */ +void esp_usb_console_interrupt(void *arg) +{ + 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) { + esp_restart(); + } +} + +/* Call the USB interrupt handler while any interrupts are pending, + * but not more than a few times. + * Non-static to allow placement into IRAM by ldgen. + */ +void esp_usb_console_poll_interrupts(void) +{ + const int max_poll_count = 10; + for (int i = 0; (USB0.gintsts & USB0.gintmsk) != 0 && i < max_poll_count; i++) { + usb_dc_check_poll_for_interrupts(); + } +} + +/* This function gets registered as a restart handler. + * Prepares USB peripheral for restart and sets up persistence. + * Non-static to allow placement into IRAM by ldgen. + */ +void esp_usb_console_before_restart(void) +{ + esp_usb_console_poll_interrupts(); + usb_dc_prepare_persist(); + if (s_queue_reboot == REBOOT_BOOTLOADER) { + chip_usb_set_persist_flags(USBDC_PERSIST_ENA); + REG_WRITE(RTC_CNTL_OPTION1_REG, RTC_CNTL_FORCE_DOWNLOAD_BOOT); + } else if (s_queue_reboot == REBOOT_BOOTLOADER_DFU) { + chip_usb_set_persist_flags(USBDC_BOOT_DFU); + REG_WRITE(RTC_CNTL_OPTION1_REG, RTC_CNTL_FORCE_DOWNLOAD_BOOT); + } else { + chip_usb_set_persist_flags(USBDC_PERSIST_ENA); + esp_usb_console_poll_interrupts(); + } +} + +/* Reset some static state in ROM, which survives when going from the + * 2nd stage bootloader into the app. This cleans some variables which + * indicates that the driver is already initialized, allowing us to + * initialize it again, in the app. + */ +static void esp_usb_console_rom_cleanup(void) +{ + extern char rom_usb_dev, rom_usb_dev_end; + extern char rom_usb_dw_ctrl, rom_usb_dw_ctrl_end; + + uart_acm_dev = NULL; + memset((void *) &rom_usb_dev, 0, &rom_usb_dev_end - &rom_usb_dev); + memset((void *) &rom_usb_dw_ctrl, 0, &rom_usb_dw_ctrl_end - &rom_usb_dw_ctrl); +} + +esp_err_t esp_usb_console_init(void) +{ + esp_err_t err; + err = esp_register_shutdown_handler(esp_usb_console_before_restart); + if (err != ESP_OK) { + return err; + } + + esp_usb_console_rom_cleanup(); + + /* Install OS hooks */ + rom_usb_osglue.int_dis_proc = esp_usb_console_osglue_dis_int; + rom_usb_osglue.int_ena_proc = esp_usb_console_osglue_ena_int; + rom_usb_osglue.wait_proc = esp_usb_console_osglue_wait_proc; + + /* Install interrupt. + * In case of ESP_CONSOLE_USB_CDC_SUPPORT_ETS_PRINTF: + * Note that this the interrupt handler has to be placed into IRAM because + * the interrupt handler can also be called in polling mode, when + * interrupts are disabled, and a write to USB is performed with cache disabled. + * Since the handler function is in IRAM, we can register the interrupt as IRAM capable. + * It is not because we actually need the interrupt to work with cache disabled! + */ + err = esp_intr_alloc(ETS_USB_INTR_SOURCE, ISR_FLAG | ESP_INTR_FLAG_INTRDISABLED, + esp_usb_console_interrupt, NULL, &s_usb_int_handle); + if (err != ESP_OK) { + esp_unregister_shutdown_handler(esp_usb_console_before_restart); + return err; + } + + /* Initialize USB / CDC */ + s_cdc_acm_device = cdc_acm_init(cdcmem, CDC_WORK_BUF_SIZE); + usb_dc_check_poll_for_interrupts(); + + /* Set callback for handling DTR/RTS lines and TX/RX events */ + cdc_acm_irq_callback_set(s_cdc_acm_device, esp_usb_console_cdc_acm_cb); + cdc_acm_irq_state_enable(s_cdc_acm_device); + + /* Set callback for handling DFU detach */ + usb_dfu_set_detach_cb(esp_usb_console_dfu_detach_cb); + + /* Enable interrupts on USB peripheral side */ + USB0.gahbcfg |= USB_GLBLLNTRMSK_M; + + /* Enable the interrupt handler */ + esp_intr_enable(s_usb_int_handle); + +#ifdef CONFIG_ESP_CONSOLE_USB_CDC_SUPPORT_ETS_PRINTF + /* Install ets_printf handler */ + ets_install_putc1(&esp_usb_console_write_char); +#endif // CONFIG_ESP_CONSOLE_USB_CDC_SUPPORT_ETS_PRINTF + + return ESP_OK; +} + +/* Non-static to allow placement into IRAM by ldgen. + * Must be called with the write lock held. + */ +ssize_t esp_usb_console_flush_internal(size_t last_write_size) +{ + if (s_usb_tx_buf_pos == 0) { + return 0; + } + assert(s_usb_tx_buf_pos >= last_write_size); + ssize_t ret; + size_t tx_buf_pos_before = s_usb_tx_buf_pos - last_write_size; + int sent = cdc_acm_fifo_fill(s_cdc_acm_device, (const uint8_t*) s_usb_tx_buf, s_usb_tx_buf_pos); + if (sent == last_write_size) { + /* everything was sent */ + ret = last_write_size; + s_usb_tx_buf_pos = 0; + } else if (sent == 0) { + /* nothing was sent, roll back to the original state */ + ret = 0; + s_usb_tx_buf_pos = tx_buf_pos_before; + } else { + /* Some data was sent, but not all of the buffer. + * We can still tell the caller that all the new data + * was "sent" since it is in the buffer now. + */ + ret = last_write_size; + memmove(s_usb_tx_buf, s_usb_tx_buf + sent, s_usb_tx_buf_pos - sent); + s_usb_tx_buf_pos = s_usb_tx_buf_pos - sent; + } + return ret; +} + +ssize_t esp_usb_console_flush(void) +{ + if (s_cdc_acm_device == NULL) { + return -1; + } + write_lock_acquire(); + int ret = esp_usb_console_flush_internal(0); + write_lock_release(); + return ret; +} + +ssize_t esp_usb_console_write_buf(const char* buf, size_t size) +{ + if (s_cdc_acm_device == NULL) { + return -1; + } + if (size == 0) { + return 0; + } + write_lock_acquire(); + ssize_t tx_buf_available = ACM_BYTES_PER_TX - s_usb_tx_buf_pos; + ssize_t will_write = MIN(size, tx_buf_available); + memcpy(s_usb_tx_buf + s_usb_tx_buf_pos, buf, will_write); + s_usb_tx_buf_pos += will_write; + + ssize_t ret; + if (s_usb_tx_buf_pos == ACM_BYTES_PER_TX || buf[size - 1] == '\n') { + /* Buffer is full, or a newline is found. + * For binary streams, we probably shouldn't do line buffering, + * but text streams are likely going to be the most common case. + */ + ret = esp_usb_console_flush_internal(will_write); + } else { + /* nothing sent out yet, but all the new data is in the buffer now */ + ret = will_write; + } + write_lock_release(); + return ret; +} + +ssize_t esp_usb_console_read_buf(char *buf, size_t buf_size) +{ + if (s_cdc_acm_device == NULL) { + return -1; + } + if (!esp_usb_console_read_available()) { + return 0; + } + int bytes_read = cdc_acm_fifo_read(s_cdc_acm_device, (uint8_t*) buf, buf_size); + return bytes_read; +} + +esp_err_t esp_usb_console_set_cb(esp_usb_console_cb_t rx_cb, esp_usb_console_cb_t tx_cb, void *arg) +{ + if (s_cdc_acm_device == NULL) { + return ESP_ERR_INVALID_STATE; + } + s_rx_cb = rx_cb; + if (s_rx_cb) { + cdc_acm_irq_rx_enable(s_cdc_acm_device); + } else { + cdc_acm_irq_rx_disable(s_cdc_acm_device); + } + s_tx_cb = tx_cb; + if (s_tx_cb) { + cdc_acm_irq_tx_enable(s_cdc_acm_device); + } else { + cdc_acm_irq_tx_disable(s_cdc_acm_device); + } + s_cb_arg = arg; + return ESP_OK; +} + +bool esp_usb_console_read_available(void) +{ + if (s_cdc_acm_device == NULL) { + return false; + } + return cdc_acm_rx_fifo_cnt(s_cdc_acm_device) > 0; +} + +bool esp_usb_console_write_available(void) +{ + if (s_cdc_acm_device == NULL) { + return false; + } + return cdc_acm_irq_tx_ready(s_cdc_acm_device) != 0; +} + + +#ifdef CONFIG_ESP_CONSOLE_USB_CDC_SUPPORT_ETS_PRINTF +/* Used as an output function by ets_printf. + * The LF->CRLF replacement logic replicates the one in ets_write_char_uart. + * Not static to allow placement into IRAM by ldgen. + */ +void esp_usb_console_write_char(char c) +{ + char cr = '\r'; + char lf = '\n'; + + if (c == lf) { + esp_usb_console_write_buf(&cr, 1); + esp_usb_console_write_buf(&lf, 1); + } else if (c == '\r') { + } else { + esp_usb_console_write_buf(&c, 1); + } +} +static inline void write_lock_acquire(void) +{ + spinlock_acquire(&s_write_lock, SPINLOCK_WAIT_FOREVER); +} +static inline void write_lock_release(void) +{ + spinlock_release(&s_write_lock); +} + +#else // CONFIG_ESP_CONSOLE_USB_CDC_SUPPORT_ETS_PRINTF + +static inline void write_lock_acquire(void) +{ +} + +static inline void write_lock_release(void) +{ +} +#endif // CONFIG_ESP_CONSOLE_USB_CDC_SUPPORT_ETS_PRINTF