diff --git a/cores/esp32/HWCDC.cpp b/cores/esp32/HWCDC.cpp index b97e32daf4c..956095e6ccc 100644 --- a/cores/esp32/HWCDC.cpp +++ b/cores/esp32/HWCDC.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2020 Espressif Systems (Shanghai) PTE LTD +// Copyright 2015-2024 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. @@ -30,12 +30,14 @@ static RingbufHandle_t tx_ring_buf = NULL; static xQueueHandle rx_queue = NULL; static uint8_t rx_data_buf[64] = {0}; static intr_handle_t intr_handle = NULL; -static volatile bool initial_empty = false; +static volatile bool connected = false; static xSemaphoreHandle tx_lock = NULL; -// workaround for when USB CDC is not connected -static uint32_t tx_timeout_ms = 0; -static bool tx_timeout_change_request = false; +static volatile unsigned long lastSOF_ms; +static volatile uint8_t SOF_TIMEOUT; + +// timeout has no effect when USB CDC is unplugged +static uint32_t tx_timeout_ms = 100; static esp_event_loop_handle_t arduino_hw_cdc_event_loop_handle = NULL; @@ -73,21 +75,17 @@ static void hw_cdc_isr_handler(void *arg) { if (usbjtag_intr_status & USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY) { // Interrupt tells us the host picked up the data we sent. - if (usb_serial_jtag_ll_txfifo_writable() == 1) { + if(!HWCDC::isPlugged()) { + connected = false; + usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + // USB is unplugged, nothing to be done here + return; + } else { + connected = true; + } + if (tx_ring_buf != NULL && usb_serial_jtag_ll_txfifo_writable() == 1) { // We disable the interrupt here so that the interrupt won't be triggered if there is no data to send. usb_serial_jtag_ll_disable_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); - if(!initial_empty){ - initial_empty = true; - // First time USB is plugged and the application has not explicitly set TX Timeout, set it to default 100ms. - // Otherwise, USB is still unplugged and the timeout will be kept as Zero in order to avoid any delay in the - // application whenever it uses write() and the TX Queue gets full. - if (!tx_timeout_change_request) { - tx_timeout_ms = 100; - } - //send event? - //ets_printf("CONNECTED\n"); - arduino_hw_cdc_event_post(ARDUINO_HW_CDC_EVENTS, ARDUINO_HW_CDC_CONNECTED_EVENT, &event, sizeof(arduino_hw_cdc_event_data_t), &xTaskWoken); - } size_t queued_size; uint8_t *queued_buff = (uint8_t *)xRingbufferReceiveUpToFromISR(tx_ring_buf, &queued_size, 64); // If the hardware fifo is avaliable, write in it. Otherwise, do nothing. @@ -97,7 +95,7 @@ static void hw_cdc_isr_handler(void *arg) { usb_serial_jtag_ll_write_txfifo(queued_buff, queued_size); usb_serial_jtag_ll_txfifo_flush(); vRingbufferReturnItemFromISR(tx_ring_buf, queued_buff, &xTaskWoken); - usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + if(connected) usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); //send event? //ets_printf("TX:%u\n", queued_size); event.tx.len = queued_size; @@ -119,18 +117,20 @@ static void hw_cdc_isr_handler(void *arg) { break; } } - //send event? - //ets_printf("RX:%u/%u\n", i, rx_fifo_len); event.rx.len = i; arduino_hw_cdc_event_post(ARDUINO_HW_CDC_EVENTS, ARDUINO_HW_CDC_RX_EVENT, &event, sizeof(arduino_hw_cdc_event_data_t), &xTaskWoken); + connected = true; } if (usbjtag_intr_status & USB_SERIAL_JTAG_INTR_BUS_RESET) { usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_BUS_RESET); - initial_empty = false; - usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); - //ets_printf("BUS_RESET\n"); arduino_hw_cdc_event_post(ARDUINO_HW_CDC_EVENTS, ARDUINO_HW_CDC_BUS_RESET_EVENT, &event, sizeof(arduino_hw_cdc_event_data_t), &xTaskWoken); + connected = false; + } + + if (usbjtag_intr_status & USB_SERIAL_JTAG_INTR_SOF) { + usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_SOF); + lastSOF_ms = millis(); } if (xTaskWoken == pdTRUE) { @@ -138,26 +138,115 @@ static void hw_cdc_isr_handler(void *arg) { } } +inline bool HWCDC::isPlugged(void) +{ + return (lastSOF_ms + SOF_TIMEOUT) >= millis(); +} + +bool HWCDC::isCDC_Connected() +{ + static bool running = false; + + // USB may be unplugged + if (!isPlugged()) { + connected = false; + running = false; + SOF_TIMEOUT = 5; // SOF timeout when unplugged + return false; + } else { + SOF_TIMEOUT = 50; // SOF timeout when plugged + } + + if (connected) { + running = false; + return true; + } + + if (running == false && !connected) { // enables it only once! + usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + } + // this will feed CDC TX FIFO to trigger IN_EMPTY + usb_serial_jtag_ll_txfifo_flush(); + running = true; + return false; +} + +static void flushTXBuffer(const uint8_t *buffer, size_t size) +{ + if (!tx_ring_buf) return; + UBaseType_t uxItemsWaiting= 0; + vRingbufferGetInfo(tx_ring_buf, NULL, NULL, NULL, NULL, &uxItemsWaiting); + size_t freeSpace = xRingbufferGetCurFreeSize(tx_ring_buf); + size_t ringbufferLength = freeSpace + uxItemsWaiting; + + if(buffer == NULL) { + // just flush the whole ring buffer and exit - used by HWCDC::flush() + size_t queued_size = 0; + uint8_t *queued_buff = (uint8_t *)xRingbufferReceiveUpTo(tx_ring_buf, &queued_size, 0, ringbufferLength); + if (queued_size && queued_buff != NULL) { + vRingbufferReturnItem(tx_ring_buf, (void *)queued_buff); + } + return; + } + if(size == 0) return; // nothing to do + if(freeSpace >= size){ + // just add the data to the ring buffer and exit + if(xRingbufferSend(tx_ring_buf, (void*)buffer, size, 0) != pdTRUE){ + return; + } + } else { + // how many byte should be flushed to make space for the new data + size_t to_flush = size - freeSpace; + if(to_flush > ringbufferLength) to_flush = ringbufferLength; + size_t queued_size = 0; + uint8_t *queued_buff = (uint8_t *)xRingbufferReceiveUpTo(tx_ring_buf, &queued_size, 0, to_flush); + if (queued_size && queued_buff != NULL) { + vRingbufferReturnItem(tx_ring_buf, (void *)queued_buff); + } + // now add the new data that fits to the ring buffer + uint8_t *bptr = (uint8_t *)buffer; + if (size >= ringbufferLength) { + size = ringbufferLength; + bptr = (uint8_t *)buffer + (size - ringbufferLength); + } + if(xRingbufferSend(tx_ring_buf, (void *)bptr, size, 0) != pdTRUE){ + return; + } + } + // flushes CDC FIFO + usb_serial_jtag_ll_txfifo_flush(); +} + static void ARDUINO_ISR_ATTR cdc0_write_char(char c) { + if(tx_ring_buf == NULL) { + return; + } + if(!HWCDC::isConnected()) { + // just pop/push RingBuffer and apply FIFO policy + flushTXBuffer((const uint8_t*)&c, 1); + return; + } if(xPortInIsrContext()){ xRingbufferSendFromISR(tx_ring_buf, (void*) (&c), 1, NULL); } else { xRingbufferSend(tx_ring_buf, (void*) (&c), 1, tx_timeout_ms / portTICK_PERIOD_MS); } - usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + usb_serial_jtag_ll_txfifo_flush(); } HWCDC::HWCDC() { - + lastSOF_ms = 0; + SOF_TIMEOUT = 5; } HWCDC::~HWCDC(){ end(); } +// It should return just when USB is plugged and CDC is connected. HWCDC::operator bool() const { - return initial_empty; + return HWCDC::isCDC_Connected(); } void HWCDC::onEvent(esp_event_handler_t callback){ @@ -168,6 +257,16 @@ void HWCDC::onEvent(arduino_hw_cdc_event_t event, esp_event_handler_t callback){ arduino_hw_cdc_event_handler_register_with(ARDUINO_HW_CDC_EVENTS, event, callback, this); } +void HWCDC::deinit() +{ + // Setting USB D+ D- pins + // Force the host to re-enumerate (BUS_RESET) + pinMode(USB_DM_GPIO_NUM, OUTPUT_OPEN_DRAIN); + pinMode(USB_DP_GPIO_NUM, OUTPUT_OPEN_DRAIN); + digitalWrite(USB_DM_GPIO_NUM, LOW); + digitalWrite(USB_DP_GPIO_NUM, LOW); +} + void HWCDC::begin(unsigned long baud) { if(tx_lock == NULL) { @@ -185,20 +284,35 @@ void HWCDC::begin(unsigned long baud) log_e("HW CDC TX Buffer error"); } } + + // the HW Serial pins needs to be first deinited in order to allow `if(Serial)` to work :-( + deinit(); + delay(10); // USB Host has to enumerate it again + + // Configure PHY + // USB_Serial_JTAG use internal PHY + USB_SERIAL_JTAG.conf0.phy_sel = 0; + // Disable software control USB D+ D- pullup pulldown (Device FS: dp_pullup = 1) + USB_SERIAL_JTAG.conf0.pad_pull_override = 0; + // Enable USB D+ pullup + USB_SERIAL_JTAG.conf0.dp_pullup = 1; + // Enable USB pad function + USB_SERIAL_JTAG.conf0.usb_pad_enable = 1; usb_serial_jtag_ll_disable_intr_mask(USB_SERIAL_JTAG_LL_INTR_MASK); - usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY | USB_SERIAL_JTAG_INTR_SERIAL_OUT_RECV_PKT | USB_SERIAL_JTAG_INTR_BUS_RESET); + usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY | USB_SERIAL_JTAG_INTR_SERIAL_OUT_RECV_PKT + | USB_SERIAL_JTAG_INTR_BUS_RESET | USB_SERIAL_JTAG_INTR_SOF); if(!intr_handle && esp_intr_alloc(ETS_USB_SERIAL_JTAG_INTR_SOURCE, 0, hw_cdc_isr_handler, NULL, &intr_handle) != ESP_OK){ isr_log_e("HW USB CDC failed to init interrupts"); end(); return; } - usb_serial_jtag_ll_txfifo_flush(); } void HWCDC::end() { - //Disable tx/rx interrupt. + //Disable/clear/free tx/rx interrupt. usb_serial_jtag_ll_disable_intr_mask(USB_SERIAL_JTAG_LL_INTR_MASK); + usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_LL_INTR_MASK); esp_intr_free(intr_handle); intr_handle = NULL; if(tx_lock != NULL) { @@ -211,13 +325,13 @@ void HWCDC::end() esp_event_loop_delete(arduino_hw_cdc_event_loop_handle); arduino_hw_cdc_event_loop_handle = NULL; } + deinit(); + setDebugOutput(false); + connected = false; } void HWCDC::setTxTimeoutMs(uint32_t timeout){ tx_timeout_ms = timeout; - // it registers that the user has explicitly requested to use a value as TX timeout - // used for the workaround with unplugged USB and TX Queue Full that causes a delay on every write() - tx_timeout_change_request = true; } /* @@ -260,35 +374,63 @@ size_t HWCDC::write(const uint8_t *buffer, size_t size) if(xSemaphoreTake(tx_lock, tx_timeout_ms / portTICK_PERIOD_MS) != pdPASS){ return 0; } - size_t max_size = xRingbufferGetMaxItemSize(tx_ring_buf); - size_t space = xRingbufferGetCurFreeSize(tx_ring_buf); - size_t to_send = size, so_far = 0; - - if(space > size){ - space = size; - } - // Non-Blocking method, Sending data to ringbuffer, and handle the data in ISR. - if(xRingbufferSend(tx_ring_buf, (void*) (buffer), space, 0) != pdTRUE){ - size = 0; + if(!isCDC_Connected()) { + // just pop/push RingBuffer and apply FIFO policy + flushTXBuffer(buffer, size); } else { - to_send -= space; - so_far += space; - // Now trigger the ISR to read data from the ring buffer. - usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + size_t space = xRingbufferGetCurFreeSize(tx_ring_buf); + size_t to_send = size, so_far = 0; - while(to_send){ - if(max_size > to_send){ - max_size = to_send; - } - // Blocking method, Sending data to ringbuffer, and handle the data in ISR. - if(xRingbufferSend(tx_ring_buf, (void*) (buffer+so_far), max_size, tx_timeout_ms / portTICK_PERIOD_MS) != pdTRUE){ - size = so_far; - break; - } - so_far += max_size; - to_send -= max_size; + if(space > size){ + space = size; + } + // Non-Blocking method, Sending data to ringbuffer, and handle the data in ISR. + if(space > 0 && xRingbufferSend(tx_ring_buf, (void*) (buffer), space, 0) != pdTRUE){ + size = 0; + } else { + to_send -= space; + so_far += space; // Now trigger the ISR to read data from the ring buffer. - usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + usb_serial_jtag_ll_txfifo_flush(); + if(connected) usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + // tracks CDC trasmission progress to avoid hanging if CDC is unplugged while still sending data + size_t last_toSend = to_send; + uint32_t tries = tx_timeout_ms; // waits 1ms per sending data attempt, in case CDC is unplugged + while(connected && to_send){ + space = xRingbufferGetCurFreeSize(tx_ring_buf); + if(space > to_send){ + space = to_send; + } + // Blocking method, Sending data to ringbuffer, and handle the data in ISR. + if(xRingbufferSend(tx_ring_buf, (void*) (buffer+so_far), space, tx_timeout_ms / portTICK_PERIOD_MS) != pdTRUE){ + size = so_far; + log_w("write failed due to ring buffer full - timeout"); + break; + } + so_far += space; + to_send -= space; + // Now trigger the ISR to read data from the ring buffer. + usb_serial_jtag_ll_txfifo_flush(); + if(connected) usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + if(last_toSend == to_send) { + // no progress in sending data... USB CDC is probably unplugged + tries--; + delay(1); + } else { + last_toSend = to_send; + tries = tx_timeout_ms; // reset the timeout + } + if (tries == 0) { // CDC isn't connected anymore... + size = so_far; + log_w("write failed due to waiting USB Host - timeout"); + connected = false; + } + } + } + // CDC was diconnected while sending data ==> flush the TX buffer keeping the last data + if(to_send && !usb_serial_jtag_ll_txfifo_writable()) { + connected = false; + flushTXBuffer(buffer + so_far, to_send); } } xSemaphoreGive(tx_lock); @@ -308,15 +450,34 @@ void HWCDC::flush(void) if(xSemaphoreTake(tx_lock, tx_timeout_ms / portTICK_PERIOD_MS) != pdPASS){ return; } - UBaseType_t uxItemsWaiting = 0; - vRingbufferGetInfo(tx_ring_buf, NULL, NULL, NULL, NULL, &uxItemsWaiting); - if(uxItemsWaiting){ - // Now trigger the ISR to read data from the ring buffer. - usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); - } - while(uxItemsWaiting){ - delay(5); + if(!isCDC_Connected()) { + flushTXBuffer(NULL, 0); + } else { + UBaseType_t uxItemsWaiting = 0; vRingbufferGetInfo(tx_ring_buf, NULL, NULL, NULL, NULL, &uxItemsWaiting); + if(uxItemsWaiting){ + // Now trigger the ISR to read data from the ring buffer. + usb_serial_jtag_ll_txfifo_flush(); + if(connected) { + usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + } + } + uint32_t tries = tx_timeout_ms; // waits 1ms per ISR sending data attempt, in case CDC is unplugged + while(connected && tries && uxItemsWaiting){ + delay(1); + UBaseType_t lastUxItemsWaiting = uxItemsWaiting; + vRingbufferGetInfo(tx_ring_buf, NULL, NULL, NULL, NULL, &uxItemsWaiting); + if (lastUxItemsWaiting == uxItemsWaiting) { + tries--; + } + if(connected) { + usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + } + } + if (tries == 0) { // CDC isn't connected anymore... + connected = false; + flushTXBuffer(NULL, 0); // flushes all TX Buffer + } } xSemaphoreGive(tx_lock); } @@ -407,4 +568,4 @@ HWCDC USBSerial; #endif #endif -#endif /* CONFIG_TINYUSB_CDC_ENABLED */ +#endif /* CONFIG_IDF_TARGET_ESP32C3 */ diff --git a/cores/esp32/HWCDC.h b/cores/esp32/HWCDC.h index 5878ad0377f..734e0cd5888 100644 --- a/cores/esp32/HWCDC.h +++ b/cores/esp32/HWCDC.h @@ -42,6 +42,10 @@ typedef union { class HWCDC: public Stream { +private: + static void deinit(); + static bool isCDC_Connected(); + public: HWCDC(); ~HWCDC(); @@ -64,6 +68,11 @@ class HWCDC: public Stream size_t write(const uint8_t *buffer, size_t size); void flush(void); + static bool isPlugged(void); + inline static bool isConnected(void) + { + return isCDC_Connected(); + } inline size_t read(char * buffer, size_t size) { return read((uint8_t*) buffer, size); diff --git a/libraries/ESP32/examples/HWCDC_Events/.skip.esp32 b/libraries/ESP32/examples/HWCDC_Events/.skip.esp32 new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/libraries/ESP32/examples/HWCDC_Events/.skip.esp32 @@ -0,0 +1 @@ + diff --git a/libraries/ESP32/examples/HWCDC_Events/.skip.esp32s2 b/libraries/ESP32/examples/HWCDC_Events/.skip.esp32s2 new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/libraries/ESP32/examples/HWCDC_Events/.skip.esp32s2 @@ -0,0 +1 @@ + diff --git a/libraries/ESP32/examples/HWCDC_Events/HWCDC_Events.ino b/libraries/ESP32/examples/HWCDC_Events/HWCDC_Events.ino new file mode 100644 index 00000000000..bb5f6507422 --- /dev/null +++ b/libraries/ESP32/examples/HWCDC_Events/HWCDC_Events.ino @@ -0,0 +1,95 @@ +/* + * This Example demonstrates how to receive Hardware Serial Events + * This USB interface is available for the ESP32-S3 and ESP32-C3 + * + * It will log all events and USB status (plugged/unplugged) into UART0 + * Any data read from UART0 will be sent to the USB CDC + * Any data read from USB CDC will be sent to the UART0 + * + * A suggestion is to use Arduino Serial Monitor for the UART0 port + * and some other serial monitor application for the USB CDC port + * in order to see the exchanged data and the Hardware Serial Events + * + */ + +#ifndef ARDUINO_USB_MODE +#error This ESP32 SoC has no Native USB interface +#elif ARDUINO_USB_MODE == 0 +#warning This sketch should be used when USB is in Hardware CDC and JTAG mode +void setup(){} +void loop(){} +#else + +// Makes it work always using Serial0 as UART0 and USBSerial as the HW Serial USB CDC port +#if ARDUINO_USB_CDC_ON_BOOT +// HardwareSerial::Serial0 is declared but HWCDC::USBSerial not +// Serial is the HWCDC USB CDC port +#define USBSerial Serial +#else +// HWCDC::USBSerial is declared but HardwareSerial::Serial0 not +// Serial is HardwareSerial UART0 +#define Serial0 Serial // redefine the symbol Serial0 to the default Arduino +#endif + +// USB Event Callback Function that will log CDC events into UART0 +static void usbEventCallback(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { + if (event_base == ARDUINO_HW_CDC_EVENTS) { + switch (event_id) { + case ARDUINO_HW_CDC_CONNECTED_EVENT: + Serial0.println("CDC EVENT:: ARDUINO_HW_CDC_CONNECTED_EVENT"); + break; + case ARDUINO_HW_CDC_BUS_RESET_EVENT: + Serial0.println("CDC EVENT:: ARDUINO_HW_CDC_BUS_RESET_EVENT"); + break; + case ARDUINO_HW_CDC_RX_EVENT: + Serial0.println("\nCDC EVENT:: ARDUINO_HW_CDC_RX_EVENT"); + // sends all bytes read from USB Hardware Serial to UART0 + while (USBSerial.available()) Serial0.write(USBSerial.read()); + break; + case ARDUINO_HW_CDC_TX_EVENT: + Serial0.println("CDC EVENT:: ARDUINO_HW_CDC_TX_EVENT"); + break; + + default: + break; + } + } +} + +const char* _hwcdc_status[] = { + " USB Plugged but CDC is NOT connected\r\n", + " USB Plugged and CDC is connected\r\n", + " USB Unplugged and CDC is NOT connected\r\n", + " USB Unplugged BUT CDC is connected :: PROBLEM\r\n", +}; + +const char* HWCDC_Status() { + int i = USBSerial.isPlugged() ? 0 : 2; + if(USBSerial.isConnected()) i += 1; + return _hwcdc_status[i]; +} + +void setup() { + USBSerial.onEvent(usbEventCallback); + USBSerial.begin(); + + Serial0.begin(115200); + Serial0.setDebugOutput(true); + Serial0.println("Starting..."); +} + +void loop() { + static uint32_t counter = 0; + + Serial0.print(counter); + Serial0.print(HWCDC_Status()); + + if (USBSerial) { + USBSerial.printf(" [%ld] connected\n\r", counter); + } + // sends all bytes read from UART0 to USB Hardware Serial + while (Serial0.available()) USBSerial.write(Serial0.read()); + delay(1000); + counter++; +} +#endif