Preserve ESP native USB bytes across short FIFO writes

Backport the pinned Arduino 2.0.17 HWCDC suffix and interrupt handling into a build-local source copy. Preserve unsent bytes, serialize ISR and producer rearm, and include FIFO, suffix and active writers in transport cleanup proofs without changing the loop stack allocation.

Validate zero and short writes, producer races, quarantine and stale cleanup with sanitizer-backed native tests and negative controls. Keep other frameworks and USB modes on their existing drivers; update the watchdog file mock for the existing buffer API.
This commit is contained in:
mikecarper committed 2026-10-04 06:49:31 -07:00
1 parent 0a3187ac30
commit c1a80e95c7
8 files changed
+1329 -3

No files matched your search

+230
View File
@@ -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 <string.h>\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():
+50 -3
View File
@@ -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;
@@ -0,0 +1,2 @@
# Preserve the exact upstream source fingerprint, including its whitespace.
HWCDC.cpp -text whitespace=-blank-at-eol
+613
View File
@@ -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<rx_fifo_len; i++){
if(rx_queue == NULL || !xQueueSendFromISR(rx_queue, rx_data_buf+i, &xTaskWoken)){
break;
}
}
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);
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) {
portYIELD_FROM_ISR();
}
}
bool HWCDC::isPlugged(void)
{
return s_usb_serial_jtag_conn_status; //(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_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 <true> 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 */
+10
View File
@@ -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.
+46
View File
@@ -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
+3
View File
@@ -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:
+375
View File
@@ -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 <algorithm>
#include <atomic>
#include <thread>
#include <cassert>
#include <cstdint>
#include <cstring>
#include <deque>
#include <functional>
#include <mutex>
#include <vector>
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<bool> 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<std::vector<uint8_t>> data;std::vector<uint8_t> active;unsigned returns=0;} ring;
static Ring* tx_ring_buf=&ring;
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<uint8_t> staged;
static std::vector<unsigned> event_lengths;
static std::function<void()> 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::vector<uint8_t>tail(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<uint8_t> expected(78);for(unsigned n=0;n<expected.size();n++)expected[n]=n;
ring.data.push_back(std::vector<uint8_t>(expected.begin(),expected.begin()+64));
ring.data.push_back(std::vector<uint8_t>(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<uint8_t>{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<bool> 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<uint8_t>{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<uint8_t>{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 <helpers/UsbAsciiBinarySwitch.h>
#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<void()> 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<uint32_t> esp32_hwcdc_access_generation{0},esp32_hwcdc_allowed_generation{0};
static std::atomic<uint32_t> esp32_hwcdc_bus_reset_generation{0};
static mesh::UsbSelfResetBurstGuard esp32_hwcdc_self_reset_guard;
static std::atomic<bool> esp32_hwcdc_tx_kick_pending{false},esp32_hwcdc_tx_primed{false};
static portMUX_TYPE esp32_hwcdc_session_mux;
static std::atomic<size_t> 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<uint32_t> usb_logging_tx_progress{0};
static std::atomic<bool> 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()<esp32_hwcdc_access_generation.load());
assert(resetUsbCompanionTransport());assert(mesh_hwcdc_tx_allowed&&canAccessEsp32Hwcdc(nullptr));
}
'''.replace('@FUNCTIONS@',functions)
self.compile_run(prefix+extra)
# Negative control: a stale cleanup's unconditional resume violates the
# same-epoch assertion despite all software queues being emptied.
bad=functions.replace('if (esp32_hwcdc_cleanup_generation\n == esp32_hwcdc_access_generation.load(std::memory_order_acquire)) {',
'if (true) {',1)
self.assertNotEqual(bad,functions)
self.compile_run(prefix+extra.replace(functions,bad),expect_success=False)
def test_integration_all_three_empty_proofs_and_common_gate(self):
source=(ROOT/'src/helpers/UsbLogging.cpp').read_text()
self.assertEqual(source.count('meshEsp32HwcdcTxPending()'),4) # declaration plus three proofs
self.assertEqual(source.count('meshEsp32HwcdcSetTxAllowed(false);'),2)
self.assertEqual(source.count('meshEsp32HwcdcSetTxAllowed(true);'),1)
self.assertIn('if (!meshEsp32HwcdcDiscardTxStash()) return;',source)
for signature in ['static void serviceEsp32HwcdcTxKickExclusive(',
'static void purgeEsp32HwcdcQueues(', 'UsbLoggingObservation observeUsbLoggingTransport(']:
self.assertIn('meshEsp32HwcdcTxPending()',body(source,signature))
# Explicit new-hook branch and existing fallback keep the old API/layout
# outside pinned2.0.17 C3/S3 mode1. Link hooks are strong, never silent weak stubs.
self.assertIn('ESP_ARDUINO_VERSION_PATCH == 17',source)
self.assertIn('CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3',source)
self.assertNotIn('__attribute__((weak))',source[:source.index('namespace mesh {')])
self.assertIn('Serial.setTxTimeoutMs(5);',source)
self.assertIn('tx_timeout_ms / portTICK_PERIOD_MS',PATCHED)
self.assertNotIn('max_consec_timeouts',PATCHED)
if __name__=='__main__':unittest.main(verbosity=2)