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