Skip to content

ports/stm: bootloader entry on STM32F4 - #11270

Open
mikeysklar wants to merge 2 commits into
adafruit:mainfrom
mikeysklar:stm32f4-dfu-retry
Open

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

Conversation

@mikeysklar

@mikeysklar mikeysklar commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

What

RunMode.BOOTLOADER and 1200 baud touch reach the bootloader 40% of the time on STM32F405. Retrying gives 12/12.

Why

ROM HSE detection is marginal at 12 MHz (AN2606) and resets on a miss. Firmware cannot fix it, only retry.

History

#3444 requested, #6919 implemented, #6961 fixed. Verified only on NUCLEO-F446RE, whose HSE is 8 MHz.

How

reset_to_bootloader() sets a backup register and resets. check_enter_bootloader() in port_init() jumps, retrying when the ROM resets us.

Notes

BKP1R matches other ports' double-tap state; BKP0R already used. Requests older than 5 seconds are dropped as stale.

Evidence

Feather STM32F405, 12 MHz crystal, 10.3.0-rc.0, Linux host. Each trial power cycles USB, triggers over REPL, reads idProduct.

import microcontroller
microcontroller.on_next_reset(microcontroller.RunMode.BOOTLOADER)
microcontroller.reset()
reaches DFU
stock main 5 / 12
this branch 12 / 12
exit from DFU lands in
dfu-util -s 0x08000000:leave -D firmware.bin CircuitPython, flashed
dfu-util --leave CircuitPython
power cycle CircuitPython
software reset after 8 s CircuitPython, 3 of 3
software reset within 5 s bootloader again

Decisions

decision reason
no SYSCFG remap or VTOR change measured, no difference; ROM sets its own VTOR
no crystal pre-warm HSEON before branch still needed 1 to 4 attempts
retry cap of 16 cap of 8 failed once in 12; 16 clean in 46 trials, worst success took 7
dropped HAL deinit teardown system reset supersedes it
UF2 builds unchanged #3444 UF2 preference still unimplemented, behavior unchanged

AI assistance

Claude Code was used. All hardware trials run on real boards and reviewed by hand.

@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.

@tannewt tannewt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question. Thanks for looking into this.

Comment thread ports/stm/supervisor/port.c Outdated
#define BOOTLOADER_MAGIC 0xf05a0000
#define BOOTLOADER_MAGIC_MASK 0xffff0000
#define BOOTLOADER_SECONDS_SHIFT 8
#define BOOTLOADER_SECONDS_MASK 0xff

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use port_set_saved_word instead? It is just memory that doesn't get cleared. It'd simplify this a bit.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8dacf73. Kept the RTC staleness window, hardware shows it is load-bearing.

Test Result
Bootloader entry 6 / 6 reached DFU
dfu-util --leave back to app, stayed
Power cycle out of DFU back to app, stayed
Software reset in DFU, window expired back to app
Software reset in DFU, within window retries, by design
Plain microcontroller.reset() no DFU

AI assistance: Claude wrote the patch, numbers measured on hardware.

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 <noreply@anthropic.com>
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.

2 participants