From 5372f1d8a243a36a89539f82944d627d0ba6a4fc Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Fri, 28 Aug 2026 10:32:49 +0200 Subject: [PATCH] auto-enable esp32s3 USB CDC companion in CMakeLists; board.overlay first in overlay order; esp32s3 download-mode reboot --- docs/Repeater_CLI_commands.md | 2 +- zephcore/CMakeLists.txt | 117 ++++++++++++++---- zephcore/Kconfig | 6 +- zephcore/adapters/board/ZephyrBoard.cpp | 38 ++++++ .../boards/common/esp32s3_console_uart0.dtsi | 14 ++- zephcore/boards/common/esp32s3_usb.conf | 11 +- zephcore/boards/common/esp32s3_usb.overlay | 9 +- .../esp32/heltec_wifi_lora32_v4/board.overlay | 6 +- .../heltec_wifi_lora32_v43/board.overlay | 6 +- zephcore/helpers/CommonCLI.cpp | 9 +- 10 files changed, 182 insertions(+), 36 deletions(-) diff --git a/docs/Repeater_CLI_commands.md b/docs/Repeater_CLI_commands.md index 8f6362a..c391105 100644 --- a/docs/Repeater_CLI_commands.md +++ b/docs/Repeater_CLI_commands.md @@ -27,7 +27,7 @@ All commands are sent over USB serial (CDC-ACM). Commands sent remotely over the | `ver` | Firmware version and build date | | `board` | Board manufacturer name | | `reboot` | Reboot immediately | -| `start dfu` | Reboot into UF2 bootloader for drag-and-drop firmware update | +| `start dfu` | nRF52: reboot into the UF2 bootloader for drag-and-drop update. ESP32-S3: reboot into the ROM download mode so esptool can reach the chip | | `start ota` | ESP32: start WiFi AP + HTTP OTA server. nRF52: reboot into BLE OTA DFU mode | | `stop ota` | Stop WiFi OTA server (ESP32 only) | | `clkreboot` | Set clock to a fixed reference time (15 May 2024 8:50pm UTC) then reboot | diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index 3385463..86c2b6a 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -271,12 +271,21 @@ function(zephcore_auto_pair_overlay CONF_FILE) endif() string(REPLACE ".conf" ".overlay" OVERLAY_FILE "${CONF_FILE}") if(EXISTS "${OVERLAY_FILE}") + # Append once, keeping the FIRST position. The user-extras pairing loop + # below walks the whole combined EXTRA_CONF_FILE, so it re-pairs every + # conf the auto chain already paired -- board.overlay used to land in + # the list three times that way. Position is precedence in devicetree, + # so silently re-appending an overlay also moved it later and let it + # override things it was never meant to. if(EXTRA_DTC_OVERLAY_FILE) - set(EXTRA_DTC_OVERLAY_FILE "${EXTRA_DTC_OVERLAY_FILE};${OVERLAY_FILE}" PARENT_SCOPE) + if(NOT "${OVERLAY_FILE}" IN_LIST EXTRA_DTC_OVERLAY_FILE) + set(EXTRA_DTC_OVERLAY_FILE "${EXTRA_DTC_OVERLAY_FILE};${OVERLAY_FILE}" PARENT_SCOPE) + message(STATUS " [auto-pair] ${CONF_FILE} → ${OVERLAY_FILE}") + endif() else() set(EXTRA_DTC_OVERLAY_FILE "${OVERLAY_FILE}" PARENT_SCOPE) + message(STATUS " [auto-pair] ${CONF_FILE} → ${OVERLAY_FILE}") endif() - message(STATUS " [auto-pair] ${CONF_FILE} → ${OVERLAY_FILE}") endif() endfunction() @@ -364,6 +373,45 @@ if(NOT ZEPHCORE_BOARD_CONF AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOAR set(ZEPHCORE_BOARD_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.conf") endif() +# ========== DTC Overlay Precedence ========== +# board.overlay goes in FIRST, so everything applied after it can override it. +# +# Devicetree is last-writer-wins per property, and this list is that order. The +# board overlay is the board's *baseline*; the overlays paired with the confs +# that follow (repeater/pm_esp32/esp32s3_usb/no_display/...) are deliberate +# per-role deviations from it, and the conf chain already documents that same +# precedence ("user extras last so they can override"). Until 2026-08-28 the +# overlay order was the exact opposite of the conf order, with two live +# consequences: +# - esp32s3_usb.overlay could not move the console off USB Serial/JTAG on the +# boards that pick a console in board.overlay (heltec V4/V43, wireless +# tracker v2) -- the reroute was overridden and the console pointed at a +# peripheral that no longer owned the pins. +# - pm_esp32.overlay could not hold the SX1262 NSS across light sleep on +# heltec_wireless_tracker, whose board.overlay re-declares spi2 cs-gpios +# without ESP32_GPIO_SLEEP_HOLD_EN -- i.e. the exact floating-NSS failure +# that overlay exists to prevent. +# partitions.overlay is appended after everything (see below) and stays there: +# it must also be fed verbatim to the MCUboot image. +set(ZEPHCORE_BOARD_OVERLAY "") +file(GLOB_RECURSE BOARD_OVERLAY_CANDIDATES "${CMAKE_CURRENT_SOURCE_DIR}/boards/*/${BOARD_BASE}/board.overlay") +if(BOARD_OVERLAY_CANDIDATES) + list(GET BOARD_OVERLAY_CANDIDATES 0 ZEPHCORE_BOARD_OVERLAY) +endif() +# Fallback: direct path without vendor subdirectory +if(NOT ZEPHCORE_BOARD_OVERLAY AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.overlay") + set(ZEPHCORE_BOARD_OVERLAY "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.overlay") +endif() + +# Append to EXTRA_DTC_OVERLAY_FILE (preserves any user-specified extras) +if(ZEPHCORE_BOARD_OVERLAY AND EXISTS ${ZEPHCORE_BOARD_OVERLAY}) + if(EXTRA_DTC_OVERLAY_FILE) + set(EXTRA_DTC_OVERLAY_FILE "${EXTRA_DTC_OVERLAY_FILE};${ZEPHCORE_BOARD_OVERLAY}" CACHE STRING "" FORCE) + else() + set(EXTRA_DTC_OVERLAY_FILE "${ZEPHCORE_BOARD_OVERLAY}" CACHE STRING "" FORCE) + endif() +endif() + # Build the EXTRA_CONF_FILE list (append to any user-provided extras) # Auto-pair each .conf with a same-named .overlay if it exists. set(ZEPHCORE_CONF_FILES "") @@ -466,6 +514,51 @@ if(ZEPHCORE_PLATFORM_CONF MATCHES "esp32_common" AND EXTRA_CONF_FILE MATCHES "re endif() endif() +# Auto-include the ESP32-S3 USB CDC-ACM companion transport. +# +# The policy lives here rather than in build.sh so that a locally built +# companion is the same firmware as the published release. The ESP32 release +# flow already diverges from a plain build in its *layout* (sysbuild/MCUboot); +# letting it diverge in which *transports* the app speaks as well would make +# user bug reports unreproducible from source. +# +# Detection is the board's own devicetree, not a name list: a board that +# includes esp32s3_usb_otg.dtsi has declared the cdc_acm_uart node, which is +# exactly the precondition the USB companion call sites are compiled behind +# (see ZEPHCORE_USB_STACK in src/main_companion.cpp). Boards without it are +# skipped automatically -- Heltec V3 (USB-C via CP2102, uses +# serial_companion.conf), Wireless Tracker V1, and every C3/C6/classic-ESP32 +# part, which has no DWC2 controller at all. +# +# COMPANION ROLE ONLY. esp32s3_usb.conf sets CONFIG_ZEPHCORE_COMPANION_USB=y, +# which carries no role dependency of its own, and its paired overlay moves the +# console to uart0. In a repeater/observer/room-server build that would both +# compile the companion USB stack and strand the serial CLI on GPIO43/44. +# serial_companion.conf is excluded for a different reason: COMPANION_SERIAL +# `depends on !COMPANION_USB`, so adding this would silently disable it. +# +# DEBUG BUILDS ARE EXCLUDED. ESP32 has no RTT (debug_esp32.conf routes logs to +# the console backend) and this conf hands that USB port to the binary +# companion protocol, so an auto-enabled debug build would have nowhere to log. +# Pass both confs explicitly to debug the USB transport itself; logs then need +# a UART adapter on GPIO43/44. +if(ZEPHCORE_PLATFORM_CONF MATCHES "esp32_common" AND ZEPHCORE_BOARD_CONF + AND NOT EXTRA_CONF_FILE MATCHES "repeater|observer|room_server|serial_companion|esp32s3_usb|debug") + get_filename_component(ZEPHCORE_BOARD_DIR "${ZEPHCORE_BOARD_CONF}" DIRECTORY) + if(EXISTS "${ZEPHCORE_BOARD_DIR}/board.overlay") + file(READ "${ZEPHCORE_BOARD_DIR}/board.overlay" ZEPHCORE_BOARD_OVERLAY_TEXT) + if(ZEPHCORE_BOARD_OVERLAY_TEXT MATCHES "esp32s3_usb_otg\\.dtsi") + set(ZEPHCORE_USB_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/esp32s3_usb.conf") + if(EXISTS ${ZEPHCORE_USB_CONF}) + list(APPEND ZEPHCORE_CONF_FILES ${ZEPHCORE_USB_CONF}) + zephcore_auto_pair_overlay("${ZEPHCORE_USB_CONF}") + message(STATUS " USB CDC companion: auto-enabled (board declares USB OTG CDC-ACM)") + endif() + endif() + unset(ZEPHCORE_BOARD_OVERLAY_TEXT) + endif() +endif() + # Append to EXTRA_CONF_FILE — auto-generated configs first, user extras last. # User-specified extras (repeater.conf, debug.conf) MUST come after the auto chain # so they can override settings (e.g. CONFIG_BT=n in repeater.conf). @@ -499,26 +592,6 @@ if(EXTRA_CONF_FILE) endif() # ========== DTC Overlay Hierarchy ========== -# Search for board.overlay in possible locations (same as board.conf) -set(ZEPHCORE_BOARD_OVERLAY "") -file(GLOB_RECURSE BOARD_OVERLAY_CANDIDATES "${CMAKE_CURRENT_SOURCE_DIR}/boards/*/${BOARD_BASE}/board.overlay") -if(BOARD_OVERLAY_CANDIDATES) - list(GET BOARD_OVERLAY_CANDIDATES 0 ZEPHCORE_BOARD_OVERLAY) -endif() -# Fallback: direct path without vendor subdirectory -if(NOT ZEPHCORE_BOARD_OVERLAY AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.overlay") - set(ZEPHCORE_BOARD_OVERLAY "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.overlay") -endif() - -# Append to EXTRA_DTC_OVERLAY_FILE (preserves any user-specified extras) -if(ZEPHCORE_BOARD_OVERLAY AND EXISTS ${ZEPHCORE_BOARD_OVERLAY}) - if(EXTRA_DTC_OVERLAY_FILE) - set(EXTRA_DTC_OVERLAY_FILE "${EXTRA_DTC_OVERLAY_FILE};${ZEPHCORE_BOARD_OVERLAY}" CACHE STRING "" FORCE) - else() - set(EXTRA_DTC_OVERLAY_FILE "${ZEPHCORE_BOARD_OVERLAY}" CACHE STRING "" FORCE) - endif() -endif() - # Flash partition layout lives in a separate boards/<...>//partitions.overlay # so the SAME file can be fed to the MCUboot image too (see sysbuild/CMakeLists.txt). # The app and MCUboot MUST share one flash map — otherwise MCUboot stages/looks for diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 469c4be..27c45fa 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -134,7 +134,11 @@ config ZEPHCORE_COMPANION_USB depends forces this to n automatically — no conf change needed. For ESP32-S3 boards, USB_DEVICE_STACK_NEXT must be enabled first via - boards/common/esp32s3_usb.conf before this becomes active. + boards/common/esp32s3_usb.conf before this becomes active. CMakeLists.txt + adds that conf automatically to companion builds on the S3 boards whose + board.overlay declares the USB OTG CDC-ACM node, so it is on by default + there too — but not on repeater/observer/room-server or debug builds, + which keep the port for the console. config ZEPHCORE_COMPANION_SERIAL bool "Enable plain-UART companion transport" diff --git a/zephcore/adapters/board/ZephyrBoard.cpp b/zephcore/adapters/board/ZephyrBoard.cpp index 870b840..1695635 100644 --- a/zephcore/adapters/board/ZephyrBoard.cpp +++ b/zephcore/adapters/board/ZephyrBoard.cpp @@ -33,6 +33,34 @@ #define ESP32_USB_SERIAL_DETACH 1 #endif +/* ESP32-S3: reboot into the ROM download mode rather than back into the app. + * + * The USB CDC companion transport (boards/common/esp32s3_usb.conf) hands the + * shared D+/D- pads to USB OTG and disables USB-Serial-JTAG. USJ is exactly + * what esptool drives to put the chip into download mode, so once our CDC-ACM + * device owns the port esptool's auto-reset is inert: DTR/RTS land on a Zephyr + * CDC endpoint that is not wired to EN/BOOT, and the only way back into the + * bootloader is the physical BOOT button. + * + * FORCE_DOWNLOAD_BOOT lives in the RTC domain, which a software system reset + * does not clear (only a power-on reset does), so setting it and rebooting + * brings the ROM up in download mode with USB-Serial-JTAG back on the pads. + * That gives both `start dfu` and the Arduino-style 1200-baud touch a way to + * hand the port back to esptool / esptool-js / the web configurator. + * + * Same register and sequence Arduino-ESP32 uses in usb_persist_restart() + * (RESTART_BOOTLOADER). OPTION1 carries no other bits on this SoC, but set the + * bit rather than writing the word so it stays correct if that changes. Scoped + * to the S3 deliberately: it is the only part we ship whose USB port can be + * taken away from the ROM this way (C3/C6 use a different LP_AON register, and + * classic ESP32 has no native USB — it always flashes through its UART bridge). + */ +#if defined(CONFIG_SOC_SERIES_ESP32S3) +#include +#include +#define ESP32_FORCE_DOWNLOAD_BOOT 1 +#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 = @@ -282,8 +310,18 @@ void ZephyrBoard::rebootToBootloader() /* Write magic value to GPREGRET0 - enter UF2 bootloader mode. * UF2 supports both drag-and-drop (.uf2) and serial DFU (nrfutil). */ nrf_power_gpregret_set(NRF_POWER, 0, BOOTLOADER_DFU_UF2_MAGIC); +#endif +#ifdef ESP32_FORCE_DOWNLOAD_BOOT + /* Come back up in the ROM download mode instead of the app, handing the + * USB port back to USB-Serial-JTAG so esptool can reach it. */ + REG_SET_BIT(RTC_CNTL_OPTION1_REG, RTC_CNTL_FORCE_DOWNLOAD_BOOT); #endif k_msleep(50); /* Let UART/USB flush */ +#ifdef ESP32_USB_SERIAL_DETACH + /* Clean USB disconnect before the soft reset — same reason as reboot(). */ + usb_serial_jtag_ll_phy_enable_pad(false); + k_msleep(100); +#endif sys_reboot(SYS_REBOOT_COLD); } diff --git a/zephcore/boards/common/esp32s3_console_uart0.dtsi b/zephcore/boards/common/esp32s3_console_uart0.dtsi index 7753550..066b274 100644 --- a/zephcore/boards/common/esp32s3_console_uart0.dtsi +++ b/zephcore/boards/common/esp32s3_console_uart0.dtsi @@ -13,9 +13,17 @@ * GPIO43/44. It is included from the auto-paired esp32s3_usb.overlay instead, * so it follows esp32s3_usb.conf. * - * Boards whose console really is a physical uart0 — Heltec V3/V4/V43, and - * ThinkNode M9 (USB-C via a CH9102-class bridge into UART0, native USB pads not - * bonded) — do include it unconditionally, which is correct for them. + * Boards whose console really is a physical uart0 — Heltec V3, and ThinkNode M9 + * (USB-C via a CH9102-class bridge into UART0, native USB pads not bonded) — do + * include it unconditionally, which is correct for them. + * + * Heltec V4/V4.3 and Wireless Tracker V2 are NOT in that group even though + * their board DTS names uart0: their board.overlay re-chooses usb_serial, so + * they go through the conditional path above like every other native-USB board. + * That did not work until 2026-08-28 — board.overlay was applied AFTER the + * auto-paired overlays and simply won, leaving the console pointed at a USJ + * peripheral that no longer owned the pins. See the DTC overlay precedence + * block in CMakeLists.txt. * * uart0 is disabled by default in the ESP32-S3 SoC dtsi, so we enable it and * pin it to the chip's ROM-default UART0 pads (GPIO43 TX / GPIO44 RX) — the diff --git a/zephcore/boards/common/esp32s3_usb.conf b/zephcore/boards/common/esp32s3_usb.conf index aa10a84..0624f84 100644 --- a/zephcore/boards/common/esp32s3_usb.conf +++ b/zephcore/boards/common/esp32s3_usb.conf @@ -5,9 +5,18 @@ # USB transport on S3 boards. Do NOT use on ESP32 (original) or C3/C6 — those # chips have no DWC2 controller. # -# Build with: +# Applied AUTOMATICALLY to companion builds on every S3 board whose board.overlay +# includes esp32s3_usb_otg.dtsi — see the "Auto-include the ESP32-S3 USB CDC-ACM +# companion transport" block in CMakeLists.txt for the gate (companion role only, +# and not on debug builds, which need that port for the console). Pass it by +# hand only to override that policy: # -DEXTRA_CONF_FILE="boards/common/esp32s3_usb.conf" # +# NOTE: this takes the USB port away from USB-Serial-JTAG, so esptool's +# auto-reset into download mode stops working on boards flashed over native USB. +# `start dfu` and the 1200-baud touch set FORCE_DOWNLOAD_BOOT to give it back +# (ZephyrBoard::rebootToBootloader); otherwise it is the BOOT button. +# # Also add the cdc_acm_uart DTS node to the board overlay (the overlays include # boards/common/esp32s3_usb_otg.dtsi). Without the DT node, # DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) is false and diff --git a/zephcore/boards/common/esp32s3_usb.overlay b/zephcore/boards/common/esp32s3_usb.overlay index 4cfdfec..b59dcfb 100644 --- a/zephcore/boards/common/esp32s3_usb.overlay +++ b/zephcore/boards/common/esp32s3_usb.overlay @@ -18,8 +18,11 @@ * CONFIG_ZEPHCORE_COMPANION_USB — the conf/overlay pairing is what makes this * conditional. * - * Boards whose console is already a physical uart0 (Heltec V3/V4/V43, ThinkNode - * M9) are unaffected: the include re-asserts the same uart0 + GPIO43/44 setup - * they already have. + * Boards whose console is already a physical uart0 (Heltec V3, ThinkNode M9) are + * unaffected: the include re-asserts the same uart0 + GPIO43/44 setup they + * already have. Heltec V4/V4.3 and Wireless Tracker V2 DO get moved — their + * board.overlay chooses usb_serial — which is the point, and only actually + * happens because board.overlay now goes into EXTRA_DTC_OVERLAY_FILE first + * (see the DTC overlay precedence block in CMakeLists.txt). */ #include "esp32s3_console_uart0.dtsi" diff --git a/zephcore/boards/esp32/heltec_wifi_lora32_v4/board.overlay b/zephcore/boards/esp32/heltec_wifi_lora32_v4/board.overlay index f919424..0fb6d1e 100644 --- a/zephcore/boards/esp32/heltec_wifi_lora32_v4/board.overlay +++ b/zephcore/boards/esp32/heltec_wifi_lora32_v4/board.overlay @@ -90,6 +90,8 @@ /* LittleFS auto-mount — standard /lfs mount point */ #include "../../common/filesystem.dtsi" -/* USB CDC-ACM — companion USB transport (esp32s3_usb.conf). Heltec has a - * dedicated UART console, so no console reroute is needed. */ +/* USB CDC-ACM — companion USB transport, auto-enabled for companion builds via + * boards/common/esp32s3_usb.conf (see the auto-include block in CMakeLists.txt). + * The chosen console above is usb_serial, i.e. the same D+/D- pins, so the conf's + * paired overlay moves the console to uart0/GPIO43-44 for those builds. */ #include "../../common/esp32s3_usb_otg.dtsi" diff --git a/zephcore/boards/esp32/heltec_wifi_lora32_v43/board.overlay b/zephcore/boards/esp32/heltec_wifi_lora32_v43/board.overlay index c2bed37..d8d73f8 100644 --- a/zephcore/boards/esp32/heltec_wifi_lora32_v43/board.overlay +++ b/zephcore/boards/esp32/heltec_wifi_lora32_v43/board.overlay @@ -90,6 +90,8 @@ /* LittleFS auto-mount — standard /lfs mount point */ #include "../../common/filesystem.dtsi" -/* USB CDC-ACM — companion USB transport (esp32s3_usb.conf). Heltec has a - * dedicated UART console, so no console reroute is needed. */ +/* USB CDC-ACM — companion USB transport, auto-enabled for companion builds via + * boards/common/esp32s3_usb.conf (see the auto-include block in CMakeLists.txt). + * The chosen console above is usb_serial, i.e. the same D+/D- pins, so the conf's + * paired overlay moves the console to uart0/GPIO43-44 for those builds. */ #include "../../common/esp32s3_usb_otg.dtsi" diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index a10405c..78b60dd 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -141,8 +141,15 @@ void CommonCLI::scheduleReboot(uint8_t type) */ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, char* reply) { if (strcmp(command, "start dfu") == 0) { - /* Reboot into UF2 bootloader for firmware update */ + /* Reboot into the chip's own firmware-update mode. nRF52: the Adafruit + * UF2 bootloader. ESP32-S3: the ROM download mode, which is the only + * way to get the USB port back from the CDC companion transport for + * esptool (see ZephyrBoard::rebootToBootloader). */ +#if defined(CONFIG_SOC_SERIES_ESP32S3) + strcpy(reply, "OK - rebooting to ESP32 download mode"); +#else strcpy(reply, "OK - rebooting to UF2 DFU"); +#endif scheduleReboot(REBOOT_DFU); } else if (memcmp(command, "start ota", 9) == 0) { #if IS_ENABLED(CONFIG_ZEPHCORE_WIFI_OTA)