dfu: recover serial/USB DFU automatically after a failed flash - #47
Open
fkallay1 wants to merge 1 commit into
Open
dfu: recover serial/USB DFU automatically after a failed flash#47fkallay1 wants to merge 1 commit into
fkallay1 wants to merge 1 commit into
Conversation
An interrupted serial/USB DFU left the device reachable only over BLE. The aborted transfer correctly marks the application invalid (bootloader.c writes bank_0 = BANK_INVALID_APP on DFU_BANK_0_ERASED), but check_dfu_mode() then defaulted !valid_app to OTA: ble_stack_init() ran and usb_init() never did, so the device disappeared from USB completely and needed a physical double-tap to come back. BLE OTA recovered by itself; serial/USB DFU did not. Now, when nothing but a missing application forces DFU, the bootloader comes up on USB DFU and lets USB enumeration pick the transport: a host that enumerates us keeps us in USB DFU, immediately re-flashable with no double-tap, and if nobody answers within DFU_USB_ENUM_TIMEOUT (battery, or a dumb charger with no data host) bootloader_dfu_start() returns on timeout and main() reboots into BLE OTA, as it already did when no valid application exists. Explicit requests keep priority and are excluded: GPREGRET magics, double reset, DFU+FRESET buttons. Parts without a USB peripheral keep BLE-only behaviour via the existing NRF_USBD guard. Enumeration is re-read on every boot, so recovery is power-loss safe - unlike GPREGRET, which a power-on reset clears. Gating on NRF_POWER->USBREGSTATUS.VBUSDETECT to skip the wait on battery was tried and does not work: measured over SWD on a ProMicro nRF52840, VBUSDETECT still reads 0 at this point in boot and only asserts tens of ms later, so an early read reports 'battery' while a host is attached and sends the device to BLE - the exact behaviour being fixed. Enumeration already subsumes VBUS: no VBUS and charger-only both simply fail to enumerate. The commit also makes an interrupted update resume on the transport it was using, so that preferring USB cannot hijack a BLE OTA that died mid-flash. The transport is recorded in GPREGRET at DFU_BANK_0_ERASED, reusing magics the bootloader already understands, so check_dfu_mode() consumes it with no new decision logic. On the OTA path the SoftDevice is enabled, so it goes through sd_power_gpregret_clr/set; is_ota() distinguishes the cases. The hint is dropped on DFU_UPDATE_APP_COMPLETE and before bootloader_app_start(), and re-armed on BLE entry only while !valid_app, which keeps a device with a working application from being trapped in DFU. The UF2/MSC path never reports DFU_BANK_0_ERASED, so no hint is set for it. One existing value changes, and it is worth calling out rather than leaving in the diff: the literal 3000 passed to bootloader_dfu_start() becomes DFU_USB_ENUM_TIMEOUT at 10000. That call is already used by the single-tap reset, UF2 and serial-only paths, so those get the longer wait too. It is only observable when nothing enumerates at all - cancel_timeout_on_usb stops the timer the moment a host answers - so the practical effect is that a device put into DFU deliberately while on battery takes 10 s instead of 3 s to give up and return to the application. 3 s turned out to be too tight to rely on: measured on a ProMicro nRF52840, a host re-enumerating after repeated resets is not always ready inside 3 s, and recovery then flapped to BLE with a good USB host attached, which is the failure this PR exists to remove. If you would rather not touch the existing paths, the narrower change is to keep 3000 for them and use DFU_USB_ENUM_TIMEOUT only for the recovery added here - say so and I will split it. The DFU magics move to bootloader_types.h next to BOOTLOADER_DFU_START so main.c and the bootloader core share one definition. Verified on a ProMicro nRF52840 with a J-Link, reading the settings page and GPREGRET over SWD rather than inferring state from LED blink rates: valid app boots; normal serial flash; abort + reset lands on USB DFU and re-flashes with no double-tap; same with GPREGRET cleared; charger-only falls back to BLE; an interrupted BLE OTA resumes on BLE with no USB device; full BLE OTA still completes; a valid app plus a BLE OTA request is not made sticky; and nrfjprog --recover plus reflash restores a bad bootloader. Also verified end to end on a XIAO nRF52840 Sense over an ESP32 USB/IP web-flash bridge: six abort-then-recover cycles, all six recovered on the first retry with no human action. Re-verified on the final branch, flashed on its own as well as merged with the others, since you may take only one of these. On a ProMicro nRF52840 with a J-Link, this branch alone: recovery comes up as CDC + mass storage with the PROMICRO volume; an interrupted BLE OTA leaves 0xA8 in GPREGRET and the boot after it stays on BLE instead of switching to USB. The same recovery result merged with the stall timeout, merged with the retry fix, and with all four together. Also on a XIAO nRF52840 Sense, which runs S140 7.3.0 rather than 6.1.1 and has no debugger, so the verdict comes from USB identity instead: after an interrupted serial DFU the board came back in UF2 mode on USB - CDC + mass storage, with INFO_UF2.TXT reporting this branch's build - and not on BLE. With all four merged, the boot following the stall timeout's self-reset also came up on USB with mass storage.
This was referenced Jul 28, 2026
fkallay1
added a commit
to fkallay1/Adafruit_nRF52_Bootloader_OTAFIX
that referenced
this pull request
Jul 29, 2026
…h handler enterUf2Dfu() is declared in the core's public cores/nRF5/wiring.h, so an application can call it directly - a MeshCore command "reboot to UF2" needs no core change at all. That is strictly better than patching TinyUSB_Port_EnterDFU(), which changes what a 1200-baud touch means for every application built on that core; since the touch is the normal Arduino upload path, on Windows a removable drive would appear on every upload, with an indexer or antivirus scanning a freshly mounted FAT volume while a serial DFU is in flight. Recorded the measurement that argument needs: serial DFU does work with MSC mounted - the XIAO was flashed from UF2 mode in 28.33 s, 928 marks, Device programmed. So MSC is a nuisance there, not a breakage. Also corrected an overstatement from the previous note. meshcore-dev/MeshCore is as much an upstream as oltaco's bootloader; a command added there needs its own PR. The asymmetry that survives is the cost of carrying the change until it lands - the command sits in a fork already maintained and built from, while the core patch would need a fork of meshcore-dev/Adafruit_nRF52_Arduino and a re-pinned platformio.ini, or PlatformIO overwrites it. And the three routes are not redundant: an application command covers a running application, PR oltaco#47's recovery covers an invalid one, and only PR oltaco#50 covers a bootloader already sitting in serial-only DFU.
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.
An interrupted serial/USB DFU left the device reachable only over BLE. The aborted transfer
correctly marks the application invalid (bootloader.c writes bank_0 = BANK_INVALID_APP on
DFU_BANK_0_ERASED), but check_dfu_mode() then defaulted !valid_app to OTA: ble_stack_init()
ran and usb_init() never did, so the device disappeared from USB completely and needed a
physical double-tap to come back. BLE OTA recovered by itself; serial/USB DFU did not.
Now, when nothing but a missing application forces DFU, the bootloader comes up on USB DFU
and lets USB enumeration pick the transport: a host that enumerates us keeps us in USB DFU,
immediately re-flashable with no double-tap, and if nobody answers within
DFU_USB_ENUM_TIMEOUT (battery, or a dumb charger with no data host) bootloader_dfu_start()
returns on timeout and main() reboots into BLE OTA, as it already did when no valid
application exists.
Explicit requests keep priority and are excluded: GPREGRET magics, double reset, DFU+FRESET
buttons. Parts without a USB peripheral keep BLE-only behaviour via the existing NRF_USBD
guard. Enumeration is re-read on every boot, so recovery is power-loss safe - unlike
GPREGRET, which a power-on reset clears.
Gating on NRF_POWER->USBREGSTATUS.VBUSDETECT to skip the wait on battery was tried and does
not work: measured over SWD on a ProMicro nRF52840, VBUSDETECT still reads 0 at this point
in boot and only asserts tens of ms later, so an early read reports 'battery' while a host
is attached and sends the device to BLE - the exact behaviour being fixed. Enumeration
already subsumes VBUS: no VBUS and charger-only both simply fail to enumerate.
The commit also makes an interrupted update resume on the transport it was using, so that
preferring USB cannot hijack a BLE OTA that died mid-flash. The transport is recorded in
GPREGRET at DFU_BANK_0_ERASED, reusing magics the bootloader already understands, so
check_dfu_mode() consumes it with no new decision logic. On the OTA path the SoftDevice is
enabled, so it goes through sd_power_gpregret_clr/set; is_ota() distinguishes the cases. The
hint is dropped on DFU_UPDATE_APP_COMPLETE and before bootloader_app_start(), and re-armed
on BLE entry only while !valid_app, which keeps a device with a working application from
being trapped in DFU. The UF2/MSC path never reports DFU_BANK_0_ERASED, so no hint is set
for it.
One existing value changes, and it is worth calling out rather than leaving in the diff: the
literal 3000 passed to bootloader_dfu_start() becomes DFU_USB_ENUM_TIMEOUT at 10000. That call is
already used by the single-tap reset, UF2 and serial-only paths, so those get the longer wait too.
It is only observable when nothing enumerates at all - cancel_timeout_on_usb stops the timer the
moment a host answers - so the practical effect is that a device put into DFU deliberately while on
battery takes 10 s instead of 3 s to give up and return to the application. 3 s turned out to be
too tight to rely on: measured on a ProMicro nRF52840, a host re-enumerating after repeated resets
is not always ready inside 3 s, and recovery then flapped to BLE with a good USB host attached,
which is the failure this PR exists to remove. If you would rather not touch the existing paths,
the narrower change is to keep 3000 for them and use DFU_USB_ENUM_TIMEOUT only for the recovery
added here - say so and I will split it.
The DFU magics move to bootloader_types.h next to BOOTLOADER_DFU_START so main.c and the
bootloader core share one definition.
Verified on a ProMicro nRF52840 with a J-Link, reading the settings page and GPREGRET over
SWD rather than inferring state from LED blink rates: valid app boots; normal serial flash;
abort + reset lands on USB DFU and re-flashes with no double-tap; same with GPREGRET
cleared; charger-only falls back to BLE; an interrupted BLE OTA resumes on BLE with no USB
device; full BLE OTA still completes; a valid app plus a BLE OTA request is not made sticky;
and nrfjprog --recover plus reflash restores a bad bootloader. Also verified end to end on a
XIAO nRF52840 Sense over an ESP32 USB/IP web-flash bridge: six abort-then-recover cycles,
all six recovered on the first retry with no human action.
Re-verified on the final branch, flashed on its own as well as merged with the others, since you
may take only one of these. On a ProMicro nRF52840 with a J-Link, this branch alone: recovery comes
up as CDC + mass storage with the PROMICRO volume; an interrupted BLE OTA leaves 0xA8 in GPREGRET
and the boot after it stays on BLE instead of switching to USB. The same recovery result merged with
the stall timeout, merged with the retry fix, and with all four together.
Also on a XIAO nRF52840 Sense, which runs S140 7.3.0 rather than 6.1.1 and has no debugger, so the
verdict comes from USB identity instead: after an interrupted serial DFU the board came back in UF2
mode on USB - CDC + mass storage, with INFO_UF2.TXT reporting this branch's build - and not on BLE.
With all four merged, the boot following the stall timeout's self-reset also came up on USB with
mass storage.
Related to #40, which asks for exactly this decision to be made by checking whether a USB host is
there at all. It suggests
NRF_USBD->USBADDR > 0or a VBUS read; this waits for the host toactually enumerate instead, because the register route does not survive contact with hardware:
measured on a ProMicro nRF52840,
USBREGSTATUS.VBUSDETECTstill reads 0 this early in boot and onlyasserts some tens of ms later, so an early read reports "battery" even with a host attached - which
would send the device to BLE exactly when USB was available. Enumeration subsumes VBUS anyway: no
VBUS and a charger with no data host both simply fail to enumerate.
Scope, so the issue is not closed on a half answer: the case that report starts from - no valid
application - now tries USB DFU first and falls back to BLE OTA when nothing enumerates. The
explicit paths (single tap, UF2, serial-only) have had an enumeration timeout all along; this only
lengthens it, from 3 s to 10 s. No general "is a host present" gate is added anywhere else.
One of four independent DFU fixes, listed in the order they matter:
Any subset can be taken, in any order. Verified: all four merge cleanly in five different orders and
in all twelve ordered pairs, and each was flashed and tested on hardware on its own as well as in
combination - 26 of 26 checks on a ProMicro nRF52840 with verdicts read over SWD, plus a XIAO
nRF52840 Sense on a different SoftDevice with no debugger.