From 01c5e0930e4177663d5fbd4eeed315b3485493f1 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Wed, 5 Aug 2026 23:27:02 +0200 Subject: [PATCH] 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 --- README.md | 13 +++++ bootloader/.gitignore | 5 ++ doc/10_usb_bootloader.md | 30 +++++++++++ upload_openocd.py | 111 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 bootloader/.gitignore diff --git a/README.md b/README.md index 7e07422..61290be 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,19 @@ 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). diff --git a/bootloader/.gitignore b/bootloader/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/bootloader/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/doc/10_usb_bootloader.md b/doc/10_usb_bootloader.md index 0397fa9..5aa5b89 100644 --- a/doc/10_usb_bootloader.md +++ b/doc/10_usb_bootloader.md @@ -56,6 +56,30 @@ 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 @@ -87,6 +111,12 @@ stehen in `bootloader/README.md` unter "Hardwaretest"; kurz zusammengefasst: 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 diff --git a/upload_openocd.py b/upload_openocd.py index b311728..ea69879 100644 --- a/upload_openocd.py +++ b/upload_openocd.py @@ -2,23 +2,118 @@ Import("env") import os import subprocess -def upload_via_openocd(source, target, env): - pkg_dir = env.PioPlatform().get_package_dir("tool-openocd") - openocd = os.path.join(pkg_dir, "bin", "openocd.exe") - scripts = os.path.join(pkg_dir, "scripts") - firmware = str(source[0]) # .elf path +# Real, already-built bootloader image used as the reference for the presence +# check below -- a raw synthetic probe blob turned out unreliable with +# verify_image (silent no-op on tiny files), whereas verify_image against a +# real firmware .bin has been solid throughout this project's bring-up. +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 = [ openocd, "-s", scripts, "-f", "interface/cmsis-dap.cfg", "-f", "target/at91samdXX.cfg", - "-c", 'program "{}" verify reset; shutdown'.format(firmware.replace("\\", "/")) + "-c", extra_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: 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) + + +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." + ), +)