diff --git a/scripts/esp32_usb_session_fix.py b/scripts/esp32_usb_session_fix.py index d6b6d04f..a028cc14 100644 --- a/scripts/esp32_usb_session_fix.py +++ b/scripts/esp32_usb_session_fix.py @@ -102,9 +102,239 @@ def replace_source(build_env, node): return build_env.File(str(destination)) + +# HWCDC fixes are deliberately limited to the installed Arduino2.0.17 C3/S3 +# native Serial/JTAG driver. Other versions retain their own framework driver. +PINNED_HWCDC_SHA256 = "d0a8ca606c2729c8522a041113285dbf27033c22a5a6af8649a7305ffe84c449" +PATCHED_HWCDC_SHA256 = "5107db35dc3c855326dff0a9b93b279ee9bda60b5cc1ebcdce77a9028e3577b4" +HWCDC_TX_SUPPORT = r""" +// MeshCore pinned HWCDC TX suffix/interrupt backport (upstream #12606). +static uint8_t mesh_hwcdc_tx_stash[64] = {0}; +static size_t mesh_hwcdc_tx_stash_len = 0; +static bool mesh_hwcdc_tx_allowed = true; +static bool mesh_hwcdc_fifo_pending = false; +static uint32_t mesh_hwcdc_active_writers = 0; +static portMUX_TYPE mesh_hwcdc_tx_mux = portMUX_INITIALIZER_UNLOCKED; + +static inline void mesh_hwcdc_enable_tx_intr() { + portENTER_CRITICAL_SAFE(&mesh_hwcdc_tx_mux); + if (mesh_hwcdc_tx_allowed) { + usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + } + portEXIT_CRITICAL_SAFE(&mesh_hwcdc_tx_mux); +} + +extern "C" void meshEsp32HwcdcSetTxAllowed(bool allowed) { + portENTER_CRITICAL_SAFE(&mesh_hwcdc_tx_mux); + mesh_hwcdc_tx_allowed = allowed; + if (!allowed) usb_serial_jtag_ll_disable_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + portEXIT_CRITICAL_SAFE(&mesh_hwcdc_tx_mux); +} + +extern "C" void meshEsp32HwcdcKickTx() { + portENTER_CRITICAL_SAFE(&mesh_hwcdc_tx_mux); + if (mesh_hwcdc_tx_allowed) { + usb_serial_jtag_ll_txfifo_flush(); + usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + } + portEXIT_CRITICAL_SAFE(&mesh_hwcdc_tx_mux); +} + +extern "C" bool meshEsp32HwcdcTxPending() { + portENTER_CRITICAL_SAFE(&mesh_hwcdc_tx_mux); + UBaseType_t waiting = 0; + if (tx_ring_buf) vRingbufferGetInfo(tx_ring_buf, NULL, NULL, NULL, NULL, &waiting); + const bool pending = mesh_hwcdc_tx_stash_len != 0 || waiting != 0 + || mesh_hwcdc_fifo_pending || mesh_hwcdc_active_writers != 0; + portEXIT_CRITICAL_SAFE(&mesh_hwcdc_tx_mux); + return pending; +} + +static inline void mesh_hwcdc_clear_tx_stash() { + portENTER_CRITICAL_SAFE(&mesh_hwcdc_tx_mux); + mesh_hwcdc_tx_stash_len = 0; + portEXIT_CRITICAL_SAFE(&mesh_hwcdc_tx_mux); +} + +extern "C" bool meshEsp32HwcdcDiscardTxStash() { + // Caller has detached pads and stopped producers. Do not mistake ordinary + // flush/ring-empty for completion of a previously staged FIFO payload. + portENTER_CRITICAL_SAFE(&mesh_hwcdc_tx_mux); + const bool permitted = !mesh_hwcdc_tx_allowed && mesh_hwcdc_active_writers == 0; + if (permitted) { + mesh_hwcdc_tx_stash_len = 0; + mesh_hwcdc_fifo_pending = false; + } + portEXIT_CRITICAL_SAFE(&mesh_hwcdc_tx_mux); + return permitted; +} + +class mesh_hwcdc_writer_scope { + bool _entered; +public: + mesh_hwcdc_writer_scope() { + portENTER_CRITICAL_SAFE(&mesh_hwcdc_tx_mux); + _entered = mesh_hwcdc_tx_allowed; + if (_entered) ++mesh_hwcdc_active_writers; + portEXIT_CRITICAL_SAFE(&mesh_hwcdc_tx_mux); + } + ~mesh_hwcdc_writer_scope() { + if (!_entered) return; + portENTER_CRITICAL_SAFE(&mesh_hwcdc_tx_mux); + --mesh_hwcdc_active_writers; + portEXIT_CRITICAL_SAFE(&mesh_hwcdc_tx_mux); + } + operator bool() const { return _entered; } + mesh_hwcdc_writer_scope(const mesh_hwcdc_writer_scope&) = delete; + mesh_hwcdc_writer_scope& operator=(const mesh_hwcdc_writer_scope&) = delete; +}; +""" +HWCDC_IN_EMPTY = r""" if (usbjtag_intr_status & USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY) { + // A real host pickup is stronger proof than the SOF tick heuristic. + connected = true; + usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + size_t sent_size = 0; + bool staged = false; + portENTER_CRITICAL_ISR(&mesh_hwcdc_tx_mux); + mesh_hwcdc_fifo_pending = false; // this IN_EMPTY acknowledges the previous payload + if (!mesh_hwcdc_tx_allowed) { + usb_serial_jtag_ll_disable_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + } else if (tx_ring_buf != NULL && usb_serial_jtag_ll_txfifo_writable() == 1) { + size_t queued_size = mesh_hwcdc_tx_stash_len; + bool from_stash = queued_size != 0; + uint8_t* queued_buff = from_stash ? mesh_hwcdc_tx_stash + : (uint8_t*)xRingbufferReceiveUpToFromISR(tx_ring_buf, &queued_size, 64); + if (queued_buff != NULL && queued_size != 0) { + sent_size = usb_serial_jtag_ll_write_txfifo(queued_buff, queued_size); + usb_serial_jtag_ll_txfifo_flush(); + staged = true; + mesh_hwcdc_fifo_pending = sent_size != 0; + mesh_hwcdc_tx_stash_len = queued_size - sent_size; + if (mesh_hwcdc_tx_stash_len) { + memmove(mesh_hwcdc_tx_stash, queued_buff + sent_size, mesh_hwcdc_tx_stash_len); + } + if (!from_stash) vRingbufferReturnItemFromISR(tx_ring_buf, queued_buff, &xTaskWoken); + usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + } else { + // Preserve the full-packet terminating ZLP. Disable and inspect + // again under the producer/ISR lock so a wakeup cannot be lost. + usb_serial_jtag_ll_txfifo_flush(); + usb_serial_jtag_ll_disable_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + UBaseType_t waiting = 0; + vRingbufferGetInfo(tx_ring_buf, NULL, NULL, NULL, NULL, &waiting); + if (waiting || mesh_hwcdc_tx_stash_len) { + usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + } + } + } + portEXIT_CRITICAL_ISR(&mesh_hwcdc_tx_mux); + if (staged) { + event.tx.len = sent_size; + arduino_hw_cdc_event_post(ARDUINO_HW_CDC_EVENTS, ARDUINO_HW_CDC_TX_EVENT, + &event, sizeof(arduino_hw_cdc_event_data_t), &xTaskWoken); + } + } + +""" +HWCDC_CONNECTED = r"""bool HWCDC::isCDC_Connected() +{ + if (!isPlugged()) { + connected = false; + return false; + } + if (connected) return true; + // Re-arm on every attempt. Keep a suffix across transient SOF false. + meshEsp32HwcdcKickTx(); + return false; +} + +""" + + +def patched_hwcdc_source(source): + import hashlib + source = source.replace("\r\n", "\n") + digest = hashlib.sha256(source.encode()).hexdigest() + if digest == PATCHED_HWCDC_SHA256: + return source + if digest != PINNED_HWCDC_SHA256: + raise RuntimeError("ESP32 HWCDC TX fix: changed pinned2.0.17 source; review SDK update") + # Perform exact-shape transforms only after the complete input hash passes. + source = source.replace('#include "esp_freertos_hooks.h"\n', + '#include "esp_freertos_hooks.h"\n#include \n', 1) + source = source.replace('usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY);', + 'mesh_hwcdc_enable_tx_intr();') + begin = source.index(' if (usbjtag_intr_status & USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY) {') + end = source.index(' if (usbjtag_intr_status & USB_SERIAL_JTAG_INTR_SERIAL_OUT_RECV_PKT) {', begin) + source = source[:begin] + HWCDC_IN_EMPTY + source[end:] + begin = source.index('bool HWCDC::isCDC_Connected()') + end = source.index('static void flushTXBuffer(', begin) + source = source[:begin] + HWCDC_CONNECTED + source[end:] + source = source.replace('static xSemaphoreHandle tx_lock = NULL;\n', + 'static xSemaphoreHandle tx_lock = NULL;\n' + HWCDC_TX_SUPPORT, 1) + source = source.replace(' connected = false;\n }\n\n// if (usbjtag_intr_status', + ' portENTER_CRITICAL_ISR(&mesh_hwcdc_tx_mux);\n' + ' mesh_hwcdc_tx_stash_len = 0;\n mesh_hwcdc_fifo_pending = false;\n' + ' portEXIT_CRITICAL_ISR(&mesh_hwcdc_tx_mux);\n connected = false;\n }\n\n// if (usbjtag_intr_status', 1) + source = source.replace(' if(buffer == NULL) {\n', + ' if(buffer == NULL) {\n mesh_hwcdc_clear_tx_stash();\n', 1) + source = source.replace('size_t HWCDC::setTxBufferSize(size_t tx_queue_len){\n', + 'size_t HWCDC::setTxBufferSize(size_t tx_queue_len){\n mesh_hwcdc_clear_tx_stash();\n', 1) + source = source.replace(' if(tx_ring_buf == NULL) {\n return;\n }\n if(!HWCDC::isConnected())', + ' if(tx_ring_buf == NULL) {\n return;\n }\n' + ' mesh_hwcdc_writer_scope writer;\n if (!writer) return;\n if(!HWCDC::isConnected())', 1) + source = source.replace('size_t HWCDC::write(const uint8_t *buffer, size_t size)\n{\n', + 'size_t HWCDC::write(const uint8_t *buffer, size_t size)\n{\n' + ' mesh_hwcdc_writer_scope writer;\n if (!writer) return 0;\n', 1) + source = source.replace(' flushTXBuffer((const uint8_t*)&c, 1);\n return;\n', + ' flushTXBuffer((const uint8_t*)&c, 1);\n meshEsp32HwcdcKickTx();\n return;\n', 1) + # The disconnected write path previously left new bytes parked without + # an interrupt. Kick after its existing FIFO-policy admission. + source = source.replace(' flushTXBuffer(buffer, size);\n } else {\n', + ' flushTXBuffer(buffer, size);\n meshEsp32HwcdcKickTx();\n } else {\n', 1) + return source + + +def hwcdc_primary_enabled(build_env): + defines = {} + for definition in build_env.get("CPPDEFINES", []): + if isinstance(definition, (tuple, list)): + defines[definition[0]] = str(definition[1]) + else: + defines[str(definition)] = "1" + return (defines.get("ARDUINO_USB_CDC_ON_BOOT") == "1" + and defines.get("ARDUINO_USB_MODE") == "1") + + +def pinned_hwcdc_framework(source): + import re + version = source.parent / "esp_arduino_version.h" + values = dict(re.findall(r"^#define ESP_ARDUINO_VERSION_(MAJOR|MINOR|PATCH)\s+(\d+)\s*$", + version.read_text(encoding="utf-8"), re.MULTILINE)) + if set(values) != {"MAJOR", "MINOR", "PATCH"}: + raise RuntimeError("ESP32 HWCDC TX fix: malformed framework version header") + return values == {"MAJOR": "2", "MINOR": "0", "PATCH": "17"} + + +def replace_hwcdc_source(build_env, node): + if not hwcdc_primary_enabled(build_env): + return node + source = Path(node.srcnode().get_abspath()) + if not pinned_hwcdc_framework(source): + return node + patched = patched_hwcdc_source(source.read_text(encoding="utf-8")) + destination = Path(build_env.subst("$BUILD_DIR")) / "patched-esp32-usb" / source.name + destination.parent.mkdir(parents=True, exist_ok=True) + if not destination.exists() or destination.read_text(encoding="utf-8") != patched: + destination.write_text(patched, encoding="utf-8") + build_env.AppendUnique(CPPPATH=[str(source.parent)]) + return build_env.File(str(destination)) + + def install(build_env): build_env.AddBuildMiddleware(replace_source, "*cores*esp32*USBCDC.cpp") build_env.AddBuildMiddleware(replace_source, "*cores*esp32*USB.cpp") + build_env.AddBuildMiddleware(replace_hwcdc_source, "*cores*esp32*HWCDC.cpp") if "Import" in globals(): diff --git a/src/helpers/UsbLogging.cpp b/src/helpers/UsbLogging.cpp index 1b398174..0e8669cd 100644 --- a/src/helpers/UsbLogging.cpp +++ b/src/helpers/UsbLogging.cpp @@ -9,6 +9,18 @@ #if MESH_ESP32_HWCDC_SESSION_GUARD #include "esp_idf_version.h" #include "hal/usb_serial_jtag_ll.h" + #include "esp_arduino_version.h" + #if ESP_ARDUINO_VERSION_MAJOR == 2 && ESP_ARDUINO_VERSION_MINOR == 0 \ + && ESP_ARDUINO_VERSION_PATCH == 17 \ + && (CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3) + #define MESH_HWCDC_PINNED_TX_BACKPORT 1 + extern "C" void meshEsp32HwcdcSetTxAllowed(bool allowed); + extern "C" void meshEsp32HwcdcKickTx(); + extern "C" bool meshEsp32HwcdcTxPending(); + extern "C" bool meshEsp32HwcdcDiscardTxStash(); + #else + #define MESH_HWCDC_PINNED_TX_BACKPORT 0 + #endif #endif #if defined(NRF52_PLATFORM) || MESH_ESP32_HWCDC_SESSION_GUARD \ @@ -418,8 +430,12 @@ static void handleEsp32HwcdcEvent(void*, esp_event_base_t, int32_t event_id, 1, std::memory_order_acq_rel); esp32_hwcdc_tx_kick_pending.store(false, std::memory_order_release); esp32_hwcdc_tx_primed.store(false, std::memory_order_release); +#if MESH_HWCDC_PINNED_TX_BACKPORT + meshEsp32HwcdcSetTxAllowed(false); +#else usb_serial_jtag_ll_disable_intr_mask( USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); +#endif portEXIT_CRITICAL(&esp32_hwcdc_session_mux); Serial.setDebugOutput(false); esp32_hwcdc_bus_reset_generation.fetch_add( @@ -501,7 +517,11 @@ static void serviceEsp32HwcdcTxKickExclusive(void*) { // Sample it while the shared producer gate excludes every MeshCore writer. const size_t tx_capacity = esp32_hwcdc_tx_buffer_capacity.load( std::memory_order_acquire); - if (tx_capacity != 0 && Serial.availableForWrite() >= tx_capacity) { + if (tx_capacity != 0 && Serial.availableForWrite() >= tx_capacity +#if MESH_HWCDC_PINNED_TX_BACKPORT + && !meshEsp32HwcdcTxPending() +#endif + ) { esp32_hwcdc_tx_kick_pending.store(false, std::memory_order_release); return; } @@ -513,9 +533,13 @@ static void serviceEsp32HwcdcTxKickExclusive(void*) { portENTER_CRITICAL(&esp32_hwcdc_session_mux); if (esp32_hwcdc_tx_kick_pending.load(std::memory_order_acquire) && canAccessEsp32Hwcdc(nullptr) && Serial.isPlugged()) { +#if MESH_HWCDC_PINNED_TX_BACKPORT + meshEsp32HwcdcKickTx(); +#else usb_serial_jtag_ll_txfifo_flush(); usb_serial_jtag_ll_ena_intr_mask( USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); +#endif } portEXIT_CRITICAL(&esp32_hwcdc_session_mux); } @@ -1232,6 +1256,10 @@ static void purgeEsp32HwcdcQueues(void* opaque) { return; } +#if MESH_HWCDC_PINNED_TX_BACKPORT + // The producer gate and driver TX gate are closed while pads are detached. + if (!meshEsp32HwcdcDiscardTxStash()) return; +#endif const uint32_t purge_started = millis(); uint8_t flush_attempts = 0; do { @@ -1241,7 +1269,11 @@ static void purgeEsp32HwcdcQueues(void* opaque) { Serial.flush(); ++flush_attempts; result->tx_empty = flush_attempts >= 2 && tx_capacity != 0 - && Serial.availableForWrite() >= tx_capacity; + && Serial.availableForWrite() >= tx_capacity +#if MESH_HWCDC_PINNED_TX_BACKPORT + && !meshEsp32HwcdcTxPending() +#endif + ; if (!result->tx_empty) delay(1); } while (!result->tx_empty && (uint32_t)(millis() - purge_started) < 100U); @@ -1269,8 +1301,12 @@ bool resetUsbCompanionTransport() { 1, std::memory_order_acq_rel) + 1U; esp32_hwcdc_tx_kick_pending.store(false, std::memory_order_release); esp32_hwcdc_tx_primed.store(false, std::memory_order_release); +#if MESH_HWCDC_PINNED_TX_BACKPORT + meshEsp32HwcdcSetTxAllowed(false); +#else usb_serial_jtag_ll_disable_intr_mask( USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); +#endif portEXIT_CRITICAL(&esp32_hwcdc_session_mux); setPlatformDebugOutputEnabled(false); @@ -1313,6 +1349,13 @@ bool resetUsbCompanionTransport() { esp32_hwcdc_tx_kick_pending.store(false, std::memory_order_release); esp32_hwcdc_allowed_generation.store( esp32_hwcdc_cleanup_generation, std::memory_order_release); +#if MESH_HWCDC_PINNED_TX_BACKPORT + // A newer BUS_RESET remains quarantined until its own cleanup epoch. + if (esp32_hwcdc_cleanup_generation + == esp32_hwcdc_access_generation.load(std::memory_order_acquire)) { + meshEsp32HwcdcSetTxAllowed(true); + } +#endif portEXIT_CRITICAL(&esp32_hwcdc_session_mux); esp32_hwcdc_cleanup_pending = false; // A concurrent runtime preference change is authoritative; never restore a @@ -1604,7 +1647,11 @@ UsbLoggingObservation observeUsbLoggingTransport() { result.reader_connected = result.host_connected && bool(Serial); const size_t capacity = esp32_hwcdc_tx_buffer_capacity.load(std::memory_order_acquire); result.pending = usb_logging_tx_waiting.load(std::memory_order_acquire) - || (capacity != 0 && Serial.availableForWrite() < int(capacity)); + || (capacity != 0 && Serial.availableForWrite() < int(capacity)) +#if MESH_HWCDC_PINNED_TX_BACKPORT + || meshEsp32HwcdcTxPending() +#endif + ; #elif defined(NRF52_PLATFORM) && defined(USE_TINYUSB) result.supported = true; const uint8_t instance = hasDedicatedUsbLoggingPort() ? 1 : 0; diff --git a/test/fixtures/hwcdc_tx_backport/.gitattributes b/test/fixtures/hwcdc_tx_backport/.gitattributes new file mode 100644 index 00000000..61556d91 --- /dev/null +++ b/test/fixtures/hwcdc_tx_backport/.gitattributes @@ -0,0 +1,2 @@ +# Preserve the exact upstream source fingerprint, including its whitespace. +HWCDC.cpp -text whitespace=-blank-at-eol diff --git a/test/fixtures/hwcdc_tx_backport/HWCDC.cpp b/test/fixtures/hwcdc_tx_backport/HWCDC.cpp new file mode 100644 index 00000000..8d439254 --- /dev/null +++ b/test/fixtures/hwcdc_tx_backport/HWCDC.cpp @@ -0,0 +1,613 @@ +// 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. +// 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 "USB.h" +#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 + +#include "esp32-hal.h" +#include "HWCDC.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/queue.h" +#include "freertos/ringbuf.h" +#include "esp_intr_alloc.h" +#include "soc/periph_defs.h" +#include "hal/usb_serial_jtag_ll.h" +#include "esp_private/startup_internal.h" +#include "esp_freertos_hooks.h" + +ESP_EVENT_DEFINE_BASE(ARDUINO_HW_CDC_EVENTS); + +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 connected = false; +static xSemaphoreHandle tx_lock = NULL; + +// SOF in ISR causes problems for uploading firmware +//static volatile unsigned long lastSOF_ms; +//static volatile uint8_t SOF_TIMEOUT; +// detecting SOF from a timer seems to work with esptool.py +static volatile bool s_usb_serial_jtag_conn_status; +static volatile uint32_t remaining_allowed_no_sof_ticks; +#define USJ_DISCONNECT_CONFIRM_PERIOD_MS 5 +#define ALLOWED_NO_SOF_TICKS pdMS_TO_TICKS(USJ_DISCONNECT_CONFIRM_PERIOD_MS) + +static void IRAM_ATTR usb_serial_jtag_sof_tick_hook(void) +{ + // SOF packet is sent by the HOST every 1ms on a full speed bus + // Between two consecutive tick hooks, there will be at least 1ms (selectable tick rate range is 1 - 1000Hz) + // Therefore, SOF intr bit must have be raised at every tick hook if it is connected to a HOST + // Here, the strategy is: Always assume USB Serial/JTAG is connected until we are sure it is not connected + // Consider it is disconnected only if SOF intr bit is not raised within (ALLOWED_NO_SOF_TICKS + 1) tick periods + bool sof_received = USB_SERIAL_JTAG.int_raw.sof_int_raw == 1; + usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_SOF); + + if (s_usb_serial_jtag_conn_status != sof_received) { + if (!sof_received) { + if (remaining_allowed_no_sof_ticks > 0) { + remaining_allowed_no_sof_ticks--; + } else { + s_usb_serial_jtag_conn_status = false; + } + } else { + s_usb_serial_jtag_conn_status = true; + remaining_allowed_no_sof_ticks = ALLOWED_NO_SOF_TICKS; + } + } + +} + +// runs on Core 0 +ESP_SYSTEM_INIT_FN(usb_serial_jtag_conn_status_init, BIT(0)) +{ + s_usb_serial_jtag_conn_status = true; + remaining_allowed_no_sof_ticks = ALLOWED_NO_SOF_TICKS; + esp_register_freertos_tick_hook(usb_serial_jtag_sof_tick_hook); +} + +// 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; + +static esp_err_t arduino_hw_cdc_event_post(esp_event_base_t event_base, int32_t event_id, void *event_data, size_t event_data_size, BaseType_t *task_unblocked){ + if(arduino_hw_cdc_event_loop_handle == NULL){ + return ESP_FAIL; + } + return esp_event_isr_post_to(arduino_hw_cdc_event_loop_handle, event_base, event_id, event_data, event_data_size, task_unblocked); +} + +static esp_err_t arduino_hw_cdc_event_handler_register_with(esp_event_base_t event_base, int32_t event_id, esp_event_handler_t event_handler, void *event_handler_arg){ + if (!arduino_hw_cdc_event_loop_handle) { + esp_event_loop_args_t event_task_args = { + .queue_size = 5, + .task_name = "arduino_hw_cdc_events", + .task_priority = 5, + .task_stack_size = 2048, + .task_core_id = tskNO_AFFINITY + }; + if (esp_event_loop_create(&event_task_args, &arduino_hw_cdc_event_loop_handle) != ESP_OK) { + log_e("esp_event_loop_create failed"); + } + } + if(arduino_hw_cdc_event_loop_handle == NULL){ + return ESP_FAIL; + } + return esp_event_handler_register_with(arduino_hw_cdc_event_loop_handle, event_base, event_id, event_handler, event_handler_arg); +} + +static void hw_cdc_isr_handler(void *arg) { + portBASE_TYPE xTaskWoken = 0; + uint32_t usbjtag_intr_status = 0; + arduino_hw_cdc_event_data_t event = {0}; + usbjtag_intr_status = usb_serial_jtag_ll_get_intsts_mask(); + + if (usbjtag_intr_status & USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY) { + // Interrupt tells us the host picked up the data we sent. + 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); + size_t queued_size = 0; + 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. + if (queued_buff != NULL) { //Although tx_queued_bytes may be larger than 0. We may have interrupt before xRingbufferSend() was called. + //Copy the queued buffer into the TX FIFO + usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + usb_serial_jtag_ll_write_txfifo(queued_buff, queued_size); + usb_serial_jtag_ll_txfifo_flush(); + vRingbufferReturnItemFromISR(tx_ring_buf, queued_buff, &xTaskWoken); + 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; + arduino_hw_cdc_event_post(ARDUINO_HW_CDC_EVENTS, ARDUINO_HW_CDC_TX_EVENT, &event, sizeof(arduino_hw_cdc_event_data_t), &xTaskWoken); + } + } else { + usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + } + } + + if (usbjtag_intr_status & USB_SERIAL_JTAG_INTR_SERIAL_OUT_RECV_PKT) { + // read rx buffer(max length is 64), and send avaliable data to ringbuffer. + // Ensure the rx buffer size is larger than RX_MAX_SIZE. + usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_SERIAL_OUT_RECV_PKT); + uint32_t rx_fifo_len = usb_serial_jtag_ll_read_rxfifo(rx_data_buf, 64); + uint32_t i=0; + for(i=0; i= 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_txfifo_flush(); + usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); +} + +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 HWCDC::isCDC_Connected(); +} + +void HWCDC::onEvent(esp_event_handler_t callback){ + onEvent(ARDUINO_HW_CDC_ANY_EVENT, callback); +} + +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) { + tx_lock = xSemaphoreCreateMutex(); + } + //RX Buffer default has 256 bytes if not preset + if(rx_queue == NULL) { + if (!setRxBufferSize(256)) { + log_e("HW CDC RX Buffer error"); + } + } + //TX Buffer default has 256 bytes if not preset + if (tx_ring_buf == NULL) { + if (!setTxBufferSize(256)) { + 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_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; + } +} + +void HWCDC::end() +{ + //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) { + vSemaphoreDelete(tx_lock); + tx_lock = NULL; + } + setRxBufferSize(0); + setTxBufferSize(0); + if (arduino_hw_cdc_event_loop_handle) { + 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; +} + +/* + * WRITING +*/ + +size_t HWCDC::setTxBufferSize(size_t tx_queue_len){ + if(tx_ring_buf){ + vRingbufferDelete(tx_ring_buf); + tx_ring_buf = NULL; + } + if(!tx_queue_len){ + return 0; + } + tx_ring_buf = xRingbufferCreate(tx_queue_len, RINGBUF_TYPE_BYTEBUF); + if(!tx_ring_buf){ + return 0; + } + return tx_queue_len; +} + +int HWCDC::availableForWrite(void) +{ + if(tx_ring_buf == NULL || tx_lock == NULL){ + return 0; + } + if(xSemaphoreTake(tx_lock, tx_timeout_ms / portTICK_PERIOD_MS) != pdPASS){ + return 0; + } + size_t a = xRingbufferGetCurFreeSize(tx_ring_buf); + xSemaphoreGive(tx_lock); + return a; +} + +size_t HWCDC::write(const uint8_t *buffer, size_t size) +{ + if(buffer == NULL || size == 0 || tx_ring_buf == NULL || tx_lock == NULL){ + return 0; + } + if(xSemaphoreTake(tx_lock, tx_timeout_ms / portTICK_PERIOD_MS) != pdPASS){ + return 0; + } + if(!isCDC_Connected()) { + // just pop/push RingBuffer and apply FIFO policy + flushTXBuffer(buffer, size); + } else { + 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(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_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); + return size; +} + +size_t HWCDC::write(uint8_t c) +{ + return write(&c, 1); +} + +void HWCDC::flush(void) +{ + if(tx_ring_buf == NULL || tx_lock == NULL){ + return; + } + if(xSemaphoreTake(tx_lock, tx_timeout_ms / portTICK_PERIOD_MS) != pdPASS){ + return; + } + 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); +} + +/* + * READING +*/ + +size_t HWCDC::setRxBufferSize(size_t rx_queue_len){ + if(rx_queue){ + vQueueDelete(rx_queue); + rx_queue = NULL; + } + if(!rx_queue_len){ + return 0; + } + rx_queue = xQueueCreate(rx_queue_len, sizeof(uint8_t)); + if(!rx_queue){ + return 0; + } + return rx_queue_len; +} + +int HWCDC::available(void) +{ + if(rx_queue == NULL){ + return -1; + } + return uxQueueMessagesWaiting(rx_queue); +} + +int HWCDC::peek(void) +{ + if(rx_queue == NULL){ + return -1; + } + uint8_t c; + if(xQueuePeek(rx_queue, &c, 0)) { + return c; + } + return -1; +} + +int HWCDC::read(void) +{ + if(rx_queue == NULL){ + return -1; + } + uint8_t c = 0; + if(xQueueReceive(rx_queue, &c, 0)) { + return c; + } + return -1; +} + +size_t HWCDC::read(uint8_t *buffer, size_t size) +{ + if(rx_queue == NULL){ + return -1; + } + uint8_t c = 0; + size_t count = 0; + while(count < size && xQueueReceive(rx_queue, &c, 0)){ + buffer[count++] = c; + } + return count; +} + +/* + * DEBUG +*/ + +void HWCDC::setDebugOutput(bool en) +{ + if(en) { + uartSetDebug(NULL); + ets_install_putc2((void (*)(char)) &cdc0_write_char); + } else { + ets_install_putc2(NULL); + } +} + +#if ARDUINO_USB_MODE +#if ARDUINO_USB_CDC_ON_BOOT//Serial used for USB CDC +HWCDC Serial; +#else +HWCDC USBSerial; +#endif +#endif + +#endif /* CONFIG_IDF_TARGET_ESP32C3 */ diff --git a/test/fixtures/hwcdc_tx_backport/README.txt b/test/fixtures/hwcdc_tx_backport/README.txt new file mode 100644 index 00000000..183030f2 --- /dev/null +++ b/test/fixtures/hwcdc_tx_backport/README.txt @@ -0,0 +1,10 @@ +Exact upstream Arduino-ESP32 2.0.17 HWCDC.cpp fixture for fail-closed TX transform. +Source: https://github.com/espressif/arduino-esp32/blob/2.0.17/cores/esp32/HWCDC.cpp +Revision: dcc1105b0cf1322a437b354c336f2abf72b7e512 +SHA256: d0a8ca606c2729c8522a041113285dbf27033c22a5a6af8649a7305ffe84c449 +Version-header source: same tag cores/esp32/esp_arduino_version.h. +Both files preserve their upstream copyright and Apache-2.0 notices. +The full source is necessary to verify the exact input fingerprint and all +producer/ISR/reset replacement sites without a PlatformIO package dependency. +Underlying correction: upstream commit2ecad233178bfbba9c584ffd9f67e941d84adbb7, +https://github.com/espressif/arduino-esp32/pull/12606. diff --git a/test/fixtures/hwcdc_tx_backport/esp_arduino_version.h b/test/fixtures/hwcdc_tx_backport/esp_arduino_version.h new file mode 100644 index 00000000..755652f1 --- /dev/null +++ b/test/fixtures/hwcdc_tx_backport/esp_arduino_version.h @@ -0,0 +1,46 @@ +// Copyright 2019 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 + +#ifdef __cplusplus +extern "C" { +#endif + +/** Major version number (X.x.x) */ +#define ESP_ARDUINO_VERSION_MAJOR 2 +/** Minor version number (x.X.x) */ +#define ESP_ARDUINO_VERSION_MINOR 0 +/** Patch version number (x.x.X) */ +#define ESP_ARDUINO_VERSION_PATCH 17 + +/** + * Macro to convert ARDUINO version number into an integer + * + * To be used in comparisons, such as ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(2, 0, 0) + */ +#define ESP_ARDUINO_VERSION_VAL(major, minor, patch) ((major << 16) | (minor << 8) | (patch)) + +/** + * Current ARDUINO version, as an integer + * + * To be used in comparisons, such as ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(2, 0, 0) + */ +#define ESP_ARDUINO_VERSION ESP_ARDUINO_VERSION_VAL(ESP_ARDUINO_VERSION_MAJOR, \ + ESP_ARDUINO_VERSION_MINOR, \ + ESP_ARDUINO_VERSION_PATCH) + +#ifdef __cplusplus +} +#endif diff --git a/test/fixtures/usb_logging_watchdog/mocks/FS.h b/test/fixtures/usb_logging_watchdog/mocks/FS.h index b6f59257..7ffc09d2 100644 --- a/test/fixtures/usb_logging_watchdog/mocks/FS.h +++ b/test/fixtures/usb_logging_watchdog/mocks/FS.h @@ -20,6 +20,9 @@ class File { size_t size() const; size_t read(uint8_t* buffer, size_t count); size_t write(const uint8_t* buffer, size_t count); + // This in-memory file has no stdio buffering; valid handles accept the + // configuration used by the production contact transaction. + bool setBufferSize(size_t) { return _fs != nullptr; } void flush() {} void close() { _fs = nullptr; } private: diff --git a/test/test_hwcdc_tx_backport.py b/test/test_hwcdc_tx_backport.py new file mode 100644 index 00000000..eaf484b3 --- /dev/null +++ b/test/test_hwcdc_tx_backport.py @@ -0,0 +1,375 @@ +#!/usr/bin/env python3 +import importlib.util +from pathlib import Path +import subprocess +import tempfile +import unittest + +ROOT=Path(__file__).resolve().parents[1] +CORE=ROOT/'test/fixtures/hwcdc_tx_backport' +spec=importlib.util.spec_from_file_location('usb_fix',ROOT/'scripts/esp32_usb_session_fix.py') +module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module) +RAW=(CORE/'HWCDC.cpp').read_text() +PATCHED=module.patched_hwcdc_source(RAW) + +def body(text,signature): + start=text.index(signature);open_=text.index('{',start);depth=0 + for end in range(open_,len(text)): + if text[end]=='{':depth+=1 + elif text[end]=='}': + depth-=1 + if not depth:return text[start:end+1] + raise AssertionError('unclosed function') + +HARNESS=r''' +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using UBaseType_t=unsigned;using portBASE_TYPE=int; +using portMUX_TYPE=std::recursive_mutex; +#define portMUX_INITIALIZER_UNLOCKED {} +static thread_local unsigned critical_depth; +static thread_local bool producer_thread=false; +static std::atomic producer_attempted{false}; +static void enter_mux(portMUX_TYPE* mux) { + if(producer_thread)producer_attempted.store(true,std::memory_order_release); + mux->lock();++critical_depth; +} +#define portENTER_CRITICAL_SAFE(m) enter_mux(m) +#define portEXIT_CRITICAL_SAFE(m) do{--critical_depth;(m)->unlock();}while(0) +#define portENTER_CRITICAL_ISR(m) portENTER_CRITICAL_SAFE(m) +#define portEXIT_CRITICAL_ISR(m) portEXIT_CRITICAL_SAFE(m) +#define portYIELD_FROM_ISR() ((void)0) +#define pdTRUE 1 +#define USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY 1 +#define USB_SERIAL_JTAG_INTR_SERIAL_OUT_RECV_PKT 2 +#define USB_SERIAL_JTAG_INTR_BUS_RESET 4 +#define ARDUINO_HW_CDC_EVENTS 5 +#define ARDUINO_HW_CDC_TX_EVENT 6 +#define ARDUINO_HW_CDC_RX_EVENT 7 +#define ARDUINO_HW_CDC_BUS_RESET_EVENT 8 +struct Ring {std::deque> data;std::vector active;unsigned returns=0;} ring; +static Ring* tx_ring_buf=˚ +static void* rx_queue=nullptr; +static uint8_t rx_data_buf[64]{}; +static volatile bool connected=false; +static bool plugged=false,irq_enabled=true; +static uint32_t intr_status=0; +static unsigned fifo_limit=64,flushes=0; +static std::vector staged; +static std::vector event_lengths; +static std::function before_empty_return; +static uint32_t usb_serial_jtag_ll_get_intsts_mask(){return intr_status;} +static void usb_serial_jtag_ll_clr_intsts_mask(uint32_t bits){intr_status&=~bits;} +static void usb_serial_jtag_ll_ena_intr_mask(uint32_t){assert(critical_depth);irq_enabled=true;} +static void usb_serial_jtag_ll_disable_intr_mask(uint32_t){assert(critical_depth);irq_enabled=false;} +static int usb_serial_jtag_ll_txfifo_writable(){return 1;} +static unsigned usb_serial_jtag_ll_write_txfifo(const uint8_t*data,unsigned len){ + unsigned count=std::min(len,fifo_limit);staged.insert(staged.end(),data,data+count);return count; +} +static void usb_serial_jtag_ll_txfifo_flush(){++flushes;} +static uint32_t usb_serial_jtag_ll_read_rxfifo(uint8_t*,unsigned){return 0;} +static bool xQueueSendFromISR(void*,uint8_t*,int*){return true;} +static uint8_t* xRingbufferReceiveUpToFromISR(Ring*r,size_t*size,unsigned limit){ + if(r->data.empty()){ + *size=0;if(before_empty_return){auto callback=before_empty_return;before_empty_return=nullptr;callback();}return nullptr; + } + r->active=r->data.front();r->data.pop_front(); + if(r->active.size()>limit){std::vectortail(r->active.begin()+limit,r->active.end());r->data.push_front(tail);r->active.resize(limit);} + *size=r->active.size();return r->active.data(); +} +static void vRingbufferReturnItemFromISR(Ring*r,uint8_t*,int*){++r->returns;r->active.clear();} +static void vRingbufferGetInfo(Ring*r,void*,void*,void*,void*,unsigned*waiting){ + *waiting=0;for(const auto& item:r->data)*waiting+=item.size(); +} +union arduino_hw_cdc_event_data_t{struct{unsigned len;}tx,rx;}; +static void arduino_hw_cdc_event_post(int,int event,arduino_hw_cdc_event_data_t*data,size_t,int*){ + if(event==ARDUINO_HW_CDC_TX_EVENT)event_lengths.push_back(data->tx.len); +} +struct HWCDC {static bool isPlugged(){return plugged;}static bool isCDC_Connected();}; +@SUPPORT@ +@CONNECTED@ +@ISR@ +static void pulse(unsigned limit=64){fifo_limit=limit;intr_status=1;hw_cdc_isr_handler(nullptr);} +static void reset(){ + ring=Ring{};staged.clear();event_lengths.clear();mesh_hwcdc_tx_stash_len=0; + mesh_hwcdc_fifo_pending=false;mesh_hwcdc_active_writers=0;mesh_hwcdc_tx_allowed=true; + irq_enabled=true;plugged=false;connected=false;before_empty_return=nullptr;flushes=0; +} +int main(){ + // Exact suffix ownership across zero, short and full FIFO admissions, two + // ring items and a 64-byte boundary. Later data must never precede a suffix. + reset();std::vector expected(78);for(unsigned n=0;n(expected.begin(),expected.begin()+64)); + ring.data.push_back(std::vector(expected.begin()+64,expected.end())); + pulse(0);assert(ring.returns==1&&staged.empty()&&mesh_hwcdc_tx_stash_len==64&&irq_enabled); + assert(event_lengths.back()==0&&meshEsp32HwcdcTxPending()); + pulse(1);assert(staged.size()==1&&mesh_hwcdc_tx_stash_len==63); + pulse(17);pulse(17);pulse(29);assert(staged.size()==64&&ring.returns==1); + assert(mesh_hwcdc_tx_stash_len==0&&meshEsp32HwcdcTxPending()); // later ring item still queued + pulse(64);assert(ring.returns==2&&staged==expected); + assert(mesh_hwcdc_tx_stash_len==0&&ring.data.empty()&&meshEsp32HwcdcTxPending()); // FIFO payload unacknowledged + pulse();assert(!meshEsp32HwcdcTxPending()&&!irq_enabled); // actual host pickup empties transport + assert(connected&&!plugged); // real IN_EMPTY outranks false SOF for progress + + // Synthetic same-thread ISR-reentrant enqueue/rearm witness. This tests + // the locked recheck specifically; it is NOT the ordinary SMP task race. + reset();before_empty_return=[] {ring.data.push_back({9,8,7});mesh_hwcdc_enable_tx_intr();}; + pulse();assert(irq_enabled&&meshEsp32HwcdcTxPending());pulse();pulse(); + assert((staged==std::vector{9,8,7})&&!meshEsp32HwcdcTxPending()); + + // Real cross-thread task producer: ring enqueue completes while the ISR + // owns the driver mux, then protected rearm blocks until ISR unlocks. The + // post-unlock rearm repairs progress even without the empty-branch recheck. + reset();std::atomic start{false},queued{false},rearmed{false}; + producer_attempted.store(false); + std::thread producer([&]{ + producer_thread=true; + while(!start.load(std::memory_order_acquire))std::this_thread::yield(); + ring.data.push_back({6,5,4});queued.store(true,std::memory_order_release); + mesh_hwcdc_enable_tx_intr();rearmed.store(true,std::memory_order_release); + }); + before_empty_return=[&]{ + start.store(true,std::memory_order_release); + while(!queued.load(std::memory_order_acquire))std::this_thread::yield(); + while(!producer_attempted.load(std::memory_order_acquire) + && !rearmed.load(std::memory_order_acquire))std::this_thread::yield(); + assert(producer_attempted.load()&&!rearmed.load()); + }; + pulse();producer.join();assert(rearmed.load()&&irq_enabled); + pulse();pulse();assert((staged==std::vector{6,5,4})&&!meshEsp32HwcdcTxPending()); + + // SDK status must remain suffix-aware after ring item return, and ordinary + // stash flush must not claim already staged FIFO DATA has reached the host. + reset();ring.data.push_back({1,2,3,4});pulse(2); + assert(ring.data.empty()&&mesh_hwcdc_tx_stash_len==2&&mesh_hwcdc_fifo_pending); + assert(!meshEsp32HwcdcDiscardTxStash()); // cannot discard live-session suffix + mesh_hwcdc_clear_tx_stash();assert(mesh_hwcdc_tx_stash_len==0&&meshEsp32HwcdcTxPending()); + pulse();assert(!meshEsp32HwcdcTxPending()); + + // Quarantine inhibits all kicks/ISR drains. Explicit detached discard waits + // until even previously entered raw writers have stopped, then clears the + // old epoch's stash and FIFO tracking. Resume never resurrects old bytes. + reset();ring.data.push_back({1,2,3,4});pulse(1);unsigned old_size=staged.size(); + {mesh_hwcdc_writer_scope writer;assert(writer); + meshEsp32HwcdcSetTxAllowed(false);assert(!irq_enabled); + assert(!meshEsp32HwcdcDiscardTxStash());assert(meshEsp32HwcdcTxPending()); + meshEsp32HwcdcKickTx();assert(!irq_enabled);pulse();assert(staged.size()==old_size&&!irq_enabled); + } + assert(meshEsp32HwcdcDiscardTxStash()&&!meshEsp32HwcdcTxPending()); + meshEsp32HwcdcSetTxAllowed(true);ring.data.push_back({5,6});meshEsp32HwcdcKickTx();pulse();pulse(); + assert((staged==std::vector{1,5,6})); + + // BUS_RESET itself drops the previous FIFO suffix/in-flight tracking; a SOF + // false status poll does not. Existing async callback policy stays separate. + reset();ring.data.push_back({1,2,3,4});pulse(1); + assert(!HWCDC::isCDC_Connected()&&mesh_hwcdc_tx_stash_len==3&&mesh_hwcdc_fifo_pending); + intr_status=USB_SERIAL_JTAG_INTR_BUS_RESET;hw_cdc_isr_handler(nullptr); + assert(!connected&&mesh_hwcdc_tx_stash_len==0&&!mesh_hwcdc_fifo_pending&&!meshEsp32HwcdcTxPending()); + // Every reconnect poll repairs the wakeup; no old one-shot running flag. + plugged=true;irq_enabled=false;assert(!HWCDC::isCDC_Connected()&&irq_enabled); + irq_enabled=false;assert(!HWCDC::isCDC_Connected()&&irq_enabled); +} +''' + +class BackportTest(unittest.TestCase): + def compile_run(self,harness, *, expect_success=True): + with tempfile.TemporaryDirectory(prefix='hwcdc-native-',dir=ROOT) as directory: + tmp=Path(directory);(tmp/'test.cpp').write_text(harness) + command=['g++','-std=c++11','-Os','-Wall','-Wextra','-Werror','-Wno-unused-parameter','-Wno-sign-compare', + '-pthread','-I'+str(ROOT/'src'),'-fsanitize=address,undefined','-fno-sanitize-recover=all','-fno-pie','-no-pie', + str(tmp/'test.cpp'),'-o',str(tmp/'test')] + build=subprocess.run(command,capture_output=True,text=True) + self.assertEqual(build.returncode,0,build.stdout+build.stderr) + run=subprocess.run([str(tmp/'test')],capture_output=True,text=True) + if expect_success:self.assertEqual(run.returncode,0,run.stdout+run.stderr) + else:self.assertNotEqual(run.returncode,0,'negative control did not fail') + def harness(self,source=PATCHED, *, support=None, skip_reentrant=False): + harness=HARNESS + if skip_reentrant: + start=harness.index(' // Synthetic same-thread ISR-reentrant') + end=harness.index(' // Real cross-thread task producer:',start) + harness=harness[:start]+harness[end:] + return harness.replace('@SUPPORT@',module.HWCDC_TX_SUPPORT if support is None else support).replace('@CONNECTED@', + body(source,'bool HWCDC::isCDC_Connected()')).replace('@ISR@',body(source,'static void hw_cdc_isr_handler(')) + def test_actual_isr_suffix_wakeup_pending_quarantine_and_reset(self): + self.compile_run(self.harness()) + def test_ignored_fifo_count_negative_control(self): + source=PATCHED.replace('sent_size = usb_serial_jtag_ll_write_txfifo(queued_buff, queued_size);', + 'usb_serial_jtag_ll_write_txfifo(queued_buff, queued_size);sent_size = queued_size;',1) + self.compile_run(self.harness(source),expect_success=False) + def test_missing_empty_recheck_isr_reentrant_negative_control(self): + source=PATCHED.replace('if (waiting || mesh_hwcdc_tx_stash_len) {','if (false) {',1) + self.compile_run(self.harness(source),expect_success=False) + def test_cross_thread_protected_rearm_remains_live_without_recheck(self): + source=PATCHED.replace('if (waiting || mesh_hwcdc_tx_stash_len) {','if (false) {',1) + self.compile_run(self.harness(source,skip_reentrant=True)) + def test_legacy_unprotected_rearm_and_missing_recheck_negative_control(self): + source=PATCHED.replace('if (waiting || mesh_hwcdc_tx_stash_len) {','if (false) {',1) + support=module.HWCDC_TX_SUPPORT + helper=body(support,'static inline void mesh_hwcdc_enable_tx_intr()') + broken=helper.replace(' portENTER_CRITICAL_SAFE(&mesh_hwcdc_tx_mux);\n','').replace( + ' portEXIT_CRITICAL_SAFE(&mesh_hwcdc_tx_mux);\n','') + # Leave LL's context assertion satisfied for this explicitly unsafe + # simulated legacy producer. The SMP blocking invariant must still fail. + broken=broken.replace(' if (mesh_hwcdc_tx_allowed) {', + ' ++critical_depth;\n if (mesh_hwcdc_tx_allowed) {').replace('\n}', '\n --critical_depth;\n}') + support=support.replace(helper,broken) + self.compile_run(self.harness(source,support=support,skip_reentrant=True),expect_success=False) + def test_strict_source_hash_idempotence_and_build_local_modes(self): + self.assertEqual(module.patched_hwcdc_source(PATCHED),PATCHED) + self.assertEqual(module.patched_hwcdc_source(RAW.replace('\n','\r\n')),PATCHED) + for source in [RAW+'\n',PATCHED+'\n',RAW.replace('queued_size);','queued_size-1);',1)]: + with self.assertRaises(RuntimeError):module.patched_hwcdc_source(source) + class Node: + def __init__(self,path):self.path=path + def srcnode(self):return self + def get_abspath(self):return str(self.path) + class Env(dict): + def __init__(self,defines,build):super().__init__(CPPDEFINES=defines);self.build=build;self.paths=[] + def subst(self,value):assert value=='$BUILD_DIR';return str(self.build) + def File(self,path):return path + def AppendUnique(self,**kw):self.paths+=kw['CPPPATH'] + with tempfile.TemporaryDirectory(prefix='hwcdc-source-',dir=ROOT) as directory: + tmp=Path(directory);src=tmp/'sdk';src.mkdir();path=src/'HWCDC.cpp';path.write_text(RAW) + version=src/'esp_arduino_version.h';version.write_text((CORE/'esp_arduino_version.h').read_text());node=Node(path) + for defines in [[],[('ARDUINO_USB_CDC_ON_BOOT',0),('ARDUINO_USB_MODE',1)], + [('ARDUINO_USB_CDC_ON_BOOT',1),('ARDUINO_USB_MODE',0)]]: + env=Env(defines,tmp/'build');self.assertIs(module.replace_hwcdc_source(env,node),node) + self.assertFalse((tmp/'build').exists()) + env=Env([('ARDUINO_USB_CDC_ON_BOOT',1),('ARDUINO_USB_MODE',1)],tmp/'build') + result=module.replace_hwcdc_source(env,node) + self.assertEqual(Path(result).read_text(),PATCHED);self.assertEqual(path.read_text(),RAW) + self.assertEqual(env.paths,[str(src)]) + before=Path(result).stat().st_mtime_ns;module.replace_hwcdc_source(env,node) + self.assertEqual(Path(result).stat().st_mtime_ns,before) + version.write_text('#define ESP_ARDUINO_VERSION_MAJOR 3\n#define ESP_ARDUINO_VERSION_MINOR 3\n#define ESP_ARDUINO_VERSION_PATCH 11\n') + self.assertIs(module.replace_hwcdc_source(env,node),node) + version.write_text('#define ESP_ARDUINO_VERSION_MAJOR 2\n') + with self.assertRaises(RuntimeError):module.replace_hwcdc_source(env,node) + def test_executed_logging_kick_purge_observation_and_new_reset_epoch(self): + source=(ROOT/'src/helpers/UsbLogging.cpp').read_text() + functions='\n'.join(body(source,s) for s in [ + 'static void handleEsp32HwcdcEvent(', + 'static void serviceEsp32HwcdcTxKickExclusive(', + 'static void purgeEsp32HwcdcQueues(', + 'bool resetUsbCompanionTransport(', + 'UsbLoggingObservation observeUsbLoggingTransport(']) + prefix=self.harness()[:self.harness().index('int main(){')] + extra=r''' +#include +#define MESH_ESP32_HWCDC_SESSION_GUARD 1 +#define MESH_HWCDC_PINNED_TX_BACKPORT 1 +#define portENTER_CRITICAL(m) portENTER_CRITICAL_SAFE(m) +#define portEXIT_CRITICAL(m) portEXIT_CRITICAL_SAFE(m) +using esp_event_base_t=int; +static bool xPortInIsrContext(){return false;} +static uint32_t clock_ms; +static uint32_t millis(){return clock_ms;} +static void delay(uint32_t ms){clock_ms+=ms;} +static bool pads_enabled=true; +static std::function at_restore; +static bool detachEsp32HwcdcPads(){bool prior=pads_enabled;pads_enabled=false;plugged=false;return prior;} +static void restoreEsp32HwcdcPads(bool enabled){pads_enabled=enabled;plugged=enabled;if(at_restore){auto f=at_restore;at_restore=nullptr;f();}} +static void setPlatformDebugOutputEnabled(bool){} +static bool isUsbDebugLoggingEnabled(){return false;} +static void clearUsbLoggingClientActivity(){} +static void noteUsbLoggingTxComplete(){} +static std::atomic esp32_hwcdc_access_generation{0},esp32_hwcdc_allowed_generation{0}; +static std::atomic esp32_hwcdc_bus_reset_generation{0}; +static mesh::UsbSelfResetBurstGuard esp32_hwcdc_self_reset_guard; +static std::atomic esp32_hwcdc_tx_kick_pending{false},esp32_hwcdc_tx_primed{false}; +static portMUX_TYPE esp32_hwcdc_session_mux; +static std::atomic esp32_hwcdc_tx_buffer_capacity{4096}; +static bool esp32_hwcdc_cleanup_pending=false,esp32_hwcdc_restore_pad_enabled=false; +static uint32_t esp32_hwcdc_cleanup_generation=0; +static std::atomic usb_logging_tx_progress{0}; +static std::atomic usb_logging_tx_waiting{false}; +static bool canAccessEsp32Hwcdc(void*){return esp32_hwcdc_access_generation.load()==esp32_hwcdc_allowed_generation.load();} +struct SerialMock { + unsigned flush_calls=0; + int availableForWrite(){unsigned waiting=0;vRingbufferGetInfo(&ring,nullptr,nullptr,nullptr,nullptr,&waiting);return 4096-waiting;} + void flush(){++flush_calls;if(!ring.data.empty())ring.data.pop_front();} + int read(){return -1;} + bool isPlugged(){return plugged;} + operator bool(){return connected&&plugged;} + void setDebugOutput(bool){} +} Serial; +struct Exclusive {bool busy=false;bool tryRunExclusive(void(*f)(void*),void*opaque){if(busy)return false;f(opaque);return true;}} guarded_esp32_hwcdc_port; +struct Esp32HwcdcPurgeResult{bool tx_empty=false;}; +struct UsbLoggingObservation{uint32_t tx_progress=0;bool supported=false,host_connected=false,reader_connected=false,pending=false;}; +@FUNCTIONS@ +int main(){ + // Ring capacity alone cannot stop a kick or mark a pending suffix/FIFO empty. + reset();plugged=true;ring.data.push_back({1,2,3,4});pulse(2); + assert(Serial.availableForWrite()==4096 && mesh_hwcdc_tx_stash_len==2); + esp32_hwcdc_tx_kick_pending.store(true);irq_enabled=false; + serviceEsp32HwcdcTxKickExclusive(nullptr); + assert(esp32_hwcdc_tx_kick_pending.load()&&irq_enabled); + assert(observeUsbLoggingTransport().pending); + pulse();assert(mesh_hwcdc_tx_stash_len==0&&mesh_hwcdc_fifo_pending); + assert(observeUsbLoggingTransport().pending); + serviceEsp32HwcdcTxKickExclusive(nullptr);assert(esp32_hwcdc_tx_kick_pending.load()); + pulse();assert(!observeUsbLoggingTransport().pending); + serviceEsp32HwcdcTxKickExclusive(nullptr);assert(!esp32_hwcdc_tx_kick_pending.load()); + + // No live-session discard, and no explicit purge while an entered writer remains. + reset();ring.data.push_back({1,2,3});pulse(1); + Esp32HwcdcPurgeResult result;purgeEsp32HwcdcQueues(&result);assert(!result.tx_empty&&Serial.flush_calls==0); + {mesh_hwcdc_writer_scope writer;assert(writer);meshEsp32HwcdcSetTxAllowed(false); + purgeEsp32HwcdcQueues(&result);assert(!result.tx_empty&&Serial.flush_calls==0);} + // Real reset closes generation+driver gates, detaches, discards old stash/FIFO + // tracking, purges both contiguous ring pieces, and only then reopens. + ring.data.push_back({7,8});ring.data.push_back({9}); + assert(resetUsbCompanionTransport());assert(!meshEsp32HwcdcTxPending()); + assert(mesh_hwcdc_tx_allowed&&canAccessEsp32Hwcdc(nullptr)&&pads_enabled); + assert(Serial.flush_calls>=2&&ring.data.empty()); + + // An actual newer BUS_RESET during old cleanup must not reopen the TX gate. + // Fresh RX proves enumeration ended before the later reset callback arrives. + reset();esp32_hwcdc_access_generation.store(0);esp32_hwcdc_allowed_generation.store(0); + esp32_hwcdc_cleanup_pending=false; + at_restore=[] { + handleEsp32HwcdcEvent(nullptr,0,ARDUINO_HW_CDC_RX_EVENT,nullptr); + handleEsp32HwcdcEvent(nullptr,0,ARDUINO_HW_CDC_BUS_RESET_EVENT,nullptr); + }; + assert(resetUsbCompanionTransport()); + assert(!mesh_hwcdc_tx_allowed&&!canAccessEsp32Hwcdc(nullptr)); + assert(esp32_hwcdc_allowed_generation.load()