forked from jappel/VersaMCU
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>
134 lines
6.1 KiB
Markdown
134 lines
6.1 KiB
Markdown
# 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.
|