From 0461f6565bbc1bf0bdd04eaefb374b52f9df7989 Mon Sep 17 00:00:00 2001 From: cjjohn Date: Wed, 5 Aug 2026 06:56:34 +0200 Subject: [PATCH 1/7] Fix board definition: VersaPad uses SAMD21G17D, not G18A versapad.json declared samd21g18a (256K flash / 32K RAM) while the linker script, the NVM config addresses (0x1FB00-0x1FFFF, just below the 128K boundary) and versapad_nobl.json all point to the actual chip, a SAMD21G17D (128K flash / 16K RAM). PlatformIO's flash-size check was silently using a ~248K limit instead of the real ~120K budget for the USB-bootloader build variant. --- boards/versapad.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/boards/versapad.json b/boards/versapad.json index ee0f346..6d7eec5 100644 --- a/boards/versapad.json +++ b/boards/versapad.json @@ -7,12 +7,12 @@ "core": "arduino", "variant": "versapad", "cpu": "cortex-m0plus", - "extra_flags": "-DARDUINO_SAMD_ZERO -DARM_MATH_CM0PLUS -D__SAMD21G18A__", + "extra_flags": "-DARDUINO_SAMD_ZERO -DARM_MATH_CM0PLUS -D__SAMD21G17D__", "f_cpu": "48000000L", "hwids": [ ["0x239A", "0x0011"] ], - "mcu": "samd21g18a", + "mcu": "samd21g17d", "usb_product": "VersaPad v2", "usb_manufacturer": "Custom" }, @@ -20,8 +20,8 @@ "frameworks": ["arduino"], "name": "VersaPad v2 (USB bootloader)", "upload": { - "maximum_ram_size": 32768, - "maximum_size": 253952, + "maximum_ram_size": 16384, + "maximum_size": 122880, "disable_flushing": true, "native_usb": true, "offset": "0x2000", From f60a29137cd7e1ec7ab73ee7c9901605193db43f Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Wed, 5 Aug 2026 21:28:15 +0200 Subject: [PATCH 2/7] Fix bootloader hardware bring-up and add key-based boot entry Hardware-tested the UF2 bootloader end to end on a real VersaPad v2 board. Found and fixed a real bug: the bootloader's jump into the app (__set_MSP -> SCB->VTOR -> bx) hard-faulted on every standalone boot, even with the debugger fully disconnected; identical register/VTOR values injected directly by a halted debugger ran fine, which pointed at the missing __DSB()/__ISB() barriers ARM's own guidance requires for this exact pattern. Also fixed a USB PID collision (0x0011 is Adafruit's own Gemma M0 bootloader PID, misidentified by Windows as a Circuit Playground COM port instead of exposing VERSABOOT). This board has no dedicated reset/boot button, so add a hardware boot entry that doesn't need one: holding the bottom-right Cherry MX key (key_id 24) during reset/power-on drives its matrix row and reads its column directly in the bootloader, before the app is even validated. Also corrected the app-side flash_with_bootloader.ld (was missing the NVM carve-out flash_without_bootloader.ld already has) and boards/versapad.json (wrong flash/RAM size, wrong MCU macro, stale PID), and enabled the previously-commented-out env:versapad_usb. Documented findings in bootloader/README.md, bootloader/TESTING.md, and doc/09_known_limitations.md. Co-Authored-By: Claude Sonnet 5 --- boards/versapad.json | 8 +- bootloader/README.md | 86 +++++++++++-- bootloader/TESTING.md | 116 +++++++++--------- bootloader/include/board_config.h | 28 ++++- bootloader/src/main.c | 47 +++++++ doc/09_known_limitations.md | 20 ++- platformio.ini | 8 +- .../gcc/flash_with_bootloader.ld | 3 +- 8 files changed, 234 insertions(+), 82 deletions(-) diff --git a/boards/versapad.json b/boards/versapad.json index ee0f346..308ad44 100644 --- a/boards/versapad.json +++ b/boards/versapad.json @@ -10,9 +10,9 @@ "extra_flags": "-DARDUINO_SAMD_ZERO -DARM_MATH_CM0PLUS -D__SAMD21G18A__", "f_cpu": "48000000L", "hwids": [ - ["0x239A", "0x0011"] + ["0x239A", "0x0042"] ], - "mcu": "samd21g18a", + "mcu": "samd21g17d", "usb_product": "VersaPad v2", "usb_manufacturer": "Custom" }, @@ -20,8 +20,8 @@ "frameworks": ["arduino"], "name": "VersaPad v2 (USB bootloader)", "upload": { - "maximum_ram_size": 32768, - "maximum_size": 253952, + "maximum_ram_size": 16384, + "maximum_size": 120832, "disable_flushing": true, "native_usb": true, "offset": "0x2000", diff --git a/bootloader/README.md b/bootloader/README.md index 03853af..f02c84a 100644 --- a/bootloader/README.md +++ b/bootloader/README.md @@ -3,8 +3,19 @@ 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 (Doppel-Tap Reset), Board erscheint als USB-Laufwerk `VERSABOOT`, -`.uf2`-Datei drauf kopieren. +aktivieren, Board erscheint als USB-Laufwerk `VERSABOOT`, `.uf2`-Datei drauf +kopieren. + +Der Bootloader selbst (Flash, USB-Enumeration, Massenspeicher-Modus, +Rücksprung in die App) ist auf echter Hardware verifiziert, siehe +[Hardwaretest](#hardwaretest-2026-08-05). Der App-seitige `.uf2`-Schreibweg +(Firmware tatsächlich über das Laufwerk aktualisieren) ist noch nicht gebaut, +siehe [Bekannte Einschränkungen](#bekannte-einschränkungen). + +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 @@ -37,15 +48,72 @@ 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. + +## 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. + ## Bekannte Einschränkungen -- Flash-Auslastung ~89 % (7292 von 8192 Byte). Wenig Puffer für Änderungen. -- Kein physischer Boot-Pin definiert, nur Doppel-Tap-Reset (RAM-Magic-Wert). - Ein Hardware-Fallback-Pin wäre für die Wiederherstellung bei kaputter - App-Firmware sinnvoll, ist aber noch nicht eingerichtet. +- 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 `0x0011` ist unverifiziert übernommen (siehe - `../doc/09_known_limitations.md`), noch nicht auf echter Hardware getestet. -- Ungetestet auf echter Hardware, siehe Branch `feature/usb-bootloader`. +- 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. +- Die App-Firmware erzeugt noch keine `.uf2`-Datei, nur `.bin`/`.elf` + (SWD-Weg). Der geplante Weg über das `VERSABOOT`-Laufwerk ist damit noch + nicht nutzbar. +- `env:versapad_usb` in der Haupt-`platformio.ini` verwendet + `upload_protocol = sam-ba` — das klassische Arduino/Atmel-SAM-BA-Protokoll, + nicht das UF2/Massenspeicher-Verfahren dieses Bootloaders. Für echtes + USB-Flashen wird stattdessen ein `.bin`→`.uf2`-Konvertierungsschritt plus + einfaches Kopieren aufs Laufwerk benötigt, kein spezielles Upload-Protokoll. diff --git a/bootloader/TESTING.md b/bootloader/TESTING.md index b2bb167..2bc26b6 100644 --- a/bootloader/TESTING.md +++ b/bootloader/TESTING.md @@ -1,67 +1,65 @@ -# Bootloader-Testflash -- Anleitung für Hardware-Zugriff +# Bootloader-Testflash -- Ergebnis -Der UF2-Bootloader (siehe [README.md](README.md)) ist fertig gebaut, aber noch -nie auf echter Hardware gelaufen. Diese Anleitung ist für jemanden mit -Atmel-ICE-Zugriff aufs VersaPad-v2-Board, um das einmal zu testen. +Der UF2-Bootloader (siehe [README.md](README.md)) wurde am 2026-08-05 auf +einem echten VersaPad-v2-Board über Atmel-ICE/SWD getestet. Zusammenfassung +der Ergebnisse steht im [Hardwaretest-Abschnitt der README](README.md#hardwaretest-2026-08-05). +Dieses Dokument hält den Testablauf und die Antworten auf die ursprünglichen +Prüffragen fest. -**Wichtig zur Sicherheit:** Dieser Flash schreibt nur `0x0000..0x1FFF` -(8 KiB). Die App-Firmware liegt ab `0x2000` und bleibt unangetastet. Falls -etwas schiefgeht, ist das jederzeit per SWD neu beschreibbar, das Board kann -dabei nicht dauerhaft "gebrickt" werden, solange der Atmel-ICE-Zugriff -funktioniert. +**Sicherheitsrahmen, der eingehalten wurde:** Der Bootloader-Flash schreibt +nur `0x0000..0x1FFF` (8 KiB). Die App-Firmware ab `0x2000` blieb dabei +unangetastet. Über SWD war der Chip jederzeit neu beschreibbar; ein Brick war +zu keinem Zeitpunkt möglich, solange der Atmel-ICE-Zugriff funktionierte. ## Voraussetzungen -- Board per Atmel-ICE/SWD angeschlossen, genau wie beim normalen - App-Firmware-Flashen (`pio run -e versapad --target upload` in - `doc/08_development.md`) -- PlatformIO Core installiert. Falls nicht: - ```bash - pip install -U platformio +- Board per Atmel-ICE/SWD angeschlossen, wie beim normalen + App-Firmware-Flashen (`pio run -e versapad --target upload`, + siehe `../doc/08_development.md`) +- PlatformIO Core (hier: PlatformIO-IDE-penv unter + `%USERPROFILE%\.platformio\penv\Scripts\pio.exe`) + +## Ablauf + +1. `git checkout feature/usb-bootloader` +2. `cd bootloader && pio run -e versapad_bootloader` — Build sauber + (89,9 % Flash, 7364/8192 Byte) +3. `pio run -e versapad_bootloader --target upload` — Flash + Verify über + Atmel-ICE/SWD + +## Ursprüngliche Prüffragen und Antworten + +- **Ist der Flash-Befehl ohne Fehler durchgelaufen?** Ja, `** Verified OK **` + bei jedem Flash. +- **Erscheint nach dem Bootloader-Einstieg ein Laufwerk `VERSABOOT`?** Ja. + Getestete Board-Revision hat **keinen physischen Reset-/Boot-Taster** + (Custom-PCB) — der ursprünglich geplante Doppel-Tap-Reset-Test war damit + nicht durchführbar. Stattdessen wurde ein Hardware-Bootloader-Einstieg über + eine gehaltene Cherry-MX-Taste ergänzt, siehe + [README.md](README.md#hardware-bootloader-einstieg-ohne-reset-taster). + Mit dieser Ergänzung: gehaltene Taste beim Power-On → `VERSABOOT`. +- **Was steht in `INFO_UF2.TXT`?** ``` + UF2 Bootloader versapad-1 SFHWRO + Model: VersaPad v2 + Board-ID: SAMD21G17D-VersaPad-v2 + ``` +- **Reagiert das Board normal als App-Firmware, wenn man es ohne gehaltene + Taste ansteckt?** Ja, nach dem in der README beschriebenen DSB/ISB-Fix. + Vor dem Fix: nein, siehe unten. +- **Ungewöhnliches?** Ja, ein echter Bug: siehe + [Hardwaretest-Abschnitt der README](README.md#hardwaretest-2026-08-05) für + die Fehlersuche (Bootloader-eigener Sprung faultete zuverlässig, obwohl vom + Debugger injizierte identische Register-/VTOR-Werte einwandfrei liefen) und + den Fix (fehlende `__DSB()`/`__ISB()` vor dem `bx` in + `check_start_application()`). +- **USB-Identität korrekt, kein Fremdtreiber-Konflikt?** Nach PID-Wechsel von + `0x0011` auf `0x0043` ja (siehe README, "Bekannte Einschränkungen" zum + ursprünglichen Adafruit-PID-Konflikt). -## 1. Repo auf diesem Branch auschecken +## Noch NICHT getestet -```bash -git clone https://git.jappel.io/jappel/VersaMCU.git -cd VersaMCU -git checkout feature/usb-bootloader -``` - -## 2. Bootloader bauen und flashen - -```bash -cd bootloader -pio run -e versapad_bootloader --target upload -``` - -Der erste Aufruf lädt automatisch den ARM-Toolchain und OpenOCD herunter -(einmalig, braucht Internet). Der Build sollte ohne Fehler durchlaufen -(erwartete Größe: ~7,3 von 8 KB Flash). - -## 3. Prüfen, ob der Bootloader läuft - -Nach dem Flash: Board kurz vom USB trennen und wieder anstecken, dann -**Reset-Taste zweimal kurz hintereinander drücken** (Doppel-Tap, wie bei -Arduino-Zero-artigen Boards üblich). - -Erwartung: Das Board sollte sich als **USB-Massenspeicher-Laufwerk namens -`VERSABOOT`** melden (in Windows z. B. im Explorer als neues Laufwerk). - -## 4. Ergebnis zurückmelden - -Bitte notieren und zurückgeben: - -- Ist der Flash-Befehl ohne Fehler durchgelaufen? (ggf. komplette - Fehlermeldung kopieren) -- Erscheint nach dem Doppel-Tap-Reset ein Laufwerk `VERSABOOT`? -- Falls ja: was steht in der Datei `INFO_UF2.TXT` auf dem Laufwerk? -- Falls nein: reagiert das Board überhaupt noch normal als App-Firmware - (Tasten/LEDs), wenn man es normal per USB ansteckt ohne Doppel-Tap? -- Alles, was ungewöhnlich aussieht (USB-Gerät mit falschem Namen, Windows - fragt nach Treiber, Laufwerk lässt sich nicht öffnen, etc.) - -Noch NICHT testen: Firmware tatsächlich über das `VERSABOOT`-Laufwerk -flashen. Der Firmware-Build erzeugt noch keine `.uf2`-Datei (nur `.bin` -für den bisherigen SWD-Weg), das ist der nächste Schritt, nachdem der -Bootloader selbst bestätigt läuft. +Firmware tatsächlich über das `VERSABOOT`-Laufwerk flashen. Der +Firmware-Build der App erzeugt noch keine `.uf2`-Datei (nur `.bin`/`.elf` für +den SWD-Weg) — das ist der nächste Schritt, siehe README, "Bekannte +Einschränkungen". diff --git a/bootloader/include/board_config.h b/bootloader/include/board_config.h index 68a3f4e..9ef09f0 100644 --- a/bootloader/include/board_config.h +++ b/bootloader/include/board_config.h @@ -15,9 +15,16 @@ #define BOARD_ID "SAMD21G17D-VersaPad-v2" /* Same VID as the app firmware (platformio.ini), distinct PID so the - * bootloader enumerates as a different USB device than the app. */ + * bootloader enumerates as a different USB device than the app. + * + * NOT 0x0011: that's Adafruit's own Gemma M0 bootloader PID (this file's + * upstream template), and Windows machines with an Adafruit driver already + * installed silently bind it as a "Adafruit Circuit Playground" COM port + * instead of exposing the VERSABOOT mass-storage volume. Confirmed on real + * hardware 2026-08-05. 0x0043 is app PID (0x0042) + 1, not a known + * third-party assignment. */ #define USB_VID 0x239A -#define USB_PID 0x0011 +#define USB_PID 0x0043 /* No plain GPIO status LED on this board, only a WS2812 chain on PB22. * The bootloader's LED_PIN code just toggles a digital output, that would @@ -29,4 +36,21 @@ //#define BOARD_RGBLED_CLOCK_PIN //#define BOARD_RGBLED_DATA_PIN +/* Hardware bootloader entry, no dedicated reset/boot button on this PCB. + * Reuses the bottom-right Cherry MX key (key_id 24 in the app firmware, + * see src/config/pins.h) as a boot-hold key: held while the board resets + * or powers up -> stay in the bootloader. Checked in check_start_application() + * before the app is even validated, so it works regardless of app firmware + * state. + * + * Pin numbers are PORT-flat (group*32 + pin), matching PINOP()/PINCFG() in + * uf2.h: PA15 = 15, PA08 = 8. + * + * Matrix wiring (src/config/pins.h, src/hal/matrix.cpp): key_id 24 = COL4 x + * ROW4. COL4 has an external 10k pull-up to 3V3 (idle HIGH); ROW4 is driven + * LOW during the check, matching the app's own scan polarity. + */ +#define BOOT_KEY_ROW_PIN 15 // PA15 -- ROW4, driven LOW during the check +#define BOOT_KEY_COL_PIN 8 // PA08 -- COL4, read back; LOW = key pressed + #endif diff --git a/bootloader/src/main.c b/bootloader/src/main.c index c4f6f9a..e646dcd 100644 --- a/bootloader/src/main.c +++ b/bootloader/src/main.c @@ -87,6 +87,38 @@ extern int8_t led_tick_step; #define RESET_CONTROLLER RSTC #endif +#if defined(BOOT_KEY_ROW_PIN) && defined(BOOT_KEY_COL_PIN) +/** + * \brief Check whether the boot-hold key (bottom-right Cherry MX button) is + * held down. Drives its matrix row low and reads its matrix column back, + * the same polarity the app firmware's own matrix scan uses. + */ +static bool boot_key_pressed(void) { + PORT_PINCFG_Type col_cfg = {0}; + col_cfg.bit.PMUXEN = false; + col_cfg.bit.INEN = true; // external 10k pull-up already on the board + + PORT_PINCFG_Type row_cfg = {0}; + row_cfg.bit.PMUXEN = false; + row_cfg.bit.DRVSTR = true; + + PINCFG(BOOT_KEY_COL_PIN) = col_cfg.reg; + PINOP(BOOT_KEY_COL_PIN, DIRCLR); // column stays an input + + PINOP(BOOT_KEY_ROW_PIN, OUTCLR); // pre-set drive level before enabling output + PINCFG(BOOT_KEY_ROW_PIN) = row_cfg.reg; + PINOP(BOOT_KEY_ROW_PIN, DIRSET); // row -> output, driving low + + for (volatile int i = 0; i < 200; i++) { + } // let the row settle through the diode/pull-up RC + + bool pressed = (PINIP(BOOT_KEY_COL_PIN) == 0); + + PINOP(BOOT_KEY_ROW_PIN, DIRCLR); // release row back to high-Z + return pressed; +} +#endif + /** * \brief Check the application startup condition * @@ -94,6 +126,13 @@ extern int8_t led_tick_step; static void check_start_application(void) { uint32_t app_start_address; +#if defined(BOOT_KEY_ROW_PIN) && defined(BOOT_KEY_COL_PIN) + if (boot_key_pressed()) { + /* Stay in bootloader */ + return; + } +#endif + // Check if there is an IO which will hold us inside the bootloader. #if defined(HOLD_PIN) && defined(HOLD_STATE) PORT_PINCFG_Type pincfg = {0}; @@ -167,6 +206,14 @@ static void check_start_application(void) { /* Rebase the vector table base address */ SCB->VTOR = ((uint32_t)APP_START_ADDRESS & SCB_VTOR_TBLOFF_Msk); + /* Ensure the MSP/VTOR writes are visible before jumping; without these + * barriers the app's first fetch can race the pipeline (observed on + * real hardware: identical MSP/VTOR/PC values injected by a halted + * debugger boot fine, but the bootloader's own running jump hard-faults + * every time). */ + __DSB(); + __ISB(); + /* Jump to application Reset Handler in the application */ asm("bx %0" ::"r"(app_start_address)); } diff --git a/doc/09_known_limitations.md b/doc/09_known_limitations.md index be5152d..4e16e02 100644 --- a/doc/09_known_limitations.md +++ b/doc/09_known_limitations.md @@ -6,9 +6,23 @@ Robustheitskorrekturen. Sie ist keine Liste bereits umgesetzter Features. ## Bootloader-Ziel bleibt nicht unterstützt Das aktive Ziel `versapad_nobl` reserviert den kompletten Bereich -`0x1FB00..0x1FFFF` für Makros und Config. Das auskommentierte -USB-Bootloader-Environment verwendet dagegen weiterhin eine historische -Board-/Linker-Konfiguration und ist nicht als Produktionsziel verifiziert. +`0x1FB00..0x1FFFF` für Makros und Config. + +Ein USB-Bootloader-Pfad wird im Branch `feature/usb-bootloader` aufgebaut +(`bootloader/`, App-Environment `env:versapad_usb`). Der Bootloader selbst +(UF2, `bootloader/`) ist dort auf echter Hardware verifiziert — inklusive +eines gefundenen und behobenen Bugs beim Sprung in die App (fehlende +`__DSB()`/`__ISB()` vor dem `bx`, siehe `bootloader/README.md`, +"Hardwaretest"). `env:versapad_usb`s Linkerskript +(`variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld`) reserviert +inzwischen denselben NVM-Bereich wie `flash_without_bootloader.ld`. + +Trotzdem noch kein Produktionsziel: `env:versapad_usb` verwendet +`upload_protocol = sam-ba`, das klassische Arduino/Atmel-SAM-BA-Protokoll — +das spricht der UF2-Bootloader nicht. Die App-Firmware erzeugt außerdem noch +keine `.uf2`-Datei. Vor einem Merge nach `master` fehlen also noch die +`.uf2`-Erzeugung und ein Ende-zu-Ende-Test des tatsächlichen USB-Flashwegs +(Datei aufs `VERSABOOT`-Laufwerk kopieren). Die aktive Boarddatei benennt die MCU als `samd21g17d`, setzt für den Arduino-Core aber weiterhin das Kompatibilitätsmakro `__SAMD21G18A__`. Der diff --git a/platformio.ini b/platformio.ini index 5375d4a..3b3df48 100644 --- a/platformio.ini +++ b/platformio.ini @@ -23,7 +23,7 @@ extra_scripts = upload_openocd.py debug_tool = openocd ; ── USB SAM-BA (nur wenn Bootloader geflasht ist) ───────────────────────────── -; [env:versapad_usb] -; extends = common -; board = versapad -; upload_protocol = sam-ba +[env:versapad_usb] +extends = common +board = versapad +upload_protocol = sam-ba diff --git a/variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld b/variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld index f79fcbc..889a441 100644 --- a/variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld +++ b/variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld @@ -6,7 +6,8 @@ SEARCH_DIR(.) MEMORY { - rom (rx) : ORIGIN = 0x00002000, LENGTH = 0x0001E000 /* 120K (128K - 8K bootloader) */ + rom (rx) : ORIGIN = 0x00002000, LENGTH = 0x0001D900 /* 118.25K – Firmware (128K - 8K bootloader - 1.25K NVM) */ + nvm (rx) : ORIGIN = 0x0001FB00, LENGTH = 0x00000500 /* 1.25K – Makros + Config, same layout as flash_without_bootloader.ld */ ram (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00004000 /* 16K */ } From d3252970633955ca2afcaab8ff46755b264e122a Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Wed, 5 Aug 2026 21:35:19 +0200 Subject: [PATCH 3/7] Add end-to-end USB flashing for the app firmware via UF2 Adds uf2conv.py (minimal, dependency-free .bin -> .uf2 converter matching bootloader/inc/uf2format.h's block layout) and upload_uf2.py, a PlatformIO upload hook for env:versapad_usb that finds the mounted VERSABOOT volume and copies the converted firmware onto it. env:versapad_usb previously used upload_protocol=sam-ba, the classic Arduino/Atmel protocol -- the actual bootloader speaks UF2/mass storage, not SAM-BA, so that upload path never worked. Switched to upload_protocol=custom with the new hook, and cleaned the now-unused SAM-BA-specific fields out of boards/versapad.json. Verified end to end on real hardware: pio run -e versapad_usb --target upload builds, converts, copies to the VERSABOOT drive, and the bootloader jumps into the freshly written app on its own. Co-Authored-By: Claude Sonnet 5 --- .gitignore | 3 ++ boards/versapad.json | 7 +--- bootloader/README.md | 45 ++++++++++++++------ doc/09_known_limitations.md | 25 ++++++----- platformio.ini | 7 +++- uf2conv.py | 83 +++++++++++++++++++++++++++++++++++++ upload_uf2.py | 72 ++++++++++++++++++++++++++++++++ 7 files changed, 210 insertions(+), 32 deletions(-) create mode 100644 uf2conv.py create mode 100644 upload_uf2.py diff --git a/.gitignore b/.gitignore index 18c72d2..7640e36 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # PlatformIO .pio/ +# Python +__pycache__/ + # VS Code .vscode/ diff --git a/boards/versapad.json b/boards/versapad.json index 308ad44..36e874f 100644 --- a/boards/versapad.json +++ b/boards/versapad.json @@ -22,13 +22,8 @@ "upload": { "maximum_ram_size": 16384, "maximum_size": 120832, - "disable_flushing": true, - "native_usb": true, "offset": "0x2000", - "protocol": "sam-ba", - "require_upload_port": true, - "use_1200bps_touch": true, - "wait_for_upload_port": true + "protocol": "custom" }, "url": "", "vendor": "Custom" diff --git a/bootloader/README.md b/bootloader/README.md index f02c84a..3032296 100644 --- a/bootloader/README.md +++ b/bootloader/README.md @@ -6,11 +6,9 @@ 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 Bootloader selbst (Flash, USB-Enumeration, Massenspeicher-Modus, -Rücksprung in die App) ist auf echter Hardware verifiziert, siehe -[Hardwaretest](#hardwaretest-2026-08-05). Der App-seitige `.uf2`-Schreibweg -(Firmware tatsächlich über das Laufwerk aktualisieren) ist noch nicht gebaut, -siehe [Bekannte Einschränkungen](#bekannte-einschränkungen). +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 @@ -34,6 +32,12 @@ 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 @@ -73,6 +77,17 @@ 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. + ## Hardwaretest (2026-08-05) Erster vollständiger Hardwaretest auf einem echten VersaPad-v2-Board über @@ -94,6 +109,14 @@ Atmel-ICE/SWD. Ergebnisse: 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. +- **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 @@ -109,11 +132,7 @@ Atmel-ICE/SWD. Ergebnisse: (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. -- Die App-Firmware erzeugt noch keine `.uf2`-Datei, nur `.bin`/`.elf` - (SWD-Weg). Der geplante Weg über das `VERSABOOT`-Laufwerk ist damit noch - nicht nutzbar. -- `env:versapad_usb` in der Haupt-`platformio.ini` verwendet - `upload_protocol = sam-ba` — das klassische Arduino/Atmel-SAM-BA-Protokoll, - nicht das UF2/Massenspeicher-Verfahren dieses Bootloaders. Für echtes - USB-Flashen wird stattdessen ein `.bin`→`.uf2`-Konvertierungsschritt plus - einfaches Kopieren aufs Laufwerk benötigt, kein spezielles Upload-Protokoll. +- [`../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. diff --git a/doc/09_known_limitations.md b/doc/09_known_limitations.md index 4e16e02..748cfab 100644 --- a/doc/09_known_limitations.md +++ b/doc/09_known_limitations.md @@ -9,20 +9,23 @@ Das aktive Ziel `versapad_nobl` reserviert den kompletten Bereich `0x1FB00..0x1FFFF` für Makros und Config. Ein USB-Bootloader-Pfad wird im Branch `feature/usb-bootloader` aufgebaut -(`bootloader/`, App-Environment `env:versapad_usb`). Der Bootloader selbst -(UF2, `bootloader/`) ist dort auf echter Hardware verifiziert — inklusive -eines gefundenen und behobenen Bugs beim Sprung in die App (fehlende -`__DSB()`/`__ISB()` vor dem `bx`, siehe `bootloader/README.md`, -"Hardwaretest"). `env:versapad_usb`s Linkerskript +(`bootloader/`, App-Environment `env:versapad_usb`). Der komplette Weg ist +dort auf echter Hardware verifiziert: Bootloader-Flash, USB-Enumeration, +Tastencheck-Einstieg (kein physischer Reset-Taster auf diesem Board, siehe +`bootloader/README.md`, "Hardware-Bootloader-Einstieg"), App-Firmware per +`.uf2` über `env:versapad_usb --target upload` schreiben, automatischer +Rücksprung in die neue App. Details und ein gefundener/behobener +Hard-Fault-Bug beim Sprung Bootloader→App (fehlende `__DSB()`/`__ISB()` vor +dem `bx`) stehen in `bootloader/README.md`, "Hardwaretest". +`env:versapad_usb`s Linkerskript (`variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld`) reserviert inzwischen denselben NVM-Bereich wie `flash_without_bootloader.ld`. -Trotzdem noch kein Produktionsziel: `env:versapad_usb` verwendet -`upload_protocol = sam-ba`, das klassische Arduino/Atmel-SAM-BA-Protokoll — -das spricht der UF2-Bootloader nicht. Die App-Firmware erzeugt außerdem noch -keine `.uf2`-Datei. Vor einem Merge nach `master` fehlen also noch die -`.uf2`-Erzeugung und ein Ende-zu-Ende-Test des tatsächlichen USB-Flashwegs -(Datei aufs `VERSABOOT`-Laufwerk kopieren). +Trotzdem noch kein Merge-fertiges Produktionsziel: Der `.bin`→`.uf2`-Weg +(`uf2conv.py`, `upload_uf2.py`, Repo-Root) sucht das `VERSABOOT`-Laufwerk +bisher nur über die Windows-API, keine macOS/Linux-Unterstützung. Die +Bootloader-USB-PID (`0x0043`) ist zwar kollisionsfrei verifiziert, aber kein +offiziell registrierter Wert unter Adafruits VID `0x239A`. Die aktive Boarddatei benennt die MCU als `samd21g17d`, setzt für den Arduino-Core aber weiterhin das Kompatibilitätsmakro `__SAMD21G18A__`. Der diff --git a/platformio.ini b/platformio.ini index 3b3df48..a2b71a8 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,8 +22,11 @@ upload_protocol = custom extra_scripts = upload_openocd.py debug_tool = openocd -; ── USB SAM-BA (nur wenn Bootloader geflasht ist) ───────────────────────────── +; ── USB UF2 (nur wenn bootloader/ geflasht ist) ──────────────────────────────── +; Der eigene Bootloader spricht UF2/Massenspeicher, kein SAM-BA. upload_uf2.py +; erzeugt aus firmware.bin ein .uf2 und kopiert es aufs VERSABOOT-Laufwerk. [env:versapad_usb] extends = common board = versapad -upload_protocol = sam-ba +upload_protocol = custom +extra_scripts = upload_uf2.py diff --git a/uf2conv.py b/uf2conv.py new file mode 100644 index 0000000..0053f09 --- /dev/null +++ b/uf2conv.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Minimal .bin -> .uf2 converter for the VersaMCU UF2 bootloader. + +Standalone reimplementation of the block format microsoft/uf2-samdx1's +uf2conv.py produces (256-byte payload per 512-byte block); see +bootloader/inc/uf2format.h for the struct this has to match on the device +side. No external dependencies, Python 3 only. + +Usage: + python uf2conv.py [--base 0x2000] [--family 0x68ed2b88] +""" +import argparse +import struct + +UF2_MAGIC_START0 = 0x0A324655 # "UF2\n" +UF2_MAGIC_START1 = 0x9E5D5157 # randomly selected, must match the device +UF2_MAGIC_END = 0x0AB16F30 # ditto +UF2_FLAG_FAMILYID_PRESENT = 0x00002000 + +SAMD21_FAMILY_ID = 0x68ED2B88 # bootloader/inc/uf2format.h, #ifdef SAMD21 +PAYLOAD_SIZE = 256 # bytes per block; matches the upstream uf2conv.py convention + + +def convert(bin_path: str, uf2_path: str, base_addr: int, family_id: int) -> int: + with open(bin_path, "rb") as f: + data = f.read() + + # Pad to a whole number of blocks; the bootloader writes payloadSize + # bytes per block regardless of how much of it is real firmware. + if len(data) % PAYLOAD_SIZE != 0: + data += b"\x00" * (PAYLOAD_SIZE - len(data) % PAYLOAD_SIZE) + num_blocks = len(data) // PAYLOAD_SIZE + + blocks = [] + for block_no in range(num_blocks): + offset = block_no * PAYLOAD_SIZE + chunk = data[offset : offset + PAYLOAD_SIZE] + header = struct.pack( + " None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("input", help="Path to the raw firmware .bin") + parser.add_argument("output", help="Path to write the .uf2 to") + parser.add_argument( + "--base", + type=lambda s: int(s, 0), + default=0x2000, + help="Flash base address the .bin was linked for (default: 0x2000, matches " + "variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld)", + ) + parser.add_argument( + "--family", + type=lambda s: int(s, 0), + default=SAMD21_FAMILY_ID, + help="UF2 family ID (default: SAMD21, 0x68ed2b88)", + ) + args = parser.parse_args() + + num_blocks = convert(args.input, args.output, args.base, args.family) + print(f"Wrote {args.output}: {num_blocks} blocks, base 0x{args.base:08x}") + + +if __name__ == "__main__": + main() diff --git a/upload_uf2.py b/upload_uf2.py new file mode 100644 index 0000000..0448df3 --- /dev/null +++ b/upload_uf2.py @@ -0,0 +1,72 @@ +"""PlatformIO upload hook for env:versapad_usb. + +The board's actual bootloader (bootloader/, UF2/mass-storage) does not speak +SAM-BA, so upload_protocol=sam-ba (the board.json default, inherited from an +Arduino-Zero-style template) does not work here. This converts the built +.bin to .uf2 and copies it onto the VERSABOOT mass-storage volume instead -- +Windows only for now, matching the rest of this project's dev environment. +""" +import ctypes +import os +import string +import sys + +Import("env") + +sys.path.insert(0, env.subst("$PROJECT_DIR")) +import uf2conv + +VOLUME_LABEL = "VERSABOOT" +DRIVE_UNKNOWN = 0 +DRIVE_NO_ROOT_DIR = 1 + + +def find_versaboot_drive(label=VOLUME_LABEL): + for letter in string.ascii_uppercase: + root = f"{letter}:\\" + drive_type = ctypes.windll.kernel32.GetDriveTypeW(root) + if drive_type in (DRIVE_UNKNOWN, DRIVE_NO_ROOT_DIR): + continue + vol_name_buf = ctypes.create_unicode_buffer(261) + fs_name_buf = ctypes.create_unicode_buffer(261) + ok = ctypes.windll.kernel32.GetVolumeInformationW( + ctypes.c_wchar_p(root), + vol_name_buf, + ctypes.sizeof(vol_name_buf), + None, + None, + None, + fs_name_buf, + ctypes.sizeof(fs_name_buf), + ) + if ok and vol_name_buf.value == label: + return root + return None + + +def upload_via_uf2(source, target, env): + build_dir = env.subst("$BUILD_DIR") + bin_path = os.path.join(build_dir, "firmware.bin") + uf2_path = os.path.join(build_dir, "firmware.uf2") + + if not os.path.isfile(bin_path): + print(f"error: {bin_path} not found (expected as a normal build product)") + env.Exit(1) + + num_blocks = uf2conv.convert(bin_path, uf2_path, base_addr=0x2000, family_id=uf2conv.SAMD21_FAMILY_ID) + print(f"Wrote {uf2_path}: {num_blocks} blocks") + + drive = find_versaboot_drive() + if drive is None: + print(f"error: no drive labeled '{VOLUME_LABEL}' found.") + print("Hold the bottom-right Cherry MX key while plugging in USB to enter the bootloader.") + env.Exit(1) + + dest = os.path.join(drive, "firmware.uf2") + print(f"Copying {uf2_path} -> {dest}") + with open(uf2_path, "rb") as src, open(dest, "wb") as dst: + dst.write(src.read()) + print("Upload triggers a reset into the app on the device side; no further action needed.") + + +env.Replace(UPLOADCMD=upload_via_uf2) From 6c71f5f0576082ced21dbdf4dc1f7d7862d40009 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Wed, 5 Aug 2026 22:20:23 +0200 Subject: [PATCH 4/7] 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 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 --- bootloader/README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/bootloader/README.md b/bootloader/README.md index 3032296..3090d60 100644 --- a/bootloader/README.md +++ b/bootloader/README.md @@ -88,6 +88,30 @@ 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 @@ -109,6 +133,15 @@ Atmel-ICE/SWD. Ergebnisse: 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 From 3986d2effe3a3ec7a9c629289a598428c68feb14 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Wed, 5 Aug 2026 22:30:03 +0200 Subject: [PATCH 5/7] Fix off-by-512-byte error in the app-side bootloader linker script flash_with_bootloader.ld's rom region ended 512 bytes short of the NVM region it's meant to butt up against (0x1F900 instead of 0x1FB00), leaving a small gap neither region could use. Corrected the LENGTH and the matching maximum_size in boards/versapad.json (128K - 8K bootloader - 1.25K NVM = 121600 bytes, not 120832). Found while double-checking the numbers for a flash-usage breakdown; current 19KB app image is nowhere near either boundary, so this never affected anything on hardware. Co-Authored-By: Claude Sonnet 5 --- boards/versapad.json | 2 +- variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/boards/versapad.json b/boards/versapad.json index 36e874f..fbf8d7a 100644 --- a/boards/versapad.json +++ b/boards/versapad.json @@ -21,7 +21,7 @@ "name": "VersaPad v2 (USB bootloader)", "upload": { "maximum_ram_size": 16384, - "maximum_size": 120832, + "maximum_size": 121600, "offset": "0x2000", "protocol": "custom" }, diff --git a/variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld b/variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld index 889a441..babb84e 100644 --- a/variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld +++ b/variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld @@ -6,7 +6,7 @@ SEARCH_DIR(.) MEMORY { - rom (rx) : ORIGIN = 0x00002000, LENGTH = 0x0001D900 /* 118.25K – Firmware (128K - 8K bootloader - 1.25K NVM) */ + rom (rx) : ORIGIN = 0x00002000, LENGTH = 0x0001DB00 /* 118.75K – Firmware (128K - 8K bootloader - 1.25K NVM) */ nvm (rx) : ORIGIN = 0x0001FB00, LENGTH = 0x00000500 /* 1.25K – Makros + Config, same layout as flash_without_bootloader.ld */ ram (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00004000 /* 16K */ } From 33d9e85992e8456407dbee1df104787322093aad Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Wed, 5 Aug 2026 22:49:56 +0200 Subject: [PATCH 6/7] Fold bootloader documentation into the main doc/ index Adds doc/10_usb_bootloader.md as the primary reference for the UF2 bootloader (memory layout, key-based boot entry, build/flash steps, hardware test findings, known limitations), following the existing numbered-doc convention. Updates doc/INDEX.md, doc/09_known_limitations.md (bootloader is no longer "not supported", just scoped), and doc/08_development.md accordingly. Removes bootloader/TESTING.md (its findings now live in doc/10_usb_bootloader.md) and trims bootloader/README.md down to what belongs with that subproject specifically: upstream attribution/license and local build/flash commands, plus the OpenOCD manual-flashing warning since that's implementation-specific detail that would clutter the higher-level doc. Updates the top-level README.md (feature table, hardware table, quickstart, project tree, doc links) to reflect USB flashing as a supported path alongside SWD. Co-Authored-By: Claude Sonnet 5 --- README.md | 22 ++++++-- bootloader/README.md | 109 ++---------------------------------- bootloader/TESTING.md | 65 --------------------- doc/08_development.md | 7 ++- doc/09_known_limitations.md | 45 +++++++-------- doc/10_usb_bootloader.md | 104 ++++++++++++++++++++++++++++++++++ doc/INDEX.md | 2 + 7 files changed, 153 insertions(+), 201 deletions(-) delete mode 100644 bootloader/TESTING.md create mode 100644 doc/10_usb_bootloader.md diff --git a/README.md b/README.md index b5f0b14..7e07422 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ ATSAMD21G17D mit PlatformIO und dem Arduino-SAMD-Framework. | LEDs | 20 WS2812B mit Base-/Override-Farbe und 7 Animationsmodi | | Persistenz | Config und Makros im internen Flash | | Recovery | Werksreset über zwei Tasten | +| Firmware-Update | SWD (Atmel-ICE) oder USB (UF2-Bootloader, `bootloader/`) | Die drei Fader-Pins sind im Board-Variant definiert, werden von der aktuellen Firmware aber noch nicht eingelesen. @@ -29,7 +30,7 @@ Firmware aber noch nicht eingelesen. | Encoder | 4× Quadratur über EIC-Interrupts | | LEDs | 20× WS2812B an `PB22` | | USB | Native USB als HID + CDC Composite Device | -| Programmer | Atmel-ICE/CMSIS-DAP über SWD, standardmäßig ohne Bootloader | +| Programmer | Atmel-ICE/CMSIS-DAP über SWD, oder USB über den UF2-Bootloader | ## Schnellstart @@ -46,9 +47,17 @@ Das Standard-Environment `versapad` baut für `boards/versapad_nobl.json`. Der Upload wird durch `upload_openocd.py` über das von PlatformIO installierte OpenOCD ausgeführt. -Das in `platformio.ini` auskommentierte Bootloader-Environment ist derzeit -nicht als Produktionsziel unterstützt. Details stehen unter -[bekannte Einschränkungen](doc/09_known_limitations.md). +Alternativ lässt sich die App-Firmware nach einem einmaligen +SWD-Bootloader-Flash auch über USB aktualisieren, ganz ohne Atmel-ICE: + +```bash +cd bootloader && pio run -e versapad_bootloader --target upload # einmalig +pio run -e versapad_usb --target upload # danach jedes App-Update +``` + +Details, Speicherlayout und die Bootloader-Aktivierung (kein physischer +Reset-Taster auf dieser Platine) stehen in +[10_usb_bootloader.md](doc/10_usb_bootloader.md). ## Laufzeitmodell @@ -119,9 +128,13 @@ VersaMCU/ |-- AGENTS.md # Kontext und Richtlinien für Coding-LLMs |-- README.md |-- platformio.ini +|-- upload_openocd.py # SWD-Upload-Hook (env:versapad) +|-- uf2conv.py # .bin -> .uf2 Konverter (App-Firmware) +|-- upload_uf2.py # USB-Upload-Hook (env:versapad_usb) |-- boards/ # PlatformIO-Boarddefinitionen |-- variants/versapad/ # Pinmapping und Linker-Skripte |-- doc/ # Architektur- und Protokolldokumentation +|-- bootloader/ # UF2-Bootloader (eigenes PlatformIO-Projekt) `-- src/ |-- main.cpp |-- CMainController.* # Orchestrierung @@ -157,3 +170,4 @@ Binärverträge, Änderungsregeln und die minimale Verifikation. - [CDC-Protokoll](doc/07_serial_protocol.md) - [Entwicklung und Einstieg](doc/08_development.md) - [Bekannte Einschränkungen](doc/09_known_limitations.md) +- [USB-Bootloader](doc/10_usb_bootloader.md) diff --git a/bootloader/README.md b/bootloader/README.md index 3090d60..113872e 100644 --- a/bootloader/README.md +++ b/bootloader/README.md @@ -2,18 +2,11 @@ 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. +lässt sich die App-Firmware ohne SWD über USB aktualisieren. -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). +Speicherlayout, Bootloader-Aktivierung, Bedienung und Hardwaretest-Ergebnisse +stehen in [`../doc/10_usb_bootloader.md`](../doc/10_usb_bootloader.md). Diese +Datei beschreibt nur das Bootloader-Unterprojekt selbst. ## Herkunft @@ -52,42 +45,6 @@ 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) - -```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) @@ -111,61 +68,3 @@ verwenden (nie `.elf`) und die Zieladresse **immer explizit angeben** 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. diff --git a/bootloader/TESTING.md b/bootloader/TESTING.md deleted file mode 100644 index 2bc26b6..0000000 --- a/bootloader/TESTING.md +++ /dev/null @@ -1,65 +0,0 @@ -# Bootloader-Testflash -- Ergebnis - -Der UF2-Bootloader (siehe [README.md](README.md)) wurde am 2026-08-05 auf -einem echten VersaPad-v2-Board über Atmel-ICE/SWD getestet. Zusammenfassung -der Ergebnisse steht im [Hardwaretest-Abschnitt der README](README.md#hardwaretest-2026-08-05). -Dieses Dokument hält den Testablauf und die Antworten auf die ursprünglichen -Prüffragen fest. - -**Sicherheitsrahmen, der eingehalten wurde:** Der Bootloader-Flash schreibt -nur `0x0000..0x1FFF` (8 KiB). Die App-Firmware ab `0x2000` blieb dabei -unangetastet. Über SWD war der Chip jederzeit neu beschreibbar; ein Brick war -zu keinem Zeitpunkt möglich, solange der Atmel-ICE-Zugriff funktionierte. - -## Voraussetzungen - -- Board per Atmel-ICE/SWD angeschlossen, wie beim normalen - App-Firmware-Flashen (`pio run -e versapad --target upload`, - siehe `../doc/08_development.md`) -- PlatformIO Core (hier: PlatformIO-IDE-penv unter - `%USERPROFILE%\.platformio\penv\Scripts\pio.exe`) - -## Ablauf - -1. `git checkout feature/usb-bootloader` -2. `cd bootloader && pio run -e versapad_bootloader` — Build sauber - (89,9 % Flash, 7364/8192 Byte) -3. `pio run -e versapad_bootloader --target upload` — Flash + Verify über - Atmel-ICE/SWD - -## Ursprüngliche Prüffragen und Antworten - -- **Ist der Flash-Befehl ohne Fehler durchgelaufen?** Ja, `** Verified OK **` - bei jedem Flash. -- **Erscheint nach dem Bootloader-Einstieg ein Laufwerk `VERSABOOT`?** Ja. - Getestete Board-Revision hat **keinen physischen Reset-/Boot-Taster** - (Custom-PCB) — der ursprünglich geplante Doppel-Tap-Reset-Test war damit - nicht durchführbar. Stattdessen wurde ein Hardware-Bootloader-Einstieg über - eine gehaltene Cherry-MX-Taste ergänzt, siehe - [README.md](README.md#hardware-bootloader-einstieg-ohne-reset-taster). - Mit dieser Ergänzung: gehaltene Taste beim Power-On → `VERSABOOT`. -- **Was steht in `INFO_UF2.TXT`?** - ``` - UF2 Bootloader versapad-1 SFHWRO - Model: VersaPad v2 - Board-ID: SAMD21G17D-VersaPad-v2 - ``` -- **Reagiert das Board normal als App-Firmware, wenn man es ohne gehaltene - Taste ansteckt?** Ja, nach dem in der README beschriebenen DSB/ISB-Fix. - Vor dem Fix: nein, siehe unten. -- **Ungewöhnliches?** Ja, ein echter Bug: siehe - [Hardwaretest-Abschnitt der README](README.md#hardwaretest-2026-08-05) für - die Fehlersuche (Bootloader-eigener Sprung faultete zuverlässig, obwohl vom - Debugger injizierte identische Register-/VTOR-Werte einwandfrei liefen) und - den Fix (fehlende `__DSB()`/`__ISB()` vor dem `bx` in - `check_start_application()`). -- **USB-Identität korrekt, kein Fremdtreiber-Konflikt?** Nach PID-Wechsel von - `0x0011` auf `0x0043` ja (siehe README, "Bekannte Einschränkungen" zum - ursprünglichen Adafruit-PID-Konflikt). - -## Noch NICHT getestet - -Firmware tatsächlich über das `VERSABOOT`-Laufwerk flashen. Der -Firmware-Build der App erzeugt noch keine `.uf2`-Datei (nur `.bin`/`.elf` für -den SWD-Weg) — das ist der nächste Schritt, siehe README, "Bekannte -Einschränkungen". diff --git a/doc/08_development.md b/doc/08_development.md index 9e50f00..f9607ee 100644 --- a/doc/08_development.md +++ b/doc/08_development.md @@ -26,9 +26,9 @@ pio run -e versapad --target upload Der Upload nutzt `upload_openocd.py`, das das von PlatformIO installierte OpenOCD mit `interface/cmsis-dap.cfg` und `target/at91samdXX.cfg` startet. -Das in `platformio.ini` nur als Beispiel enthaltene Environment -`versapad_usb` ist auskommentiert und mit dem aktuellen NVM-/Linker-Layout -nicht als unterstützt anzusehen. +Für App-Updates über USB (nach einem einmaligen Bootloader-Flash) gibt es +zusätzlich `env:versapad_usb`, siehe +[10_usb_bootloader.md](10_usb_bootloader.md). ## Was beim Start passiert @@ -63,6 +63,7 @@ und NVM-Schreibvorgängen. Es gibt keinen Scheduler und keine Threads. | LEDs | `05_led_system.md` | `CButton.*`, `hal/ws2812.*` | | Persistente Config | `06_nvm_config.md` | `config/nvm_config.*`, Linker-Skripte | | Host-Protokoll | `07_serial_protocol.md` | `hal/usb_serial.*`, Controller | +| USB-Bootloader | `10_usb_bootloader.md` | `bootloader/`, `variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld`, `boards/versapad.json`, `uf2conv.py`, `upload_uf2.py` | ## Verifikation diff --git a/doc/09_known_limitations.md b/doc/09_known_limitations.md index 748cfab..9c79ffa 100644 --- a/doc/09_known_limitations.md +++ b/doc/09_known_limitations.md @@ -3,36 +3,33 @@ Diese Liste beschreibt den aktuellen Implementierungsstand nach den Robustheitskorrekturen. Sie ist keine Liste bereits umgesetzter Features. -## Bootloader-Ziel bleibt nicht unterstützt +## USB-Bootloader hat einen eingeschränkten Gültigkeitsbereich -Das aktive Ziel `versapad_nobl` reserviert den kompletten Bereich -`0x1FB00..0x1FFFF` für Makros und Config. +Der UF2-Bootloader (`bootloader/`, App-Environment `env:versapad_usb`, +Details in [10_usb_bootloader.md](10_usb_bootloader.md)) ist auf echter +Hardware verifiziert und ergänzt den SWD-Weg, ersetzt ihn aber nicht: der +Bootloader selbst muss weiterhin einmalig per Atmel-ICE/SWD geflasht werden. -Ein USB-Bootloader-Pfad wird im Branch `feature/usb-bootloader` aufgebaut -(`bootloader/`, App-Environment `env:versapad_usb`). Der komplette Weg ist -dort auf echter Hardware verifiziert: Bootloader-Flash, USB-Enumeration, -Tastencheck-Einstieg (kein physischer Reset-Taster auf diesem Board, siehe -`bootloader/README.md`, "Hardware-Bootloader-Einstieg"), App-Firmware per -`.uf2` über `env:versapad_usb --target upload` schreiben, automatischer -Rücksprung in die neue App. Details und ein gefundener/behobener -Hard-Fault-Bug beim Sprung Bootloader→App (fehlende `__DSB()`/`__ISB()` vor -dem `bx`) stehen in `bootloader/README.md`, "Hardwaretest". -`env:versapad_usb`s Linkerskript -(`variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld`) reserviert -inzwischen denselben NVM-Bereich wie `flash_without_bootloader.ld`. +Verbleibende Einschränkungen: -Trotzdem noch kein Merge-fertiges Produktionsziel: Der `.bin`→`.uf2`-Weg -(`uf2conv.py`, `upload_uf2.py`, Repo-Root) sucht das `VERSABOOT`-Laufwerk -bisher nur über die Windows-API, keine macOS/Linux-Unterstützung. Die -Bootloader-USB-PID (`0x0043`) ist zwar kollisionsfrei verifiziert, aber kein -offiziell registrierter Wert unter Adafruits VID `0x239A`. +- `uf2conv.py`/`upload_uf2.py` (Repo-Root) suchen das `VERSABOOT`-Laufwerk + nur über die Windows-API, keine macOS/Linux-Unterstützung. +- Die Bootloader-USB-PID (`0x0043`) ist kollisionsfrei verifiziert, aber kein + offiziell registrierter Wert unter Adafruits VID `0x239A`. +- Kein Software-Trigger, um aus der laufenden App heraus in den + Bootloader-Modus zu wechseln — nur der physische Weg (Kabel ziehen, Taste + halten, wieder einstecken). +- Bootloader-Flash-Auslastung ~90 % (7364 von 8192 Byte), wenig Puffer für + Änderungen am Bootloader selbst. -Die aktive Boarddatei benennt die MCU als `samd21g17d`, setzt für den +Sowohl `boards/versapad_nobl.json` (SWD-Ziel) als auch `boards/versapad.json` +(USB-Bootloader-Ziel) benennen die MCU als `samd21g17d`, setzen für den Arduino-Core aber weiterhin das Kompatibilitätsmakro `__SAMD21G18A__`. Der PlatformIO-Build meldet korrekt 128 KiB physischen Flash, 16 KiB RAM und -129.792 Byte nutzbaren Firmwarebereich. Vor device-spezifischen -Core-Änderungen sollte die historische Makro-Abweichung trotzdem geprüft -werden. +129.792 Byte (`versapad_nobl`) beziehungsweise 121.600 Byte +(`versapad`, abzüglich 8 KiB Bootloader) nutzbaren Firmwarebereich. Vor +device-spezifischen Core-Änderungen sollte die historische Makro-Abweichung +trotzdem geprüft werden. ## Event-Queue hat eine feste Kapazität diff --git a/doc/10_usb_bootloader.md b/doc/10_usb_bootloader.md new file mode 100644 index 0000000..0397fa9 --- /dev/null +++ b/doc/10_usb_bootloader.md @@ -0,0 +1,104 @@ +# 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. + +## 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". + +## 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. diff --git a/doc/INDEX.md b/doc/INDEX.md index 91852c7..d70d4d3 100644 --- a/doc/INDEX.md +++ b/doc/INDEX.md @@ -17,6 +17,7 @@ Bereiche stehen ausdrücklich in | [07_serial_protocol.md](07_serial_protocol.md) | 8-Byte-Protokoll, Config-/Makro-Transfer, ACK/NACK | | [08_development.md](08_development.md) | Setup, Build, Einstieg nach Änderungstyp, Verifikation | | [09_known_limitations.md](09_known_limitations.md) | Aktuelle technische Einschränkungen und Risiken | +| [10_usb_bootloader.md](10_usb_bootloader.md) | UF2-Bootloader, Speicherlayout, Tastencheck-Einstieg, USB-Flashweg | Die Repository-weiten Richtlinien und der kompakte LLM-Kontext stehen in [`../AGENTS.md`](../AGENTS.md). @@ -29,3 +30,4 @@ Die Repository-weiten Richtlinien und der kompakte LLM-Kontext stehen in - Action-Semantik und HID-Hold: [03_action_engine.md](03_action_engine.md) - CDC-Protokoll und Chunk-Zahlen: [07_serial_protocol.md](07_serial_protocol.md) - bekannte Risiken vor strukturellen Änderungen: [09_known_limitations.md](09_known_limitations.md) +- Firmware per USB statt SWD flashen: [10_usb_bootloader.md](10_usb_bootloader.md) From 01c5e0930e4177663d5fbd4eeed315b3485493f1 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Wed, 5 Aug 2026 23:27:02 +0200 Subject: [PATCH 7/7] 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 --- README.md | 13 +++++ bootloader/.gitignore | 5 ++ doc/10_usb_bootloader.md | 30 +++++++++++ upload_openocd.py | 111 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 bootloader/.gitignore diff --git a/README.md b/README.md index 7e07422..61290be 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,19 @@ cd bootloader && pio run -e versapad_bootloader --target upload # einmalig pio run -e versapad_usb --target upload # danach jedes App-Update ``` +`bootloader/` ist ein eigenständiges PlatformIO-Projekt (eigene +`platformio.ini`, kein Arduino-Framework). Für die PlatformIO-IDE-Buttons in +VS Code müsste der Ordner separat als eigener Workspace geöffnet werden; über +die Kommandozeile reicht `cd bootloader && pio run ...` im selben Fenster. + +Sobald der Bootloader installiert ist, **verweigert `versapad --target +upload` den normalen Upload** — dieser Weg schreibt ab `0x0000` und würde den +Bootloader sonst kommentarlos überschreiben (`upload_openocd.py` prüft das +vorher automatisch). Bauen und Debuggen über `versapad` bleiben uneingeschränkt +möglich, nur der Upload ist betroffen. Für App-Updates danach `versapad_usb` +verwenden; bewusst zurück zu reinem SWD-Betrieb geht über +`pio run -e versapad -t erase-bootloader-and-flash`. + Details, Speicherlayout und die Bootloader-Aktivierung (kein physischer Reset-Taster auf dieser Platine) stehen in [10_usb_bootloader.md](doc/10_usb_bootloader.md). diff --git a/bootloader/.gitignore b/bootloader/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/bootloader/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/doc/10_usb_bootloader.md b/doc/10_usb_bootloader.md index 0397fa9..5aa5b89 100644 --- a/doc/10_usb_bootloader.md +++ b/doc/10_usb_bootloader.md @@ -56,6 +56,30 @@ 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 @@ -87,6 +111,12 @@ stehen in `bootloader/README.md` unter "Hardwaretest"; kurz zusammengefasst: 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 diff --git a/upload_openocd.py b/upload_openocd.py index b311728..ea69879 100644 --- a/upload_openocd.py +++ b/upload_openocd.py @@ -2,23 +2,118 @@ Import("env") import os import subprocess -def upload_via_openocd(source, target, env): - pkg_dir = env.PioPlatform().get_package_dir("tool-openocd") - openocd = os.path.join(pkg_dir, "bin", "openocd.exe") - scripts = os.path.join(pkg_dir, "scripts") - firmware = str(source[0]) # .elf path +# Real, already-built bootloader image used as the reference for the presence +# check below -- a raw synthetic probe blob turned out unreliable with +# verify_image (silent no-op on tiny files), whereas verify_image against a +# real firmware .bin has been solid throughout this project's bring-up. +BOOTLOADER_BIN = os.path.join( + "bootloader", ".pio", "build", "versapad_bootloader", "firmware.bin" +) + +def _openocd_paths(env): + pkg_dir = env.PioPlatform().get_package_dir("tool-openocd") + return ( + os.path.join(pkg_dir, "bin", "openocd.exe"), + os.path.join(pkg_dir, "scripts"), + ) + + +def _run_openocd(env, extra_cmd, capture=False): + openocd, scripts = _openocd_paths(env) cmd = [ openocd, "-s", scripts, "-f", "interface/cmsis-dap.cfg", "-f", "target/at91samdXX.cfg", - "-c", 'program "{}" verify reset; shutdown'.format(firmware.replace("\\", "/")) + "-c", extra_cmd, ] - print(" ".join(cmd)) - result = subprocess.run(cmd) + if capture: + return subprocess.run(cmd, capture_output=True, text=True) + return subprocess.run(cmd) + + +def _bootloader_present(env): + bootloader_bin = env.subst( + os.path.join("$PROJECT_DIR", BOOTLOADER_BIN) + ) + if not os.path.isfile(bootloader_bin): + print("WARNING: {} not found (build it with".format(BOOTLOADER_BIN)) + print("'cd bootloader && pio run -e versapad_bootloader') -- can't check") + print("for an installed bootloader, refusing to flash as a precaution.") + print("Use 'pio run -e versapad -t erase-bootloader-and-flash' to override.") + return True + + result = _run_openocd( + env, + 'init; reset halt; verify_image "{}" 0x0; shutdown'.format( + bootloader_bin.replace("\\", "/") + ), + capture=True, + ) + output = result.stdout + result.stderr + if "checksum mismatch" in output or "diff " in output: + return False # something else is at 0x0000 -- not this bootloader + if "halted due to debug-request" in output and "Error" not in output: + return True # verify_image is silent on a match; absence of a + # mismatch/error after a successful connect means it matched + print("WARNING: could not read flash to check for an installed bootloader") + print("(inconclusive) -- refusing to flash as a precaution.") + print("Use 'pio run -e versapad -t erase-bootloader-and-flash' to override.") + return True + + +def _flash(env, firmware): + result = _run_openocd( + env, 'program "{}" verify reset; shutdown'.format(firmware.replace("\\", "/")) + ) if result.returncode != 0: env.Exit(1) + +def upload_via_openocd(source, target, env): + firmware = str(source[0]) # .elf path + + # This script is shared with bootloader/platformio.ini's own upload + # (env:versapad_bootloader), which legitimately writes 0x0000 every time + # -- the guard below only makes sense for the app-without-bootloader + # target (env:versapad). + if env["PIOENV"] == "versapad" and _bootloader_present(env): + print("=" * 78) + print("REFUSING TO FLASH: a UF2 bootloader looks like it's installed at 0x0000.") + print("") + print("This target (env:versapad) writes the app starting at 0x0000 and") + print("would silently overwrite it -- the board would lose its USB flashing") + print("path (see doc/10_usb_bootloader.md).") + print("") + print("Use instead:") + print(" pio run -e versapad_usb --target upload # flash over USB, keeps the bootloader") + print("or, if you deliberately want to erase the bootloader and go back to") + print("standalone SWD-only operation:") + print(" pio run -e versapad -t erase-bootloader-and-flash") + print("=" * 78) + env.Exit(1) + + _flash(env, firmware) + + env.Replace(UPLOADCMD=upload_via_openocd) + + +def erase_bootloader_and_flash(*_args, **_kwargs): + firmware = env.subst(os.path.join("$BUILD_DIR", "${PROGNAME}.elf")) + print("Overwriting 0x0000..0x1FAFF -- any installed UF2 bootloader will be erased.") + _flash(env, firmware) + + +env.AddCustomTarget( + name="erase-bootloader-and-flash", + dependencies=["buildprog"], + actions=[erase_bootloader_and_flash], + title="Erase bootloader + flash (standalone SWD)", + description=( + "Unconditionally overwrites 0x0000..0x1FAFF, wiping any installed UF2 " + "bootloader. Use only to return to standalone SWD-only operation." + ), +)