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>
119 lines
4.3 KiB
Python
119 lines
4.3 KiB
Python
Import("env")
|
|
import os
|
|
import subprocess
|
|
|
|
# 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", extra_cmd,
|
|
]
|
|
print(" ".join(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."
|
|
),
|
|
)
|