Guard startup resets in the pinned Arduino 3.1.3 HWCDC driver

This commit is contained in:
mikecarper committed 2026-10-05 13:08:55 -07:00
1 parent 4b95a288ca
commit 30464b022d
8 files changed
+829 -51

No files matched your search

+1
View File
@@ -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_313_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
+26 -19
View File
@@ -104,7 +104,7 @@ def replace_source(build_env, node):
# 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.
# The separately pinned 3.x transforms below only classify startup resets.
# Other framework versions retain their own driver unchanged.
PINNED_HWCDC_SHA256 = "d0a8ca606c2729c8522a041113285dbf27033c22a5a6af8649a7305ffe84c449"
PATCHED_HWCDC_SHA256 = "0969a94ae32edcfeeedb3daab19e5697e6c7647dc31578cdb0c3a0cb29e7c6d6"
@@ -359,33 +359,41 @@ def hwcdc_primary_enabled(build_env):
and defines.get("ARDUINO_USB_MODE") == "1")
# 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.
# Keep each reviewed 3.x driver's own TX/FIFO/PHY implementation intact. Only
# classify the startup BUS_RESET event at capture, under an exact source pin.
PINNED_HWCDC_313_SHA256 = "a09e99f1a37931269625019f51f1541a1185cce83f686d7aca4204ecdfea91dd"
PATCHED_HWCDC_313_SHA256 = "a2efe89e363908baf955e60751985edf1411fa930f2971e2d98b69fda3a1d19c"
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 = (
HWCDC_STARTUP_PINS = {
(3, 1, 3): (PINNED_HWCDC_313_SHA256, PATCHED_HWCDC_313_SHA256),
(3, 3, 11): (PINNED_HWCDC_3311_SHA256, PATCHED_HWCDC_3311_SHA256),
}
HWCDC_STARTUP_INCLUDE = '#include "hal/usb_serial_jtag_ll.h"\n'
HWCDC_STARTUP_HOOK = 'extern "C" bool meshEsp32HwcdcShouldReportBusReset() __attribute__((weak));\n'
HWCDC_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 = (
HWCDC_RESET_CAPTURE = (
' // Snapshot startup before the framework event task can delay delivery.\n'
' if (!meshEsp32HwcdcShouldReportBusReset || meshEsp32HwcdcShouldReportBusReset()) {\n'
+ ' ' + HWCDC_3311_RESET_POST + '\n'
+ ' ' + HWCDC_RESET_POST + '\n'
' }')
def patched_hwcdc_startup_source(source):
def patched_hwcdc_startup_source(source, *, version=(3, 3, 11)):
import hashlib
original_digest, patched_digest = HWCDC_STARTUP_PINS[version]
source = source.replace("\r\n", "\n")
digest = hashlib.sha256(source.encode()).hexdigest()
if digest == PATCHED_HWCDC_3311_SHA256:
if digest == patched_digest:
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)
if digest != original_digest:
label = ".".join(str(part) for part in version)
raise RuntimeError("ESP32 HWCDC startup fix: changed pinned" + label + " source; review SDK update")
return source.replace(HWCDC_STARTUP_INCLUDE,
HWCDC_STARTUP_INCLUDE + HWCDC_STARTUP_HOOK, 1).replace(
HWCDC_RESET_POST, HWCDC_RESET_CAPTURE, 1)
def hwcdc_framework_version(source):
@@ -408,12 +416,11 @@ def replace_hwcdc_source(build_env, node):
source = Path(node.srcnode().get_abspath())
version = hwcdc_framework_version(source)
if version == (2, 0, 17):
patcher = patched_hwcdc_source
elif version == (3, 3, 11):
patcher = patched_hwcdc_startup_source
patched = patched_hwcdc_source(source.read_text(encoding="utf-8"))
elif version in HWCDC_STARTUP_PINS:
patched = patched_hwcdc_startup_source(source.read_text(encoding="utf-8"), version=version)
else:
return node
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:
+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 SOC_USB_SERIAL_JTAG_SUPPORTED
#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"
#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};
static intr_handle_t intr_handle = NULL;
static SemaphoreHandle_t tx_lock = NULL;
static volatile bool connected = false;
// 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 = 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 available, 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 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);
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() {
static bool running = false;
// USB may be unplugged
if (!isPlugged()) {
connected = false;
running = false;
// SOF in ISR causes problems for uploading firmware
//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) {
// 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);
} 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() {
perimanSetBusDeinit(ESP32_BUS_TYPE_USB_DM, HWCDC::deinit);
perimanSetBusDeinit(ESP32_BUS_TYPE_USB_DP, HWCDC::deinit);
// 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 (!perimanSetPinBus(pin, ESP32_BUS_TYPE_USB_DM, (void *)this, -1, -1)) {
goto err;
}
pin = USB_INT_PHY0_DP_GPIO_NUM;
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::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) {
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 transmission 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 disconnected 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 && 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 */
+16
View File
@@ -0,0 +1,16 @@
The original Arduino-ESP32 3.1.3 HWCDC.cpp and version header are copied from
the framework source resolved by the Xiao_C6_repeater_ build:
framework-arduinoespressif32, ESP_ARDUINO_VERSION 3.1.3
Upstream repository: https://github.com/espressif/arduino-esp32
The original source retains its Espressif copyright and Apache-2.0 header.
HWCDC.cpp SHA256:
a09e99f1a37931269625019f51f1541a1185cce83f686d7aca4204ecdfea91dd
Tests use the real ISR and application reset helpers against host peripheral
stubs. The 3.1.3 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. Unlike 3.3.11, this driver has no partial-TX suffix stash or
TX interrupt spinlock; these tests retain those existing behavior limits.
These tests do not substitute for physical C6 testing.
+51
View File
@@ -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 1
/** Patch version number (x.x.X) */
#define ESP_ARDUINO_VERSION_PATCH 3
/**
* 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
+47 -6
View File
@@ -38,6 +38,20 @@ void tud_umount_cb(void){
'''
def installed_tested_tinyusb_cores(packages):
# This optional installed-source qualification covers the 2.0.17 layout
# used by the golden anchors. A C6 HWCDC build can replace the default
# package with 3.x; it must not silently change which TinyUSB SDK is tested.
result = []
for package in sorted(packages.glob("framework-arduinoespressif32*")):
core = package / "cores/esp32"
if not (core / "USBCDC.cpp").is_file() or not (core / "esp_arduino_version.h").is_file():
continue
if FIX.hwcdc_framework_version(core / "USBCDC.cpp") == (2, 0, 17):
result.append(core)
return result
class Esp32UsbSessionFixTests(unittest.TestCase):
def test_owner_hooks_precede_state_changes_and_rx_queue(self):
patched = FIX.patched_cdc_source(CDC)
@@ -92,17 +106,44 @@ class Esp32UsbSessionFixTests(unittest.TestCase):
[("ARDUINO_USB_CDC_ON_BOOT", 0), ("ARDUINO_USB_MODE", 0)]):
env["CPPDEFINES"] = definitions
self.assertIs(FIX.replace_source(env, node), node)
# Selecting a newer HWCDC SDK for optional qualification must
# never relax the actual mode0 build's changed-layout rejection.
env["CPPDEFINES"] = [("ARDUINO_USB_CDC_ON_BOOT", 1), ("ARDUINO_USB_MODE", 0)]
source.write_text(CDC.replace(FIX.RX, "void USBCDC::_onRX() {\n"), encoding="utf-8")
with self.assertRaises(RuntimeError):
FIX.replace_source(env, node)
def test_installed_sdk_selection_uses_exact_version_and_alternate_packages(self):
with tempfile.TemporaryDirectory(prefix="meshcore-tinyusb-sdk-selection-") as directory:
packages = Path(directory)
cores = {}
for name, version in (("framework-arduinoespressif32", (3, 1, 3)),
("framework-arduinoespressif32@src-old", (2, 0, 17)),
("framework-arduinoespressif32@src-new", (3, 3, 11))):
core = packages / name / "cores/esp32"
core.mkdir(parents=True)
(core / "USBCDC.cpp").write_text(CDC, encoding="ascii")
(core / "esp_arduino_version.h").write_text(
"\n".join("#define ESP_ARDUINO_VERSION_" + key + " " + str(value)
for key, value in zip(("MAJOR", "MINOR", "PATCH"), version)) + "\n",
encoding="ascii")
cores[version] = core
self.assertEqual(installed_tested_tinyusb_cores(packages), [cores[(2, 0, 17)]])
(cores[(2, 0, 17)] / "USBCDC.cpp").unlink()
self.assertEqual(installed_tested_tinyusb_cores(packages), [])
def test_installed_sdk_compatibility_when_available(self):
# Package-free CI still exercises the golden anchors above. Check an
# installed SDK on Windows and Unix, including a custom PIO core dir.
core = Path(os.environ.get("PLATFORMIO_CORE_DIR", str(Path.home() / ".platformio")))
package = core / "packages/framework-arduinoespressif32/cores/esp32"
if not (package / "USBCDC.cpp").is_file():
self.skipTest("installed Arduino-ESP32 SDK unavailable")
for name, patcher in (("USBCDC.cpp", FIX.patched_cdc_source), ("USB.cpp", FIX.patched_usb_source)):
original = (package / name).read_text(encoding="utf-8")
self.assertIn("meshEsp32TinyUsb", patcher(original))
packages = installed_tested_tinyusb_cores(core / "packages")
if not packages:
self.skipTest("installed Arduino-ESP32 2.0.17 TinyUSB SDK unavailable")
for package in packages:
for name, patcher in (("USBCDC.cpp", FIX.patched_cdc_source), ("USB.cpp", FIX.patched_usb_source)):
with self.subTest(package=package, source=name):
original = (package / name).read_text(encoding="utf-8")
self.assertIn("meshEsp32TinyUsb", patcher(original))
def test_all_esp32_profiles_inherit_hook(self):
config = (ROOT / "platformio.ini").read_text()
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env python3
"""Qualify the capture-only startup hook against Arduino3.1.3's actual ISR."""
from pathlib import Path
import unittest
import test_hwcdc_3311_startup_session as reviewed_v3
CORE = Path(__file__).resolve().parent / "fixtures/hwcdc_313"
RAW = (CORE / "HWCDC.cpp").read_text()
PATCHED = reviewed_v3.FIX.patched_hwcdc_startup_source(RAW, version=(3, 1, 3))
class Hwcdc313StartupSessionTests(reviewed_v3.Hwcdc3311StartupSessionTests):
# Same production owner gate/session assertions and negative controls,
# with 3.1.3's own ISR and application non-backport cleanup path.
core, raw, patched, version = CORE, RAW, PATCHED, (3, 1, 3)
if __name__ == "__main__":
unittest.main(verbosity=2)
+55 -26
View File
@@ -17,10 +17,10 @@ RAW = (CORE / "HWCDC.cpp").read_text()
PATCHED = FIX.patched_hwcdc_startup_source(RAW)
def harness(**controls):
def harness(patched=PATCHED, **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.
# the reviewed 3.x 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"
@@ -29,17 +29,24 @@ def harness(**controls):
" 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);")
has_stash = "static volatile size_t tx_stash_len" in patched
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 (
patched, re.MULTILINE)[0]
for name in ("tx_stash_buf", "tx_stash_len", "hw_cdc_tx_mux")) if has_stash else ""
signatures = (
"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(",
))
)
if not has_stash:
# The 3.1.3 driver has no TX stash/mux helpers and directly updates
# interrupts. Do not invent the newer driver's locking guarantees.
signatures = signatures[-2:]
prefix = prefix.replace("assert(critical_depth);", "")
driver = "\n".join(backport.body(patched, signature) for signature in signatures)
service = r'''
static void pulse(unsigned limit=64) {
fifo_limit=limit; intr_status=USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY;
@@ -47,29 +54,41 @@ static void pulse(unsigned limit=64) {
}
static void reset() {
ring=Ring{}; staged.clear(); event_lengths.clear(); queued_events.clear();
tx_stash_len=0; irq_enabled=true; plugged=false; connected=false;
@RESET_STASH@ irq_enabled=true; plugged=false; connected=false;
before_empty_return=nullptr; flushes=0;
}
'''
service = service.replace("@RESET_STASH@", "tx_stash_len=0;" if has_stash else "")
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", "")
if not has_stash:
# 3.1.3 does not retain partially accepted FIFO suffixes. Exercise its
# real boot TX event with a fully accepted packet, then enumeration.
extra = extra.replace(
"// Real ISR stages a partial boot diagnostic before the host enumerates.",
"// Real 3.1.3 ISR stages a boot diagnostic before the host enumerates.")
extra = extra.replace("pulse(4); assert(tx_stash_len != 0);",
"pulse(); assert(staged.size() == 11 && ring.data.empty());")
extra = extra.replace(" assert(tx_stash_len == 0);\n", "")
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
return prefix + FIX.HWCDC_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.
core, raw, patched, version = CORE, RAW, PATCHED, (3, 3, 11)
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")
cpp.write_text(harness(self.patched, **controls), encoding="ascii")
compiled = subprocess.run([
"g++", "-std=c++17", "-pthread", "-Wall", "-Wextra",
"-Wno-unused-parameter", "-Wno-sign-compare",
@@ -85,20 +104,22 @@ class Hwcdc3311StartupSessionTests(startup.HwcdcStartupSessionTests):
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)):
raw_pin, patched_pin = FIX.HWCDC_STARTUP_PINS[self.version]
self.assertEqual(hashlib.sha256(self.raw.encode()).hexdigest(), raw_pin)
self.assertEqual(hashlib.sha256(self.patched.encode()).hexdigest(), patched_pin)
self.assertEqual(self.patched.count(FIX.HWCDC_STARTUP_HOOK), 1)
self.assertEqual(self.patched.count(FIX.HWCDC_RESET_CAPTURE), 1)
reverted = self.patched.replace(FIX.HWCDC_STARTUP_HOOK, "", 1).replace(
FIX.HWCDC_RESET_CAPTURE, FIX.HWCDC_RESET_POST, 1)
self.assertEqual(reverted, self.raw, "Unrelated driver code changed")
patch = lambda source: FIX.patched_hwcdc_startup_source(source, version=self.version)
self.assertEqual(patch(self.patched), self.patched)
self.assertEqual(patch(self.raw.replace("\n", "\r\n")), self.patched)
for changed in (self.raw + "\n", self.patched + "\n",
self.raw.replace("connected = false;", "connected = true;", 1)):
with self.subTest(changed=hashlib.sha256(changed.encode()).hexdigest()):
with self.assertRaises(RuntimeError):
FIX.patched_hwcdc_startup_source(changed)
patch(changed)
def test_build_local_patch_has_exact_version_and_mode_scope(self):
class Node:
@@ -118,8 +139,8 @@ class Hwcdc3311StartupSessionTests(startup.HwcdcStartupSessionTests):
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")
path.write_text(self.raw, encoding="ascii")
version.write_text((self.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)]):
@@ -129,13 +150,21 @@ class Hwcdc3311StartupSessionTests(startup.HwcdcStartupSessionTests):
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(result.read_text(), self.patched)
self.assertEqual(path.read_text(), self.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")
path.write_text(self.raw + "\n", encoding="ascii")
with self.assertRaises(RuntimeError):
FIX.replace_hwcdc_source(env, node)
# Reviewed versions cannot borrow another version's source pin.
path.write_text(self.raw, encoding="ascii")
other = (3, 1, 3) if self.version == (3, 3, 11) else (3, 3, 11)
version.write_text("\n".join("#define ESP_ARDUINO_VERSION_" + name + " " + str(value)
for name, value in zip(("MAJOR", "MINOR", "PATCH"), other)) + "\n",
encoding="ascii")
with self.assertRaises(RuntimeError):
FIX.replace_hwcdc_source(env, node)
# An unreviewed framework version keeps its existing driver.