diff --git a/.gitignore b/.gitignore index 7640e36..18c72d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,6 @@ # PlatformIO .pio/ -# Python -__pycache__/ - # VS Code .vscode/ diff --git a/README.md b/README.md index 61290be..b5f0b14 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,6 @@ 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. @@ -30,7 +29,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, oder USB über den UF2-Bootloader | +| Programmer | Atmel-ICE/CMSIS-DAP über SWD, standardmäßig ohne Bootloader | ## Schnellstart @@ -47,30 +46,9 @@ 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. -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 -``` - -`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). +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). ## Laufzeitmodell @@ -141,13 +119,9 @@ 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 @@ -183,4 +157,3 @@ 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/boards/versapad.json b/boards/versapad.json index fbf8d7a..6d7eec5 100644 --- a/boards/versapad.json +++ b/boards/versapad.json @@ -7,10 +7,10 @@ "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", "0x0042"] + ["0x239A", "0x0011"] ], "mcu": "samd21g17d", "usb_product": "VersaPad v2", @@ -21,9 +21,14 @@ "name": "VersaPad v2 (USB bootloader)", "upload": { "maximum_ram_size": 16384, - "maximum_size": 121600, + "maximum_size": 122880, + "disable_flushing": true, + "native_usb": true, "offset": "0x2000", - "protocol": "custom" + "protocol": "sam-ba", + "require_upload_port": true, + "use_1200bps_touch": true, + "wait_for_upload_port": true }, "url": "", "vendor": "Custom" diff --git a/bootloader/.gitignore b/bootloader/.gitignore deleted file mode 100644 index 89cc49c..0000000 --- a/bootloader/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.pio -.vscode/.browse.c_cpp.db* -.vscode/c_cpp_properties.json -.vscode/launch.json -.vscode/ipch diff --git a/bootloader/README.md b/bootloader/README.md index 113872e..03853af 100644 --- a/bootloader/README.md +++ b/bootloader/README.md @@ -2,11 +2,9 @@ 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. - -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. +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. ## Herkunft @@ -25,12 +23,6 @@ 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 @@ -45,26 +37,15 @@ da nur `0x0000..0x1FFF` beschrieben wird) pio run -e versapad_bootloader --target upload ``` -## Achtung bei manuellem SWD-Flashen von Bootloader UND App +## Bekannte Einschränkungen -`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. +- 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. +- 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`. diff --git a/bootloader/TESTING.md b/bootloader/TESTING.md new file mode 100644 index 0000000..b2bb167 --- /dev/null +++ b/bootloader/TESTING.md @@ -0,0 +1,67 @@ +# Bootloader-Testflash -- Anleitung für Hardware-Zugriff + +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. + +**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. + +## 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 + ``` + +## 1. Repo auf diesem Branch auschecken + +```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. diff --git a/bootloader/include/board_config.h b/bootloader/include/board_config.h index 9ef09f0..68a3f4e 100644 --- a/bootloader/include/board_config.h +++ b/bootloader/include/board_config.h @@ -15,16 +15,9 @@ #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. - * - * 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. */ + * bootloader enumerates as a different USB device than the app. */ #define USB_VID 0x239A -#define USB_PID 0x0043 +#define USB_PID 0x0011 /* 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 @@ -36,21 +29,4 @@ //#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 e646dcd..c4f6f9a 100644 --- a/bootloader/src/main.c +++ b/bootloader/src/main.c @@ -87,38 +87,6 @@ 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 * @@ -126,13 +94,6 @@ static bool boot_key_pressed(void) { 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}; @@ -206,14 +167,6 @@ 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/08_development.md b/doc/08_development.md index f9607ee..9e50f00 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. -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). +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. ## Was beim Start passiert @@ -63,7 +63,6 @@ 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 9c79ffa..be5152d 100644 --- a/doc/09_known_limitations.md +++ b/doc/09_known_limitations.md @@ -3,33 +3,19 @@ Diese Liste beschreibt den aktuellen Implementierungsstand nach den Robustheitskorrekturen. Sie ist keine Liste bereits umgesetzter Features. -## USB-Bootloader hat einen eingeschränkten Gültigkeitsbereich +## Bootloader-Ziel bleibt nicht unterstützt -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. +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. -Verbleibende Einschränkungen: - -- `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. - -Sowohl `boards/versapad_nobl.json` (SWD-Ziel) als auch `boards/versapad.json` -(USB-Bootloader-Ziel) benennen die MCU als `samd21g17d`, setzen für den +Die aktive Boarddatei benennt die MCU als `samd21g17d`, setzt 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 (`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. +129.792 Byte 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 deleted file mode 100644 index 5aa5b89..0000000 --- a/doc/10_usb_bootloader.md +++ /dev/null @@ -1,134 +0,0 @@ -# USB-Bootloader (UF2) - -Zweiter Firmware-Update-Weg neben SWD: ein UF2-Bootloader unter -`bootloader/` erlaubt, die App-Firmware über USB zu aktualisieren, ohne -Atmel-ICE. Der Bootloader selbst wird einmalig per SWD geflasht; danach -reicht für App-Updates ein PlatformIO-Befehl. - -## Speicherlayout - -| Bereich | Adresse | Größe | Environment | -|---|---|---|---| -| Bootloader | `0x00000..0x01FFF` | 8 KiB | `versapad_bootloader` (in `bootloader/`) | -| App-Firmware | `0x02000..0x1FAFF` | 118,75 KiB | `versapad_usb` | -| NVM (Makros + Config) | `0x1FB00..0x1FFFF` | 1,25 KiB | — | - -Gleiches NVM-Layout wie beim SWD-Ziel `versapad_nobl` -(siehe [06_nvm_config.md](06_nvm_config.md)); nur die Firmware-Obergrenze -sinkt um die 8 KiB Bootloader. Linkerskript: -`variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld`, Boarddatei: -`boards/versapad.json`. - -## Bootloader-Einstieg ohne Reset-Taster - -Die Platine hat keinen dedizierten Reset-/Boot-Taster. Statt eines -physischen Pins wird die unterste rechte Cherry-MX-Taste (`key_id 24`, -COL4 × ROW4, siehe [01_matrix.md](01_matrix.md)) beim Boot geprüft: -`check_start_application()` in `bootloader/src/main.c` treibt ROW4 kurz auf -LOW und liest COL4 zurück — noch vor der App-Adress-Validierung, funktioniert -also auch bei kaputter oder fehlender App-Firmware. - -Bedienung: USB-Kabel ziehen, Taste gedrückt halten, wieder einstecken (Taste -weiter halten) → Board bootet in den Massenspeicher-Modus (`VERSABOOT`). -Ohne gehaltene Taste startet die App normal. - -## Bauen und Flashen - -Bootloader einmalig per SWD (überschreibt nur `0x0000..0x1FFF`): - -```bash -cd bootloader -pio run -e versapad_bootloader --target upload -``` - -Danach App-Firmware per USB (Board vorher wie oben in den Bootloader-Modus -versetzen): - -```bash -pio run -e versapad_usb --target upload -``` - -Baut die App, wandelt `firmware.bin` über `uf2conv.py` (Repo-Root, eigene -minimale Implementierung, kein Upstream-Code) in `firmware.uf2` und kopiert -es über `upload_uf2.py` aufs erkannte `VERSABOOT`-Laufwerk. Der Bootloader -erkennt den Schreibzugriff und springt selbständig in die neue App. - -`upload_uf2.py` sucht das Laufwerk aktuell nur über die Windows-API -(`GetVolumeInformationW`) — keine macOS/Linux-Unterstützung. - -## Schutz gegen versehentliches Überschreiben (env:versapad) - -`env:versapad` (SWD, `boards/versapad_nobl.json`) schreibt die App-Firmware -ab `0x0000` und überschreibt damit einen installierten Bootloader -kommentarlos — genau das ist am 2026-08-05 während der Entwicklung passiert -(zweimal). `upload_openocd.py` prüft das seither vor jedem `versapad`-Upload: - -- vergleicht per `verify_image` gegen die lokal gebaute - `bootloader/.pio/build/versapad_bootloader/firmware.bin` -- Bootloader erkannt → Upload wird verweigert, mit Hinweis auf - `versapad_usb` oder den expliziten Override -- lässt sich die Prüfung nicht eindeutig durchführen (z. B. `bootloader/` - noch nicht gebaut, oder die SWD-Verbindung antwortet nicht) → wird - sicherheitshalber ebenfalls verweigert, nicht durchgelassen -- gilt nur für `env:versapad` — `env:versapad_bootloader` nutzt dasselbe - Skript (`extra_scripts = ../upload_openocd.py`) und schreibt bewusst immer - auf `0x0000`, ungeprüft - -Bewusstes Überschreiben (zurück zu reinem SWD-Betrieb ohne Bootloader): - -```bash -pio run -e versapad -t erase-bootloader-and-flash -``` - -## Herkunft und Details - -Der Bootloader ist abgeleitet und stark eingekürzt aus -[microsoft/uf2-samdx1](https://github.com/microsoft/uf2-samdx1) (MIT), auf -den `SAMD21G17D`-Chip dieses Boards zugeschnitten. Herkunft, Lizenz und -vollständige Implementierungsdetails stehen in `bootloader/README.md`. - -## Hardwaretest (2026-08-05) und gefundene Bugs - -Kompletter Weg auf echter Hardware verifiziert: Bootloader-Flash, -USB-Enumeration, Tastencheck-Einstieg, App-Firmware per `.uf2` schreiben, -automatischer Rücksprung in die neue App. Details, Fehlerbilder und Fixes -stehen in `bootloader/README.md` unter "Hardwaretest"; kurz zusammengefasst: - -- **Fehlende Synchronisationsbarriere beim Sprung Bootloader→App.** Der - Sprung (`__set_MSP` → `SCB->VTOR` → `bx`) faultete bei jedem - eigenständigen Boot. Fix: `__DSB(); __ISB();` vor dem `bx` in - `check_start_application()` — von ARM für dieses Pattern vorgeschrieben, - fehlte im vendorten Code. -- **USB-PID-Kollision.** Die ursprünglich übernommene PID `0x0011` ist - Adafruits eigene Gemma-M0-Bootloader-PID; Windows-Rechner mit - installiertem Adafruit-Treiber banden das Board fälschlich als - `Adafruit Circuit Playground`-COM-Port statt als Massenspeicher. Fix: - eigene PID `0x0043`. -- **OpenOCD-Falle beim manuellen SWD-Debuggen:** `program datei.elf verify` - ohne explizite Zieladresse hat wiederholt den Bootloader mit rohen - ELF-Datei-Bytes überschrieben, sobald Bootloader- und App-Flash im selben - OpenOCD-Aufruf kombiniert wurden. Bei manuellem Flashen beider Bereiche - immer `.bin` mit expliziter Adresse und getrennte OpenOCD-Aufrufe - verwenden — Details in `bootloader/README.md`, "Achtung bei manuellem - SWD-Flashen". -- **`env:versapad` überschreibt den Bootloader kommentarlos.** Ein normaler - `pio run -e versapad --target upload` (der alte, gewohnte SWD-Weg für - App-Updates) schreibt ab `0x0000` und hat den Bootloader dabei zweimal - ohne jede Warnung zerstört. Fix: automatischer Presence-Check in - `upload_openocd.py`, siehe "Schutz gegen versehentliches Überschreiben" - oben. - -## Bekannte Einschränkungen - -- Bootloader-Flash-Auslastung ~90 % (7364 von 8192 Byte) — wenig Puffer für - Änderungen am Bootloader selbst. -- Kein Status-LED-Feedback im Bootloader-Modus (nur WS2812-Kette, dafür ist - der Original-Code nicht ausgelegt). -- USB-PID `0x0043` ist kollisionsfrei verifiziert, aber kein offiziell - registrierter Wert unter Adafruits VID `0x239A`. -- `upload_uf2.py` ist Windows-only (siehe oben). -- Kein Software-Trigger, um aus der laufenden App heraus in den Bootloader - zurückzuspringen — nur der physische Kabel-raus/Taste-halten/Kabel-rein-Weg. - Wäre über einen neuen CDC-Befehl (siehe - [07_serial_protocol.md](07_serial_protocol.md)) nachrüstbar, der dieselbe - `DBL_TAP_MAGIC`-RAM-Adresse setzt und `NVIC_SystemReset()` aufruft. diff --git a/doc/INDEX.md b/doc/INDEX.md index d70d4d3..91852c7 100644 --- a/doc/INDEX.md +++ b/doc/INDEX.md @@ -17,7 +17,6 @@ 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). @@ -30,4 +29,3 @@ 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) diff --git a/platformio.ini b/platformio.ini index a2b71a8..5375d4a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,11 +22,8 @@ upload_protocol = custom extra_scripts = upload_openocd.py debug_tool = openocd -; ── 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 = custom -extra_scripts = upload_uf2.py +; ── USB SAM-BA (nur wenn Bootloader geflasht ist) ───────────────────────────── +; [env:versapad_usb] +; extends = common +; board = versapad +; upload_protocol = sam-ba diff --git a/uf2conv.py b/uf2conv.py deleted file mode 100644 index 0053f09..0000000 --- a/uf2conv.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/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_openocd.py b/upload_openocd.py index ea69879..b311728 100644 --- a/upload_openocd.py +++ b/upload_openocd.py @@ -2,118 +2,23 @@ Import("env") import os import subprocess -# 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 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 - -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", extra_cmd, + "-c", 'program "{}" verify reset; shutdown'.format(firmware.replace("\\", "/")) ] + print(" ".join(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("\\", "/")) - ) + result = subprocess.run(cmd) 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." - ), -) diff --git a/upload_uf2.py b/upload_uf2.py deleted file mode 100644 index 0448df3..0000000 --- a/upload_uf2.py +++ /dev/null @@ -1,72 +0,0 @@ -"""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) diff --git a/variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld b/variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld index babb84e..f79fcbc 100644 --- a/variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld +++ b/variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld @@ -6,8 +6,7 @@ SEARCH_DIR(.) MEMORY { - 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 */ + rom (rx) : ORIGIN = 0x00002000, LENGTH = 0x0001E000 /* 120K (128K - 8K bootloader) */ ram (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00004000 /* 16K */ }