From bc208ade266bdb9e2ab3855bb3a10efa446e5867 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Fri, 28 Aug 2026 10:46:46 +0200 Subject: [PATCH] detach USB device stack before reboot (esp_restart_noos leaves the PHY attached) --- zephcore/adapters/board/ZephyrBoard.cpp | 33 +++++++++++++++++++++++++ zephcore/adapters/usb/ZephyrUSBCDC.cpp | 24 ++++++++++++++++++ zephcore/adapters/usb/ZephyrUSBCDC.h | 7 ++++++ 3 files changed, 64 insertions(+) diff --git a/zephcore/adapters/board/ZephyrBoard.cpp b/zephcore/adapters/board/ZephyrBoard.cpp index 1695635..be208d9 100644 --- a/zephcore/adapters/board/ZephyrBoard.cpp +++ b/zephcore/adapters/board/ZephyrBoard.cpp @@ -61,6 +61,26 @@ #define ESP32_FORCE_DOWNLOAD_BOOT 1 #endif +/* Detach the USB device stack before a reset so the host sees a real unplug — + * see zephcore_usbd_detach(). Matters most on the ESP32-S3 USB companion, + * whose OTG PHY survives a soft reset with D+ still pulled up. + * + * The condition below must be EXACTLY the one CMakeLists.txt uses to compile + * adapters/usb/ZephyrUSBCDC.cpp, or this call site compiles against an + * implementation that is never linked (findings #22 — that failure mode has + * already cost this repo four separate undefined-reference bugs). The + * repeater/observer/room-server branches use the inner half alone; the + * companion branch wraps it in (LOG || COMPANION_USB || COMPANION_SERIAL), and + * ZEPHCORE_COMPANION is the compile definition that identifies that branch. */ +#if !defined(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT) && \ + (defined(CONFIG_USB_CDC_ACM) || defined(CONFIG_USBD_CDC_ACM_CLASS)) +#if !defined(ZEPHCORE_COMPANION) || defined(CONFIG_LOG) || \ + defined(CONFIG_ZEPHCORE_COMPANION_USB) || defined(CONFIG_ZEPHCORE_COMPANION_SERIAL) +#include +#define ZEPHCORE_USBD_DETACH 1 +#endif +#endif + /* LoRa TX activity LED (optional — defined per-board via DT alias) */ #if DT_NODE_EXISTS(DT_ALIAS(lora_tx_led)) static const struct gpio_dt_spec tx_led = @@ -295,6 +315,11 @@ void ZephyrBoard::onAfterTransmit() void ZephyrBoard::reboot() { k_msleep(50); /* Let UART/USB flush */ +#ifdef ZEPHCORE_USBD_DETACH + /* USB device stack (CDC ACM): detach so the host sees an unplug. */ + zephcore_usbd_detach(); + k_msleep(100); /* Hold SE0 long enough for host disconnect debounce */ +#endif #ifdef ESP32_USB_SERIAL_DETACH /* Drop the D+ pull-up so the host sees a clean USB disconnect before the * soft reset (GH #43 — wedged serial port on macOS). */ @@ -317,6 +342,14 @@ void ZephyrBoard::rebootToBootloader() REG_SET_BIT(RTC_CNTL_OPTION1_REG, RTC_CNTL_FORCE_DOWNLOAD_BOOT); #endif k_msleep(50); /* Let UART/USB flush */ +#ifdef ZEPHCORE_USBD_DETACH + /* Detach the CDC ACM device — same reason as reboot(), and doubly so here: + * the ESP32-S3 comes back in ROM download mode, where the port belongs to + * USB-Serial-JTAG. The host must see the old device leave the bus first or + * it will not enumerate the new one. */ + zephcore_usbd_detach(); + k_msleep(100); +#endif #ifdef ESP32_USB_SERIAL_DETACH /* Clean USB disconnect before the soft reset — same reason as reboot(). */ usb_serial_jtag_ll_phy_enable_pad(false); diff --git a/zephcore/adapters/usb/ZephyrUSBCDC.cpp b/zephcore/adapters/usb/ZephyrUSBCDC.cpp index 41377ea..b5eafa2 100644 --- a/zephcore/adapters/usb/ZephyrUSBCDC.cpp +++ b/zephcore/adapters/usb/ZephyrUSBCDC.cpp @@ -267,3 +267,27 @@ extern "C" void zephcore_usbd_set_dtr_cb(zephcore_usbd_cdc_dtr_cb_t cb) { s_dtr_cb = cb; } + +extern "C" void zephcore_usbd_detach(void) +{ + if (!s_initialized) { + return; + } + + /* usbd_disable() -> udc_disable() drops the D+ pull-up, which is the only + * thing that makes the host see an unplug. A soft reset does not: on the + * ESP32-S3, esp_restart_noos() resets WiFi/BT, timers, SPI, UART, DMA and + * crypto but nothing USB, so the PHY pad stays enabled across the reset + * and the host keeps talking to an endpoint the firmware has abandoned. + * (Same root cause as GH #43, which hit the USB-Serial-JTAG controller; + * this covers the USB OTG one that the CDC companion transport uses.) + * + * Safe from the usbd message callback: CONFIG_USBD_MSG_DEFERRED_MODE is + * on by default, so that callback runs on the system workqueue rather + * than in the device stack context, and usbd_disable() is not re-entered + * from the thread it stops. + */ + (void)usbd_disable(&zephcore_usbd); + s_initialized = false; + s_dtr_active = false; +} diff --git a/zephcore/adapters/usb/ZephyrUSBCDC.h b/zephcore/adapters/usb/ZephyrUSBCDC.h index 625ab4c..dc7e732 100644 --- a/zephcore/adapters/usb/ZephyrUSBCDC.h +++ b/zephcore/adapters/usb/ZephyrUSBCDC.h @@ -40,6 +40,13 @@ bool zephcore_usbd_is_dtr_active(void); * Companion uses this to reset its RX state and flip active_iface on drop. */ void zephcore_usbd_set_dtr_cb(zephcore_usbd_cdc_dtr_cb_t cb); +/* Detach from the USB bus (drop the D+ pull-up) so the host sees an unplug. + * Call before a reset: a soft reset leaves the PHY attached on some SoCs, and + * the host then keeps a stale endpoint open across the reboot. No-op if the + * stack was never initialized; after this, zephcore_usbd_init() would have to + * run again to come back. */ +void zephcore_usbd_detach(void); + #ifdef __cplusplus } #endif