Compare commits

...
Sign in to create a new pull request.

7 commits

Author SHA1 Message Date
01c5e0930e Guard env:versapad's upload against overwriting the bootloader
pio run -e versapad --target upload writes the app starting at
0x0000 and silently destroyed the installed UF2 bootloader twice
today during testing -- no warning, no error, just a board that
stopped responding to the boot-key hold.

upload_openocd.py now checks for the bootloader (verify_image against
the locally built bootloader/.pio/build/versapad_bootloader/firmware.bin)
before an env:versapad upload and refuses if one is present, pointing
at env:versapad_usb instead. Fails closed: an inconclusive check (e.g.
bootloader not built locally, SWD not responding) blocks rather than
proceeding on a guess -- confirmed necessary the hard way, since a
"fail open" first attempt let the destructive upload through silently.
Scoped to PIOENV == "versapad" only, since bootloader/platformio.ini's
own upload reuses this same script and must always be allowed to write
0x0000. A new erase-bootloader-and-flash custom target remains as the
explicit, deliberate override.

Documented the workflow (bootloader is its own PlatformIO project,
flashed once via SWD; versapad_usb is the normal path afterward;
versapad's upload is now guarded) in README.md and
doc/10_usb_bootloader.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 23:27:02 +02:00
cd27639e44 Merge feature/usb-bootloader: USB firmware flashing via UF2 bootloader
Adds a UF2 bootloader (bootloader/) that lets the app firmware be
updated over USB instead of requiring an Atmel-ICE, after a one-time
SWD bootloader flash. Since this board has no dedicated reset/boot
button, bootloader entry is done by holding the bottom-right Cherry MX
key during power-on/reset.

Verified end to end on real hardware, including two hardware bugs
found and fixed along the way: a missing DSB/ISB barrier in the
bootloader's jump-to-app sequence (hard-faulted on every standalone
boot), and a USB PID collision with Adafruit's own Gemma M0 bootloader
PID. See doc/10_usb_bootloader.md for the full writeup, memory layout,
and known limitations.
2026-08-05 22:50:20 +02:00
33d9e85992 Fold bootloader documentation into the main doc/ index
Adds doc/10_usb_bootloader.md as the primary reference for the UF2
bootloader (memory layout, key-based boot entry, build/flash steps,
hardware test findings, known limitations), following the existing
numbered-doc convention. Updates doc/INDEX.md, doc/09_known_limitations.md
(bootloader is no longer "not supported", just scoped), and
doc/08_development.md accordingly.

Removes bootloader/TESTING.md (its findings now live in
doc/10_usb_bootloader.md) and trims bootloader/README.md down to what
belongs with that subproject specifically: upstream attribution/license
and local build/flash commands, plus the OpenOCD manual-flashing
warning since that's implementation-specific detail that would clutter
the higher-level doc.

Updates the top-level README.md (feature table, hardware table,
quickstart, project tree, doc links) to reflect USB flashing as a
supported path alongside SWD.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 22:49:56 +02:00
3986d2effe Fix off-by-512-byte error in the app-side bootloader linker script
flash_with_bootloader.ld's rom region ended 512 bytes short of the
NVM region it's meant to butt up against (0x1F900 instead of
0x1FB00), leaving a small gap neither region could use. Corrected the
LENGTH and the matching maximum_size in boards/versapad.json (128K -
8K bootloader - 1.25K NVM = 121600 bytes, not 120832). Found while
double-checking the numbers for a flash-usage breakdown; current
19KB app image is nowhere near either boundary, so this never
affected anything on hardware.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 22:30:03 +02:00
6c71f5f057 Document an OpenOCD ELF-flash pitfall found during hardware debugging
While chasing why the boot-key check stopped working, traced it to
openocd's "program <elf> verify" silently writing raw file bytes
starting at flash 0x0 instead of the ELF's own section addresses,
whenever combined with a prior bootloader write in the same OpenOCD
invocation -- repeatedly clobbering the just-flashed bootloader with
the app's ELF header. Recovered via full chip-erase and reflashing
bootloader and app as separate .bin writes with explicit addresses in
isolated OpenOCD sessions; both regions verified correct afterward and
confirmed working on hardware (key-hold entry and normal app boot).

Documented the pitfall and the safe manual-flashing rule so it doesn't
get rediscovered the expensive way again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 22:20:23 +02:00
d325297063 Add end-to-end USB flashing for the app firmware via UF2
Adds uf2conv.py (minimal, dependency-free .bin -> .uf2 converter
matching bootloader/inc/uf2format.h's block layout) and upload_uf2.py,
a PlatformIO upload hook for env:versapad_usb that finds the mounted
VERSABOOT volume and copies the converted firmware onto it.

env:versapad_usb previously used upload_protocol=sam-ba, the classic
Arduino/Atmel protocol -- the actual bootloader speaks UF2/mass
storage, not SAM-BA, so that upload path never worked. Switched to
upload_protocol=custom with the new hook, and cleaned the now-unused
SAM-BA-specific fields out of boards/versapad.json.

Verified end to end on real hardware: pio run -e versapad_usb
--target upload builds, converts, copies to the VERSABOOT drive, and
the bootloader jumps into the freshly written app on its own.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 21:35:19 +02:00
f60a29137c Fix bootloader hardware bring-up and add key-based boot entry
Hardware-tested the UF2 bootloader end to end on a real VersaPad v2
board. Found and fixed a real bug: the bootloader's jump into the app
(__set_MSP -> SCB->VTOR -> bx) hard-faulted on every standalone boot,
even with the debugger fully disconnected; identical register/VTOR
values injected directly by a halted debugger ran fine, which pointed
at the missing __DSB()/__ISB() barriers ARM's own guidance requires
for this exact pattern. Also fixed a USB PID collision (0x0011 is
Adafruit's own Gemma M0 bootloader PID, misidentified by Windows as a
Circuit Playground COM port instead of exposing VERSABOOT).

This board has no dedicated reset/boot button, so add a hardware boot
entry that doesn't need one: holding the bottom-right Cherry MX key
(key_id 24) during reset/power-on drives its matrix row and reads its
column directly in the bootloader, before the app is even validated.

Also corrected the app-side flash_with_bootloader.ld (was missing the
NVM carve-out flash_without_bootloader.ld already has) and
boards/versapad.json (wrong flash/RAM size, wrong MCU macro, stale
PID), and enabled the previously-commented-out env:versapad_usb.

Documented findings in bootloader/README.md, bootloader/TESTING.md,
and doc/09_known_limitations.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 21:28:15 +02:00
17 changed files with 581 additions and 123 deletions

3
.gitignore vendored
View file

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

View file

@ -15,6 +15,7 @@ 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.
@ -29,7 +30,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, standardmäßig ohne Bootloader | | Programmer | Atmel-ICE/CMSIS-DAP über SWD, oder USB über den UF2-Bootloader |
## Schnellstart ## Schnellstart
@ -46,9 +47,30 @@ 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.
Das in `platformio.ini` auskommentierte Bootloader-Environment ist derzeit Alternativ lässt sich die App-Firmware nach einem einmaligen
nicht als Produktionsziel unterstützt. Details stehen unter SWD-Bootloader-Flash auch über USB aktualisieren, ganz ohne Atmel-ICE:
[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
@ -119,9 +141,13 @@ 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
@ -157,3 +183,4 @@ 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

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

5
bootloader/.gitignore vendored Normal file
View file

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

View file

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

View file

@ -1,67 +0,0 @@
# 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,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

View file

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

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.
Das in `platformio.ini` nur als Beispiel enthaltene Environment Für App-Updates über USB (nach einem einmaligen Bootloader-Flash) gibt es
`versapad_usb` ist auskommentiert und mit dem aktuellen NVM-/Linker-Layout zusätzlich `env:versapad_usb`, siehe
nicht als unterstützt anzusehen. [10_usb_bootloader.md](10_usb_bootloader.md).
## Was beim Start passiert ## Was beim Start passiert
@ -63,6 +63,7 @@ und NVM-Schreibvorgängen. Es gibt keinen Scheduler und keine Threads.
| LEDs | `05_led_system.md` | `CButton.*`, `hal/ws2812.*` | | 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,19 +3,33 @@
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.
## Bootloader-Ziel bleibt nicht unterstützt ## USB-Bootloader hat einen eingeschränkten Gültigkeitsbereich
Das aktive Ziel `versapad_nobl` reserviert den kompletten Bereich Der UF2-Bootloader (`bootloader/`, App-Environment `env:versapad_usb`,
`0x1FB00..0x1FFFF` für Makros und Config. Das auskommentierte Details in [10_usb_bootloader.md](10_usb_bootloader.md)) ist auf echter
USB-Bootloader-Environment verwendet dagegen weiterhin eine historische Hardware verifiziert und ergänzt den SWD-Weg, ersetzt ihn aber nicht: der
Board-/Linker-Konfiguration und ist nicht als Produktionsziel verifiziert. Bootloader selbst muss weiterhin einmalig per Atmel-ICE/SWD geflasht werden.
Die aktive Boarddatei benennt die MCU als `samd21g17d`, setzt für den 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
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 nutzbaren Firmwarebereich. Vor device-spezifischen 129.792 Byte (`versapad_nobl`) beziehungsweise 121.600 Byte
Core-Änderungen sollte die historische Makro-Abweichung trotzdem geprüft (`versapad`, abzüglich 8 KiB Bootloader) nutzbaren Firmwarebereich. Vor
werden. device-spezifischen Core-Änderungen sollte die historische Makro-Abweichung
trotzdem geprüft werden.
## Event-Queue hat eine feste Kapazität ## Event-Queue hat eine feste Kapazität

134
doc/10_usb_bootloader.md Normal file
View file

@ -0,0 +1,134 @@
# 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,6 +17,7 @@ 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).
@ -29,3 +30,4 @@ Die Repository-weiten Richtlinien und der kompakte LLM-Kontext stehen in
- Action-Semantik und HID-Hold: [03_action_engine.md](03_action_engine.md) - 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,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
View 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()

View file

@ -2,23 +2,118 @@ Import("env")
import os import os
import subprocess import subprocess
def upload_via_openocd(source, target, env): # Real, already-built bootloader image used as the reference for the presence
pkg_dir = env.PioPlatform().get_package_dir("tool-openocd") # check below -- a raw synthetic probe blob turned out unreliable with
openocd = os.path.join(pkg_dir, "bin", "openocd.exe") # verify_image (silent no-op on tiny files), whereas verify_image against a
scripts = os.path.join(pkg_dir, "scripts") # real firmware .bin has been solid throughout this project's bring-up.
firmware = str(source[0]) # .elf path BOOTLOADER_BIN = os.path.join(
"bootloader", ".pio", "build", "versapad_bootloader", "firmware.bin"
)
def _openocd_paths(env):
pkg_dir = env.PioPlatform().get_package_dir("tool-openocd")
return (
os.path.join(pkg_dir, "bin", "openocd.exe"),
os.path.join(pkg_dir, "scripts"),
)
def _run_openocd(env, extra_cmd, capture=False):
openocd, scripts = _openocd_paths(env)
cmd = [ 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", 'program "{}" verify reset; shutdown'.format(firmware.replace("\\", "/")) "-c", extra_cmd,
] ]
print(" ".join(cmd)) print(" ".join(cmd))
result = subprocess.run(cmd) if capture:
return subprocess.run(cmd, capture_output=True, text=True)
return subprocess.run(cmd)
def _bootloader_present(env):
bootloader_bin = env.subst(
os.path.join("$PROJECT_DIR", BOOTLOADER_BIN)
)
if not os.path.isfile(bootloader_bin):
print("WARNING: {} not found (build it with".format(BOOTLOADER_BIN))
print("'cd bootloader && pio run -e versapad_bootloader') -- can't check")
print("for an installed bootloader, refusing to flash as a precaution.")
print("Use 'pio run -e versapad -t erase-bootloader-and-flash' to override.")
return True
result = _run_openocd(
env,
'init; reset halt; verify_image "{}" 0x0; shutdown'.format(
bootloader_bin.replace("\\", "/")
),
capture=True,
)
output = result.stdout + result.stderr
if "checksum mismatch" in output or "diff " in output:
return False # something else is at 0x0000 -- not this bootloader
if "halted due to debug-request" in output and "Error" not in output:
return True # verify_image is silent on a match; absence of a
# mismatch/error after a successful connect means it matched
print("WARNING: could not read flash to check for an installed bootloader")
print("(inconclusive) -- refusing to flash as a precaution.")
print("Use 'pio run -e versapad -t erase-bootloader-and-flash' to override.")
return True
def _flash(env, firmware):
result = _run_openocd(
env, 'program "{}" verify reset; shutdown'.format(firmware.replace("\\", "/"))
)
if result.returncode != 0: 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."
),
)

72
upload_uf2.py Normal file
View 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)

View file

@ -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 */
} }