VersaMCU/doc/10_usb_bootloader.md
Julian Appel 01c5e0930e Guard env:versapad's upload against overwriting the bootloader
pio run -e versapad --target upload writes the app starting at
0x0000 and silently destroyed the installed UF2 bootloader twice
today during testing -- no warning, no error, just a board that
stopped responding to the boot-key hold.

upload_openocd.py now checks for the bootloader (verify_image against
the locally built bootloader/.pio/build/versapad_bootloader/firmware.bin)
before an env:versapad upload and refuses if one is present, pointing
at env:versapad_usb instead. Fails closed: an inconclusive check (e.g.
bootloader not built locally, SWD not responding) blocks rather than
proceeding on a guess -- confirmed necessary the hard way, since a
"fail open" first attempt let the destructive upload through silently.
Scoped to PIOENV == "versapad" only, since bootloader/platformio.ini's
own upload reuses this same script and must always be allowed to write
0x0000. A new erase-bootloader-and-flash custom target remains as the
explicit, deliberate override.

Documented the workflow (bootloader is its own PlatformIO project,
flashed once via SWD; versapad_usb is the normal path afterward;
versapad's upload is now guarded) in README.md and
doc/10_usb_bootloader.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 23:27:02 +02:00

6.1 KiB
Raw Permalink Blame History

USB-Bootloader (UF2)

Zweiter Firmware-Update-Weg neben SWD: ein UF2-Bootloader unter bootloader/ erlaubt, die App-Firmware über USB zu aktualisieren, ohne Atmel-ICE. Der Bootloader selbst wird einmalig per SWD geflasht; danach reicht für App-Updates ein PlatformIO-Befehl.

Speicherlayout

Bereich Adresse Größe Environment
Bootloader 0x00000..0x01FFF 8 KiB versapad_bootloader (in bootloader/)
App-Firmware 0x02000..0x1FAFF 118,75 KiB versapad_usb
NVM (Makros + Config) 0x1FB00..0x1FFFF 1,25 KiB

Gleiches NVM-Layout wie beim SWD-Ziel versapad_nobl (siehe 06_nvm_config.md); nur die Firmware-Obergrenze sinkt um die 8 KiB Bootloader. Linkerskript: variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld, Boarddatei: boards/versapad.json.

Bootloader-Einstieg ohne Reset-Taster

Die Platine hat keinen dedizierten Reset-/Boot-Taster. Statt eines physischen Pins wird die unterste rechte Cherry-MX-Taste (key_id 24, COL4 × ROW4, siehe 01_matrix.md) beim Boot geprüft: check_start_application() in bootloader/src/main.c treibt ROW4 kurz auf LOW und liest COL4 zurück — noch vor der App-Adress-Validierung, funktioniert also auch bei kaputter oder fehlender App-Firmware.

Bedienung: USB-Kabel ziehen, Taste gedrückt halten, wieder einstecken (Taste weiter halten) → Board bootet in den Massenspeicher-Modus (VERSABOOT). Ohne gehaltene Taste startet die App normal.

Bauen und Flashen

Bootloader einmalig per SWD (überschreibt nur 0x0000..0x1FFF):

cd bootloader
pio run -e versapad_bootloader --target upload

Danach App-Firmware per USB (Board vorher wie oben in den Bootloader-Modus versetzen):

pio run -e versapad_usb --target upload

Baut die App, wandelt firmware.bin über uf2conv.py (Repo-Root, eigene minimale Implementierung, kein Upstream-Code) in firmware.uf2 und kopiert es über upload_uf2.py aufs erkannte VERSABOOT-Laufwerk. Der Bootloader erkennt den Schreibzugriff und springt selbständig in die neue App.

upload_uf2.py sucht das Laufwerk aktuell nur über die Windows-API (GetVolumeInformationW) — keine macOS/Linux-Unterstützung.

Schutz gegen versehentliches Überschreiben (env:versapad)

env:versapad (SWD, boards/versapad_nobl.json) schreibt die App-Firmware ab 0x0000 und überschreibt damit einen installierten Bootloader kommentarlos — genau das ist am 2026-08-05 während der Entwicklung passiert (zweimal). upload_openocd.py prüft das seither vor jedem versapad-Upload:

  • vergleicht per verify_image gegen die lokal gebaute bootloader/.pio/build/versapad_bootloader/firmware.bin
  • Bootloader erkannt → Upload wird verweigert, mit Hinweis auf versapad_usb oder den expliziten Override
  • lässt sich die Prüfung nicht eindeutig durchführen (z. B. bootloader/ noch nicht gebaut, oder die SWD-Verbindung antwortet nicht) → wird sicherheitshalber ebenfalls verweigert, nicht durchgelassen
  • gilt nur für env:versapadenv:versapad_bootloader nutzt dasselbe Skript (extra_scripts = ../upload_openocd.py) und schreibt bewusst immer auf 0x0000, ungeprüft

Bewusstes Überschreiben (zurück zu reinem SWD-Betrieb ohne Bootloader):

pio run -e versapad -t erase-bootloader-and-flash

Herkunft und Details

Der Bootloader ist abgeleitet und stark eingekürzt aus microsoft/uf2-samdx1 (MIT), auf den SAMD21G17D-Chip dieses Boards zugeschnitten. Herkunft, Lizenz und vollständige Implementierungsdetails stehen in bootloader/README.md.

Hardwaretest (2026-08-05) und gefundene Bugs

Kompletter Weg auf echter Hardware verifiziert: Bootloader-Flash, USB-Enumeration, Tastencheck-Einstieg, App-Firmware per .uf2 schreiben, automatischer Rücksprung in die neue App. Details, Fehlerbilder und Fixes stehen in bootloader/README.md unter "Hardwaretest"; kurz zusammengefasst:

  • Fehlende Synchronisationsbarriere beim Sprung Bootloader→App. Der Sprung (__set_MSPSCB->VTORbx) faultete bei jedem eigenständigen Boot. Fix: __DSB(); __ISB(); vor dem bx in check_start_application() — von ARM für dieses Pattern vorgeschrieben, fehlte im vendorten Code.
  • USB-PID-Kollision. Die ursprünglich übernommene PID 0x0011 ist Adafruits eigene Gemma-M0-Bootloader-PID; Windows-Rechner mit installiertem Adafruit-Treiber banden das Board fälschlich als Adafruit Circuit Playground-COM-Port statt als Massenspeicher. Fix: eigene PID 0x0043.
  • OpenOCD-Falle beim manuellen SWD-Debuggen: program datei.elf verify ohne explizite Zieladresse hat wiederholt den Bootloader mit rohen ELF-Datei-Bytes überschrieben, sobald Bootloader- und App-Flash im selben OpenOCD-Aufruf kombiniert wurden. Bei manuellem Flashen beider Bereiche immer .bin mit expliziter Adresse und getrennte OpenOCD-Aufrufe verwenden — Details in bootloader/README.md, "Achtung bei manuellem SWD-Flashen".
  • env:versapad überschreibt den Bootloader kommentarlos. Ein normaler pio run -e versapad --target upload (der alte, gewohnte SWD-Weg für App-Updates) schreibt ab 0x0000 und hat den Bootloader dabei zweimal ohne jede Warnung zerstört. Fix: automatischer Presence-Check in upload_openocd.py, siehe "Schutz gegen versehentliches Überschreiben" oben.

Bekannte Einschränkungen

  • Bootloader-Flash-Auslastung ~90 % (7364 von 8192 Byte) — wenig Puffer für Änderungen am Bootloader selbst.
  • Kein Status-LED-Feedback im Bootloader-Modus (nur WS2812-Kette, dafür ist der Original-Code nicht ausgelegt).
  • USB-PID 0x0043 ist kollisionsfrei verifiziert, aber kein offiziell registrierter Wert unter Adafruits VID 0x239A.
  • upload_uf2.py ist Windows-only (siehe oben).
  • Kein Software-Trigger, um aus der laufenden App heraus in den Bootloader zurückzuspringen — nur der physische Kabel-raus/Taste-halten/Kabel-rein-Weg. Wäre über einen neuen CDC-Befehl (siehe 07_serial_protocol.md) nachrüstbar, der dieselbe DBL_TAP_MAGIC-RAM-Adresse setzt und NVIC_SystemReset() aufruft.