VersaMCU/bootloader/README.md
Julian Appel 6c71f5f057 Document an OpenOCD ELF-flash pitfall found during hardware debugging
While chasing why the boot-key check stopped working, traced it to
openocd's "program <elf> verify" silently writing raw file bytes
starting at flash 0x0 instead of the ELF's own section addresses,
whenever combined with a prior bootloader write in the same OpenOCD
invocation -- repeatedly clobbering the just-flashed bootloader with
the app's ELF header. Recovered via full chip-erase and reflashing
bootloader and app as separate .bin writes with explicit addresses in
isolated OpenOCD sessions; both regions verified correct afterward and
confirmed working on hardware (key-hold entry and normal app boot).

Documented the pitfall and the safe manual-flashing rule so it doesn't
get rediscovered the expensive way again.

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

171 lines
8.9 KiB
Markdown
Raw 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.

# VersaMCU UF2-Bootloader
USB-Bootloader für das VersaPad-v2-Makropad. Wird per Atmel-ICE/SWD einmalig
auf den ATSAMD21G17D geflasht und belegt `0x0000..0x1FFF` (8 KiB). Danach
lässt sich die App-Firmware ohne SWD über USB aktualisieren: Bootloader-Modus
aktivieren, Board erscheint als USB-Laufwerk `VERSABOOT`, `.uf2`-Datei drauf
kopieren.
Der komplette Weg — Bootloader-Einstieg, `.uf2`-Erzeugung, Kopieren aufs
Laufwerk, automatischer Rücksprung in die neu geschriebene App — ist auf
echter Hardware verifiziert, siehe [Hardwaretest](#hardwaretest-2026-08-05).
Diese Platine hat keinen dedizierten Reset-/Boot-Taster. Bootloader-Modus
aktivieren heißt hier: unterste rechte Cherry-MX-Taste (key_id 24) beim
Einstecken/Reset gedrückt halten. Siehe
[Hardware-Bootloader-Einstieg](#hardware-bootloader-einstieg-ohne-reset-taster).
## Herkunft
Abgeleitet und stark eingekürzt aus
[microsoft/uf2-samdx1](https://github.com/microsoft/uf2-samdx1) (MIT-Lizenz,
siehe [`LICENSE-uf2-samdx1`](LICENSE-uf2-samdx1)). Übernommen wurden nur die
SAMD21-Bootloader-Sourcen (`src/`, `inc/`), die zugehörigen CMSIS-/ASF-Header
für genau den `SAMD21G17A`-Chip (`lib/`) sowie das Linkerskript, angepasst auf
den 128-KiB-Flash/16-KiB-RAM-Chip auf diesem Board (Original-Vorlagen zielen
auf 256-KiB-Chips wie den SAMD21G18A). Board-Vorlage war `boards/gemma_m0`
aus dem Original-Repo (crystalless, gleiche USB_VID wie unser Board).
Nicht übernommen: SAMD51/SAME54-Support, der Bootloader-Selbstupdate-Pfad
(`selfmain.c`, `sketch.cpp`, das `python2`-Tooling `uf2conv.py`/`gendata.py`)
und das Node-/Makefile-basierte Build-System — stattdessen ein eigenständiges
PlatformIO-Environment, damit dasselbe Tooling wie für die App-Firmware
ausreicht.
Für die App-Seite (nicht diesen Bootloader-Build) gibt es unter
[`../uf2conv.py`](../uf2conv.py) eine eigene, minimale Python-3-Neuimplemen-
tierung des `.bin``.uf2`-Konverters (kein Upstream-Code, passendes
Blockformat zu `inc/uf2format.h`), eingebunden über
[`../upload_uf2.py`](../upload_uf2.py) als `env:versapad_usb`-Upload-Hook.
## Build
```bash
cd bootloader
pio run -e versapad_bootloader
```
## Flash (Atmel-ICE/SWD, einmalig, überschreibt ggf. vorhandene App-Firmware nicht,
da nur `0x0000..0x1FFF` beschrieben wird)
```bash
pio run -e versapad_bootloader --target upload
```
## Hardware-Bootloader-Einstieg ohne Reset-Taster
Die Platine hat keinen dedizierten Reset-/Boot-Taster (Custom-PCB-Design).
Der klassische UF2-"Doppel-Tap-Reset" braucht aber gar keinen physischen Pin —
`check_start_application()` in `src/main.c` prüft ein RAM-Flag
(`DBL_TAP_PTR`/`DBL_TAP_MAGIC`), das bei jedem Reset gesetzt wird, unabhängig
von der Reset-Quelle.
Statt eines eigenen Tasters wird die unterste rechte Cherry-MX-Taste
(`key_id 24` in der App-Firmware, siehe `../src/config/pins.h`) missbraucht:
- `key_id 24` liegt auf COL4 (`PA08`) × ROW4 (`PA15`)
- `boot_key_pressed()` in `src/main.c` treibt ROW4 kurz auf LOW und liest
COL4 zurück — kein voller Matrixscan nötig, nur ein Drei-Pin-Check
- der Check läuft ganz am Anfang von `check_start_application()`, noch vor
der App-Adress-Validierung und vor der RCAUSE-/DBL-TAP-Logik: funktioniert
also auch bei kaputter/gelöschter App-Firmware und bei normalem Power-On
(Stecker ziehen/reinstecken), kein Software-Trigger in der App nötig
- Pin-Konstanten stehen in `include/board_config.h`
(`BOOT_KEY_ROW_PIN`/`BOOT_KEY_COL_PIN`)
Bedienung: USB-Kabel ziehen, unterste rechte Taste gedrückt halten, Kabel
wieder einstecken (Taste dabei weiter halten) → Board bootet direkt in
`VERSABOOT`. Ohne gehaltene Taste startet die App normal.
## App-Firmware per USB flashen (nach dem einmaligen Bootloader-Flash)
```bash
# Board zuerst in den Bootloader-Modus versetzen: USB ziehen, unterste
# rechte Taste halten, wieder einstecken (siehe oben)
pio run -e versapad_usb --target upload
```
Baut die App-Firmware (Repo-Root, nicht `bootloader/`), erzeugt `firmware.uf2`
und kopiert es aufs `VERSABOOT`-Laufwerk. Kein Atmel-ICE mehr nötig.
## Achtung bei manuellem SWD-Flashen von Bootloader UND App
`upload_openocd.py` (App, `env:versapad`) und der Bootloader-Upload (oben)
sind sicher, weil sie jeweils nur ihren eigenen Bereich anfassen. **Beim
manuellen Debuggen über OpenOCD-Kommandozeile aber Vorsicht:**
`openocd -c "program datei.elf verify"` **ohne explizite Zieladresse** hat
sich in dieser Kombination aus OpenOCD-Version/CMSIS-DAP-Adapter/Target-Skript
als unzuverlässig erwiesen — statt die im ELF hinterlegten Sektionsadressen
(`0x2000` für die App) zu nutzen, landeten die rohen Datei-Bytes teils direkt
ab Flash-Adresse `0x0000` und haben damit den frisch geschriebenen Bootloader
sofort wieder überschrieben (bestätigt am 2026-08-05: Byte 0 an Adresse
`0x0000` war `0x7f`, der Beginn der ELF-Magic `\x7fELF` — die rohe Datei, kein
Firmware-Code). Passierte zuverlässig, wenn Bootloader- und App-Flash im
selben OpenOCD-Aufruf kombiniert wurden.
**Regel für manuelles SWD-Flashen beider Bereiche:** immer die `.bin`-Datei
verwenden (nie `.elf`) und die Zieladresse **immer explizit angeben**
(`program firmware.bin 0x0 verify` für den Bootloader,
`program firmware.bin 0x2000 verify` für die App), und Bootloader- und
App-Schreibvorgang **in getrennten OpenOCD-Aufrufen**, nicht in einer
gemeinsamen `-c`-Kommandokette. Im Zweifel danach mit `dump_image` in einer
frischen, unabhängigen Sitzung verifizieren.
## Hardwaretest (2026-08-05)
Erster vollständiger Hardwaretest auf einem echten VersaPad-v2-Board über
Atmel-ICE/SWD. Ergebnisse:
- Bootloader-Build/-Flash/-Verify laufen sauber (89,9 % Flash, 7364/8192 Byte).
- USB-Enumeration und `VERSABOOT`-Massenspeicher-Modus funktionieren nach dem
PID-Fix (siehe unten) korrekt, inklusive korrektem `INFO_UF2.TXT`.
- Der Tastencheck (oben) funktioniert wie vorgesehen: gehaltene Taste beim
Boot → `VERSABOOT`, sonst normaler App-Start.
- **Kritischer Bug gefunden und behoben:** Der Sprung vom Bootloader in die
App (`__set_MSP``SCB->VTOR``bx`) führte bei jedem echten,
eigenständigen Boot (auch bei komplett getrenntem Debugger) zu einem
Hard-Fault/Lockup der CPU. Identische Register-/VTOR-Werte, vom Debugger
bei angehaltener CPU direkt injiziert, liefen dagegen einwandfrei — das
grenzte den Fehler auf die *Ausführung* der Sprungsequenz selbst ein, nicht
auf falsche Werte. Fix: `__DSB(); __ISB();` zwischen dem `SCB->VTOR`-Schreib-
zugriff und dem `bx` in `check_start_application()` (`src/main.c`) — von ARM
für genau dieses Bootloader-Pattern vorgeschrieben, hat im vendorten Code
gefehlt. Nach dem Fix bootet die App-Firmware zuverlässig, mit und ohne
angeschlossenen Debugger.
- **Zweiter Bug beim manuellen Debuggen gefunden:** Beim anschließenden
manuellen SWD-Debugging (Suche nach der Ursache des Tastencheck-Problems,
siehe oben) hat `openocd -c "program app.elf verify"` ohne explizite
Zieladresse wiederholt den Bootloader mit rohen ELF-Datei-Bytes
überschrieben, sobald Bootloader- und App-Flash im selben OpenOCD-Aufruf
kombiniert wurden — siehe "Achtung bei manuellem SWD-Flashen" oben. Nach
einem vollständigen Chip-Erase und getrennten `.bin`-Flashes mit expliziten
Adressen liefen beide Bereiche wieder zuverlässig, Tastencheck und
App-Start bestätigt funktionsfähig.
- **Kompletter USB-Flashweg getestet:** `pio run -e versapad_usb --target
upload` (App-Firmware, Repo-Root) baut `firmware.bin`, wandelt es über
[`../uf2conv.py`](../uf2conv.py) in `firmware.uf2` und kopiert es über
[`../upload_uf2.py`](../upload_uf2.py) automatisch aufs erkannte
`VERSABOOT`-Laufwerk. Der Bootloader erkennt den Schreibzugriff und
springt danach selbständig in die neue App — kein manuelles Auswerfen
oder Reset nötig. Voraussetzung: Board zuvor per gehaltener Taste (siehe
oben) in den Bootloader-Modus versetzt.
## Bekannte Einschränkungen
- Flash-Auslastung ~90 % (7364 von 8192 Byte). Wenig Puffer für Änderungen.
- Kein Status-LED-Feedback im Bootloader-Modus, da die Platine nur eine
WS2812-Kette (kein einfaches GPIO-LED oder DotStar) hat und der
Original-Code dafür nicht ausgelegt ist.
- USB_PID war ursprünglich `0x0011` (Adafruit Gemma M0s eigene
Bootloader-PID, unverändert aus der Vorlage übernommen) — kollidierte auf
Rechnern mit installiertem Adafruit-Treiber, wurde als `Adafruit Circuit
Playground`-COM-Port statt als Massenspeicher gebunden. Verifiziert am
2026-08-05, seither `0x0043`. Bleibt ein Wert ohne echte Registrierung
(kein offiziell zugeteilter PID unter Adafruits VID); ein sauber eigener
VID (z. B. über pid.codes) wäre die langfristig korrekte Lösung, ist aber
nicht Teil dieses Branches.
- [`../upload_uf2.py`](../upload_uf2.py) sucht das `VERSABOOT`-Laufwerk aktuell
nur über die Windows-API (`GetVolumeInformationW`) — passend zur bisherigen
Dev-Umgebung dieses Projekts, aber nicht plattformübergreifend. Für
macOS/Linux müsste die Laufwerkssuche noch ergänzt werden.