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>
This commit is contained in:
parent
cd27639e44
commit
01c5e0930e
4 changed files with 151 additions and 8 deletions
13
README.md
13
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
|
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
|
Details, Speicherlayout und die Bootloader-Aktivierung (kein physischer
|
||||||
Reset-Taster auf dieser Platine) stehen in
|
Reset-Taster auf dieser Platine) stehen in
|
||||||
[10_usb_bootloader.md](doc/10_usb_bootloader.md).
|
[10_usb_bootloader.md](doc/10_usb_bootloader.md).
|
||||||
|
|
|
||||||
5
bootloader/.gitignore
vendored
Normal file
5
bootloader/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
.pio
|
||||||
|
.vscode/.browse.c_cpp.db*
|
||||||
|
.vscode/c_cpp_properties.json
|
||||||
|
.vscode/launch.json
|
||||||
|
.vscode/ipch
|
||||||
|
|
@ -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
|
`upload_uf2.py` sucht das Laufwerk aktuell nur über die Windows-API
|
||||||
(`GetVolumeInformationW`) — keine macOS/Linux-Unterstützung.
|
(`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
|
## Herkunft und Details
|
||||||
|
|
||||||
Der Bootloader ist abgeleitet und stark eingekürzt aus
|
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
|
immer `.bin` mit expliziter Adresse und getrennte OpenOCD-Aufrufe
|
||||||
verwenden — Details in `bootloader/README.md`, "Achtung bei manuellem
|
verwenden — Details in `bootloader/README.md`, "Achtung bei manuellem
|
||||||
SWD-Flashen".
|
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
|
## Bekannte Einschränkungen
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue