Skip to content

ports/stm: bootloader entry on STM32F4 - #11270

Open
mikeysklar wants to merge 1 commit into
adafruit:mainfrom
mikeysklar:stm32f4-dfu-retry
Open

ports/stm: bootloader entry on STM32F4#11270
mikeysklar wants to merge 1 commit into
adafruit:mainfrom
mikeysklar:stm32f4-dfu-retry

Conversation

@mikeysklar

@mikeysklar mikeysklar commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

What

microcontroller.on_next_reset(RunMode.BOOTLOADER) and the 1200-baud touch reach the ST system bootloader about 40% of the time on an STM32F405. This retries, taking it to 12 of 12.

Why

The ROM clocks USB from the HSE but does not know which crystal is fitted, so it measures one against the HSI with TIM11. On a miss it resets the part instead of starting DFU: AN2606 Rev 61 Figure 32/33, HSE detected -> no -> "Generate System reset". Section 28.2.1 says the measurement is marginal, and that lower frequencies detect better, "better to use 8 MHz instead of 25 MHz". The Feather has 12 MHz.

Nothing in firmware can change the outcome of that measurement, so the only response is to ask again.

Not a new feature: #3444 asked for it, #6919 implemented it, #6961 fixed it, #3444 was closed as fixed. #6919 was verified only on a NUCLEO-F446RE, whose HSE is 8 MHz from the ST-LINK MCO.

How

reset_to_bootloader() sets a backup register and takes a real reset instead of jumping from the running application. check_enter_bootloader() runs at the top of port_init() and jumps, retrying when the ROM resets us. Gated on RCC_CSR.SFTRSTF, so a power cycle always exits.

BKP1R matches how the other ports hold this state (_bootloader_dbl_tap, NRF_POWER->GPREGRET, SNVS->LPGPR[3]). BKP0R is already STM_ALARM_FLAG.

Evidence

Adafruit Feather STM32F405 Express, 12 MHz crystal, CircuitPython 10.3.0-rc.0, Linux host, arm-none-eabi-gcc 14.2.1.

Each trial power cycles the USB port, waits for boot, then over the REPL:

import microcontroller
microcontroller.on_next_reset(microcontroller.RunMode.BOOTLOADER)
microcontroller.reset()

Result read from /sys/bus/usb/devices/3-3.3.4.4/idProduct (df11 = DFU, 805a = CircuitPython). The harness asserts the REPL accepted both statements, so a failed trigger is not counted as a firmware result.

reaches DFU
stock main 5 / 12
this branch 12 / 12

Typically under 1 s. Attempt counts, read out of the backup register over SWD on an earlier build of this change, were 1 to 4.

Also verified on hardware:

  • dfu-util -a 0 -s 0x08000000:leave -D firmware.bin flashes and returns to CircuitPython.
  • Power cycling out of DFU boots CircuitPython and stays there.

A retry only counts if it follows the previous attempt within 5 seconds, read from the RTC. The ROM fails and resets in well under a second, so anything slower is the part being reset by something else while the bootloader was running, and the request is dropped as stale. Exit paths, each verified:

exit from DFU lands in
dfu-util --leave CircuitPython
power cycle CircuitPython
software reset after 8 s (SYSRESETREQ) CircuitPython, 3 of 3
software reset within 5 s bootloader again

The last row is inherent: inside the window the ROM's own reset and an external one are indistinguishable. A flashing session takes longer than that.

Decisions

  • No SYSCFG remap, no VTOR change. I added both first; measured, they make no difference. The ROM sets its own VTOR and runs from 0x1FFF0000.
  • Pre-warming the crystal does not help. Setting HSEON and spinning on HSERDY before the branch, so the ROM gets a settled crystal, still needed 1 to 4 attempts over 12 trials. The ROM appears to re-initialise RCC and re-measure regardless, so the retry is not covering for a startup race.
  • Cap of 16. A cap of 8 failed once in 12; 16 has not failed in 46 trials, and one success needed 7 attempts.
  • Dropped the HAL_RCC_DeInit()/HAL_DeInit()/NVIC teardown. A system reset supersedes it.
  • UF2 builds unchanged. jepler asked in Implement reset-to-dfu on STM MCUs without UF2 bootloaders #3444 for #if UF2 handling to prefer a UF2 bootloader; still not implemented, and this does not change that behaviour.

Claude Code was used.

@mikeysklar mikeysklar changed the title ports/stm: retry ST system bootloader entry on STM32F4 ports/stm: bootloader entry on STM32F4 Aug 29, 2026
@mikeysklar
mikeysklar force-pushed the stm32f4-dfu-retry branch 2 times, most recently from 243c173 to 78db9aa Compare August 29, 2026 03:06
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TehTMf9ApHxxU5UNHosXKj
@mikeysklar

Copy link
Copy Markdown
Collaborator Author

The tests / zephyr failure is test_saved_word.py::test_saved_word_survives_reboot_into_safe_mode, a 20s simulator reconnect timeout, 1 failed and 218 passed. This PR touches only ports/stm/supervisor/port.c, which zephyr does not build, and it does not modify the saved word functions. Looks like the same flaky class as #11262. A re-run of that job would confirm.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant