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>
6.1 KiB
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_imagegegen die lokal gebautebootloader/.pio/build/versapad_bootloader/firmware.bin - Bootloader erkannt → Upload wird verweigert, mit Hinweis auf
versapad_usboder 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:versapad—env:versapad_bootloadernutzt dasselbe Skript (extra_scripts = ../upload_openocd.py) und schreibt bewusst immer auf0x0000, 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_MSP→SCB->VTOR→bx) faultete bei jedem eigenständigen Boot. Fix:__DSB(); __ISB();vor dembxincheck_start_application()— von ARM für dieses Pattern vorgeschrieben, fehlte im vendorten Code. - USB-PID-Kollision. Die ursprünglich übernommene PID
0x0011ist Adafruits eigene Gemma-M0-Bootloader-PID; Windows-Rechner mit installiertem Adafruit-Treiber banden das Board fälschlich alsAdafruit Circuit Playground-COM-Port statt als Massenspeicher. Fix: eigene PID0x0043. - OpenOCD-Falle beim manuellen SWD-Debuggen:
program datei.elf verifyohne 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.binmit expliziter Adresse und getrennte OpenOCD-Aufrufe verwenden — Details inbootloader/README.md, "Achtung bei manuellem SWD-Flashen". env:versapadüberschreibt den Bootloader kommentarlos. Ein normalerpio run -e versapad --target upload(der alte, gewohnte SWD-Weg für App-Updates) schreibt ab0x0000und hat den Bootloader dabei zweimal ohne jede Warnung zerstört. Fix: automatischer Presence-Check inupload_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
0x0043ist kollisionsfrei verifiziert, aber kein offiziell registrierter Wert unter Adafruits VID0x239A. upload_uf2.pyist 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 undNVIC_SystemReset()aufruft.