From d3252970633955ca2afcaab8ff46755b264e122a Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Wed, 5 Aug 2026 21:35:19 +0200 Subject: [PATCH] Add end-to-end USB flashing for the app firmware via UF2 Adds uf2conv.py (minimal, dependency-free .bin -> .uf2 converter matching bootloader/inc/uf2format.h's block layout) and upload_uf2.py, a PlatformIO upload hook for env:versapad_usb that finds the mounted VERSABOOT volume and copies the converted firmware onto it. env:versapad_usb previously used upload_protocol=sam-ba, the classic Arduino/Atmel protocol -- the actual bootloader speaks UF2/mass storage, not SAM-BA, so that upload path never worked. Switched to upload_protocol=custom with the new hook, and cleaned the now-unused SAM-BA-specific fields out of boards/versapad.json. Verified end to end on real hardware: pio run -e versapad_usb --target upload builds, converts, copies to the VERSABOOT drive, and the bootloader jumps into the freshly written app on its own. Co-Authored-By: Claude Sonnet 5 --- .gitignore | 3 ++ boards/versapad.json | 7 +--- bootloader/README.md | 45 ++++++++++++++------ doc/09_known_limitations.md | 25 ++++++----- platformio.ini | 7 +++- uf2conv.py | 83 +++++++++++++++++++++++++++++++++++++ upload_uf2.py | 72 ++++++++++++++++++++++++++++++++ 7 files changed, 210 insertions(+), 32 deletions(-) create mode 100644 uf2conv.py create mode 100644 upload_uf2.py diff --git a/.gitignore b/.gitignore index 18c72d2..7640e36 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # PlatformIO .pio/ +# Python +__pycache__/ + # VS Code .vscode/ diff --git a/boards/versapad.json b/boards/versapad.json index 308ad44..36e874f 100644 --- a/boards/versapad.json +++ b/boards/versapad.json @@ -22,13 +22,8 @@ "upload": { "maximum_ram_size": 16384, "maximum_size": 120832, - "disable_flushing": true, - "native_usb": true, "offset": "0x2000", - "protocol": "sam-ba", - "require_upload_port": true, - "use_1200bps_touch": true, - "wait_for_upload_port": true + "protocol": "custom" }, "url": "", "vendor": "Custom" diff --git a/bootloader/README.md b/bootloader/README.md index f02c84a..3032296 100644 --- a/bootloader/README.md +++ b/bootloader/README.md @@ -6,11 +6,9 @@ lässt sich die App-Firmware ohne SWD über USB aktualisieren: Bootloader-Modus aktivieren, Board erscheint als USB-Laufwerk `VERSABOOT`, `.uf2`-Datei drauf kopieren. -Der Bootloader selbst (Flash, USB-Enumeration, Massenspeicher-Modus, -Rücksprung in die App) ist auf echter Hardware verifiziert, siehe -[Hardwaretest](#hardwaretest-2026-08-05). Der App-seitige `.uf2`-Schreibweg -(Firmware tatsächlich über das Laufwerk aktualisieren) ist noch nicht gebaut, -siehe [Bekannte Einschränkungen](#bekannte-einschränkungen). +Der komplette Weg — Bootloader-Einstieg, `.uf2`-Erzeugung, Kopieren aufs +Laufwerk, automatischer Rücksprung in die neu geschriebene App — ist auf +echter Hardware verifiziert, siehe [Hardwaretest](#hardwaretest-2026-08-05). Diese Platine hat keinen dedizierten Reset-/Boot-Taster. Bootloader-Modus aktivieren heißt hier: unterste rechte Cherry-MX-Taste (key_id 24) beim @@ -34,6 +32,12 @@ und das Node-/Makefile-basierte Build-System — stattdessen ein eigenständiges PlatformIO-Environment, damit dasselbe Tooling wie für die App-Firmware ausreicht. +Für die App-Seite (nicht diesen Bootloader-Build) gibt es unter +[`../uf2conv.py`](../uf2conv.py) eine eigene, minimale Python-3-Neuimplemen- +tierung des `.bin`→`.uf2`-Konverters (kein Upstream-Code, passendes +Blockformat zu `inc/uf2format.h`), eingebunden über +[`../upload_uf2.py`](../upload_uf2.py) als `env:versapad_usb`-Upload-Hook. + ## Build ```bash @@ -73,6 +77,17 @@ Bedienung: USB-Kabel ziehen, unterste rechte Taste gedrückt halten, Kabel wieder einstecken (Taste dabei weiter halten) → Board bootet direkt in `VERSABOOT`. Ohne gehaltene Taste startet die App normal. +## App-Firmware per USB flashen (nach dem einmaligen Bootloader-Flash) + +```bash +# Board zuerst in den Bootloader-Modus versetzen: USB ziehen, unterste +# rechte Taste halten, wieder einstecken (siehe oben) +pio run -e versapad_usb --target upload +``` + +Baut die App-Firmware (Repo-Root, nicht `bootloader/`), erzeugt `firmware.uf2` +und kopiert es aufs `VERSABOOT`-Laufwerk. Kein Atmel-ICE mehr nötig. + ## Hardwaretest (2026-08-05) Erster vollständiger Hardwaretest auf einem echten VersaPad-v2-Board über @@ -94,6 +109,14 @@ Atmel-ICE/SWD. Ergebnisse: für genau dieses Bootloader-Pattern vorgeschrieben, hat im vendorten Code gefehlt. Nach dem Fix bootet die App-Firmware zuverlässig, mit und ohne angeschlossenen Debugger. +- **Kompletter USB-Flashweg getestet:** `pio run -e versapad_usb --target + upload` (App-Firmware, Repo-Root) baut `firmware.bin`, wandelt es über + [`../uf2conv.py`](../uf2conv.py) in `firmware.uf2` und kopiert es über + [`../upload_uf2.py`](../upload_uf2.py) automatisch aufs erkannte + `VERSABOOT`-Laufwerk. Der Bootloader erkennt den Schreibzugriff und + springt danach selbständig in die neue App — kein manuelles Auswerfen + oder Reset nötig. Voraussetzung: Board zuvor per gehaltener Taste (siehe + oben) in den Bootloader-Modus versetzt. ## Bekannte Einschränkungen @@ -109,11 +132,7 @@ Atmel-ICE/SWD. Ergebnisse: (kein offiziell zugeteilter PID unter Adafruits VID); ein sauber eigener VID (z. B. über pid.codes) wäre die langfristig korrekte Lösung, ist aber nicht Teil dieses Branches. -- Die App-Firmware erzeugt noch keine `.uf2`-Datei, nur `.bin`/`.elf` - (SWD-Weg). Der geplante Weg über das `VERSABOOT`-Laufwerk ist damit noch - nicht nutzbar. -- `env:versapad_usb` in der Haupt-`platformio.ini` verwendet - `upload_protocol = sam-ba` — das klassische Arduino/Atmel-SAM-BA-Protokoll, - nicht das UF2/Massenspeicher-Verfahren dieses Bootloaders. Für echtes - USB-Flashen wird stattdessen ein `.bin`→`.uf2`-Konvertierungsschritt plus - einfaches Kopieren aufs Laufwerk benötigt, kein spezielles Upload-Protokoll. +- [`../upload_uf2.py`](../upload_uf2.py) sucht das `VERSABOOT`-Laufwerk aktuell + nur über die Windows-API (`GetVolumeInformationW`) — passend zur bisherigen + Dev-Umgebung dieses Projekts, aber nicht plattformübergreifend. Für + macOS/Linux müsste die Laufwerkssuche noch ergänzt werden. diff --git a/doc/09_known_limitations.md b/doc/09_known_limitations.md index 4e16e02..748cfab 100644 --- a/doc/09_known_limitations.md +++ b/doc/09_known_limitations.md @@ -9,20 +9,23 @@ Das aktive Ziel `versapad_nobl` reserviert den kompletten Bereich `0x1FB00..0x1FFFF` für Makros und Config. Ein USB-Bootloader-Pfad wird im Branch `feature/usb-bootloader` aufgebaut -(`bootloader/`, App-Environment `env:versapad_usb`). Der Bootloader selbst -(UF2, `bootloader/`) ist dort auf echter Hardware verifiziert — inklusive -eines gefundenen und behobenen Bugs beim Sprung in die App (fehlende -`__DSB()`/`__ISB()` vor dem `bx`, siehe `bootloader/README.md`, -"Hardwaretest"). `env:versapad_usb`s Linkerskript +(`bootloader/`, App-Environment `env:versapad_usb`). Der komplette Weg ist +dort auf echter Hardware verifiziert: Bootloader-Flash, USB-Enumeration, +Tastencheck-Einstieg (kein physischer Reset-Taster auf diesem Board, siehe +`bootloader/README.md`, "Hardware-Bootloader-Einstieg"), App-Firmware per +`.uf2` über `env:versapad_usb --target upload` schreiben, automatischer +Rücksprung in die neue App. Details und ein gefundener/behobener +Hard-Fault-Bug beim Sprung Bootloader→App (fehlende `__DSB()`/`__ISB()` vor +dem `bx`) stehen in `bootloader/README.md`, "Hardwaretest". +`env:versapad_usb`s Linkerskript (`variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld`) reserviert inzwischen denselben NVM-Bereich wie `flash_without_bootloader.ld`. -Trotzdem noch kein Produktionsziel: `env:versapad_usb` verwendet -`upload_protocol = sam-ba`, das klassische Arduino/Atmel-SAM-BA-Protokoll — -das spricht der UF2-Bootloader nicht. Die App-Firmware erzeugt außerdem noch -keine `.uf2`-Datei. Vor einem Merge nach `master` fehlen also noch die -`.uf2`-Erzeugung und ein Ende-zu-Ende-Test des tatsächlichen USB-Flashwegs -(Datei aufs `VERSABOOT`-Laufwerk kopieren). +Trotzdem noch kein Merge-fertiges Produktionsziel: Der `.bin`→`.uf2`-Weg +(`uf2conv.py`, `upload_uf2.py`, Repo-Root) sucht das `VERSABOOT`-Laufwerk +bisher nur über die Windows-API, keine macOS/Linux-Unterstützung. Die +Bootloader-USB-PID (`0x0043`) ist zwar kollisionsfrei verifiziert, aber kein +offiziell registrierter Wert unter Adafruits VID `0x239A`. Die aktive Boarddatei benennt die MCU als `samd21g17d`, setzt für den Arduino-Core aber weiterhin das Kompatibilitätsmakro `__SAMD21G18A__`. Der diff --git a/platformio.ini b/platformio.ini index 3b3df48..a2b71a8 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,8 +22,11 @@ upload_protocol = custom extra_scripts = upload_openocd.py debug_tool = openocd -; ── USB SAM-BA (nur wenn Bootloader geflasht ist) ───────────────────────────── +; ── USB UF2 (nur wenn bootloader/ geflasht ist) ──────────────────────────────── +; Der eigene Bootloader spricht UF2/Massenspeicher, kein SAM-BA. upload_uf2.py +; erzeugt aus firmware.bin ein .uf2 und kopiert es aufs VERSABOOT-Laufwerk. [env:versapad_usb] extends = common board = versapad -upload_protocol = sam-ba +upload_protocol = custom +extra_scripts = upload_uf2.py diff --git a/uf2conv.py b/uf2conv.py new file mode 100644 index 0000000..0053f09 --- /dev/null +++ b/uf2conv.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Minimal .bin -> .uf2 converter for the VersaMCU UF2 bootloader. + +Standalone reimplementation of the block format microsoft/uf2-samdx1's +uf2conv.py produces (256-byte payload per 512-byte block); see +bootloader/inc/uf2format.h for the struct this has to match on the device +side. No external dependencies, Python 3 only. + +Usage: + python uf2conv.py [--base 0x2000] [--family 0x68ed2b88] +""" +import argparse +import struct + +UF2_MAGIC_START0 = 0x0A324655 # "UF2\n" +UF2_MAGIC_START1 = 0x9E5D5157 # randomly selected, must match the device +UF2_MAGIC_END = 0x0AB16F30 # ditto +UF2_FLAG_FAMILYID_PRESENT = 0x00002000 + +SAMD21_FAMILY_ID = 0x68ED2B88 # bootloader/inc/uf2format.h, #ifdef SAMD21 +PAYLOAD_SIZE = 256 # bytes per block; matches the upstream uf2conv.py convention + + +def convert(bin_path: str, uf2_path: str, base_addr: int, family_id: int) -> int: + with open(bin_path, "rb") as f: + data = f.read() + + # Pad to a whole number of blocks; the bootloader writes payloadSize + # bytes per block regardless of how much of it is real firmware. + if len(data) % PAYLOAD_SIZE != 0: + data += b"\x00" * (PAYLOAD_SIZE - len(data) % PAYLOAD_SIZE) + num_blocks = len(data) // PAYLOAD_SIZE + + blocks = [] + for block_no in range(num_blocks): + offset = block_no * PAYLOAD_SIZE + chunk = data[offset : offset + PAYLOAD_SIZE] + header = struct.pack( + " None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("input", help="Path to the raw firmware .bin") + parser.add_argument("output", help="Path to write the .uf2 to") + parser.add_argument( + "--base", + type=lambda s: int(s, 0), + default=0x2000, + help="Flash base address the .bin was linked for (default: 0x2000, matches " + "variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld)", + ) + parser.add_argument( + "--family", + type=lambda s: int(s, 0), + default=SAMD21_FAMILY_ID, + help="UF2 family ID (default: SAMD21, 0x68ed2b88)", + ) + args = parser.parse_args() + + num_blocks = convert(args.input, args.output, args.base, args.family) + print(f"Wrote {args.output}: {num_blocks} blocks, base 0x{args.base:08x}") + + +if __name__ == "__main__": + main() diff --git a/upload_uf2.py b/upload_uf2.py new file mode 100644 index 0000000..0448df3 --- /dev/null +++ b/upload_uf2.py @@ -0,0 +1,72 @@ +"""PlatformIO upload hook for env:versapad_usb. + +The board's actual bootloader (bootloader/, UF2/mass-storage) does not speak +SAM-BA, so upload_protocol=sam-ba (the board.json default, inherited from an +Arduino-Zero-style template) does not work here. This converts the built +.bin to .uf2 and copies it onto the VERSABOOT mass-storage volume instead -- +Windows only for now, matching the rest of this project's dev environment. +""" +import ctypes +import os +import string +import sys + +Import("env") + +sys.path.insert(0, env.subst("$PROJECT_DIR")) +import uf2conv + +VOLUME_LABEL = "VERSABOOT" +DRIVE_UNKNOWN = 0 +DRIVE_NO_ROOT_DIR = 1 + + +def find_versaboot_drive(label=VOLUME_LABEL): + for letter in string.ascii_uppercase: + root = f"{letter}:\\" + drive_type = ctypes.windll.kernel32.GetDriveTypeW(root) + if drive_type in (DRIVE_UNKNOWN, DRIVE_NO_ROOT_DIR): + continue + vol_name_buf = ctypes.create_unicode_buffer(261) + fs_name_buf = ctypes.create_unicode_buffer(261) + ok = ctypes.windll.kernel32.GetVolumeInformationW( + ctypes.c_wchar_p(root), + vol_name_buf, + ctypes.sizeof(vol_name_buf), + None, + None, + None, + fs_name_buf, + ctypes.sizeof(fs_name_buf), + ) + if ok and vol_name_buf.value == label: + return root + return None + + +def upload_via_uf2(source, target, env): + build_dir = env.subst("$BUILD_DIR") + bin_path = os.path.join(build_dir, "firmware.bin") + uf2_path = os.path.join(build_dir, "firmware.uf2") + + if not os.path.isfile(bin_path): + print(f"error: {bin_path} not found (expected as a normal build product)") + env.Exit(1) + + num_blocks = uf2conv.convert(bin_path, uf2_path, base_addr=0x2000, family_id=uf2conv.SAMD21_FAMILY_ID) + print(f"Wrote {uf2_path}: {num_blocks} blocks") + + drive = find_versaboot_drive() + if drive is None: + print(f"error: no drive labeled '{VOLUME_LABEL}' found.") + print("Hold the bottom-right Cherry MX key while plugging in USB to enter the bootloader.") + env.Exit(1) + + dest = os.path.join(drive, "firmware.uf2") + print(f"Copying {uf2_path} -> {dest}") + with open(uf2_path, "rb") as src, open(dest, "wb") as dst: + dst.write(src.read()) + print("Upload triggers a reset into the app on the device side; no further action needed.") + + +env.Replace(UPLOADCMD=upload_via_uf2)