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

8.9 KiB
Raw Blame History

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.

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.

Herkunft

Abgeleitet und stark eingekürzt aus microsoft/uf2-samdx1 (MIT-Lizenz, siehe 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 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 als env:versapad_usb-Upload-Hook.

Build

cd bootloader
pio run -e versapad_bootloader

Flash (Atmel-ICE/SWD, einmalig, überschreibt ggf. vorhandene App-Firmware nicht,

da nur 0x0000..0x1FFF beschrieben wird)

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)

# 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_MSPSCB->VTORbx) 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 in firmware.uf2 und kopiert es über ../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 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.