Date: 2026-06-11
This document records the current upload truth exposed by the package. It does not claim that every clone board is fully verified.
These boards currently declare USB-backed upload support in package metadata:
promicro_nrf52840nicenano_v2supermini_nrf52840nrfmicro_nrf52840mini_nrf52840xiao_nrf52840devboard_nrf52840pitaya_go_nrf52840usb_dongle_nrf52840
Boards with SWD upload support expose explicit Arduino IDE Upload Method entries for the probe type:
SWD programmer (CMSIS-DAP)uses OpenOCD withtools/openocd/nrf52-cmsis-dap.cfg.SWD programmer (SEGGER J-Link)uses SEGGERJLink.exethroughupload.ps1.
These entries are for the normal Upload button. They do not depend on the
IDE Tools -> Programmer selection, because Arduino upload recipes use the
board's selected Upload Method properties. Use Tools -> Programmer for
Sketch -> Upload Using Programmer and Tools -> Burn Bootloader.
SEGGER J-Link (SWD) uses SEGGER's command-line tools; CMSIS-DAP (SWD) uses
OpenOCD for locked-target recover. For locked nRF52 parts, the recover/erase
step is what clears APPROTECT; the bootloader HEX is flashed afterward.
Arduino IDE 2 SWD Debug follows the selected Upload Method: CMSIS-DAP keeps
the OpenOCD server, while the J-Link upload method switches the debug server
metadata to Arduino IDE's jlink server type.
The devboard_nrf52833 target is SWD-first and exposes only these SWD upload
choices because it has no native USB upload path in this package.
tools/niusrobotlab/upload.ps1owns the touch/reset sequence on Windows instead of relying on the Arduino CLI default touch path.Bootloader / DFU → Auto-detectprefers a matching UF2 mass-storage volume when the selected board is already in bootloader mode. Explicit serial-DFU menu entries still useadafruit-nrfutil.- UF2 drives are matched to the selected serial port by stable USB identity. If two boards expose the same volume label, the wrapper refuses ambiguous matches instead of choosing the first drive.
- When a UF2 volume is visible,
upload.ps1readsINFO_UF2.TXTand uses theSoftDevicefield to infer the mounted layout (0x1000,0x26000, or0x27000). If that layout conflicts with the app start used by the selected Arduino IDEBootloader / DFUoption, upload fails before copying firmware. This applies to UF2 deploy, serial DFU, and the UF2→serial fallback path. Serial DFU now requires a scoped UF2 volume withINFO_UF2.TXTon the selected board before transfer; the wrapper polls up to ~12s (and may re-touch 1200 bps once) and fails withlayoutif layout evidence is missing or mismatched. No firmware is written when the pre-flash guard fires; the board stays in UF2/DFU. This is intentional: Arduino compiles before upload, so the wrapper cannot safely relocate an already-linked image. - Layout guard (
layoutfailure): compares the IDEBootloader / DFUapp start (0x1000/0x26000/0x27000) againstINFO_UF2.TXTon the selected board's UF2 drive (matched by stable USB identity, not drive letter). Disable withNIUS_DISABLE_LAYOUT_GUARD=1(not recommended). - Misflash guard (
misflashfailure): after same-PID serial DFU, waits for the service COM to return in application mode. If USB never comes back (typical when app start was wrong), the wrapper attempts 1200 bps touch / UF2 recovery and fails with an IDE-visible message. Disable withNIUS_DISABLE_MISFLASH_GUARD=1. - Manual UF2 drag in DFU mode is not guarded. Copying a
.uf2from Explorer bypassesupload.ps1. You must match bootloader layout, not just the version string inINFO_UF2.TXT(for example0.6.0exists in both S140@0x26000and no-SoftDevice@0x1000variants). Sketch UF2 must match the mounted layout. Adafruit update-* bootloader packages (family0xd663823c) rewrite the bootloader and reboot into application mode; they are not sketch images. After a layout switch, also flash a matching app or USB may disappear. See ../bootloaders/README.md. - USB silent / COM missing after a bad or partial flash: the host cannot
1200-touch a port that is gone. Recovery is manual: double-tap RESET to
re-enter UF2 (on boards without a reset button, short RST to GND twice
quickly, like a double-tap), then fix the
Bootloader / DFUmenu and upload again; or recover over SWD (Tools → Programmer → SEGGER J-Link (SWD)or CMSIS-DAP, then Burn Bootloader / sketch upload). See ../bootloaders/README.md. Upload Method → Enter UF2 drive only (no upload)performs the touch/bootloader wait, reports the matched drive, and exits before copying firmware.- If the selected upload COM is stale after a mode change, the wrapper fails with a clear "re-select the current SERVICE/DFU port" message instead of falling through to another board.
- ProMicro-class application firmware uses runtime PID
0x00B4; PID0x00B3belongs to the UF2 bootloader. Keep this split even for no-SoftDevice builds: the bootloader exposes interface 2 as mass storage, while TaichiUSB exposes it as the user CDC port. Reusing one PID and chip serial for both descriptor layouts makes Windows retain the wrong per-interface driver binding. - After serial DFU, the upload wrapper accepts the selected runtime COM when it returns or a newly enumerated COM with the expected runtime VID/PID. Windows may assign a different number when the bootloader and application expose different composite interfaces; that remap is not an upload failure.
- The runtime DFU interface is hidden by default. Hands-free upload uses the service CDC's 1200-bps touch, so the extra driverless DFU-runtime node is not needed for ordinary use. Enable it explicitly only for a workflow that sends USB DFU runtime requests directly.
- The previous service-port "boot token" fallback (
~NIUSBL!42\rafter arming with line coding134/8/2/2 + DTR+RTS) has been removed — the standard 1200 bps touch path is now the single primary trigger.
- first upload from manual bootloader mode works on the user's board
- the board returns to user mode afterward
- with
usbcdc=disabled, the board keeps a single visible SERVICE CDC path - UF2 upload from the current DFU port works in both
bootloader=autoand explicit UF2 menu modes - explicit Adafruit serial DFU from the current DFU port works and is not confused by a mounted UF2 volume
- with two boards simultaneously mounted as
NICENANO, the selected board maps to its own drive (J:vsK:in the hardware run) - selecting a stale COM after the board re-enumerates is rejected before any upload can target another board
- a second upload from user mode works — the 1200 bps touch triggers
NVIC_SystemReset()into the bootloader, the selected transport streams the image, and the board re-boots into user mode.
Before the V1 firmware fixes, adafruit-nrfutil would stall at
Sending DFU start packet during the second upload because the firmware
never re-entered bootloader in response to the 1200 bps touch. The host then
saw Port never detached after touch (port stayed present) repeatedly across
the four host-side trigger mechanisms upload.ps1 used to try.
Root cause: three cooperating firmware bugs in NrfUsbd.cpp — EP0 OUT
EasyDMA never triggered, EP0 OUT direction routed by stale BMREQUESTTYPE,
and a subsequent DTR=true cancelling serviceTouchPending_ inside the 40 ms
confirm window.
For a while, an in-app upload to a usbcdc=disabled board stalled at
adafruit-dfu even though the firmware touch path was correct. Root cause was
host-side: with no user CDC, the runtime DFU "Bootloader Control" vendor
interface lands on MI_02 (where usbcdc=enabled puts the user CDC). upload.ps1
counted any non-Ports MI_02 interface as MSC/bootloader evidence, decided the
board was already in the bootloader, skipped the 1200-touch entirely, and
ran adafruit-nrfutil against the still-running app. The fix excludes the
Bootloader Control interface from that evidence (the real bootloader is
identified by its UF2 mass-storage volume, never by this control interface), so
the touch runs and the board reboots normally. Verified 3× back-to-back plus
usbcdc transitions in both directions.
- Boards beyond the user's ProMicro clone that share the firmware path
- Linux/macOS UF2 parity; those platforms still use the Python/Adafruit serial-DFU recipe