Skip to content

Verify UF2 flashes instead of assuming the copy worked - #8

Open
bdbarnett wants to merge 1 commit into
mainfrom
uf2-flash
Open

Verify UF2 flashes instead of assuming the copy worked#8
bdbarnett wants to merge 1 commit into
mainfrom
uf2-flash

Conversation

@bdbarnett

Copy link
Copy Markdown
Collaborator

Problem

firmware flash already copied a .uf2 onto a bootloader drive for rp2 and samd, but it reported success whenever shutil.copyfile did not raise. That is not evidence of anything. A UF2 flash has two failure modes that look identical to success from the host side:

  • a copy that reports fine and writes nothing — PowerShell's Copy-Item fails non-terminatingly (exit 0, no file), and
  • a bootloader that silently skips every block whose family ID it does not own, leaving a perfectly successful copy sitting on the drive.

For a flash operation, "succeeded" and "wrote nothing" being indistinguishable is the worst possible ambiguity.

Approach

The copy is no longer the proof. The proof is that the volume unmounts. A UF2 bootloader reboots into the new firmware once it has accepted a complete image, and the mount goes with it — a signal that comes from the board rather than from the host filesystem. A volume still mounted after --uf2-timeout (default 30s) is now a failure that names the image's family, since a family mismatch is the usual cause.

The one assumption is a bootloader that accepts an image without rebooting. None of the bootloaders in scope (RP2040/RP2350, SAMD, nRF, tinyuf2) behave that way, and it is documented at wait_for_volume_gone since a false negative there would report failure on a flash that worked.

Changes

New python/uf2.py — file validation, volume discovery, copy/verify. Validates block magic, payload bounds, and the block count against the header: a file that disagrees with its own numBlocks makes a bootloader wait forever rather than flash.

Family IDs are reported, not enforced. The upstream registry and CircuitPython's espressif Makefile disagree about 0x540ddf62 (ESP32-C6 vs esp32p4), so a hard check would reject working images.

--uf2 forces the path for any port, which is what reaches a board whose UF2 bootloader is not implied by its MicroPython port — an ESP32-S3 carrying tinyuf2 being the case that matters:

mpftp bootloader -d COM7
mpftp firmware flash --port esp32 --uf2 --artifact build/firmware.uf2

Discovery moves to INFO_UF2.TXT rather than volume labels, which differ per family (RPI-RP2, FTHRS3BOOT, …), and asks Windows for drive letters so it works under WSL, where a removable drive is usually not mounted under /mnt at all. Two mounted volumes now stop the command instead of picking one, because guessing overwrites the firmware on a board the caller did not name.

test:python needed PYTHONPATH to import the engine at all — three test files were erroring out before this.

Verification

  • 123 python tests pass (35 new).
  • parse_uf2 checked against four real artifacts across four families: SAMD21, MIMXRT10XX, ESP32S3, RP2040.
  • Both end-to-end paths exercised against a simulated volume — success when it unmounts, and the correct failure when it does not, which surfaced the SAMD21-image/RPI-RP2-board mismatch in the error text.
  • Real discovery on WSL: finds Windows drive letters in ~2s, reports no bootloader volume when none is mounted.
  • mypy clean on the new module; the 4 remaining errors are pre-existing in firmware_download.py.

Not yet verified against real hardware — that needs a board in bootloader mode, which interrupts whatever is running on it.

🤖 Generated with Claude Code

`firmware flash` already copied a .uf2 onto a bootloader drive for rp2 and
samd, but it reported success whenever shutil.copyfile did not raise. That
is not evidence of anything. A UF2 flash has two failure modes that look
identical to success from the host side:

  - a copy that reports fine and writes nothing (PowerShell's Copy-Item
    fails non-terminatingly, exit 0, no file), and
  - a bootloader that silently skips every block whose family ID it does
    not own, leaving a perfectly successful copy sitting on the drive.

So the copy is no longer the proof. The proof is that the volume unmounts:
a UF2 bootloader reboots into the new firmware once it has accepted a
complete image, and the mount goes with it. That signal comes from the
board rather than from the host filesystem. A volume still mounted after
--uf2-timeout (default 30s) is now a failure that names the image's family,
since a family mismatch is the usual cause.

Before copying, the new uf2 module validates the file: block magic, payload
bounds, and the block count against the header -- a file that disagrees
with its own numBlocks makes a bootloader wait forever rather than flash.
Family IDs are reported, not enforced: the upstream registry and
CircuitPython's espressif Makefile disagree about 0x540ddf62 (ESP32-C6 vs
esp32p4), so a hard check would reject working images.

--uf2 forces this path for any port, which is what reaches a board whose
UF2 bootloader is not implied by its MicroPython port -- an ESP32-S3
carrying tinyuf2 being the case that matters.

Volume discovery moves to INFO_UF2.TXT rather than labels, which differ per
family, and asks Windows for drive letters so it works under WSL, where a
removable drive is usually not mounted under /mnt at all. Two mounted
volumes now stop the command instead of picking one, because guessing
overwrites the firmware on a board the caller did not name.

test:python needed PYTHONPATH to import the engine at all; three test files
were erroring out before this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant