diff --git a/ports/espressif/supervisor/usb.c b/ports/espressif/supervisor/usb.c index 7c3b1561a44..ccf18ef96c0 100644 --- a/ports/espressif/supervisor/usb.c +++ b/ports/espressif/supervisor/usb.c @@ -30,6 +30,59 @@ #include "hal/usb_serial_jtag_ll.h" #endif +#if CIRCUITPY_USB_DEVICE && CIRCUITPY_USB_CDC +#include "py/ringbuf.h" +#include "shared-bindings/microcontroller/__init__.h" +#include "shared-module/usb_cdc/__init__.h" + +// tud_task() runs in the usbd task below. Reading the TinyUSB fifo from the VM +// task could re-arm the CDC OUT endpoint while tud_task() is still copying the +// previous packet out of it, so console input is staged in this ringbuf on the +// usbd task. Both tasks use it, so every access runs with interrupts disabled. +static uint8_t _cdc_rx_buf[256]; +static ringbuf_t _cdc_rx_ringbuf = { .buf = _cdc_rx_buf, .size = sizeof(_cdc_rx_buf) }; + +void usb_cdc_rx_drain(void) { + if (!usb_cdc_console_enabled()) { + return; + } + uint8_t chunk[64]; + while (tud_cdc_available() > 0) { + common_hal_mcu_disable_interrupts(); + size_t room = ringbuf_num_empty(&_cdc_rx_ringbuf); + common_hal_mcu_enable_interrupts(); + if (room == 0) { + return; + } + // tud_cdc_read() can wait on the TinyUSB fifo mutex, so it runs with interrupts enabled. + uint32_t count = tud_cdc_read(chunk, MIN(room, sizeof(chunk))); + common_hal_mcu_disable_interrupts(); + ringbuf_put_n(&_cdc_rx_ringbuf, chunk, count); + common_hal_mcu_enable_interrupts(); + } +} + +size_t usb_cdc_rx_read(uint8_t *data, size_t len) { + common_hal_mcu_disable_interrupts(); + size_t count = ringbuf_get_n(&_cdc_rx_ringbuf, data, len); + common_hal_mcu_enable_interrupts(); + return count; +} + +size_t usb_cdc_rx_available(void) { + common_hal_mcu_disable_interrupts(); + size_t count = ringbuf_num_filled(&_cdc_rx_ringbuf); + common_hal_mcu_enable_interrupts(); + return count; +} + +void usb_cdc_rx_clear(void) { + common_hal_mcu_disable_interrupts(); + ringbuf_clear(&_cdc_rx_ringbuf); + common_hal_mcu_enable_interrupts(); +} +#endif // CIRCUITPY_USB_DEVICE && CIRCUITPY_USB_CDC + #if CIRCUITPY_USB_DEVICE #ifdef CFG_TUSB_DEBUG #define USBD_STACK_SIZE (3 * configMINIMAL_STACK_SIZE) @@ -51,8 +104,12 @@ static void usb_device_task(void *param) { while (1) { // tinyusb device task if (tusb_inited()) { - tud_task(); + // Time out so console input left in the fifo while the ringbuf was full is moved in. + tud_task_ext(10, false); tud_cdc_write_flush(); + #if CIRCUITPY_USB_CDC + usb_cdc_rx_drain(); + #endif } // Yield with zero delay to switch to any other tasks at same priority. port_task_yield(); diff --git a/shared-module/usb_cdc/Serial.c b/shared-module/usb_cdc/Serial.c index 348729365c2..58445514f65 100644 --- a/shared-module/usb_cdc/Serial.c +++ b/shared-module/usb_cdc/Serial.c @@ -6,11 +6,25 @@ #include "shared/runtime/interrupt_char.h" #include "shared-bindings/usb_cdc/Serial.h" +#include "shared-module/usb_cdc/__init__.h" #include "shared-module/usb_cdc/Serial.h" #include "supervisor/shared/tick.h" +#include "supervisor/usb.h" #include "tusb.h" +// Console input goes through usb_cdc_rx_*, which a port may stage elsewhere. +static bool _is_console(usb_cdc_serial_obj_t *self) { + return self->idx == 0 && usb_cdc_console_enabled(); +} + +static uint32_t _read(usb_cdc_serial_obj_t *self, uint8_t *data, size_t len) { + if (_is_console(self)) { + return usb_cdc_rx_read(data, len); + } + return tud_cdc_n_read(self->idx, data, len); +} + size_t common_hal_usb_cdc_serial_read(usb_cdc_serial_obj_t *self, uint8_t *data, size_t len, int *errcode) { const bool wait_forever = self->timeout < 0.0f; @@ -19,7 +33,7 @@ size_t common_hal_usb_cdc_serial_read(usb_cdc_serial_obj_t *self, uint8_t *data, // Read up to len bytes immediately. // The number of bytes read will not be larger than what is already in the TinyUSB FIFO. uint32_t total_num_read = 0; - total_num_read = tud_cdc_n_read(self->idx, data, len); + total_num_read = _read(self, data, len); if (wait_forever || wait_for_timeout) { // Continue filling the buffer past what we already read. @@ -46,7 +60,7 @@ size_t common_hal_usb_cdc_serial_read(usb_cdc_serial_obj_t *self, uint8_t *data, data += num_read; // Try to read another batch of bytes. - num_read = tud_cdc_n_read(self->idx, data, len); + num_read = _read(self, data, len); total_num_read += num_read; } } @@ -98,6 +112,9 @@ size_t common_hal_usb_cdc_serial_write(usb_cdc_serial_obj_t *self, const uint8_t } uint32_t common_hal_usb_cdc_serial_get_in_waiting(usb_cdc_serial_obj_t *self) { + if (_is_console(self)) { + return usb_cdc_rx_available(); + } return tud_cdc_n_available(self->idx); } @@ -108,6 +125,9 @@ uint32_t common_hal_usb_cdc_serial_get_out_waiting(usb_cdc_serial_obj_t *self) { void common_hal_usb_cdc_serial_reset_input_buffer(usb_cdc_serial_obj_t *self) { tud_cdc_n_read_flush(self->idx); + if (_is_console(self)) { + usb_cdc_rx_clear(); + } } uint32_t common_hal_usb_cdc_serial_reset_output_buffer(usb_cdc_serial_obj_t *self) { diff --git a/supervisor/shared/serial.c b/supervisor/shared/serial.c index 1bfb8d73b3c..5728ce646be 100644 --- a/supervisor/shared/serial.c +++ b/supervisor/shared/serial.c @@ -327,8 +327,11 @@ char serial_read(void) { return -1; } #endif - #if CIRCUITPY_TINYUSB && CIRCUITPY_USB_DEVICE - return (char)tud_cdc_read_char(); + #if CIRCUITPY_TINYUSB && CIRCUITPY_USB_DEVICE && CIRCUITPY_USB_CDC + uint8_t c; + if (usb_cdc_rx_read(&c, 1) == 1) { + return c; + } #endif return -1; @@ -362,7 +365,7 @@ uint32_t serial_bytes_available(void) { #if CIRCUITPY_TINYUSB && CIRCUITPY_USB_DEVICE && CIRCUITPY_USB_CDC if (usb_cdc_console_enabled()) { - count += tud_cdc_available(); + count += usb_cdc_rx_available(); } #endif diff --git a/supervisor/shared/usb/usb_device.c b/supervisor/shared/usb/usb_device.c index e3f1a0d4e26..34def8d75a3 100644 --- a/supervisor/shared/usb/usb_device.c +++ b/supervisor/shared/usb/usb_device.c @@ -149,6 +149,23 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_requ #endif // CIRCUITPY_USB_VENDOR +#if CIRCUITPY_USB_CDC +// Defaults: tud_task() runs on the VM task here, so the fifo is read directly. +MP_WEAK void usb_cdc_rx_drain(void) { +} + +MP_WEAK size_t usb_cdc_rx_read(uint8_t *data, size_t len) { + return tud_cdc_read(data, len); +} + +MP_WEAK size_t usb_cdc_rx_available(void) { + return tud_cdc_available(); +} + +MP_WEAK void usb_cdc_rx_clear(void) { +} +#endif + #if MICROPY_KBD_EXCEPTION && CIRCUITPY_USB_CDC // The CDC RX buffer impacts monitoring for ctrl-c. TinyUSB will only ask for @@ -173,6 +190,7 @@ void tud_cdc_rx_wanted_cb(uint8_t itf, char wanted_char) { // Compare mp_interrupt_char with wanted_char and ignore if not matched if (mp_interrupt_char == wanted_char) { tud_cdc_n_read_flush(itf); // flush read fifo + usb_cdc_rx_clear(); mp_sched_keyboard_interrupt(); } } @@ -186,6 +204,7 @@ void tud_cdc_send_break_cb(uint8_t itf, uint16_t duration_ms) { void tud_cdc_rx_cb(uint8_t itf) { (void)itf; + usb_cdc_rx_drain(); // Workaround for "press any key to enter REPL" response being delayed on espressif. // Wake main task when any key is pressed. port_wake_main_task(); diff --git a/supervisor/usb.h b/supervisor/usb.h index d6e4236c9b4..d53e516064f 100644 --- a/supervisor/usb.h +++ b/supervisor/usb.h @@ -19,6 +19,16 @@ void usb_background(void); // Schedule usb background void usb_background_schedule(void); +// Console (CDC interface 0) input. Defaults read the TinyUSB fifo directly; a port +// whose tud_task() runs on another task overrides them and stages input there. +// usb_cdc_rx_drain() runs on that task, the other three on the VM task. +// read() fills *data with up to len bytes and returns the count, available() +// returns the pending count, both 0 when empty. clear() drops staged input only. +void usb_cdc_rx_drain(void); +size_t usb_cdc_rx_read(uint8_t *data, size_t len); +size_t usb_cdc_rx_available(void); +void usb_cdc_rx_clear(void); + // Ports must call this from their particular USB IRQ handler void usb_irq_handler(int instance);