VersaMCU/upload_uf2.py
Julian Appel d325297063 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>
2026-08-05 21:35:19 +02:00

72 lines
2.4 KiB
Python

"""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)