feat: reboot into the ST system bootloader on a 1200 bps touch - #3063
Open
mikeysklar wants to merge 2 commits into
Open
feat: reboot into the ST system bootloader on a 1200 bps touch#3063mikeysklar wants to merge 2 commits into
mikeysklar wants to merge 2 commits into
Conversation
mikeysklar
force-pushed
the
cdc-1200bps-touch-hook
branch
2 times, most recently
from
August 29, 2026 13:42
d2088fb to
60b909d
Compare
The Arduino convention for asking a board to reboot into its bootloader is for the host to set the CDC line coding to 1200 bps and then close the port. The core tracks both halves already, in linecoding.bitrate and dtrState, but nothing acts on the combination, so a sketch has to poll Serial.baud() and Serial.dtr() from loop() and misses the event whenever loop() is busy. Add cdc_1200bps_touchHook(), a weak no-op alongside yield() and dtr_togglingHook(), called from CDC_SET_CONTROL_LINE_STATE when the port is closed at 1200 bps. dtr_togglingHook() cannot serve here because it is invoked from USBD_CDC_Receive and so requires the host to send data, which a 1200 bps touch does not. The hook only reports the event. It adds no series specific code, no backup register use and no policy about which bootloader to enter, so boards and sketches remain free to decide that. Refs stm32duino#706, stm32duino#710
Selecting the STM32CubeProgrammer (DFU) upload method gets you a board that can be flashed over DFU, but nothing puts it into DFU, so uploading still means reaching for BOOT0. Wire the 1200 bps touch to the ST system memory bootloader so the upload works on its own. cdc_1200bps_touchHook() records the request in a .noinit word and resets. A reset is safe from the USB control transfer callback where a branch is not, and .noinit holds an arbitrary value after a power cycle, which is why the request is only trusted after a software reset. A backup register would not serve, some series have none. premain() then branches before init() runs HAL_Init(), so the ROM gets the part close to reset state. systemBootloaderAddress() remaps system memory to zero rather than carrying a table of per series addresses, and is WEAK so a board can point it elsewhere. The ROM clocks USB from the HSE but does not know which crystal is fitted, so it measures one against the HSI, and per AN2606 it resets the part when that measurement misses. On a 12 MHz Feather STM32F405 Express a single attempt reached DFU 5 times in 12, so the request survives the reset and is retried. Fixes stm32duino#706
mikeysklar
force-pushed
the
cdc-1200bps-touch-hook
branch
from
August 29, 2026 13:43
60b909d to
8e41a8b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes the STM32CubeProgrammer (DFU) upload method work on its own: closing the USB CDC port at 1200 bps now reboots the board into the ST system memory bootloader, so a sketch can be uploaded without touching BOOT0 or power cycling.
This PR fixes/implements the following bugs/features
Fixes #706
Motivation
dfuMethodalready exists for essentially every series, so the board can be flashed over DFU. What is missing is anything to put it into DFU, which is why uploading still means reaching for BOOT0. This closes that gap.Two commits, each self-contained:
feat(cdc): add a 1200 bps touch hook— a weakcdc_1200bps_touchHook()incores/arduino/hooks.c, called fromCDC_SET_CONTROL_LINE_STATEwhen the host closes the port at 1200 bps. It followsyield()anddtr_togglingHook(), has no series specific content, and does nothing by default.dtr_togglingHook()cannot serve here: it is invoked fromUSBD_CDC_Receive, so it needs the host to send data, and a 1200 bps touch sends none.feat(bootloader): add ST system bootloader entry— the policy, behind-DBL_SYSTEMon the existingdfuMethodentry.Design notes, each of which cost a measurement
NVIC_SystemReset(), which is safe from handler mode..noinit, not a backup register. Some series have none, and.noinitholds an arbitrary value after a power cycle, so the request is only trusted after a software reset, which is also what makes leaving DFU clear it rather than re-enter.premain()for the branch, beforeinit()callsHAL_Init(), so the ROM gets the part close to reset state. NoReset_Handleroverride and no startup assembly change.HSE detected-> no -> "Generate System reset". Section 28.2.1 says lower frequencies detect better, "it is better to use 8 MHz instead of 25 MHz"; this board is 12 MHz. A single attempt reached DFU 5 times in 12. Retrying reaches it every time. The software reset gate is what makes leaving DFU clear the request rather than re-enter it. Pre-starting HSE and waiting forHSERDYdoes not help, so this is not a crystal startup race.Series coverage
systemBootloaderAddress()remaps system memory to zero with__HAL_SYSCFG_REMAPMEMORY_SYSTEMFLASH()and returns 0, rather than carrying a table of per series addresses. That macro is provided by the HAL for F0, F2, F3, F4, C0, G0, G4, L0, L1 and L4 among others, so the implementation is not F4 specific. Where it is absent the#elseis a compile time error rather than a silent no-op, and since-DBL_SYSTEMis only set by theboards.txtentry, no series that does not opt in is affected.systemBootloaderAddress()isWEAKso a board with the bootloader somewhere else can redefine it.I have only been able to verify this on STM32F4. I enabled the flag for GenF4 alone for that reason, rather than turning it on for series I cannot test.
Validation
Board: Adafruit Feather STM32F405 Express, 12 MHz crystal. Core built from this branch. FQBN
STMicroelectronics:stm32:GenF4:pnum=FEATHER_F405,usb=CDCgen,xusb=FS,upload_method=dfuMethod. Host: Linux, board on a hub with per port power control, flashed over SWD with OpenOCD for the initial load.The sketch is a stock blink with no bootloader code at all, which is the point:
Each trial power cycles the port, waits for enumeration, then opens the port at 1200 bps, drops DTR and closes it. Entry is read from
/sys/bus/usb/devices/.../idProduct.dfu-util ... :leave, back to sketchupload_method=swdMethodNot tested: series other than STM32F4, and hosts other than Linux.
Code formatting
astyle --options=CI/astyle/.astylercreports every changed file unchanged. No generated or vendored file is touched.cmake/scripts/cmake_updater_hook.pywas re-run after theboards.txtchange, with submodules checked out, and produced no diff.Prior art
#710 covered this ground in 2019. Credit to @fpistm and @matthijskooijman: the
.noinitplus software reset gate, thepremain()placement, theWEAKaddress function that remaps rather than tabulating addresses, and the inline asm branch all follow that branch. This is written fresh against currentmainrather than rebased on it, since the file layout has moved since and that branch is unfinished.Claude Code was used. The measurements above are USB enumeration state read from sysfs, on the hardware named, not estimates.