diff --git a/src/Power.cpp b/src/Power.cpp index 84bf12aaf5..09b20f6ff6 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -976,6 +976,14 @@ void Power::powerCommandsCheck() shutdownAtMsec = 0; shutdown(); } + +#ifdef ARCH_STM32 + // Deferred DFU entry; the delay is armed by AdminModule's enter_dfu handler (rationale there). + if (enterDfuAtMsec && Throttle::deadlinePassed(enterDfuAtMsec)) { + enterDfuAtMsec = 0; + enterDfuMode(); // never returns + } +#endif } void Power::reboot() diff --git a/src/main.cpp b/src/main.cpp index b5db79b6bb..7fde17346e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1283,6 +1283,9 @@ void setup() uint32_t rebootAtMsec; // If not zero we will reboot at this time (used to reboot shortly after the update completes) uint32_t shutdownAtMsec; // If not zero we will shutdown at this time (used to shutdown from python or mobile client) bool suppressRebootBanner; // If true, suppress "Rebooting..." overlay (used for OTA handoff) +#ifdef ARCH_STM32 +uint32_t enterDfuAtMsec; // If not zero, enter DFU mode at this millis() deadline (see main.h) +#endif #if defined(MESHTASTIC_ENCRYPTED_STORAGE) && defined(MESHTASTIC_PHONEAPI_ACCESS_CONTROL) volatile bool lockdownReloadPending; // see main.h - deferred NodeDB reload after lockdown unlock diff --git a/src/main.h b/src/main.h index 19b1bace0d..49a63365bc 100644 --- a/src/main.h +++ b/src/main.h @@ -92,6 +92,9 @@ extern uint32_t timeLastPowered; extern uint32_t rebootAtMsec; extern uint32_t shutdownAtMsec; extern bool suppressRebootBanner; +#ifdef ARCH_STM32 +extern uint32_t enterDfuAtMsec; // 0 = unset; else millis() deadline for the deferred DFU jump +#endif #if defined(MESHTASTIC_ENCRYPTED_STORAGE) && defined(MESHTASTIC_PHONEAPI_ACCESS_CONTROL) // Set by PhoneAPI::handleLockdownAuthInline after a successful unlock. diff --git a/src/mesh/Throttle.h b/src/mesh/Throttle.h index f9d68a4143..7df932dfab 100644 --- a/src/mesh/Throttle.h +++ b/src/mesh/Throttle.h @@ -34,8 +34,8 @@ class Throttle /// "passed" - the split that has to survive, because which way "inactive" falls is the caller's /// to decide. Same size and cost as the bare uint32_t. The conversion sites, grouped by the four /// meanings they give the sentinel today: - /// 0 = unarmed - Power.cpp rebootAtMsec/shutdownAtMsec (the cheapest pair to convert), and - /// GPS.cpp fixHoldEnds, whose arm site remaps a 0 result to 1 by hand. + /// 0 = unarmed - Power.cpp rebootAtMsec/shutdownAtMsec, GPS.cpp fixHoldEnds, AdminModule.cpp + /// enterDfuAtMsec - the last two remap a 0 result to 1 at the arm site by hand. /// 0 = forever - NotificationRenderer.cpp alertBannerUntil. Every read spells its own `> 0` /// guard, so this third state wants naming rather than repeating. /// 0 = due now - ethClient.cpp ntp_renew, forced at link-up. diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 4995f3c822..abc71034e6 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -68,6 +68,11 @@ AdminModule *adminModule; +#ifdef ARCH_STM32 +// Client detach window before the DFU jump (see the enter_dfu case). +static constexpr uint32_t STM32_DFU_DETACH_DELAY_MS = 5000; +#endif + #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) static bool licensedIdentityWillMigrate() { @@ -619,7 +624,15 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta #if HAS_SCREEN IF_SCREEN(screen->showSimpleBanner("Device is rebooting\ninto DFU mode.", 0)); #endif -#if defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32) +#if defined(ARCH_STM32) + // Delay the jump so this ACK reaches the client and it releases the port before the + // STM32WL ROM bootloader takes the UART and autobauds off the next byte it sees. + LOG_INFO("Entering DFU in %us - disconnect now", (STM32_DFU_DETACH_DELAY_MS + 999) / 1000); + enterDfuAtMsec = millis() + STM32_DFU_DETACH_DELAY_MS; + // Guard against enterDfuAtMsec rolling over to 0, the sentinel powerCommandsCheck() reads as unarmed. + if (enterDfuAtMsec == 0) + enterDfuAtMsec = 1; +#elif defined(ARCH_NRF52) || defined(ARCH_RP2040) enterDfuMode(); #endif break; diff --git a/src/platform/stm32wl/main-stm32wl.cpp b/src/platform/stm32wl/main-stm32wl.cpp index 4d54f22895..1f363edfa9 100644 --- a/src/platform/stm32wl/main-stm32wl.cpp +++ b/src/platform/stm32wl/main-stm32wl.cpp @@ -1,6 +1,7 @@ #include "FSCommon.h" #include "configuration.h" #include "error.h" +#include "gps/GPS.h" #include "gps/RTC.h" #include #include @@ -20,23 +21,27 @@ static bool stm32wlRtcValid = false; #endif // ─── Bootloader redirect ────────────────────────────────────────────────────── -// Uses .noinit SRAM instead of TAMP backup registers: STM32duino's clock init can wipe the -// backup domain via __HAL_RCC_BACKUPRESET_FORCE/RELEASE before setup() runs, but .noinit -// survives NVIC_SystemReset() and this constructor fires before HAL_Init() touches anything. +// Magic word in .noinit, not TAMP backup regs (STM32duino clock init can wipe those): it +// survives NVIC_SystemReset(), and the .preinit_array hook below runs before HAL_Init(). +// STM32WLxx system-memory bootloader base, AN2606 "STM32WLxx bootloader": +// https://www.st.com/resource/en/application_note/an2606-stm32-microcontroller-system-memory-boot-mode-stmicroelectronics.pdf #define BOOTLOADER_MAGIC 0xD00DB007UL #define SYS_MEM_BASE 0x1FFF0000UL -// Placed in .noinit - not zeroed at startup, survives NVIC_SystemReset(). +// .noinit - not zeroed at startup, survives NVIC_SystemReset(). __attribute__((section(".noinit"), used)) volatile uint32_t g_bootloaderMagic; -// Fires before main() / HAL_Init(). Must use only core Cortex-M registers. -__attribute__((constructor(101), used)) static void earlyBootCheck(void) +// Runs from .preinit_array (below), before every constructor incl. the core's premain()/init(), +// so RCC/SysTick/HAL are still at reset. Core Cortex-M / CMSIS registers only. +__attribute__((used)) static void earlyBootCheck(void) { if (g_bootloaderMagic != BOOTLOADER_MAGIC) return; g_bootloaderMagic = 0; + // Return SysTick/NVIC/RCC to reset state before the jump - ST's system bootloader expects it. + // https://community.st.com/t5/stm32-mcus/how-to-jump-to-system-bootloader-from-application-code-on-stm32/ta-p/49424 SysTick->CTRL = 0; SysTick->LOAD = 0; SysTick->VAL = 0; @@ -44,20 +49,56 @@ __attribute__((constructor(101), used)) static void earlyBootCheck(void) NVIC->ICER[i] = 0xFFFFFFFF; NVIC->ICPR[i] = 0xFFFFFFFF; } + + // Same writes CMSIS system_stm32wlxx.c SystemInit() makes, repeated here in case a hook + // between reset and .preinit_array moved SYSCLK onto the PLL (RM0461 reset values). + RCC->CR |= 0x00000061U; // MSION, MSI range 4 MHz + RCC->CFGR = 0x00070000U; // SYSCLK=MSI, AHB/APB prescalers /1, MCO off + RCC->CR = 0x00000061U; // HSEON/HSEBYP/CSSON/PLLON off + RCC->PLLCFGR = 0x22040100U; + RCC->CIER = 0x00000000U; + RCC->CICR = 0x0000033FU; // clear all RCC interrupt flags + __DSB(); __ISB(); SCB->VTOR = SYS_MEM_BASE; __set_MSP(*(volatile uint32_t *)SYS_MEM_BASE); ((void (*)(void))(*(volatile uint32_t *)(SYS_MEM_BASE + 4)))(); - // Should never be reached: the bootloader ROM does not return. A bare reset - // (rather than returning normally) avoids unwinding through this function's - // epilogue, which would restore registers relative to the now-repointed MSP. + // Not reached: the bootloader ROM does not return. Reset rather than return, to avoid + // unwinding this function's epilogue against the now-repointed MSP. NVIC_SystemReset(); } +// __libc_init_array runs .preinit_array entries before any .init_array constructor, so this +// precedes the core's premain() (constructor(101)) regardless of link order. +__attribute__((section(".preinit_array"), used)) static void (*const earlyBootCheckEntry)(void) = &earlyBootCheck; + +// Drain and release a UART before the reset, as cpuDeepSleep() does. +static void quiesceSerial(HardwareSerial &port) +{ + if (port) { + port.flush(); + port.end(); + } +} + void enterDfuMode() { g_bootloaderMagic = BOOTLOADER_MAGIC; + + // The ROM bootloader autobauds off the first byte on USART1 (PB6/PB7) or USART2 (PA2/PA3), + // and every WL UART and GPS sits on those pins. Silence them all before the reset. +#if !MESHTASTIC_EXCLUDE_GPS + if (gps) + gps->disable(); +#endif + quiesceSerial(Serial); +#ifdef ENABLE_HWSERIAL1 + quiesceSerial(Serial1); +#endif +#ifdef ENABLE_HWSERIAL2 + quiesceSerial(Serial2); +#endif HAL_NVIC_SystemReset(); } @@ -140,25 +181,19 @@ void cpuDeepSleep(uint32_t msecToWake) // Hardware can't shutdown, but firmware has already prepared itself for shutdown // Do not leave the device unresponsive, reset instead LOG_WARN("STM32WL: hardware RTC failed, can't deep sleep/shutdown"); - if (Serial) { - Serial.flush(); - Serial.end(); + quiesceSerial(Serial); + HAL_NVIC_SystemReset(); + } else { + quiesceSerial(Serial); + + if (msecToWake != portMAX_DELAY) { + LowPower.shutdown(msecToWake); + } else { + LowPower.shutdown(); } + // RTC wakes from shutdown into MCU reset, so this code should never be reached HAL_NVIC_SystemReset(); } - - if (Serial) { - Serial.flush(); - Serial.end(); - } - - if (msecToWake != portMAX_DELAY) { - LowPower.shutdown(msecToWake); - } else { - LowPower.shutdown(); - } - // RTC wakes from shutdown into MCU reset, so this code should never be reached - HAL_NVIC_SystemReset(); #endif }