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>
This commit is contained in:
parent
f60a29137c
commit
d325297063
7 changed files with 210 additions and 32 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,6 +1,9 @@
|
||||||
# PlatformIO
|
# PlatformIO
|
||||||
.pio/
|
.pio/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
# VS Code
|
# VS Code
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,8 @@
|
||||||
"upload": {
|
"upload": {
|
||||||
"maximum_ram_size": 16384,
|
"maximum_ram_size": 16384,
|
||||||
"maximum_size": 120832,
|
"maximum_size": 120832,
|
||||||
"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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
aktivieren, Board erscheint als USB-Laufwerk `VERSABOOT`, `.uf2`-Datei drauf
|
||||||
kopieren.
|
kopieren.
|
||||||
|
|
||||||
Der Bootloader selbst (Flash, USB-Enumeration, Massenspeicher-Modus,
|
Der komplette Weg — Bootloader-Einstieg, `.uf2`-Erzeugung, Kopieren aufs
|
||||||
Rücksprung in die App) ist auf echter Hardware verifiziert, siehe
|
Laufwerk, automatischer Rücksprung in die neu geschriebene App — ist auf
|
||||||
[Hardwaretest](#hardwaretest-2026-08-05). Der App-seitige `.uf2`-Schreibweg
|
echter Hardware verifiziert, siehe [Hardwaretest](#hardwaretest-2026-08-05).
|
||||||
(Firmware tatsächlich über das Laufwerk aktualisieren) ist noch nicht gebaut,
|
|
||||||
siehe [Bekannte Einschränkungen](#bekannte-einschränkungen).
|
|
||||||
|
|
||||||
Diese Platine hat keinen dedizierten Reset-/Boot-Taster. Bootloader-Modus
|
Diese Platine hat keinen dedizierten Reset-/Boot-Taster. Bootloader-Modus
|
||||||
aktivieren heißt hier: unterste rechte Cherry-MX-Taste (key_id 24) beim
|
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
|
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
|
||||||
|
|
@ -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
|
wieder einstecken (Taste dabei weiter halten) → Board bootet direkt in
|
||||||
`VERSABOOT`. Ohne gehaltene Taste startet die App normal.
|
`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)
|
## Hardwaretest (2026-08-05)
|
||||||
|
|
||||||
Erster vollständiger Hardwaretest auf einem echten VersaPad-v2-Board über
|
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
|
für genau dieses Bootloader-Pattern vorgeschrieben, hat im vendorten Code
|
||||||
gefehlt. Nach dem Fix bootet die App-Firmware zuverlässig, mit und ohne
|
gefehlt. Nach dem Fix bootet die App-Firmware zuverlässig, mit und ohne
|
||||||
angeschlossenen Debugger.
|
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
|
## Bekannte Einschränkungen
|
||||||
|
|
||||||
|
|
@ -109,11 +132,7 @@ Atmel-ICE/SWD. Ergebnisse:
|
||||||
(kein offiziell zugeteilter PID unter Adafruits VID); ein sauber eigener
|
(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
|
VID (z. B. über pid.codes) wäre die langfristig korrekte Lösung, ist aber
|
||||||
nicht Teil dieses Branches.
|
nicht Teil dieses Branches.
|
||||||
- Die App-Firmware erzeugt noch keine `.uf2`-Datei, nur `.bin`/`.elf`
|
- [`../upload_uf2.py`](../upload_uf2.py) sucht das `VERSABOOT`-Laufwerk aktuell
|
||||||
(SWD-Weg). Der geplante Weg über das `VERSABOOT`-Laufwerk ist damit noch
|
nur über die Windows-API (`GetVolumeInformationW`) — passend zur bisherigen
|
||||||
nicht nutzbar.
|
Dev-Umgebung dieses Projekts, aber nicht plattformübergreifend. Für
|
||||||
- `env:versapad_usb` in der Haupt-`platformio.ini` verwendet
|
macOS/Linux müsste die Laufwerkssuche noch ergänzt werden.
|
||||||
`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.
|
|
||||||
|
|
|
||||||
|
|
@ -9,20 +9,23 @@ Das aktive Ziel `versapad_nobl` reserviert den kompletten Bereich
|
||||||
`0x1FB00..0x1FFFF` für Makros und Config.
|
`0x1FB00..0x1FFFF` für Makros und Config.
|
||||||
|
|
||||||
Ein USB-Bootloader-Pfad wird im Branch `feature/usb-bootloader` aufgebaut
|
Ein USB-Bootloader-Pfad wird im Branch `feature/usb-bootloader` aufgebaut
|
||||||
(`bootloader/`, App-Environment `env:versapad_usb`). Der Bootloader selbst
|
(`bootloader/`, App-Environment `env:versapad_usb`). Der komplette Weg ist
|
||||||
(UF2, `bootloader/`) ist dort auf echter Hardware verifiziert — inklusive
|
dort auf echter Hardware verifiziert: Bootloader-Flash, USB-Enumeration,
|
||||||
eines gefundenen und behobenen Bugs beim Sprung in die App (fehlende
|
Tastencheck-Einstieg (kein physischer Reset-Taster auf diesem Board, siehe
|
||||||
`__DSB()`/`__ISB()` vor dem `bx`, siehe `bootloader/README.md`,
|
`bootloader/README.md`, "Hardware-Bootloader-Einstieg"), App-Firmware per
|
||||||
"Hardwaretest"). `env:versapad_usb`s Linkerskript
|
`.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
|
(`variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld`) reserviert
|
||||||
inzwischen denselben NVM-Bereich wie `flash_without_bootloader.ld`.
|
inzwischen denselben NVM-Bereich wie `flash_without_bootloader.ld`.
|
||||||
|
|
||||||
Trotzdem noch kein Produktionsziel: `env:versapad_usb` verwendet
|
Trotzdem noch kein Merge-fertiges Produktionsziel: Der `.bin`→`.uf2`-Weg
|
||||||
`upload_protocol = sam-ba`, das klassische Arduino/Atmel-SAM-BA-Protokoll —
|
(`uf2conv.py`, `upload_uf2.py`, Repo-Root) sucht das `VERSABOOT`-Laufwerk
|
||||||
das spricht der UF2-Bootloader nicht. Die App-Firmware erzeugt außerdem noch
|
bisher nur über die Windows-API, keine macOS/Linux-Unterstützung. Die
|
||||||
keine `.uf2`-Datei. Vor einem Merge nach `master` fehlen also noch die
|
Bootloader-USB-PID (`0x0043`) ist zwar kollisionsfrei verifiziert, aber kein
|
||||||
`.uf2`-Erzeugung und ein Ende-zu-Ende-Test des tatsächlichen USB-Flashwegs
|
offiziell registrierter Wert unter Adafruits VID `0x239A`.
|
||||||
(Datei aufs `VERSABOOT`-Laufwerk kopieren).
|
|
||||||
|
|
||||||
Die aktive Boarddatei benennt die MCU als `samd21g17d`, setzt für den
|
Die aktive Boarddatei benennt die MCU als `samd21g17d`, setzt für den
|
||||||
Arduino-Core aber weiterhin das Kompatibilitätsmakro `__SAMD21G18A__`. Der
|
Arduino-Core aber weiterhin das Kompatibilitätsmakro `__SAMD21G18A__`. Der
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,11 @@ upload_protocol = custom
|
||||||
extra_scripts = upload_openocd.py
|
extra_scripts = upload_openocd.py
|
||||||
debug_tool = openocd
|
debug_tool = openocd
|
||||||
|
|
||||||
; ── USB SAM-BA (nur wenn Bootloader geflasht ist) ─────────────────────────────
|
; ── USB UF2 (nur wenn bootloader/ geflasht ist) ────────────────────────────────
|
||||||
|
; 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]
|
[env:versapad_usb]
|
||||||
extends = common
|
extends = common
|
||||||
board = versapad
|
board = versapad
|
||||||
upload_protocol = sam-ba
|
upload_protocol = custom
|
||||||
|
extra_scripts = upload_uf2.py
|
||||||
|
|
|
||||||
83
uf2conv.py
Normal file
83
uf2conv.py
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Minimal .bin -> .uf2 converter for the VersaMCU UF2 bootloader.
|
||||||
|
|
||||||
|
Standalone reimplementation of the block format microsoft/uf2-samdx1's
|
||||||
|
uf2conv.py produces (256-byte payload per 512-byte block); see
|
||||||
|
bootloader/inc/uf2format.h for the struct this has to match on the device
|
||||||
|
side. No external dependencies, Python 3 only.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python uf2conv.py <input.bin> <output.uf2> [--base 0x2000] [--family 0x68ed2b88]
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import struct
|
||||||
|
|
||||||
|
UF2_MAGIC_START0 = 0x0A324655 # "UF2\n"
|
||||||
|
UF2_MAGIC_START1 = 0x9E5D5157 # randomly selected, must match the device
|
||||||
|
UF2_MAGIC_END = 0x0AB16F30 # ditto
|
||||||
|
UF2_FLAG_FAMILYID_PRESENT = 0x00002000
|
||||||
|
|
||||||
|
SAMD21_FAMILY_ID = 0x68ED2B88 # bootloader/inc/uf2format.h, #ifdef SAMD21
|
||||||
|
PAYLOAD_SIZE = 256 # bytes per block; matches the upstream uf2conv.py convention
|
||||||
|
|
||||||
|
|
||||||
|
def convert(bin_path: str, uf2_path: str, base_addr: int, family_id: int) -> int:
|
||||||
|
with open(bin_path, "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
# Pad to a whole number of blocks; the bootloader writes payloadSize
|
||||||
|
# bytes per block regardless of how much of it is real firmware.
|
||||||
|
if len(data) % PAYLOAD_SIZE != 0:
|
||||||
|
data += b"\x00" * (PAYLOAD_SIZE - len(data) % PAYLOAD_SIZE)
|
||||||
|
num_blocks = len(data) // PAYLOAD_SIZE
|
||||||
|
|
||||||
|
blocks = []
|
||||||
|
for block_no in range(num_blocks):
|
||||||
|
offset = block_no * PAYLOAD_SIZE
|
||||||
|
chunk = data[offset : offset + PAYLOAD_SIZE]
|
||||||
|
header = struct.pack(
|
||||||
|
"<IIIIIIII",
|
||||||
|
UF2_MAGIC_START0,
|
||||||
|
UF2_MAGIC_START1,
|
||||||
|
UF2_FLAG_FAMILYID_PRESENT,
|
||||||
|
base_addr + offset,
|
||||||
|
PAYLOAD_SIZE,
|
||||||
|
block_no,
|
||||||
|
num_blocks,
|
||||||
|
family_id,
|
||||||
|
)
|
||||||
|
padding = b"\x00" * (476 - PAYLOAD_SIZE)
|
||||||
|
footer = struct.pack("<I", UF2_MAGIC_END)
|
||||||
|
blocks.append(header + chunk + padding + footer)
|
||||||
|
|
||||||
|
with open(uf2_path, "wb") as f:
|
||||||
|
f.write(b"".join(blocks))
|
||||||
|
|
||||||
|
return num_blocks
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("input", help="Path to the raw firmware .bin")
|
||||||
|
parser.add_argument("output", help="Path to write the .uf2 to")
|
||||||
|
parser.add_argument(
|
||||||
|
"--base",
|
||||||
|
type=lambda s: int(s, 0),
|
||||||
|
default=0x2000,
|
||||||
|
help="Flash base address the .bin was linked for (default: 0x2000, matches "
|
||||||
|
"variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--family",
|
||||||
|
type=lambda s: int(s, 0),
|
||||||
|
default=SAMD21_FAMILY_ID,
|
||||||
|
help="UF2 family ID (default: SAMD21, 0x68ed2b88)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
num_blocks = convert(args.input, args.output, args.base, args.family)
|
||||||
|
print(f"Wrote {args.output}: {num_blocks} blocks, base 0x{args.base:08x}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
72
upload_uf2.py
Normal file
72
upload_uf2.py
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
"""PlatformIO upload hook for env:versapad_usb.
|
||||||
|
|
||||||
|
The board's actual bootloader (bootloader/, UF2/mass-storage) does not speak
|
||||||
|
SAM-BA, so upload_protocol=sam-ba (the board.json default, inherited from an
|
||||||
|
Arduino-Zero-style template) does not work here. This converts the built
|
||||||
|
.bin to .uf2 and copies it onto the VERSABOOT mass-storage volume instead --
|
||||||
|
Windows only for now, matching the rest of this project's dev environment.
|
||||||
|
"""
|
||||||
|
import ctypes
|
||||||
|
import os
|
||||||
|
import string
|
||||||
|
import sys
|
||||||
|
|
||||||
|
Import("env")
|
||||||
|
|
||||||
|
sys.path.insert(0, env.subst("$PROJECT_DIR"))
|
||||||
|
import uf2conv
|
||||||
|
|
||||||
|
VOLUME_LABEL = "VERSABOOT"
|
||||||
|
DRIVE_UNKNOWN = 0
|
||||||
|
DRIVE_NO_ROOT_DIR = 1
|
||||||
|
|
||||||
|
|
||||||
|
def find_versaboot_drive(label=VOLUME_LABEL):
|
||||||
|
for letter in string.ascii_uppercase:
|
||||||
|
root = f"{letter}:\\"
|
||||||
|
drive_type = ctypes.windll.kernel32.GetDriveTypeW(root)
|
||||||
|
if drive_type in (DRIVE_UNKNOWN, DRIVE_NO_ROOT_DIR):
|
||||||
|
continue
|
||||||
|
vol_name_buf = ctypes.create_unicode_buffer(261)
|
||||||
|
fs_name_buf = ctypes.create_unicode_buffer(261)
|
||||||
|
ok = ctypes.windll.kernel32.GetVolumeInformationW(
|
||||||
|
ctypes.c_wchar_p(root),
|
||||||
|
vol_name_buf,
|
||||||
|
ctypes.sizeof(vol_name_buf),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
fs_name_buf,
|
||||||
|
ctypes.sizeof(fs_name_buf),
|
||||||
|
)
|
||||||
|
if ok and vol_name_buf.value == label:
|
||||||
|
return root
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def upload_via_uf2(source, target, env):
|
||||||
|
build_dir = env.subst("$BUILD_DIR")
|
||||||
|
bin_path = os.path.join(build_dir, "firmware.bin")
|
||||||
|
uf2_path = os.path.join(build_dir, "firmware.uf2")
|
||||||
|
|
||||||
|
if not os.path.isfile(bin_path):
|
||||||
|
print(f"error: {bin_path} not found (expected as a normal build product)")
|
||||||
|
env.Exit(1)
|
||||||
|
|
||||||
|
num_blocks = uf2conv.convert(bin_path, uf2_path, base_addr=0x2000, family_id=uf2conv.SAMD21_FAMILY_ID)
|
||||||
|
print(f"Wrote {uf2_path}: {num_blocks} blocks")
|
||||||
|
|
||||||
|
drive = find_versaboot_drive()
|
||||||
|
if drive is None:
|
||||||
|
print(f"error: no drive labeled '{VOLUME_LABEL}' found.")
|
||||||
|
print("Hold the bottom-right Cherry MX key while plugging in USB to enter the bootloader.")
|
||||||
|
env.Exit(1)
|
||||||
|
|
||||||
|
dest = os.path.join(drive, "firmware.uf2")
|
||||||
|
print(f"Copying {uf2_path} -> {dest}")
|
||||||
|
with open(uf2_path, "rb") as src, open(dest, "wb") as dst:
|
||||||
|
dst.write(src.read())
|
||||||
|
print("Upload triggers a reset into the app on the device side; no further action needed.")
|
||||||
|
|
||||||
|
|
||||||
|
env.Replace(UPLOADCMD=upload_via_uf2)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue