Compare commits

..

1 commit

Author SHA1 Message Date
cjjohn
0461f6565b 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.
2026-08-05 06:56:34 +02:00
17 changed files with 122 additions and 580 deletions

3
.gitignore vendored
View file

@ -1,9 +1,6 @@
# PlatformIO # PlatformIO
.pio/ .pio/
# Python
__pycache__/
# VS Code # VS Code
.vscode/ .vscode/

View file

@ -15,7 +15,6 @@ ATSAMD21G17D mit PlatformIO und dem Arduino-SAMD-Framework.
| LEDs | 20 WS2812B mit Base-/Override-Farbe und 7 Animationsmodi | | LEDs | 20 WS2812B mit Base-/Override-Farbe und 7 Animationsmodi |
| Persistenz | Config und Makros im internen Flash | | Persistenz | Config und Makros im internen Flash |
| Recovery | Werksreset über zwei Tasten | | 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 Die drei Fader-Pins sind im Board-Variant definiert, werden von der aktuellen
Firmware aber noch nicht eingelesen. Firmware aber noch nicht eingelesen.
@ -30,7 +29,7 @@ Firmware aber noch nicht eingelesen.
| Encoder | 4× Quadratur über EIC-Interrupts | | Encoder | 4× Quadratur über EIC-Interrupts |
| LEDs | 20× WS2812B an `PB22` | | LEDs | 20× WS2812B an `PB22` |
| USB | Native USB als HID + CDC Composite Device | | 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 ## 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 Upload wird durch `upload_openocd.py` über das von PlatformIO installierte
OpenOCD ausgeführt. OpenOCD ausgeführt.
Alternativ lässt sich die App-Firmware nach einem einmaligen Das in `platformio.ini` auskommentierte Bootloader-Environment ist derzeit
SWD-Bootloader-Flash auch über USB aktualisieren, ganz ohne Atmel-ICE: nicht als Produktionsziel unterstützt. Details stehen unter
[bekannte Einschränkungen](doc/09_known_limitations.md).
```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).
## Laufzeitmodell ## Laufzeitmodell
@ -141,13 +119,9 @@ VersaMCU/
|-- AGENTS.md # Kontext und Richtlinien für Coding-LLMs |-- AGENTS.md # Kontext und Richtlinien für Coding-LLMs
|-- README.md |-- README.md
|-- platformio.ini |-- 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 |-- boards/ # PlatformIO-Boarddefinitionen
|-- variants/versapad/ # Pinmapping und Linker-Skripte |-- variants/versapad/ # Pinmapping und Linker-Skripte
|-- doc/ # Architektur- und Protokolldokumentation |-- doc/ # Architektur- und Protokolldokumentation
|-- bootloader/ # UF2-Bootloader (eigenes PlatformIO-Projekt)
`-- src/ `-- src/
|-- main.cpp |-- main.cpp
|-- CMainController.* # Orchestrierung |-- CMainController.* # Orchestrierung
@ -183,4 +157,3 @@ Binärverträge, Änderungsregeln und die minimale Verifikation.
- [CDC-Protokoll](doc/07_serial_protocol.md) - [CDC-Protokoll](doc/07_serial_protocol.md)
- [Entwicklung und Einstieg](doc/08_development.md) - [Entwicklung und Einstieg](doc/08_development.md)
- [Bekannte Einschränkungen](doc/09_known_limitations.md) - [Bekannte Einschränkungen](doc/09_known_limitations.md)
- [USB-Bootloader](doc/10_usb_bootloader.md)

View file

@ -7,10 +7,10 @@
"core": "arduino", "core": "arduino",
"variant": "versapad", "variant": "versapad",
"cpu": "cortex-m0plus", "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", "f_cpu": "48000000L",
"hwids": [ "hwids": [
["0x239A", "0x0042"] ["0x239A", "0x0011"]
], ],
"mcu": "samd21g17d", "mcu": "samd21g17d",
"usb_product": "VersaPad v2", "usb_product": "VersaPad v2",
@ -21,9 +21,14 @@
"name": "VersaPad v2 (USB bootloader)", "name": "VersaPad v2 (USB bootloader)",
"upload": { "upload": {
"maximum_ram_size": 16384, "maximum_ram_size": 16384,
"maximum_size": 121600, "maximum_size": 122880,
"disable_flushing": true,
"native_usb": true,
"offset": "0x2000", "offset": "0x2000",
"protocol": "custom" "protocol": "sam-ba",
"require_upload_port": true,
"use_1200bps_touch": true,
"wait_for_upload_port": true
}, },
"url": "", "url": "",
"vendor": "Custom" "vendor": "Custom"

View file

@ -1,5 +0,0 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch

View file

@ -2,11 +2,9 @@
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. lässt sich die App-Firmware ohne SWD über USB aktualisieren: Bootloader-Modus
aktivieren (Doppel-Tap Reset), Board erscheint als USB-Laufwerk `VERSABOOT`,
Speicherlayout, Bootloader-Aktivierung, Bedienung und Hardwaretest-Ergebnisse `.uf2`-Datei drauf kopieren.
stehen in [`../doc/10_usb_bootloader.md`](../doc/10_usb_bootloader.md). Diese
Datei beschreibt nur das Bootloader-Unterprojekt selbst.
## Herkunft ## 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 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
@ -45,26 +37,15 @@ da nur `0x0000..0x1FFF` beschrieben wird)
pio run -e versapad_bootloader --target upload 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) - Flash-Auslastung ~89 % (7292 von 8192 Byte). Wenig Puffer für Änderungen.
sind sicher, weil sie jeweils nur ihren eigenen Bereich anfassen. **Beim - Kein physischer Boot-Pin definiert, nur Doppel-Tap-Reset (RAM-Magic-Wert).
manuellen Debuggen über OpenOCD-Kommandozeile aber Vorsicht:** Ein Hardware-Fallback-Pin wäre für die Wiederherstellung bei kaputter
App-Firmware sinnvoll, ist aber noch nicht eingerichtet.
`openocd -c "program datei.elf verify"` **ohne explizite Zieladresse** hat - Kein Status-LED-Feedback im Bootloader-Modus, da die Platine nur eine
sich in dieser Kombination aus OpenOCD-Version/CMSIS-DAP-Adapter/Target-Skript WS2812-Kette (kein einfaches GPIO-LED oder DotStar) hat und der
als unzuverlässig erwiesen — statt die im ELF hinterlegten Sektionsadressen Original-Code dafür nicht ausgelegt ist.
(`0x2000` für die App) zu nutzen, landeten die rohen Datei-Bytes teils direkt - USB_PID `0x0011` ist unverifiziert übernommen (siehe
ab Flash-Adresse `0x0000` und haben damit den frisch geschriebenen Bootloader `../doc/09_known_limitations.md`), noch nicht auf echter Hardware getestet.
sofort wieder überschrieben (bestätigt am 2026-08-05: Byte 0 an Adresse - Ungetestet auf echter Hardware, siehe Branch `feature/usb-bootloader`.
`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.

67
bootloader/TESTING.md Normal file
View file

@ -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.

View file

@ -15,16 +15,9 @@
#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 0x0043 #define USB_PID 0x0011
/* 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
@ -36,21 +29,4 @@
//#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

View file

@ -87,38 +87,6 @@ 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
* *
@ -126,13 +94,6 @@ static bool boot_key_pressed(void) {
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};
@ -206,14 +167,6 @@ 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));
} }

View file

@ -26,9 +26,9 @@ pio run -e versapad --target upload
Der Upload nutzt `upload_openocd.py`, das das von PlatformIO installierte Der Upload nutzt `upload_openocd.py`, das das von PlatformIO installierte
OpenOCD mit `interface/cmsis-dap.cfg` und `target/at91samdXX.cfg` startet. OpenOCD mit `interface/cmsis-dap.cfg` und `target/at91samdXX.cfg` startet.
Für App-Updates über USB (nach einem einmaligen Bootloader-Flash) gibt es Das in `platformio.ini` nur als Beispiel enthaltene Environment
zusätzlich `env:versapad_usb`, siehe `versapad_usb` ist auskommentiert und mit dem aktuellen NVM-/Linker-Layout
[10_usb_bootloader.md](10_usb_bootloader.md). nicht als unterstützt anzusehen.
## Was beim Start passiert ## 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.*` | | LEDs | `05_led_system.md` | `CButton.*`, `hal/ws2812.*` |
| Persistente Config | `06_nvm_config.md` | `config/nvm_config.*`, Linker-Skripte | | Persistente Config | `06_nvm_config.md` | `config/nvm_config.*`, Linker-Skripte |
| Host-Protokoll | `07_serial_protocol.md` | `hal/usb_serial.*`, Controller | | 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 ## Verifikation

View file

@ -3,33 +3,19 @@
Diese Liste beschreibt den aktuellen Implementierungsstand nach den Diese Liste beschreibt den aktuellen Implementierungsstand nach den
Robustheitskorrekturen. Sie ist keine Liste bereits umgesetzter Features. 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`, Das aktive Ziel `versapad_nobl` reserviert den kompletten Bereich
Details in [10_usb_bootloader.md](10_usb_bootloader.md)) ist auf echter `0x1FB00..0x1FFFF` für Makros und Config. Das auskommentierte
Hardware verifiziert und ergänzt den SWD-Weg, ersetzt ihn aber nicht: der USB-Bootloader-Environment verwendet dagegen weiterhin eine historische
Bootloader selbst muss weiterhin einmalig per Atmel-ICE/SWD geflasht werden. Board-/Linker-Konfiguration und ist nicht als Produktionsziel verifiziert.
Verbleibende Einschränkungen: Die aktive Boarddatei benennt die MCU als `samd21g17d`, setzt für den
- `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
Arduino-Core aber weiterhin das Kompatibilitätsmakro `__SAMD21G18A__`. Der Arduino-Core aber weiterhin das Kompatibilitätsmakro `__SAMD21G18A__`. Der
PlatformIO-Build meldet korrekt 128 KiB physischen Flash, 16 KiB RAM und PlatformIO-Build meldet korrekt 128 KiB physischen Flash, 16 KiB RAM und
129.792 Byte (`versapad_nobl`) beziehungsweise 121.600 Byte 129.792 Byte nutzbaren Firmwarebereich. Vor device-spezifischen
(`versapad`, abzüglich 8 KiB Bootloader) nutzbaren Firmwarebereich. Vor Core-Änderungen sollte die historische Makro-Abweichung trotzdem geprüft
device-spezifischen Core-Änderungen sollte die historische Makro-Abweichung werden.
trotzdem geprüft werden.
## Event-Queue hat eine feste Kapazität ## Event-Queue hat eine feste Kapazität

View file

@ -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.

View file

@ -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 | | [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 | | [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 | | [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 Die Repository-weiten Richtlinien und der kompakte LLM-Kontext stehen in
[`../AGENTS.md`](../AGENTS.md). [`../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) - 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) - 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) - 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)

View file

@ -22,11 +22,8 @@ upload_protocol = custom
extra_scripts = upload_openocd.py extra_scripts = upload_openocd.py
debug_tool = openocd debug_tool = openocd
; ── USB UF2 (nur wenn bootloader/ geflasht ist) ──────────────────────────────── ; ── USB SAM-BA (nur wenn Bootloader geflasht ist) ─────────────────────────────
; Der eigene Bootloader spricht UF2/Massenspeicher, kein SAM-BA. upload_uf2.py ; [env:versapad_usb]
; erzeugt aus firmware.bin ein .uf2 und kopiert es aufs VERSABOOT-Laufwerk. ; extends = common
[env:versapad_usb] ; board = versapad
extends = common ; upload_protocol = sam-ba
board = versapad
upload_protocol = custom
extra_scripts = upload_uf2.py

View file

@ -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 <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()

View file

@ -2,118 +2,23 @@ Import("env")
import os import os
import subprocess import subprocess
# Real, already-built bootloader image used as the reference for the presence def upload_via_openocd(source, target, env):
# check below -- a raw synthetic probe blob turned out unreliable with pkg_dir = env.PioPlatform().get_package_dir("tool-openocd")
# verify_image (silent no-op on tiny files), whereas verify_image against a openocd = os.path.join(pkg_dir, "bin", "openocd.exe")
# real firmware .bin has been solid throughout this project's bring-up. scripts = os.path.join(pkg_dir, "scripts")
BOOTLOADER_BIN = os.path.join( firmware = str(source[0]) # .elf path
"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 = [ cmd = [
openocd, openocd,
"-s", scripts, "-s", scripts,
"-f", "interface/cmsis-dap.cfg", "-f", "interface/cmsis-dap.cfg",
"-f", "target/at91samdXX.cfg", "-f", "target/at91samdXX.cfg",
"-c", extra_cmd, "-c", 'program "{}" verify reset; shutdown'.format(firmware.replace("\\", "/"))
] ]
print(" ".join(cmd)) print(" ".join(cmd))
if capture: result = subprocess.run(cmd)
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: if result.returncode != 0:
env.Exit(1) 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) 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."
),
)

View file

@ -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)

View file

@ -6,8 +6,7 @@ SEARCH_DIR(.)
MEMORY MEMORY
{ {
rom (rx) : ORIGIN = 0x00002000, LENGTH = 0x0001DB00 /* 118.75K Firmware (128K - 8K bootloader - 1.25K NVM) */ rom (rx) : ORIGIN = 0x00002000, LENGTH = 0x0001E000 /* 120K (128K - 8K bootloader) */
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 */
} }