From 77179266a99573dd83632e69d8587df5cb46d0e8 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Fri, 28 Aug 2026 19:47:20 -0700 Subject: [PATCH 1/2] ports/stm: retry ST system bootloader entry on STM32F4 The ROM clocks USB from the HSE but does not know which crystal is fitted, so it measures one against the HSI. On a miss it resets the part instead of starting DFU: AN2606 Figure 32/33, "HSE detected" -> no -> "Generate System reset". On a Feather STM32F405 Express and its 12 MHz crystal a single jump reached DFU 5 times in 12. Record the request in a backup register and take a real reset, then jump from the top of port_init() and retry when the ROM bounces us. That reaches DFU 12 times in 12. Also drops the HAL_RCC_DeInit()/HAL_DeInit()/NVIC teardown, which a system reset supersedes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TehTMf9ApHxxU5UNHosXKj --- ports/stm/supervisor/port.c | 136 ++++++++++++++++++++++++++---------- 1 file changed, 99 insertions(+), 37 deletions(-) diff --git a/ports/stm/supervisor/port.c b/ports/stm/supervisor/port.c index 1da862cd886..4c1fc77f51a 100644 --- a/ports/stm/supervisor/port.c +++ b/ports/stm/supervisor/port.c @@ -235,7 +235,96 @@ void *port_realloc(void *ptr, size_t size, bool dma_capable) { } #endif +// Pending request for the ST system bootloader, kept where the other ports keep +// theirs: _bootloader_dbl_tap on atmel-samd, NRF_POWER->GPREGRET on nordic, +// SNVS->LPGPR[3] on mimxrt10xx. BKP0R is already STM_ALARM_FLAG. The low half is +// the attempt count. +#define STM_BOOTLOADER_FLAG (RTC->BKP1R) +#define BOOTLOADER_MAGIC 0xf05a0000 +#define BOOTLOADER_MAGIC_MASK 0xffff0000 +#define BOOTLOADER_SECONDS_SHIFT 8 +#define BOOTLOADER_SECONDS_MASK 0xff + +// A retry only counts if it follows the previous attempt closely. The ROM fails +// and resets within a second, so anything slower is the part being reset by +// something else while the bootloader was running, and the request is stale. +// Without this, exiting DFU with a software reset lands straight back in DFU. +#define BOOTLOADER_RETRY_WINDOW_S 5 + +// The ROM clocks USB from the HSE but does not know which crystal is fitted, so +// it measures one against the HSI and resets the part when that misses: AN2606 +// Figure 32/33, "HSE detected" -> no -> "Generate System reset". On the 12 MHz +// Feather STM32F405 Express one attempt reached DFU 5 times in 12. +#define BOOTLOADER_MAX_ATTEMPTS 16 + +#if CPY_STM32F4 +// Seconds field of the RTC clock, read straight from the register because the +// HAL handle is not initialised this early. rtc_init() leaves shadow bypass on +// and that setting survives a reset, so the register reads live. +static uint32_t bootloader_rtc_seconds(void) { + uint32_t tr = RTC->TR; + return ((tr >> 4) & 0x7) * 10 + (tr & 0xF); +} + +// Naked so nothing touches the stack between setting MSP and the branch. +MP_NORETURN static __attribute__((naked)) void branch_to_bootloader(uint32_t bl_addr) { + __asm volatile ( + "ldr r2, [r0, #0]\n" + "msr msp, r2\n" + "ldr r2, [r0, #4]\n" + "bx r2\n" + ); +} + +// Runs before HAL_Init() starts SysTick, so the ROM gets the part close to reset +// state, and ahead of the __HAL_RCC_BACKUPRESET_FORCE() further down port_init() +// which clears STM_BOOTLOADER_FLAG. The ROM sets its own VTOR and runs from +// 0x1FFF0000, so no SYSCFG remap is needed here. +static void check_enter_bootloader(void) { + __HAL_RCC_PWR_CLK_ENABLE(); + HAL_PWR_EnableBkUpAccess(); + + uint32_t request = STM_BOOTLOADER_FLAG; + if ((request & BOOTLOADER_MAGIC_MASK) != BOOTLOADER_MAGIC) { + return; + } + + // The ROM signals its HSE-detect failure with a software reset, so SFTRSTF is + // what tells a retry apart from a cold boot, and a power cycle always leaves + // the bootloader. + if (!(RCC->CSR & RCC_CSR_SFTRSTF)) { + STM_BOOTLOADER_FLAG = 0; + return; + } + + uint32_t now = bootloader_rtc_seconds(); + uint32_t then = (request >> BOOTLOADER_SECONDS_SHIFT) & BOOTLOADER_SECONDS_MASK; + if ((now + 60 - then) % 60 > BOOTLOADER_RETRY_WINDOW_S) { + STM_BOOTLOADER_FLAG = 0; + return; + } + + uint32_t attempts = request & BOOTLOADER_SECONDS_MASK; + if (attempts >= BOOTLOADER_MAX_ATTEMPTS) { + STM_BOOTLOADER_FLAG = 0; + return; + } + STM_BOOTLOADER_FLAG = BOOTLOADER_MAGIC | + (now << BOOTLOADER_SECONDS_SHIFT) | (attempts + 1); + // Clearing the flags cancels the request once the ROM succeeds: leaving DFU + // is a jump, not a reset, so SFTRSTF stays clear and the branch above zeroes + // the flag on the way back into the application. + RCC->CSR |= RCC_CSR_RMVF; + + branch_to_bootloader(0x1FFF0000); +} +#endif + safe_mode_t port_init(void) { + #if CPY_STM32F4 + check_enter_bootloader(); + #endif + HAL_Init(); // Turns on SysTick __HAL_RCC_SYSCFG_CLK_ENABLE(); @@ -256,6 +345,8 @@ safe_mode_t port_init(void) { } #endif + // This clears STM_BOOTLOADER_FLAG too, so check_enter_bootloader() above has + // to run before it. __HAL_RCC_BACKUPRESET_FORCE(); __HAL_RCC_BACKUPRESET_RELEASE(); @@ -324,44 +415,15 @@ void reset_port(void) { } void reset_to_bootloader(void) { - -/* -From STM AN2606: -Before jumping to bootloader user must: -• Disable all peripheral clocks -• Disable used PLL -• Disable interrupts -• Clear pending interrupts -System memory boot mode can be exited by getting out from bootloader activation -condition and generating hardware reset or using Go command to execute user code -*/ - HAL_RCC_DeInit(); - HAL_DeInit(); - - // Disable all pending interrupts using NVIC - for (uint8_t i = 0; i < MP_ARRAY_SIZE(NVIC->ICER); ++i) { - NVIC->ICER[i] = 0xFFFFFFFF; - } - - // if it is necessary to ensure an interrupt will not be triggered after disabling it in the NVIC, - // add a DSB instruction and then an ISB instruction. (ARM Cortex™-M Programming Guide to - // Memory Barrier Instructions, 4.6 Disabling Interrupts using NVIC) - __DSB(); - __ISB(); - - // Clear all pending interrupts using NVIC - for (uint8_t i = 0; i < MP_ARRAY_SIZE(NVIC->ICPR); ++i) { - NVIC->ICPR[i] = 0xFFFFFFFF; - } - - // information about jump addresses has been taken from STM AN2606. - #if defined(STM32F4) - __set_MSP(*((uint32_t *)0x1FFF0000)); - ((void (*)(void)) * ((uint32_t *)0x1FFF0004))(); - #else - // DFU mode for STM32 variant note implemented. - NVIC_SystemReset(); + #if CPY_STM32F4 + // Record the request and reset, rather than jumping from a running + // application. check_enter_bootloader() jumps on the way back up. + __HAL_RCC_PWR_CLK_ENABLE(); + HAL_PWR_EnableBkUpAccess(); + STM_BOOTLOADER_FLAG = BOOTLOADER_MAGIC | + (bootloader_rtc_seconds() << BOOTLOADER_SECONDS_SHIFT); #endif + NVIC_SystemReset(); while (true) { asm ("nop;"); From 8dacf7383fdf59d7de70851f9a65db45fc43ddb9 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Mon, 31 Aug 2026 17:10:38 -0700 Subject: [PATCH 2/2] ports/stm: keep the bootloader request in the saved word Move the pending ST system bootloader request from RTC->BKP1R to port_set_saved_word(), which is RAM that startup does not clear. This drops the backup domain clock enable and access unlock at both call sites, and removes the ordering constraint against the __HAL_RCC_BACKUPRESET_FORCE() further down port_init(). The RTC staleness window stays. It only needs a read of RTC->TR, which does not require unlocking the backup domain for write. safe_mode.c shares the saved word. Its guard is 0xad0000af under mask 0xff0000ff and the bootloader magic is 0xf05a0000 under mask 0xffff0000, so neither can be read as the other, and the word is only zeroed when the magic is ours. Co-Authored-By: Claude --- ports/stm/supervisor/port.c | 49 +++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/ports/stm/supervisor/port.c b/ports/stm/supervisor/port.c index 4c1fc77f51a..21da8b2d653 100644 --- a/ports/stm/supervisor/port.c +++ b/ports/stm/supervisor/port.c @@ -235,15 +235,16 @@ void *port_realloc(void *ptr, size_t size, bool dma_capable) { } #endif -// Pending request for the ST system bootloader, kept where the other ports keep -// theirs: _bootloader_dbl_tap on atmel-samd, NRF_POWER->GPREGRET on nordic, -// SNVS->LPGPR[3] on mimxrt10xx. BKP0R is already STM_ALARM_FLAG. The low half is -// the attempt count. -#define STM_BOOTLOADER_FLAG (RTC->BKP1R) +// Pending request for the ST system bootloader. This lives in the saved word, +// RAM that startup does not clear, which is where the other ports keep theirs: +// _bootloader_dbl_tap on atmel-samd, NRF_POWER->GPREGRET on nordic, +// SNVS->LPGPR[3] on mimxrt10xx. The low byte is the attempt count. #define BOOTLOADER_MAGIC 0xf05a0000 #define BOOTLOADER_MAGIC_MASK 0xffff0000 +// Magic in the top half, RTC seconds of the last attempt in bits 15:8, attempt +// count in bits 7:0. #define BOOTLOADER_SECONDS_SHIFT 8 -#define BOOTLOADER_SECONDS_MASK 0xff +#define BOOTLOADER_FIELD_MASK 0xff // A retry only counts if it follows the previous attempt closely. The ROM fails // and resets within a second, so anything slower is the part being reset by @@ -260,7 +261,8 @@ void *port_realloc(void *ptr, size_t size, bool dma_capable) { #if CPY_STM32F4 // Seconds field of the RTC clock, read straight from the register because the // HAL handle is not initialised this early. rtc_init() leaves shadow bypass on -// and that setting survives a reset, so the register reads live. +// and that setting survives a reset, so the register reads live. Only a read is +// needed, so the backup domain does not have to be unlocked for write. static uint32_t bootloader_rtc_seconds(void) { uint32_t tr = RTC->TR; return ((tr >> 4) & 0x7) * 10 + (tr & 0xF); @@ -277,15 +279,12 @@ MP_NORETURN static __attribute__((naked)) void branch_to_bootloader(uint32_t bl_ } // Runs before HAL_Init() starts SysTick, so the ROM gets the part close to reset -// state, and ahead of the __HAL_RCC_BACKUPRESET_FORCE() further down port_init() -// which clears STM_BOOTLOADER_FLAG. The ROM sets its own VTOR and runs from -// 0x1FFF0000, so no SYSCFG remap is needed here. +// state. The ROM sets its own VTOR and runs from 0x1FFF0000, so no SYSCFG remap +// is needed here. static void check_enter_bootloader(void) { - __HAL_RCC_PWR_CLK_ENABLE(); - HAL_PWR_EnableBkUpAccess(); - - uint32_t request = STM_BOOTLOADER_FLAG; + uint32_t request = port_get_saved_word(); if ((request & BOOTLOADER_MAGIC_MASK) != BOOTLOADER_MAGIC) { + // Not our word. Safe mode shares it, so leave it alone. return; } @@ -293,24 +292,24 @@ static void check_enter_bootloader(void) { // what tells a retry apart from a cold boot, and a power cycle always leaves // the bootloader. if (!(RCC->CSR & RCC_CSR_SFTRSTF)) { - STM_BOOTLOADER_FLAG = 0; + port_set_saved_word(0); return; } uint32_t now = bootloader_rtc_seconds(); - uint32_t then = (request >> BOOTLOADER_SECONDS_SHIFT) & BOOTLOADER_SECONDS_MASK; + uint32_t then = (request >> BOOTLOADER_SECONDS_SHIFT) & BOOTLOADER_FIELD_MASK; if ((now + 60 - then) % 60 > BOOTLOADER_RETRY_WINDOW_S) { - STM_BOOTLOADER_FLAG = 0; + port_set_saved_word(0); return; } - uint32_t attempts = request & BOOTLOADER_SECONDS_MASK; + uint32_t attempts = request & BOOTLOADER_FIELD_MASK; if (attempts >= BOOTLOADER_MAX_ATTEMPTS) { - STM_BOOTLOADER_FLAG = 0; + port_set_saved_word(0); return; } - STM_BOOTLOADER_FLAG = BOOTLOADER_MAGIC | - (now << BOOTLOADER_SECONDS_SHIFT) | (attempts + 1); + port_set_saved_word(BOOTLOADER_MAGIC | + (now << BOOTLOADER_SECONDS_SHIFT) | (attempts + 1)); // Clearing the flags cancels the request once the ROM succeeds: leaving DFU // is a jump, not a reset, so SFTRSTF stays clear and the branch above zeroes // the flag on the way back into the application. @@ -345,8 +344,6 @@ safe_mode_t port_init(void) { } #endif - // This clears STM_BOOTLOADER_FLAG too, so check_enter_bootloader() above has - // to run before it. __HAL_RCC_BACKUPRESET_FORCE(); __HAL_RCC_BACKUPRESET_RELEASE(); @@ -418,10 +415,8 @@ void reset_to_bootloader(void) { #if CPY_STM32F4 // Record the request and reset, rather than jumping from a running // application. check_enter_bootloader() jumps on the way back up. - __HAL_RCC_PWR_CLK_ENABLE(); - HAL_PWR_EnableBkUpAccess(); - STM_BOOTLOADER_FLAG = BOOTLOADER_MAGIC | - (bootloader_rtc_seconds() << BOOTLOADER_SECONDS_SHIFT); + port_set_saved_word(BOOTLOADER_MAGIC | + (bootloader_rtc_seconds() << BOOTLOADER_SECONDS_SHIFT)); #endif NVIC_SystemReset();