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

134 lines
6.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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](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](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`):
```bash
cd bootloader
pio run -e versapad_bootloader --target upload
```
Danach App-Firmware per USB (Board vorher wie oben in den Bootloader-Modus
versetzen):
```bash
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:versapad``env: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):
```bash
pio run -e versapad -t erase-bootloader-and-flash
```
## Herkunft und Details
Der Bootloader ist abgeleitet und stark eingekürzt aus
[microsoft/uf2-samdx1](https://github.com/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 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](07_serial_protocol.md)) nachrüstbar, der dieselbe
`DBL_TAP_MAGIC`-RAM-Adresse setzt und `NVIC_SystemReset()` aufruft.