Compare commits
4 commits
4749adcd1d
...
3986d2effe
| Author | SHA1 | Date | |
|---|---|---|---|
| 3986d2effe | |||
| 6c71f5f057 | |||
| d325297063 | |||
| f60a29137c |
11 changed files with 452 additions and 89 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,6 +1,9 @@
|
||||||
# PlatformIO
|
# PlatformIO
|
||||||
.pio/
|
.pio/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
# VS Code
|
# VS Code
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@
|
||||||
"extra_flags": "-DARDUINO_SAMD_ZERO -DARM_MATH_CM0PLUS -D__SAMD21G18A__",
|
"extra_flags": "-DARDUINO_SAMD_ZERO -DARM_MATH_CM0PLUS -D__SAMD21G18A__",
|
||||||
"f_cpu": "48000000L",
|
"f_cpu": "48000000L",
|
||||||
"hwids": [
|
"hwids": [
|
||||||
["0x239A", "0x0011"]
|
["0x239A", "0x0042"]
|
||||||
],
|
],
|
||||||
"mcu": "samd21g18a",
|
"mcu": "samd21g17d",
|
||||||
"usb_product": "VersaPad v2",
|
"usb_product": "VersaPad v2",
|
||||||
"usb_manufacturer": "Custom"
|
"usb_manufacturer": "Custom"
|
||||||
},
|
},
|
||||||
|
|
@ -20,15 +20,10 @@
|
||||||
"frameworks": ["arduino"],
|
"frameworks": ["arduino"],
|
||||||
"name": "VersaPad v2 (USB bootloader)",
|
"name": "VersaPad v2 (USB bootloader)",
|
||||||
"upload": {
|
"upload": {
|
||||||
"maximum_ram_size": 32768,
|
"maximum_ram_size": 16384,
|
||||||
"maximum_size": 253952,
|
"maximum_size": 121600,
|
||||||
"disable_flushing": true,
|
|
||||||
"native_usb": true,
|
|
||||||
"offset": "0x2000",
|
"offset": "0x2000",
|
||||||
"protocol": "sam-ba",
|
"protocol": "custom"
|
||||||
"require_upload_port": true,
|
|
||||||
"use_1200bps_touch": true,
|
|
||||||
"wait_for_upload_port": true
|
|
||||||
},
|
},
|
||||||
"url": "",
|
"url": "",
|
||||||
"vendor": "Custom"
|
"vendor": "Custom"
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,17 @@
|
||||||
USB-Bootloader für das VersaPad-v2-Makropad. Wird per Atmel-ICE/SWD einmalig
|
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
|
auf den ATSAMD21G17D geflasht und belegt `0x0000..0x1FFF` (8 KiB). Danach
|
||||||
lässt sich die App-Firmware ohne SWD über USB aktualisieren: Bootloader-Modus
|
lässt sich die App-Firmware ohne SWD über USB aktualisieren: Bootloader-Modus
|
||||||
aktivieren (Doppel-Tap Reset), Board erscheint als USB-Laufwerk `VERSABOOT`,
|
aktivieren, Board erscheint als USB-Laufwerk `VERSABOOT`, `.uf2`-Datei drauf
|
||||||
`.uf2`-Datei drauf kopieren.
|
kopieren.
|
||||||
|
|
||||||
|
Der komplette Weg — Bootloader-Einstieg, `.uf2`-Erzeugung, Kopieren aufs
|
||||||
|
Laufwerk, automatischer Rücksprung in die neu geschriebene App — ist auf
|
||||||
|
echter Hardware verifiziert, siehe [Hardwaretest](#hardwaretest-2026-08-05).
|
||||||
|
|
||||||
|
Diese Platine hat keinen dedizierten Reset-/Boot-Taster. Bootloader-Modus
|
||||||
|
aktivieren heißt hier: unterste rechte Cherry-MX-Taste (key_id 24) beim
|
||||||
|
Einstecken/Reset gedrückt halten. Siehe
|
||||||
|
[Hardware-Bootloader-Einstieg](#hardware-bootloader-einstieg-ohne-reset-taster).
|
||||||
|
|
||||||
## Herkunft
|
## Herkunft
|
||||||
|
|
||||||
|
|
@ -23,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
|
PlatformIO-Environment, damit dasselbe Tooling wie für die App-Firmware
|
||||||
ausreicht.
|
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
|
## Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -37,15 +52,120 @@ da nur `0x0000..0x1FFF` beschrieben wird)
|
||||||
pio run -e versapad_bootloader --target upload
|
pio run -e versapad_bootloader --target upload
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Hardware-Bootloader-Einstieg ohne Reset-Taster
|
||||||
|
|
||||||
|
Die Platine hat keinen dedizierten Reset-/Boot-Taster (Custom-PCB-Design).
|
||||||
|
Der klassische UF2-"Doppel-Tap-Reset" braucht aber gar keinen physischen Pin —
|
||||||
|
`check_start_application()` in `src/main.c` prüft ein RAM-Flag
|
||||||
|
(`DBL_TAP_PTR`/`DBL_TAP_MAGIC`), das bei jedem Reset gesetzt wird, unabhängig
|
||||||
|
von der Reset-Quelle.
|
||||||
|
|
||||||
|
Statt eines eigenen Tasters wird die unterste rechte Cherry-MX-Taste
|
||||||
|
(`key_id 24` in der App-Firmware, siehe `../src/config/pins.h`) missbraucht:
|
||||||
|
|
||||||
|
- `key_id 24` liegt auf COL4 (`PA08`) × ROW4 (`PA15`)
|
||||||
|
- `boot_key_pressed()` in `src/main.c` treibt ROW4 kurz auf LOW und liest
|
||||||
|
COL4 zurück — kein voller Matrixscan nötig, nur ein Drei-Pin-Check
|
||||||
|
- der Check läuft ganz am Anfang von `check_start_application()`, noch vor
|
||||||
|
der App-Adress-Validierung und vor der RCAUSE-/DBL-TAP-Logik: funktioniert
|
||||||
|
also auch bei kaputter/gelöschter App-Firmware und bei normalem Power-On
|
||||||
|
(Stecker ziehen/reinstecken), kein Software-Trigger in der App nötig
|
||||||
|
- Pin-Konstanten stehen in `include/board_config.h`
|
||||||
|
(`BOOT_KEY_ROW_PIN`/`BOOT_KEY_COL_PIN`)
|
||||||
|
|
||||||
|
Bedienung: USB-Kabel ziehen, unterste rechte Taste gedrückt halten, Kabel
|
||||||
|
wieder einstecken (Taste dabei weiter halten) → Board bootet direkt in
|
||||||
|
`VERSABOOT`. Ohne gehaltene Taste startet die App normal.
|
||||||
|
|
||||||
|
## App-Firmware per USB flashen (nach dem einmaligen Bootloader-Flash)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Board zuerst in den Bootloader-Modus versetzen: USB ziehen, unterste
|
||||||
|
# rechte Taste halten, wieder einstecken (siehe oben)
|
||||||
|
pio run -e versapad_usb --target upload
|
||||||
|
```
|
||||||
|
|
||||||
|
Baut die App-Firmware (Repo-Root, nicht `bootloader/`), erzeugt `firmware.uf2`
|
||||||
|
und kopiert es aufs `VERSABOOT`-Laufwerk. Kein Atmel-ICE mehr nötig.
|
||||||
|
|
||||||
|
## Achtung bei manuellem SWD-Flashen von Bootloader UND App
|
||||||
|
|
||||||
|
`upload_openocd.py` (App, `env:versapad`) und der Bootloader-Upload (oben)
|
||||||
|
sind sicher, weil sie jeweils nur ihren eigenen Bereich anfassen. **Beim
|
||||||
|
manuellen Debuggen über OpenOCD-Kommandozeile aber Vorsicht:**
|
||||||
|
|
||||||
|
`openocd -c "program datei.elf verify"` **ohne explizite Zieladresse** hat
|
||||||
|
sich in dieser Kombination aus OpenOCD-Version/CMSIS-DAP-Adapter/Target-Skript
|
||||||
|
als unzuverlässig erwiesen — statt die im ELF hinterlegten Sektionsadressen
|
||||||
|
(`0x2000` für die App) zu nutzen, landeten die rohen Datei-Bytes teils direkt
|
||||||
|
ab Flash-Adresse `0x0000` und haben damit den frisch geschriebenen Bootloader
|
||||||
|
sofort wieder überschrieben (bestätigt am 2026-08-05: Byte 0 an Adresse
|
||||||
|
`0x0000` war `0x7f`, der Beginn der ELF-Magic `\x7fELF` — die rohe Datei, kein
|
||||||
|
Firmware-Code). Passierte zuverlässig, wenn Bootloader- und App-Flash im
|
||||||
|
selben OpenOCD-Aufruf kombiniert wurden.
|
||||||
|
|
||||||
|
**Regel für manuelles SWD-Flashen beider Bereiche:** immer die `.bin`-Datei
|
||||||
|
verwenden (nie `.elf`) und die Zieladresse **immer explizit angeben**
|
||||||
|
(`program firmware.bin 0x0 verify` für den Bootloader,
|
||||||
|
`program firmware.bin 0x2000 verify` für die App), und Bootloader- und
|
||||||
|
App-Schreibvorgang **in getrennten OpenOCD-Aufrufen**, nicht in einer
|
||||||
|
gemeinsamen `-c`-Kommandokette. Im Zweifel danach mit `dump_image` in einer
|
||||||
|
frischen, unabhängigen Sitzung verifizieren.
|
||||||
|
|
||||||
|
## Hardwaretest (2026-08-05)
|
||||||
|
|
||||||
|
Erster vollständiger Hardwaretest auf einem echten VersaPad-v2-Board über
|
||||||
|
Atmel-ICE/SWD. Ergebnisse:
|
||||||
|
|
||||||
|
- Bootloader-Build/-Flash/-Verify laufen sauber (89,9 % Flash, 7364/8192 Byte).
|
||||||
|
- USB-Enumeration und `VERSABOOT`-Massenspeicher-Modus funktionieren nach dem
|
||||||
|
PID-Fix (siehe unten) korrekt, inklusive korrektem `INFO_UF2.TXT`.
|
||||||
|
- Der Tastencheck (oben) funktioniert wie vorgesehen: gehaltene Taste beim
|
||||||
|
Boot → `VERSABOOT`, sonst normaler App-Start.
|
||||||
|
- **Kritischer Bug gefunden und behoben:** Der Sprung vom Bootloader in die
|
||||||
|
App (`__set_MSP` → `SCB->VTOR` → `bx`) führte bei jedem echten,
|
||||||
|
eigenständigen Boot (auch bei komplett getrenntem Debugger) zu einem
|
||||||
|
Hard-Fault/Lockup der CPU. Identische Register-/VTOR-Werte, vom Debugger
|
||||||
|
bei angehaltener CPU direkt injiziert, liefen dagegen einwandfrei — das
|
||||||
|
grenzte den Fehler auf die *Ausführung* der Sprungsequenz selbst ein, nicht
|
||||||
|
auf falsche Werte. Fix: `__DSB(); __ISB();` zwischen dem `SCB->VTOR`-Schreib-
|
||||||
|
zugriff und dem `bx` in `check_start_application()` (`src/main.c`) — von ARM
|
||||||
|
für genau dieses Bootloader-Pattern vorgeschrieben, hat im vendorten Code
|
||||||
|
gefehlt. Nach dem Fix bootet die App-Firmware zuverlässig, mit und ohne
|
||||||
|
angeschlossenen Debugger.
|
||||||
|
- **Zweiter Bug beim manuellen Debuggen gefunden:** Beim anschließenden
|
||||||
|
manuellen SWD-Debugging (Suche nach der Ursache des Tastencheck-Problems,
|
||||||
|
siehe oben) hat `openocd -c "program app.elf verify"` ohne explizite
|
||||||
|
Zieladresse wiederholt den Bootloader mit rohen ELF-Datei-Bytes
|
||||||
|
überschrieben, sobald Bootloader- und App-Flash im selben OpenOCD-Aufruf
|
||||||
|
kombiniert wurden — siehe "Achtung bei manuellem SWD-Flashen" oben. Nach
|
||||||
|
einem vollständigen Chip-Erase und getrennten `.bin`-Flashes mit expliziten
|
||||||
|
Adressen liefen beide Bereiche wieder zuverlässig, Tastencheck und
|
||||||
|
App-Start bestätigt funktionsfähig.
|
||||||
|
- **Kompletter USB-Flashweg getestet:** `pio run -e versapad_usb --target
|
||||||
|
upload` (App-Firmware, Repo-Root) baut `firmware.bin`, wandelt es über
|
||||||
|
[`../uf2conv.py`](../uf2conv.py) in `firmware.uf2` und kopiert es über
|
||||||
|
[`../upload_uf2.py`](../upload_uf2.py) automatisch aufs erkannte
|
||||||
|
`VERSABOOT`-Laufwerk. Der Bootloader erkennt den Schreibzugriff und
|
||||||
|
springt danach selbständig in die neue App — kein manuelles Auswerfen
|
||||||
|
oder Reset nötig. Voraussetzung: Board zuvor per gehaltener Taste (siehe
|
||||||
|
oben) in den Bootloader-Modus versetzt.
|
||||||
|
|
||||||
## Bekannte Einschränkungen
|
## Bekannte Einschränkungen
|
||||||
|
|
||||||
- Flash-Auslastung ~89 % (7292 von 8192 Byte). Wenig Puffer für Änderungen.
|
- Flash-Auslastung ~90 % (7364 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
|
- Kein Status-LED-Feedback im Bootloader-Modus, da die Platine nur eine
|
||||||
WS2812-Kette (kein einfaches GPIO-LED oder DotStar) hat und der
|
WS2812-Kette (kein einfaches GPIO-LED oder DotStar) hat und der
|
||||||
Original-Code dafür nicht ausgelegt ist.
|
Original-Code dafür nicht ausgelegt ist.
|
||||||
- USB_PID `0x0011` ist unverifiziert übernommen (siehe
|
- USB_PID war ursprünglich `0x0011` (Adafruit Gemma M0s eigene
|
||||||
`../doc/09_known_limitations.md`), noch nicht auf echter Hardware getestet.
|
Bootloader-PID, unverändert aus der Vorlage übernommen) — kollidierte auf
|
||||||
- Ungetestet auf echter Hardware, siehe Branch `feature/usb-bootloader`.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
Der UF2-Bootloader (siehe [README.md](README.md)) wurde am 2026-08-05 auf
|
||||||
nie auf echter Hardware gelaufen. Diese Anleitung ist für jemanden mit
|
einem echten VersaPad-v2-Board über Atmel-ICE/SWD getestet. Zusammenfassung
|
||||||
Atmel-ICE-Zugriff aufs VersaPad-v2-Board, um das einmal zu testen.
|
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`
|
**Sicherheitsrahmen, der eingehalten wurde:** Der Bootloader-Flash schreibt
|
||||||
(8 KiB). Die App-Firmware liegt ab `0x2000` und bleibt unangetastet. Falls
|
nur `0x0000..0x1FFF` (8 KiB). Die App-Firmware ab `0x2000` blieb dabei
|
||||||
etwas schiefgeht, ist das jederzeit per SWD neu beschreibbar, das Board kann
|
unangetastet. Über SWD war der Chip jederzeit neu beschreibbar; ein Brick war
|
||||||
dabei nicht dauerhaft "gebrickt" werden, solange der Atmel-ICE-Zugriff
|
zu keinem Zeitpunkt möglich, solange der Atmel-ICE-Zugriff funktionierte.
|
||||||
funktioniert.
|
|
||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
- Board per Atmel-ICE/SWD angeschlossen, genau wie beim normalen
|
- Board per Atmel-ICE/SWD angeschlossen, wie beim normalen
|
||||||
App-Firmware-Flashen (`pio run -e versapad --target upload` in
|
App-Firmware-Flashen (`pio run -e versapad --target upload`,
|
||||||
`doc/08_development.md`)
|
siehe `../doc/08_development.md`)
|
||||||
- PlatformIO Core installiert. Falls nicht:
|
- PlatformIO Core (hier: PlatformIO-IDE-penv unter
|
||||||
```bash
|
`%USERPROFILE%\.platformio\penv\Scripts\pio.exe`)
|
||||||
pip install -U platformio
|
|
||||||
|
## 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
|
Firmware tatsächlich über das `VERSABOOT`-Laufwerk flashen. Der
|
||||||
git clone https://git.jappel.io/jappel/VersaMCU.git
|
Firmware-Build der App erzeugt noch keine `.uf2`-Datei (nur `.bin`/`.elf` für
|
||||||
cd VersaMCU
|
den SWD-Weg) — das ist der nächste Schritt, siehe README, "Bekannte
|
||||||
git checkout feature/usb-bootloader
|
Einschränkungen".
|
||||||
```
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,16 @@
|
||||||
#define BOARD_ID "SAMD21G17D-VersaPad-v2"
|
#define BOARD_ID "SAMD21G17D-VersaPad-v2"
|
||||||
|
|
||||||
/* Same VID as the app firmware (platformio.ini), distinct PID so the
|
/* 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_VID 0x239A
|
||||||
#define USB_PID 0x0011
|
#define USB_PID 0x0043
|
||||||
|
|
||||||
/* No plain GPIO status LED on this board, only a WS2812 chain on PB22.
|
/* 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
|
* 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_CLOCK_PIN
|
||||||
//#define BOARD_RGBLED_DATA_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
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,38 @@ extern int8_t led_tick_step;
|
||||||
#define RESET_CONTROLLER RSTC
|
#define RESET_CONTROLLER RSTC
|
||||||
#endif
|
#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
|
* \brief Check the application startup condition
|
||||||
*
|
*
|
||||||
|
|
@ -94,6 +126,13 @@ extern int8_t led_tick_step;
|
||||||
static void check_start_application(void) {
|
static void check_start_application(void) {
|
||||||
uint32_t app_start_address;
|
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.
|
// Check if there is an IO which will hold us inside the bootloader.
|
||||||
#if defined(HOLD_PIN) && defined(HOLD_STATE)
|
#if defined(HOLD_PIN) && defined(HOLD_STATE)
|
||||||
PORT_PINCFG_Type pincfg = {0};
|
PORT_PINCFG_Type pincfg = {0};
|
||||||
|
|
@ -167,6 +206,14 @@ static void check_start_application(void) {
|
||||||
/* Rebase the vector table base address */
|
/* Rebase the vector table base address */
|
||||||
SCB->VTOR = ((uint32_t)APP_START_ADDRESS & SCB_VTOR_TBLOFF_Msk);
|
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 */
|
/* Jump to application Reset Handler in the application */
|
||||||
asm("bx %0" ::"r"(app_start_address));
|
asm("bx %0" ::"r"(app_start_address));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,26 @@ Robustheitskorrekturen. Sie ist keine Liste bereits umgesetzter Features.
|
||||||
## Bootloader-Ziel bleibt nicht unterstützt
|
## Bootloader-Ziel bleibt nicht unterstützt
|
||||||
|
|
||||||
Das aktive Ziel `versapad_nobl` reserviert den kompletten Bereich
|
Das aktive Ziel `versapad_nobl` reserviert den kompletten Bereich
|
||||||
`0x1FB00..0x1FFFF` für Makros und Config. Das auskommentierte
|
`0x1FB00..0x1FFFF` für Makros und Config.
|
||||||
USB-Bootloader-Environment verwendet dagegen weiterhin eine historische
|
|
||||||
Board-/Linker-Konfiguration und ist nicht als Produktionsziel verifiziert.
|
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`.
|
||||||
|
|
||||||
|
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
|
Die aktive Boarddatei benennt die MCU als `samd21g17d`, setzt für den
|
||||||
Arduino-Core aber weiterhin das Kompatibilitätsmakro `__SAMD21G18A__`. Der
|
Arduino-Core aber weiterhin das Kompatibilitätsmakro `__SAMD21G18A__`. Der
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,11 @@ upload_protocol = custom
|
||||||
extra_scripts = upload_openocd.py
|
extra_scripts = upload_openocd.py
|
||||||
debug_tool = openocd
|
debug_tool = openocd
|
||||||
|
|
||||||
; ── USB SAM-BA (nur wenn Bootloader geflasht ist) ─────────────────────────────
|
; ── USB UF2 (nur wenn bootloader/ geflasht ist) ────────────────────────────────
|
||||||
; [env:versapad_usb]
|
; Der eigene Bootloader spricht UF2/Massenspeicher, kein SAM-BA. upload_uf2.py
|
||||||
; extends = common
|
; erzeugt aus firmware.bin ein .uf2 und kopiert es aufs VERSABOOT-Laufwerk.
|
||||||
; board = versapad
|
[env:versapad_usb]
|
||||||
; upload_protocol = sam-ba
|
extends = common
|
||||||
|
board = versapad
|
||||||
|
upload_protocol = custom
|
||||||
|
extra_scripts = upload_uf2.py
|
||||||
|
|
|
||||||
83
uf2conv.py
Normal file
83
uf2conv.py
Normal file
|
|
@ -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 <input.bin> <output.uf2> [--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(
|
||||||
|
"<IIIIIIII",
|
||||||
|
UF2_MAGIC_START0,
|
||||||
|
UF2_MAGIC_START1,
|
||||||
|
UF2_FLAG_FAMILYID_PRESENT,
|
||||||
|
base_addr + offset,
|
||||||
|
PAYLOAD_SIZE,
|
||||||
|
block_no,
|
||||||
|
num_blocks,
|
||||||
|
family_id,
|
||||||
|
)
|
||||||
|
padding = b"\x00" * (476 - PAYLOAD_SIZE)
|
||||||
|
footer = struct.pack("<I", UF2_MAGIC_END)
|
||||||
|
blocks.append(header + chunk + padding + footer)
|
||||||
|
|
||||||
|
with open(uf2_path, "wb") as f:
|
||||||
|
f.write(b"".join(blocks))
|
||||||
|
|
||||||
|
return num_blocks
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> 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()
|
||||||
72
upload_uf2.py
Normal file
72
upload_uf2.py
Normal file
|
|
@ -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)
|
||||||
|
|
@ -6,7 +6,8 @@ SEARCH_DIR(.)
|
||||||
|
|
||||||
MEMORY
|
MEMORY
|
||||||
{
|
{
|
||||||
rom (rx) : ORIGIN = 0x00002000, LENGTH = 0x0001E000 /* 120K (128K - 8K bootloader) */
|
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 */
|
ram (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00004000 /* 16K */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue