Capture HWCDC startup resets in the pinned Arduino 3.3.11 driver
This commit is contained in:
1 parent
459d2a8cef
commit
4b95a288ca
9 files changed
+1038
-13
No files matched your search
@@ -283,6 +283,7 @@ jobs:
|
||||
python3 -B test/test_esp32_usb_session_fix.py
|
||||
python3 -B test/test_hwcdc_tx_backport.py -v
|
||||
python3 -B test/test_hwcdc_startup_session.py -v
|
||||
python3 -B test/test_hwcdc_3311_startup_session.py -v
|
||||
python3 -B test/test_esp32_hwcdc_recipes.py -v
|
||||
python3 -B test/test_hwcdc_role_transport.py -v
|
||||
python3 -B test/test_hwcdc_write_capacity.py -v
|
||||
|
||||
@@ -103,8 +103,9 @@ def replace_source(build_env, node):
|
||||
|
||||
|
||||
|
||||
# HWCDC fixes are deliberately limited to the installed Arduino2.0.17 C3/S3
|
||||
# native Serial/JTAG driver. Other versions retain their own framework driver.
|
||||
# HWCDC TX/PHY fixes are limited to the installed Arduino2.0.17 C3/S3 driver.
|
||||
# The separately pinned3.3.11 transform below only classifies startup resets.
|
||||
# Other framework versions retain their own driver unchanged.
|
||||
PINNED_HWCDC_SHA256 = "d0a8ca606c2729c8522a041113285dbf27033c22a5a6af8649a7305ffe84c449"
|
||||
PATCHED_HWCDC_SHA256 = "0969a94ae32edcfeeedb3daab19e5697e6c7647dc31578cdb0c3a0cb29e7c6d6"
|
||||
HWCDC_S3_PHY_GUARD = "#if CONFIG_IDF_TARGET_ESP32S3 && ARDUINO_USB_MODE && ARDUINO_USB_CDC_ON_BOOT\n"
|
||||
@@ -358,23 +359,61 @@ def hwcdc_primary_enabled(build_env):
|
||||
and defines.get("ARDUINO_USB_MODE") == "1")
|
||||
|
||||
|
||||
def pinned_hwcdc_framework(source):
|
||||
# Arduino3.3.11 already supplies its own TX suffix and interrupt fixes. Only
|
||||
# classify the startup BUS_RESET event at capture; leave that driver intact.
|
||||
PINNED_HWCDC_3311_SHA256 = "c5ed5fdd05aa0df9b74d390812643599223f96b459256718d0ad328aeaba6a8a"
|
||||
PATCHED_HWCDC_3311_SHA256 = "7b693bf5d72df97336360969ff429c67f3783d472123aeb79b3aabc8154b51ba"
|
||||
HWCDC_3311_INCLUDE = '#include "hal/usb_serial_jtag_ll.h"\n'
|
||||
HWCDC_3311_STARTUP_HOOK = 'extern "C" bool meshEsp32HwcdcShouldReportBusReset() __attribute__((weak));\n'
|
||||
HWCDC_3311_RESET_POST = (
|
||||
' arduino_hw_cdc_event_post(ARDUINO_HW_CDC_EVENTS, ARDUINO_HW_CDC_BUS_RESET_EVENT, '
|
||||
'&event, sizeof(arduino_hw_cdc_event_data_t), &xTaskWoken);')
|
||||
HWCDC_3311_RESET_CAPTURE = (
|
||||
' // Snapshot startup before the framework event task can delay delivery.\n'
|
||||
' if (!meshEsp32HwcdcShouldReportBusReset || meshEsp32HwcdcShouldReportBusReset()) {\n'
|
||||
+ ' ' + HWCDC_3311_RESET_POST + '\n'
|
||||
' }')
|
||||
|
||||
|
||||
def patched_hwcdc_startup_source(source):
|
||||
import hashlib
|
||||
source = source.replace("\r\n", "\n")
|
||||
digest = hashlib.sha256(source.encode()).hexdigest()
|
||||
if digest == PATCHED_HWCDC_3311_SHA256:
|
||||
return source
|
||||
if digest != PINNED_HWCDC_3311_SHA256:
|
||||
raise RuntimeError("ESP32 HWCDC startup fix: changed pinned3.3.11 source; review SDK update")
|
||||
return source.replace(HWCDC_3311_INCLUDE,
|
||||
HWCDC_3311_INCLUDE + HWCDC_3311_STARTUP_HOOK, 1).replace(
|
||||
HWCDC_3311_RESET_POST, HWCDC_3311_RESET_CAPTURE, 1)
|
||||
|
||||
|
||||
def hwcdc_framework_version(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"}
|
||||
return tuple(int(values[key]) for key in ("MAJOR", "MINOR", "PATCH"))
|
||||
|
||||
|
||||
def pinned_hwcdc_framework(source):
|
||||
return hwcdc_framework_version(source) == (2, 0, 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):
|
||||
version = hwcdc_framework_version(source)
|
||||
if version == (2, 0, 17):
|
||||
patcher = patched_hwcdc_source
|
||||
elif version == (3, 3, 11):
|
||||
patcher = patched_hwcdc_startup_source
|
||||
else:
|
||||
return node
|
||||
patched = patched_hwcdc_source(source.read_text(encoding="utf-8"))
|
||||
patched = patcher(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:
|
||||
|
||||
@@ -1799,7 +1799,7 @@ bool isUsbLoggingTransportRecoveryPending() {
|
||||
|
||||
} // namespace mesh
|
||||
|
||||
#if MESH_ESP32_HWCDC_SESSION_GUARD && MESH_HWCDC_PINNED_TX_BACKPORT
|
||||
#if MESH_ESP32_HWCDC_SESSION_GUARD
|
||||
// Capture startup at the actual hardware reset, not when the finite framework
|
||||
// event queue eventually delivers it. A delayed initial enumeration event
|
||||
// must not be reclassified as an active-session reset after loop() has begun.
|
||||
|
||||
Vendored
+743
@@ -0,0 +1,743 @@
|
||||
// Copyright 2015-2026 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 SOC_USB_SERIAL_JTAG_SUPPORTED
|
||||
|
||||
#include "Arduino.h" // defines ARDUINO_SERIAL_EVENT_TASK_STACK_SIZE and ARDUINO_SERIAL_EVENT_TASK_PRIORITY
|
||||
#include "esp32-hal.h"
|
||||
#include "esp32-hal-periman.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 "soc/io_mux_reg.h"
|
||||
#include "soc/usb_serial_jtag_struct.h"
|
||||
#include <string.h>
|
||||
#pragma GCC diagnostic ignored "-Wvolatile"
|
||||
#include "hal/usb_serial_jtag_ll.h"
|
||||
#pragma GCC diagnostic warning "-Wvolatile"
|
||||
#include "rom/ets_sys.h"
|
||||
|
||||
ESP_EVENT_DEFINE_BASE(ARDUINO_HW_CDC_EVENTS);
|
||||
|
||||
static RingbufHandle_t tx_ring_buf = NULL;
|
||||
static QueueHandle_t rx_queue = NULL;
|
||||
static uint8_t rx_data_buf[64] = {0};
|
||||
// tx_stash_buf / tx_stash_len hold the tail of a FIFO write the hardware
|
||||
// couldn't accept in one shot. They are written by the ISR (drain path and
|
||||
// BUS_RESET) and cleared by task-side reset paths (flushTXBuffer(NULL,...) and
|
||||
// setTxBufferSize()), so every access must go through hw_cdc_tx_mux below.
|
||||
static uint8_t tx_stash_buf[64] = {0};
|
||||
static volatile size_t tx_stash_len = 0;
|
||||
static intr_handle_t intr_handle = NULL;
|
||||
static SemaphoreHandle_t tx_lock = NULL;
|
||||
static volatile bool connected = false;
|
||||
|
||||
// Protects TX state shared between the producer task and the ISR (which may
|
||||
// run on the other core on dual-core SoCs). It covers:
|
||||
// - read-modify-write of the USB_SERIAL_JTAG INT_ENA register (without this
|
||||
// a producer's "enable IN_EMPTY" can race with the ISR's "disable
|
||||
// IN_EMPTY" in the empty-ring branch and get silently dropped, leaving
|
||||
// the link permanently stalled),
|
||||
// - all accesses to tx_stash_buf / tx_stash_len (without this a task-side
|
||||
// "clear stash" can be lost when the ISR's later write-back resurrects
|
||||
// stale state, and a torn tx_stash_len read could make the ISR
|
||||
// dereference an inconsistent stash).
|
||||
static portMUX_TYPE hw_cdc_tx_mux = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
// Context-safe variants: use portENTER_CRITICAL_SAFE which picks the ISR or
|
||||
// task critical-section macro at runtime based on xPortInIsrContext(). These
|
||||
// are reachable from ISR context via, e.g., cdc0_write_char() (installed as
|
||||
// ets_putc2 for the debug log path) -> HWCDC::isConnected() ->
|
||||
// isCDC_Connected() -> hw_cdc_enable_tx_intr(). Calling the plain task
|
||||
// portENTER_CRITICAL() from ISR asserts on ESP-IDF, so the helpers must
|
||||
// auto-select.
|
||||
static inline void hw_cdc_enable_tx_intr(void) {
|
||||
portENTER_CRITICAL_SAFE(&hw_cdc_tx_mux);
|
||||
usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY);
|
||||
portEXIT_CRITICAL_SAFE(&hw_cdc_tx_mux);
|
||||
}
|
||||
|
||||
// ISR-only fast paths: skip the runtime context check when we know statically
|
||||
// that we're already inside the hw_cdc_isr_handler. Functionally identical to
|
||||
// the _SAFE variants but a touch leaner in the interrupt hot path.
|
||||
static inline void hw_cdc_enable_tx_intr_from_isr(void) {
|
||||
portENTER_CRITICAL_ISR(&hw_cdc_tx_mux);
|
||||
usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY);
|
||||
portEXIT_CRITICAL_ISR(&hw_cdc_tx_mux);
|
||||
}
|
||||
|
||||
static inline void hw_cdc_disable_tx_intr_from_isr(void) {
|
||||
portENTER_CRITICAL_ISR(&hw_cdc_tx_mux);
|
||||
usb_serial_jtag_ll_disable_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY);
|
||||
portEXIT_CRITICAL_ISR(&hw_cdc_tx_mux);
|
||||
}
|
||||
|
||||
// Atomically drops any partial FIFO bytes the ISR may have stashed. Without
|
||||
// the lock, the clear could be lost if the ISR is mid-drain and writes a
|
||||
// fresh tx_stash_len after the task's store. Also _SAFE because callers like
|
||||
// flushTXBuffer() are reachable from cdc0_write_char() in ISR context.
|
||||
static inline void hw_cdc_clear_tx_stash(void) {
|
||||
portENTER_CRITICAL_SAFE(&hw_cdc_tx_mux);
|
||||
tx_stash_len = 0;
|
||||
portEXIT_CRITICAL_SAFE(&hw_cdc_tx_mux);
|
||||
}
|
||||
|
||||
// SOF in ISR causes problems for uploading firmware
|
||||
//static volatile unsigned long lastSOF_ms;
|
||||
//static volatile uint8_t SOF_TIMEOUT;
|
||||
|
||||
// timeout has no effect when USB CDC is unplugged
|
||||
static uint32_t tx_timeout_ms = 100;
|
||||
|
||||
static esp_event_loop_handle_t arduino_hw_cdc_event_loop_handle = NULL;
|
||||
|
||||
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 = ARDUINO_SERIAL_EVENT_TASK_PRIORITY,
|
||||
.task_stack_size = ARDUINO_SERIAL_EVENT_TASK_STACK_SIZE,
|
||||
.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) {
|
||||
// IN_EMPTY firing means the host just picked up our last IN packet, so the
|
||||
// CDC link is healthy regardless of any transient isPlugged() glitches
|
||||
// (isPlugged() is driven by a tick-hook SOF watchdog with several ms of
|
||||
// tolerance and can briefly flap to false even while USB is fine).
|
||||
connected = true;
|
||||
if (tx_ring_buf != NULL && usb_serial_jtag_ll_txfifo_writable() == 1) {
|
||||
size_t queued_size = 0;
|
||||
uint8_t *queued_buff = NULL;
|
||||
bool from_stash = false;
|
||||
uint32_t sent_size = 0;
|
||||
bool wrote_anything = false;
|
||||
|
||||
// Clear interrupt so we won't be called again until this transfer finishes.
|
||||
usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY);
|
||||
|
||||
// Hold hw_cdc_tx_mux across the entire stash read/write region so that a
|
||||
// task-side hw_cdc_clear_tx_stash() (from flushTXBuffer(NULL,...) or
|
||||
// setTxBufferSize()) cannot interleave with us deciding to drain the
|
||||
// stash, writing to the FIFO, and writing back the new tx_stash_len.
|
||||
// The lock is per-core re-entrant, so the nested calls in
|
||||
// hw_cdc_enable_tx_intr_from_isr() / hw_cdc_disable_tx_intr_from_isr()
|
||||
// are safe.
|
||||
portENTER_CRITICAL_ISR(&hw_cdc_tx_mux);
|
||||
|
||||
if (tx_stash_len) {
|
||||
queued_buff = tx_stash_buf;
|
||||
queued_size = tx_stash_len;
|
||||
from_stash = true;
|
||||
} else {
|
||||
queued_buff = (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();
|
||||
wrote_anything = true;
|
||||
|
||||
if (sent_size < queued_size) {
|
||||
tx_stash_len = queued_size - sent_size;
|
||||
memmove(tx_stash_buf, queued_buff + sent_size, tx_stash_len);
|
||||
} else {
|
||||
tx_stash_len = 0;
|
||||
}
|
||||
if (!from_stash) {
|
||||
vRingbufferReturnItemFromISR(tx_ring_buf, queued_buff, &xTaskWoken);
|
||||
}
|
||||
// Always re-arm IN_EMPTY so that the next host pickup brings us back to
|
||||
// drain whatever remains (stash + ring). Don't gate on `connected`: the
|
||||
// SOF-watchdog can lag the real state and we'd rather get one spurious
|
||||
// ISR than permanently lose the wakeup.
|
||||
hw_cdc_enable_tx_intr_from_isr();
|
||||
} else {
|
||||
// A full 64-byte packet needs an extra flush to send the terminating zero-length packet.
|
||||
usb_serial_jtag_ll_txfifo_flush();
|
||||
hw_cdc_disable_tx_intr_from_isr();
|
||||
|
||||
// Re-check the ring AFTER disabling the interrupt. If a producer
|
||||
// queued data between our ReceiveUpToFromISR() above and the disable,
|
||||
// its hw_cdc_enable_tx_intr() may have raced with our disable and got
|
||||
// overwritten. Re-arming here under the same spinlock guarantees we
|
||||
// don't lose that wakeup. tx_stash_len is also re-checked because the
|
||||
// task could have cleared it (e.g. via flushTXBuffer(NULL,...)) right
|
||||
// before we took the lock, while a previous ISR pass had left
|
||||
// residual data we no longer need to chase.
|
||||
UBaseType_t bytes_waiting = 0;
|
||||
vRingbufferGetInfo(tx_ring_buf, NULL, NULL, NULL, NULL, &bytes_waiting);
|
||||
if (bytes_waiting || tx_stash_len) {
|
||||
hw_cdc_enable_tx_intr_from_isr();
|
||||
}
|
||||
}
|
||||
|
||||
portEXIT_CRITICAL_ISR(&hw_cdc_tx_mux);
|
||||
|
||||
if (wrote_anything) {
|
||||
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);
|
||||
}
|
||||
} 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 available 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);
|
||||
portENTER_CRITICAL_ISR(&hw_cdc_tx_mux);
|
||||
tx_stash_len = 0;
|
||||
portEXIT_CRITICAL_ISR(&hw_cdc_tx_mux);
|
||||
connected = false;
|
||||
}
|
||||
|
||||
// SOF ISR is causing esptool to be unable to upload firmware to the board
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
// Moved to header file as inline function. Kept just as future reference.
|
||||
//inline bool HWCDC::isPlugged(void) {
|
||||
// SOF ISR is causing esptool to be unable to upload firmware to the board
|
||||
// Timer test for SOF seems to work when uploading firmware
|
||||
// return usb_serial_jtag_is_connected();//(lastSOF_ms + SOF_TIMEOUT) >= millis();
|
||||
//}
|
||||
bool HWCDC::isCDC_Connected() {
|
||||
// USB may be unplugged. Do NOT touch tx_stash_len here: a single transient
|
||||
// !isPlugged() reading (the SOF watchdog has a few-ms tolerance and is
|
||||
// known to flap even on a healthy link) would otherwise silently drop bytes
|
||||
// we already pulled out of the ring buffer into the stash.
|
||||
if (!isPlugged()) {
|
||||
connected = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (connected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Not yet (re)connected. Make sure the IN_EMPTY interrupt is armed and that
|
||||
// there is something for the host to clock out so the next IN token will
|
||||
// generate the interrupt that flips us back to connected = true. We arm on
|
||||
// every call (not just once) so that a stalled int_ena -- whether from a
|
||||
// producer/ISR race or from the ISR's empty-branch disable -- always gets
|
||||
// re-enabled, breaking what would otherwise be a permanent TX deadlock.
|
||||
hw_cdc_enable_tx_intr();
|
||||
usb_serial_jtag_ll_txfifo_flush();
|
||||
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()
|
||||
hw_cdc_clear_tx_stash();
|
||||
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) {
|
||||
// there is enough space, just add the data to the ring buffer
|
||||
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 into 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);
|
||||
hw_cdc_enable_tx_intr_from_isr();
|
||||
} else {
|
||||
xRingbufferSend(tx_ring_buf, (void *)(&c), 1, tx_timeout_ms / portTICK_PERIOD_MS);
|
||||
hw_cdc_enable_tx_intr();
|
||||
}
|
||||
usb_serial_jtag_ll_txfifo_flush();
|
||||
}
|
||||
|
||||
HWCDC::HWCDC() {
|
||||
// SOF in ISR causes problems for uploading firmware
|
||||
// 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);
|
||||
}
|
||||
|
||||
bool HWCDC::deinit(void *busptr) {
|
||||
// avoid any recursion issue with Peripheral Manager perimanSetPinBus() call
|
||||
static bool running = false;
|
||||
if (running) {
|
||||
return true;
|
||||
}
|
||||
running = true;
|
||||
// Setting USB D+ D- pins
|
||||
bool retCode = true;
|
||||
retCode &= perimanClearPinBus(USB_INT_PHY0_DM_GPIO_NUM);
|
||||
retCode &= perimanClearPinBus(USB_INT_PHY0_DP_GPIO_NUM);
|
||||
if (retCode) {
|
||||
// Force the host to re-enumerate (BUS_RESET)
|
||||
pinMode(USB_INT_PHY0_DM_GPIO_NUM, OUTPUT_OPEN_DRAIN);
|
||||
pinMode(USB_INT_PHY0_DP_GPIO_NUM, OUTPUT_OPEN_DRAIN);
|
||||
digitalWrite(USB_INT_PHY0_DM_GPIO_NUM, LOW);
|
||||
digitalWrite(USB_INT_PHY0_DP_GPIO_NUM, LOW);
|
||||
}
|
||||
// release the flag
|
||||
running = false;
|
||||
return retCode;
|
||||
}
|
||||
|
||||
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 :-(
|
||||
// But this is also causing terminal to hang, so they are disabled
|
||||
// deinit(NULL);
|
||||
// delay(10); // USB Host has to enumerate it again
|
||||
|
||||
// Peripheral Manager setting for USB D+ D- pins
|
||||
uint8_t pin = USB_INT_PHY0_DM_GPIO_NUM;
|
||||
if (perimanGetBusDeinit(ESP32_BUS_TYPE_USB_DM) == NULL) {
|
||||
perimanSetBusDeinit(ESP32_BUS_TYPE_USB_DM, HWCDC::deinit);
|
||||
}
|
||||
if (!perimanSetPinBus(pin, ESP32_BUS_TYPE_USB_DM, (void *)this, -1, -1)) {
|
||||
goto err;
|
||||
}
|
||||
pin = USB_INT_PHY0_DP_GPIO_NUM;
|
||||
if (perimanGetBusDeinit(ESP32_BUS_TYPE_USB_DP) == NULL) {
|
||||
perimanSetBusDeinit(ESP32_BUS_TYPE_USB_DP, HWCDC::deinit);
|
||||
}
|
||||
if (!perimanSetPinBus(pin, ESP32_BUS_TYPE_USB_DP, (void *)this, -1, -1)) {
|
||||
goto err;
|
||||
}
|
||||
// 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);
|
||||
// SOF ISR is causing esptool to be unable to upload firmware to the board
|
||||
// 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;
|
||||
}
|
||||
return;
|
||||
|
||||
err:
|
||||
log_e("Serial JTAG Pin %u can't be set into Peripheral Manager.", pin);
|
||||
end();
|
||||
}
|
||||
|
||||
void HWCDC::updateBaudRate(unsigned long baud) {
|
||||
log_w("USB Serial/JTAG baud rate is set by the USB standard; updateBaudRate(%lu) has no effect", baud);
|
||||
}
|
||||
|
||||
uint32_t HWCDC::baudRate() {
|
||||
log_w("USB Serial/JTAG does not have a configurable baud rate");
|
||||
return 115200;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
HWCDC::deinit(this);
|
||||
setDebugOutput(false);
|
||||
connected = false;
|
||||
}
|
||||
|
||||
void HWCDC::setTxTimeoutMs(uint32_t timeout) {
|
||||
tx_timeout_ms = timeout;
|
||||
}
|
||||
|
||||
/*
|
||||
* WRITING
|
||||
*/
|
||||
|
||||
size_t HWCDC::setTxBufferSize(size_t tx_queue_len) {
|
||||
hw_cdc_clear_tx_stash();
|
||||
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);
|
||||
// Kick the ISR after queuing: the not-connected path doesn't otherwise
|
||||
// re-arm IN_EMPTY, which means a transient isPlugged() glitch that flipped
|
||||
// `connected` to false would leave new data sitting in the ring with no
|
||||
// wakeup pending -- a permanent hang. Re-arming here lets the ISR drain
|
||||
// the ring and flip `connected` back to true on the next host pickup.
|
||||
hw_cdc_enable_tx_intr();
|
||||
} else {
|
||||
size_t to_send = size, so_far = 0;
|
||||
size_t ring_max = xRingbufferGetMaxItemSize(tx_ring_buf);
|
||||
if (ring_max == 0) {
|
||||
ring_max = size;
|
||||
}
|
||||
|
||||
// Two independent progress caps share the same per-iteration timeout so
|
||||
// the total bounded wait is roughly max_consec_timeouts * tx_timeout_ms:
|
||||
//
|
||||
// - "real disconnect" -> consec_timeouts >= cap AND !isPlugged().
|
||||
// Mark connected = false and route the remainder through the
|
||||
// not-connected FIFO-replacement policy.
|
||||
//
|
||||
// - "host backpressure" -> consec_timeouts >= cap, but isPlugged() is
|
||||
// still true (e.g. the OS read buffer filled because the host app
|
||||
// stopped reading). USB is still alive, so we do NOT touch
|
||||
// `connected`; we just return what we managed to enqueue. Anything
|
||||
// already in the ring stays queued for whenever the host resumes,
|
||||
// and the caller sees a short write (Arduino's Stream::write
|
||||
// contract) so it can decide whether to retry or drop.
|
||||
//
|
||||
// Without this second cap the loop could block forever whenever the
|
||||
// host stops consuming -- isPlugged() (SOF watchdog) keeps reporting
|
||||
// true, so the original `&& !isPlugged()` guard never fires.
|
||||
uint32_t consec_timeouts = 0;
|
||||
const uint32_t max_consec_timeouts = 20; // ~ max_consec_timeouts * tx_timeout_ms total wait
|
||||
bool gave_up_disconnect = false;
|
||||
|
||||
while (to_send) {
|
||||
size_t chunk = to_send > ring_max ? ring_max : to_send;
|
||||
|
||||
// Blocking send into the TX ring buffer. The ISR drains the buffer into
|
||||
// the USB Serial/JTAG FIFO whenever the host picks up data.
|
||||
if (xRingbufferSend(tx_ring_buf, (void *)(buffer + so_far), chunk, tx_timeout_ms / portTICK_PERIOD_MS) != pdTRUE) {
|
||||
// Kick the ISR in case it stopped draining the ring.
|
||||
hw_cdc_enable_tx_intr();
|
||||
consec_timeouts++;
|
||||
if (consec_timeouts >= max_consec_timeouts) {
|
||||
// Bounded-progress deadline reached. Decide why and bail out.
|
||||
if (!isPlugged()) {
|
||||
connected = false;
|
||||
gave_up_disconnect = true;
|
||||
}
|
||||
size = so_far;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
consec_timeouts = 0;
|
||||
so_far += chunk;
|
||||
to_send -= chunk;
|
||||
// Notify the ISR that new data is ready to be moved into the TX FIFO.
|
||||
hw_cdc_enable_tx_intr();
|
||||
}
|
||||
|
||||
// Only dump pending bytes through the not-connected FIFO policy if we
|
||||
// actually decided this is a real disconnection. On plain host
|
||||
// backpressure we leave already-queued bytes alone and just report the
|
||||
// short write.
|
||||
if (gave_up_disconnect && to_send) {
|
||||
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();
|
||||
hw_cdc_enable_tx_intr();
|
||||
}
|
||||
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--;
|
||||
}
|
||||
hw_cdc_enable_tx_intr();
|
||||
}
|
||||
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);
|
||||
}
|
||||
ets_install_putc1(NULL); // closes UART log output
|
||||
}
|
||||
|
||||
#if ARDUINO_USB_MODE && ARDUINO_USB_CDC_ON_BOOT // Hardware JTAG CDC selected
|
||||
// USBSerial is always available to be used
|
||||
HWCDC HWCDCSerial;
|
||||
#endif
|
||||
|
||||
#endif /* SOC_USB_SERIAL_JTAG_SUPPORTED */
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
The original Arduino-ESP32 3.3.11 HWCDC.cpp and version header are copied from
|
||||
the installed, pinned framework source package:
|
||||
framework-arduinoespressif32@src-533e72d076ed3cacc98127f709cb528f
|
||||
|
||||
Upstream repository: https://github.com/espressif/arduino-esp32
|
||||
The original source retains its Espressif copyright and Apache-2.0 header.
|
||||
|
||||
HWCDC.cpp SHA256:
|
||||
c5ed5fdd05aa0df9b74d390812643599223f96b459256718d0ad328aeaba6a8a
|
||||
|
||||
Tests use the real ISR and application reset helpers against host peripheral
|
||||
stubs. The 3.3.11 transform only adds the startup predicate declaration and
|
||||
wraps BUS_RESET event posting. Existing TX, FIFO, PHY, and pin-manager code is
|
||||
kept byte-for-byte. These tests do not substitute for physical C6 testing.
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
// 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 3
|
||||
/** Minor version number (x.X.x) */
|
||||
#define ESP_ARDUINO_VERSION_MINOR 3
|
||||
/** Patch version number (x.x.X) */
|
||||
#define ESP_ARDUINO_VERSION_PATCH 11
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
/**
|
||||
* Current ARDUINO version, as string
|
||||
*/
|
||||
#define df2xstr(s) #s
|
||||
#define df2str(s) df2xstr(s)
|
||||
#define ESP_ARDUINO_VERSION_STR df2str(ESP_ARDUINO_VERSION_MAJOR) "." df2str(ESP_ARDUINO_VERSION_MINOR) "." df2str(ESP_ARDUINO_VERSION_PATCH)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Qualify the capture-only startup hook against Arduino3.3.11's actual ISR."""
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import test_hwcdc_startup_session as startup
|
||||
import test_hwcdc_tx_backport as backport
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CORE = ROOT / "test/fixtures/hwcdc_3311"
|
||||
FIX = backport.module
|
||||
RAW = (CORE / "HWCDC.cpp").read_text()
|
||||
PATCHED = FIX.patched_hwcdc_startup_source(RAW)
|
||||
|
||||
|
||||
def harness(**controls):
|
||||
state, functions, owner = startup.application_sources(**controls)
|
||||
# Reuse only the peripheral boundary. All driver methods below come from
|
||||
# the original 3.3.11 implementation with its capture-only startup patch.
|
||||
prefix = backport.HARNESS.split("@SUPPORT@", 1)[0]
|
||||
prefix = prefix.replace("static std::vector<unsigned> event_lengths;",
|
||||
"static std::vector<unsigned> event_lengths;\n"
|
||||
"static std::deque<int> queued_events;")
|
||||
prefix = prefix.replace(
|
||||
" if(event==ARDUINO_HW_CDC_TX_EVENT)event_lengths.push_back(data->tx.len);",
|
||||
" queued_events.push_back(event);\n"
|
||||
" if(event==ARDUINO_HW_CDC_TX_EVENT)event_lengths.push_back(data->tx.len);")
|
||||
fields = "\n".join(re.search(r"^static [^\n]*\b" + name + r"[^\n]*;",
|
||||
PATCHED, re.MULTILINE)[0]
|
||||
for name in ("tx_stash_buf", "tx_stash_len", "hw_cdc_tx_mux"))
|
||||
driver = "\n".join(backport.body(PATCHED, signature) for signature in (
|
||||
"static inline void hw_cdc_enable_tx_intr(void)",
|
||||
"static inline void hw_cdc_enable_tx_intr_from_isr(void)",
|
||||
"static inline void hw_cdc_disable_tx_intr_from_isr(void)",
|
||||
"static inline void hw_cdc_clear_tx_stash(void)",
|
||||
"bool HWCDC::isCDC_Connected()",
|
||||
"static void hw_cdc_isr_handler(",
|
||||
))
|
||||
service = r'''
|
||||
static void pulse(unsigned limit=64) {
|
||||
fifo_limit=limit; intr_status=USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY;
|
||||
hw_cdc_isr_handler(nullptr);
|
||||
}
|
||||
static void reset() {
|
||||
ring=Ring{}; staged.clear(); event_lengths.clear(); queued_events.clear();
|
||||
tx_stash_len=0; irq_enabled=true; plugged=false; connected=false;
|
||||
before_empty_return=nullptr; flushes=0;
|
||||
}
|
||||
'''
|
||||
extra = startup.EXTRA.replace("#define MESH_HWCDC_PINNED_TX_BACKPORT 1",
|
||||
"#define MESH_HWCDC_PINNED_TX_BACKPORT 0")
|
||||
extra = extra.replace("mesh_hwcdc_tx_stash_len", "tx_stash_len")
|
||||
extra = extra.replace(" && mesh_hwcdc_fifo_pending", "")
|
||||
extra = extra.replace(" && !mesh_hwcdc_fifo_pending", "")
|
||||
extra = extra.replace(" && mesh_hwcdc_tx_allowed", "")
|
||||
extra = extra.replace("@STARTUP_STATE@", state).replace(
|
||||
"@FUNCTIONS@", functions).replace("@OWNER_RESET_HOOK@", owner)
|
||||
return prefix + FIX.HWCDC_3311_STARTUP_HOOK + fields + "\n" + driver + service + extra
|
||||
|
||||
|
||||
class Hwcdc3311StartupSessionTests(startup.HwcdcStartupSessionTests):
|
||||
# Run the exact same client/session assertions and negative controls with
|
||||
# the actual 3.3.11 ISR and non-backport application cleanup branches.
|
||||
def run_case(self, case, *, expect_success=True, **controls):
|
||||
with tempfile.TemporaryDirectory(prefix="hwcdc-3311-startup-") as directory:
|
||||
directory = Path(directory)
|
||||
cpp, binary = directory / "startup.cpp", directory / "startup"
|
||||
cpp.write_text(harness(**controls), encoding="ascii")
|
||||
compiled = subprocess.run([
|
||||
"g++", "-std=c++17", "-pthread", "-Wall", "-Wextra",
|
||||
"-Wno-unused-parameter", "-Wno-sign-compare",
|
||||
"-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
||||
"-fno-pie", "-no-pie", "-I" + str(ROOT / "src"),
|
||||
str(cpp), "-o", str(binary)], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
|
||||
result = subprocess.run([str(binary), case], capture_output=True,
|
||||
text=True, timeout=10)
|
||||
if expect_success:
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
else:
|
||||
self.assertNotEqual(result.returncode, 0, "Negative control did not fail")
|
||||
|
||||
def test_only_reset_event_posting_and_hook_declaration_change(self):
|
||||
self.assertEqual(hashlib.sha256(RAW.encode()).hexdigest(), FIX.PINNED_HWCDC_3311_SHA256)
|
||||
self.assertEqual(hashlib.sha256(PATCHED.encode()).hexdigest(), FIX.PATCHED_HWCDC_3311_SHA256)
|
||||
self.assertEqual(PATCHED.count(FIX.HWCDC_3311_STARTUP_HOOK), 1)
|
||||
self.assertEqual(PATCHED.count(FIX.HWCDC_3311_RESET_CAPTURE), 1)
|
||||
reverted = PATCHED.replace(FIX.HWCDC_3311_STARTUP_HOOK, "", 1).replace(
|
||||
FIX.HWCDC_3311_RESET_CAPTURE, FIX.HWCDC_3311_RESET_POST, 1)
|
||||
self.assertEqual(reverted, RAW, "Unrelated driver code changed")
|
||||
self.assertEqual(FIX.patched_hwcdc_startup_source(PATCHED), PATCHED)
|
||||
self.assertEqual(FIX.patched_hwcdc_startup_source(RAW.replace("\n", "\r\n")), PATCHED)
|
||||
for changed in (RAW + "\n", PATCHED + "\n",
|
||||
RAW.replace("tx_stash_len = 0;", "tx_stash_len = 1;", 1)):
|
||||
with self.subTest(changed=hashlib.sha256(changed.encode()).hexdigest()):
|
||||
with self.assertRaises(RuntimeError):
|
||||
FIX.patched_hwcdc_startup_source(changed)
|
||||
|
||||
def test_build_local_patch_has_exact_version_and_mode_scope(self):
|
||||
class Node:
|
||||
def __init__(self, path): self.path = path
|
||||
def srcnode(self): return self
|
||||
def get_abspath(self): return str(self.path)
|
||||
class Environment(dict):
|
||||
def __init__(self, definitions, build):
|
||||
super().__init__(CPPDEFINES=definitions)
|
||||
self.build, self.paths = build, []
|
||||
def subst(self, value):
|
||||
assert value == "$BUILD_DIR"
|
||||
return str(self.build)
|
||||
def File(self, path): return path
|
||||
def AppendUnique(self, **kwargs): self.paths += kwargs["CPPPATH"]
|
||||
with tempfile.TemporaryDirectory(prefix="hwcdc-3311-source-") as directory:
|
||||
directory = Path(directory)
|
||||
sdk = directory / "sdk"; sdk.mkdir()
|
||||
path, version = sdk / "HWCDC.cpp", sdk / "esp_arduino_version.h"
|
||||
path.write_text(RAW, encoding="ascii")
|
||||
version.write_text((CORE / "esp_arduino_version.h").read_text(), encoding="ascii")
|
||||
node = Node(path)
|
||||
for definitions in ([], [("ARDUINO_USB_CDC_ON_BOOT", 0), ("ARDUINO_USB_MODE", 1)],
|
||||
[("ARDUINO_USB_CDC_ON_BOOT", 1), ("ARDUINO_USB_MODE", 0)]):
|
||||
env = Environment(definitions, directory / "build")
|
||||
self.assertIs(FIX.replace_hwcdc_source(env, node), node)
|
||||
self.assertFalse(env.build.exists())
|
||||
env = Environment([("ARDUINO_USB_CDC_ON_BOOT", 1), ("ARDUINO_USB_MODE", 1)],
|
||||
directory / "build")
|
||||
result = Path(FIX.replace_hwcdc_source(env, node))
|
||||
self.assertEqual(result.read_text(), PATCHED)
|
||||
self.assertEqual(path.read_text(), RAW)
|
||||
self.assertEqual(env.paths, [str(sdk)])
|
||||
before = result.stat().st_mtime_ns
|
||||
FIX.replace_hwcdc_source(env, node)
|
||||
self.assertEqual(result.stat().st_mtime_ns, before)
|
||||
path.write_text(RAW + "\n", encoding="ascii")
|
||||
with self.assertRaises(RuntimeError):
|
||||
FIX.replace_hwcdc_source(env, node)
|
||||
# An unreviewed framework version keeps its existing driver.
|
||||
version.write_text("#define ESP_ARDUINO_VERSION_MAJOR 3\n"
|
||||
"#define ESP_ARDUINO_VERSION_MINOR 3\n"
|
||||
"#define ESP_ARDUINO_VERSION_PATCH 12\n", encoding="ascii")
|
||||
self.assertIs(FIX.replace_hwcdc_source(env, node), node)
|
||||
|
||||
def test_shared_predicate_is_exported_without_the_217_tx_backport(self):
|
||||
source = (ROOT / "src/helpers/UsbLogging.cpp").read_text()
|
||||
hook = source.index('extern "C" bool meshEsp32HwcdcShouldReportBusReset()')
|
||||
start = source.rfind("#if ", 0, hook)
|
||||
end = source.index("#endif", hook) + len("#endif")
|
||||
block = source[start:end]
|
||||
for guard in (0, 1):
|
||||
code = "#include <atomic>\n#include <cassert>\n"
|
||||
code += "#define MESH_ESP32_HWCDC_SESSION_GUARD " + str(guard) + "\n"
|
||||
code += "#define MESH_HWCDC_PINNED_TX_BACKPORT 0\n"
|
||||
code += "namespace mesh { static std::atomic<bool> esp32_hwcdc_startup_pending{true}; }\n"
|
||||
code += block + "\nint main() {\n#if MESH_ESP32_HWCDC_SESSION_GUARD\n"
|
||||
code += "assert(!meshEsp32HwcdcShouldReportBusReset());\n"
|
||||
code += "mesh::esp32_hwcdc_startup_pending.store(false);\n"
|
||||
code += "assert(meshEsp32HwcdcShouldReportBusReset());\n#endif\n}\n"
|
||||
with self.subTest(guard=guard), tempfile.TemporaryDirectory() as directory:
|
||||
cpp, binary = Path(directory) / "export.cpp", Path(directory) / "export"
|
||||
cpp.write_text(code, encoding="ascii")
|
||||
result = subprocess.run(["g++", "-std=c++17", str(cpp), "-o", str(binary)],
|
||||
capture_output=True, text=True, timeout=30)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -161,8 +161,8 @@ int main(int argc, char** argv) {
|
||||
'''
|
||||
|
||||
|
||||
def harness(*, remove_boot_gate=False, remove_owner_hook=False,
|
||||
boot_traffic_arms=False, keep_startup_forever=False):
|
||||
def application_sources(*, remove_boot_gate=False, remove_owner_hook=False,
|
||||
boot_traffic_arms=False, keep_startup_forever=False):
|
||||
source = (ROOT / "src/helpers/UsbLogging.cpp").read_text()
|
||||
signatures = (
|
||||
"static bool canAccessEsp32Hwcdc(void*)",
|
||||
@@ -182,7 +182,6 @@ def harness(*, remove_boot_gate=False, remove_owner_hook=False,
|
||||
state = re.search(r"^static std::atomic<bool> esp32_hwcdc_startup_pending.*;",
|
||||
source, re.MULTILINE)
|
||||
assert state is not None
|
||||
sdk = PATCHED
|
||||
if remove_boot_gate:
|
||||
functions = functions.replace(
|
||||
" if (esp32_hwcdc_startup_pending.load(std::memory_order_acquire)) return;\n", "")
|
||||
@@ -197,7 +196,12 @@ def harness(*, remove_boot_gate=False, remove_owner_hook=False,
|
||||
if keep_startup_forever:
|
||||
functions = functions.replace(
|
||||
" esp32_hwcdc_startup_pending.store(false, std::memory_order_release);", "")
|
||||
prefix = backport.BackportTest().harness(sdk).split("int main(){", 1)[0]
|
||||
return state[0], functions, owner
|
||||
|
||||
|
||||
def harness(**controls):
|
||||
state, functions, owner = application_sources(**controls)
|
||||
prefix = backport.BackportTest().harness(PATCHED).split("int main(){", 1)[0]
|
||||
prefix = prefix.replace("static std::vector<unsigned> event_lengths;",
|
||||
"static std::vector<unsigned> event_lengths;\n"
|
||||
"static std::deque<int> queued_events;")
|
||||
@@ -205,7 +209,7 @@ def harness(*, remove_boot_gate=False, remove_owner_hook=False,
|
||||
" if(event==ARDUINO_HW_CDC_TX_EVENT)event_lengths.push_back(data->tx.len);",
|
||||
" queued_events.push_back(event);\n"
|
||||
" if(event==ARDUINO_HW_CDC_TX_EVENT)event_lengths.push_back(data->tx.len);")
|
||||
return prefix + EXTRA.replace("@STARTUP_STATE@", state[0]).replace(
|
||||
return prefix + EXTRA.replace("@STARTUP_STATE@", state).replace(
|
||||
"@FUNCTIONS@", functions).replace("@OWNER_RESET_HOOK@", owner)
|
||||
|
||||
|
||||
|
||||
@@ -360,7 +360,7 @@ int main(){
|
||||
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')
|
||||
version.write_text('#define ESP_ARDUINO_VERSION_MAJOR 3\n#define ESP_ARDUINO_VERSION_MINOR 3\n#define ESP_ARDUINO_VERSION_PATCH 12\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)
|
||||
|
||||
Reference in new issue
Block a user