Files
bootloader/pureboot/pureboot.py
BlackMark f964b875b9 pureboot: one position-independent binary — its own staging loader
The image now runs from any 512-byte slot with every command intact:
control flow stays PC-relative, the write guard keys on the running slot
(the return-address anchor, computed once), the info block is addressed
from that same anchor as a byte pair (no absolute 16-bit address in the
image), and the application jump is an indirect call through a noipa-
laundered pointer to the absolute entry. 'J' — jump to a wire word
address, the one transfer primitive — replaces 'G': the host knows the
application entry from the info block, and moving between loader copies
needs arbitrary targets. The activation window is a compile-time 8 s
(PUREBOOT_TIMEOUT overrides), counted as a single calibrated poll loop.

A refused page no longer poisons the write-once temporary buffer (a real
silicon trap: the next write would program the drained data): every page
write discards the buffer first — CTPB on the tinies, on the mega the same
RWWSRE store that re-enables RWW after programming. The tinies' post-op
busy-waits go with it: their CPU halts through page erase and write.

488 / 502 / 504 B on t13a / t85 / mega — under the tinies' 510-byte budget,
whose last slot word is the host-managed trampoline: the resident's holds
the application entry, a staging copy's the jump through which an abandoned
update still times out into a loader.

The host tool updates the loader with itself: --update-loader installs the
identical image one slot below the resident, jumps into it, lets it rewrite
the resident, and restores the staging region from a state file — each
phase idempotent off the flash state, resumable after any interruption
(t13a: the staging slot carries the reset vector, written last in and
first out; t85: word 0 redirected around the resident rewrite; mega:
fuse-matrix preflight with a hard BOOTSZ gate and --assume-fuses for
simulators). Application flashing recovers by reset from any interruption:
patched page 0 and trampoline first, erase descending, and a walk-region
refusal behind --force on BOOTRST-below-loader megas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 19:37:43 +02:00

719 lines
29 KiB
Python

#!/usr/bin/env python3
"""pureboot host tool — the smart half of the pureboot protocol (README.md).
The device exposes primitives; this tool composes them: image loading (raw
binary or Intel HEX), flash programming with read-back verification, erase as
writing 0xff, EEPROM programming, fuse and info readout, the hand-over jump,
and — on chips without a hardware boot section — the reset-vector surgery
that re-homes the application's entry through the trampoline word below the
loader. Page 0 and the trampoline are written first, so every interruption
point of a flash leaves the chip reset-recoverable into the loader.
It also updates the loader itself (--update-loader): pureboot's image is
position-independent, so the tool installs the identical binary one 512-byte
slot below the resident loader, jumps into that staging copy, lets it rewrite
the resident slot, and restores what the staging slot held — resumable at
every phase from the flash state plus a host-side state file carrying the
saved bytes.
Python standard library only; the serial port is driven with termios, so any
tty works — a USB adapter as well as a simavr pty.
"""
import argparse
import json
import os
import select
import sys
import termios
import time
PROMPT = b"+"
PROTOCOL_VERSION = 1
SLOT = 512 # the loader slot size; also the self-update staging distance
class Error(Exception):
pass
# ---------------------------------------------------------------- serial ---
class Port:
"""A raw serial port with deadline-based reads."""
def __init__(self, path, baud):
self.fd = os.open(path, os.O_RDWR | os.O_NOCTTY)
attrs = termios.tcgetattr(self.fd)
attrs[0] = 0 # iflag
attrs[1] = 0 # oflag
attrs[2] = termios.CREAD | termios.CLOCAL | termios.CS8 # cflag
attrs[3] = 0 # lflag
try:
speed = getattr(termios, f"B{baud}")
except AttributeError:
raise Error(f"unsupported baud rate {baud}") from None
attrs[4] = attrs[5] = speed
attrs[6][termios.VMIN] = 0
attrs[6][termios.VTIME] = 0
termios.tcsetattr(self.fd, termios.TCSANOW, attrs)
def close(self):
os.close(self.fd)
def write(self, data):
os.write(self.fd, data)
def flush_input(self):
termios.tcflush(self.fd, termios.TCIFLUSH)
def read_available(self, wait):
"""Everything that arrives within `wait` seconds of quiet start."""
ready, _, _ = select.select([self.fd], [], [], wait)
return os.read(self.fd, 4096) if ready else b""
def read_exact(self, count, timeout):
data = b""
deadline = time.monotonic() + timeout
while len(data) < count:
remaining = deadline - time.monotonic()
if remaining <= 0:
raise Error(f"timeout: got {len(data)} of {count} bytes")
ready, _, _ = select.select([self.fd], [], [], remaining)
if ready:
data += os.read(self.fd, count - len(data))
return data
# -------------------------------------------------------------- protocol ---
class Info:
"""The 12-byte info block."""
def __init__(self, raw):
if len(raw) != 12 or raw[0:2] != b"PB":
raise Error(f"bad info block: {raw.hex()}")
if raw[2] != PROTOCOL_VERSION:
raise Error(f"protocol version {raw[2]}, tool speaks {PROTOCOL_VERSION}")
self.raw = bytes(raw)
self.signature = raw[3:6]
self.page = raw[6]
self.base = raw[7] | (raw[8] << 8)
self.eeprom_size = raw[9] | (raw[10] << 8)
self.patch_vector = bool(raw[11] & 1)
self.flash_size = self.base + SLOT
self.stage = self.base - SLOT # where a staging copy of the loader goes
# The hand-over target, as the word address 'J' takes: the trampoline
# below the loader (tinies), or word 0 (mega — the application's own
# reset vector; BOOTRST re-vectors a reset into the loader instead).
self.app_entry_word = (self.base - 2) // 2 if self.patch_vector else 0
def describe(self):
sig = " ".join(f"{b:02x}" for b in self.signature)
vector = "host-patched reset vector" if self.patch_vector else "hardware boot section"
return (
f"signature {sig}, page {self.page} B, "
f"app flash {self.base} B (loader at {self.base:#06x}), "
f"EEPROM {self.eeprom_size} B, {vector}"
)
class Loader:
"""A pureboot session. Between commands the loader has prompted `+` and
awaits a command byte; every method restores that invariant — except
jump(), after which the target must be knocked afresh."""
def __init__(self, port):
self.port = port
self.info = None
def connect(self, wait):
"""Knock until the activation window answers, then read the info
block. Also converges when the loader already sits in its command
loop: the knock bytes are ignored-or-executed there, and the drain
absorbs whatever they produced."""
self.port.flush_input()
deadline = time.monotonic() + wait
while True:
self.port.write(b"pb")
if PROMPT in self.port.read_available(0.4):
break
if time.monotonic() > deadline:
raise Error("no answer — reset the device within its activation window")
while self.port.read_available(0.3):
pass
self.port.write(b"b")
self.info = Info(self.port.read_exact(12, 2.0))
self._expect_prompt()
return self.info
def _expect_prompt(self, timeout=2.0):
byte = self.port.read_exact(1, timeout)
if byte != PROMPT:
raise Error(f"expected prompt, got {byte.hex()}")
def _command(self, tx, reply_len=0, timeout=2.0):
self.port.write(tx)
reply = self.port.read_exact(reply_len, timeout) if reply_len else b""
self._expect_prompt(timeout)
return reply
def _stream_read(self, command, address, count):
data = b""
while count:
chunk = min(count, 256)
head = bytes((ord(command), address & 0xFF, address >> 8, chunk & 0xFF))
data += self._command(head, chunk, 5.0)
address += chunk
count -= chunk
return data
def read_flash(self, address, count):
return self._stream_read("R", address, count)
def read_eeprom(self, address, count):
return self._stream_read("r", address, count)
def write_page(self, address, data):
assert len(data) == self.info.page and address % self.info.page == 0
head = bytes((ord("W"), address & 0xFF, address >> 8))
self._command(head + data, 0, 2.0)
def write_eeprom(self, address, data):
offset = 0
while offset < len(data):
chunk = data[offset : offset + 256]
head = bytes((ord("w"), address & 0xFF, address >> 8, len(chunk) & 0xFF))
self.port.write(head)
for byte in chunk:
self.port.write(bytes((byte,)))
self._expect_prompt() # per-byte ack: the write has begun
self._expect_prompt() # the next command prompt
address += len(chunk)
offset += len(chunk)
def read_fuses(self):
return self._command(b"F", 4, 2.0)
def jump(self, word_address):
"""'J': the device acks, then execution continues at the word
address — a loader slot's base (whose copy must then be knocked
afresh) or the application entry."""
self.port.write(bytes((ord("J"), word_address & 0xFF, word_address >> 8)))
self._expect_prompt()
def enter_copy(self, byte_address, wait):
"""Jump into the loader copy at `byte_address` and knock it. Ending
up in the copy addressed is guaranteed by construction: a jump to a
slot base lands in that slot's entry stub."""
self.jump(byte_address // 2)
return self.connect(wait)
def run_application(self):
self.jump(self.info.app_entry_word)
# ---------------------------------------------------------------- images ---
def load_image(path):
"""Raw binary, or Intel HEX by extension (.hex/.ihx/.ihex)."""
data = open(path, "rb").read()
if not path.lower().endswith((".hex", ".ihx", ".ihex")):
if not data:
raise Error(f"{path}: empty image")
return data
memory = {}
for number, line in enumerate(data.decode("ascii", "replace").splitlines(), 1):
line = line.strip()
if not line:
continue
if not line.startswith(":"):
raise Error(f"{path}:{number}: not an Intel HEX record")
record = bytes.fromhex(line[1:])
if sum(record) & 0xFF:
raise Error(f"{path}:{number}: checksum mismatch")
count, address, kind = record[0], (record[1] << 8) | record[2], record[3]
payload = record[4 : 4 + count]
if kind == 0:
for i, byte in enumerate(payload):
memory[address + i] = byte
elif kind == 1:
break
elif kind in (2, 4) and not any(payload):
continue # a zero base extends nothing
elif kind in (3, 5):
continue # start address: irrelevant, reset is the entry
else:
raise Error(f"{path}:{number}: record type {kind} reaches beyond the 16-bit space")
if not memory:
raise Error(f"{path}: empty image")
return bytes(memory.get(i, 0xFF) for i in range(max(memory) + 1))
# --------------------------------------------------------------- surgery ---
def rjmp_target(word_address, opcode, flash_words):
return (word_address + 1 + (opcode & 0x0FFF)) % flash_words
def rjmp_to(word_address, destination, flash_words):
return 0xC000 | ((destination - word_address - 1) % flash_words % 0x1000)
def plan_flash(image, info):
"""The pages to program, as {page_address: bytes}, already carrying the
reset-vector surgery where the chip needs it."""
page = info.page
limit = info.base - (2 if info.patch_vector else 0)
if len(image) > limit:
raise Error(f"image is {len(image)} B, application flash ends at {limit}")
final = bytearray(image) + bytearray([0xFF] * (-len(image) % page))
if info.patch_vector:
flash_words = info.flash_size // 2
word0 = final[0] | (final[1] << 8)
if word0 & 0xF000 != 0xC000:
raise Error(
"the image's reset vector is not an rjmp — pureboot's vector "
"surgery cannot re-home it (crt-less entry at address 0?)"
)
entry = rjmp_target(0, word0, flash_words)
if entry >= info.base // 2:
raise Error(
"the image's reset vector already targets the loader — this "
"is a read-back of a patched image; flash the original"
)
trampoline_word = (info.base - 2) // 2
patch = rjmp_to(0, info.base // 2, flash_words)
final[0], final[1] = patch & 0xFF, patch >> 8
trampoline_page = info.base - page
if len(final) < trampoline_page + page:
final += bytearray([0xFF] * (trampoline_page + page - len(final)))
jump = rjmp_to(trampoline_word, entry, flash_words)
final[info.base - 2], final[info.base - 1] = jump & 0xFF, jump >> 8
pages = {a: bytes(final[a : a + page]) for a in range(0, len(final), page)}
return pages
def covered(pages, info, skip_blank):
"""Pages in programming order; optionally dropping all-0xff pages (sound
only over erased flash) — never a load-bearing one.
With a patched vector (tinies), the patched page 0 goes first and the
trampoline page second: from the first write on, a reset lands in the
loader and the loader's own fall-through lands on the application entry,
so every interruption point of the flash is recoverable. With a hardware
boot section a reset re-vectors to the loader regardless; ascending
order, page 0 last, maximizes what an interrupted image retains."""
trampoline_page = info.base - info.page if info.patch_vector else None
first = [0, trampoline_page] if info.patch_vector else []
rest = [a for a in sorted(pages) if a not in first]
if skip_blank:
rest = [a for a in rest if pages[a].count(0xFF) != len(pages[a])]
order = [a for a in first if a in pages] + rest
if not info.patch_vector:
order = [a for a in order if a != 0] + ([0] if 0 in pages else [])
return order
# ----------------------------------------------------------------- fuses ---
def mega_boot(high_fuse):
"""Decode the ATmega328P high fuse's boot configuration (DS40002061B
§27.3, Table 27-13/27-16): BOOTSZ1:0 in bits 2:1 select the boot-section
words, BOOTRST in bit 0 (programmed = 0) re-vectors reset to its start.
Returns (bootrst_programmed, boot_section_start_byte)."""
bootsz = (high_fuse >> 1) & 0x03
words = {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}[bootsz]
return (high_fuse & 1) == 0, 0x8000 - words * 2
# ---------------------------------------------------------- loader update ---
def image_info(image):
"""The info block embedded in a pureboot binary, or None."""
at = image.find(b"PB" + bytes((PROTOCOL_VERSION,)))
return Info(image[at : at + 12]) if 0 <= at <= len(image) - 12 else None
def staging_content(image, info):
"""The 512-byte staging-slot content: the image, padding, and — on
chips whose hand-over jumps through the word below the resident loader —
that word, which for a staging copy is the slot's own last word: an rjmp
to the resident base. The staging copy's fall-through and 'J'-free exit
both land in a loader instead of garbage."""
if len(image) > (SLOT - 2 if info.patch_vector else SLOT):
raise Error(f"loader image is {len(image)} B, the slot holds {SLOT - 2 if info.patch_vector else SLOT}")
content = bytearray(image) + bytearray([0xFF] * (SLOT - len(image)))
if info.patch_vector:
through = rjmp_to((info.base - 2) // 2, info.base // 2, info.flash_size // 2)
content[SLOT - 2], content[SLOT - 1] = through & 0xFF, through >> 8
return bytes(content)
def update_preflight(image, info, fuse_bytes):
"""Errors and warnings before any flash is touched. Returns warnings."""
embedded = image_info(image)
if embedded is None:
raise Error("no pureboot info block in the update image — not a pureboot binary?")
if embedded.raw[3:] != info.raw[3:]:
raise Error(
f"update image is for another target: it declares "
f"[{embedded.describe()}], the device says [{info.describe()}]"
)
warnings = []
if not info.patch_vector:
if fuse_bytes is None:
raise Error("a loader update on this chip needs its fuses — unreadable? pass --assume-fuses")
high = fuse_bytes[3]
bootrst, bls_start = mega_boot(high)
if info.stage < bls_start:
raise Error(
f"cannot self-update: the staging slot {info.stage:#06x} lies below the "
f"boot section ({bls_start:#06x}, high fuse {high:#04x}) where SPM is disabled "
f"— a boot section of at least 1 KB (BOOTSZ) is required, and only an "
f"external programmer can change fuses"
)
if not bootrst:
warnings.append(
"BOOTRST unprogrammed: reset boots the application throughout the update; "
"an interruption is recovered by re-running this update"
)
elif bls_start == info.stage:
warnings.append(
"BOOTRST targets the staging slot: brief unrecoverable windows exist while "
"the staging copy itself is being installed or retired (page-write scale)"
)
else:
warnings.append(
f"BOOTRST targets {bls_start:#06x}, inside application flash: reset reaches a "
f"loader only across erased flash from there"
)
return warnings
class UpdateState:
"""The host-side memory of an update in flight: what the staging slot
held (and page 0, where the update repoints it). Losing this file after
the staging slot was overwritten loses those saved bytes — the update
still completes, but the staging region can then only be restored by
reflashing the application."""
def __init__(self, path):
self.path = path
self.data = None
def load_or_save(self, loader):
info = loader.info
if os.path.exists(self.path):
self.data = json.load(open(self.path))
if bytes.fromhex(self.data["signature"]) != info.signature or self.data["base"] != info.base:
raise Error(f"{self.path} belongs to a different device — remove it to start over")
return
self.data = {
"signature": info.signature.hex(),
"base": info.base,
"staging": loader.read_flash(info.stage, SLOT).hex(),
"page0": loader.read_flash(0, info.page).hex() if info.patch_vector else "",
}
with open(self.path, "w") as f:
json.dump(self.data, f)
@property
def staging(self):
return bytes.fromhex(self.data["staging"])
@property
def page0(self):
return bytes.fromhex(self.data["page0"])
def discard(self):
os.unlink(self.path)
def write_differing(loader, base, content, order=None):
"""Program the pages of `content` at `base` that differ from flash —
idempotent, so a resumed phase redoes only what an interruption left."""
page = loader.info.page
offsets = order if order is not None else range(0, len(content), page)
written = 0
for offset in offsets:
want = content[offset : offset + page]
if loader.read_flash(base + offset, page) != want:
loader.write_page(base + offset, want)
written += 1
for at in range(0, len(content), 256):
if loader.read_flash(base + at, min(256, len(content) - at)) != content[at : at + 256]:
raise Error(f"verify failed at {base + at:#06x} after programming")
return written
def patch_word0(loader, page0, target_base):
"""Rewrite page 0 with its word 0 re-aimed at `target_base` — the
resume insurance around rewriting a loader slot the reset path uses."""
info = loader.info
patched = bytearray(page0)
word = rjmp_to(0, target_base // 2, info.flash_size // 2)
patched[0], patched[1] = word & 0xFF, word >> 8
write_differing(loader, 0, bytes(patched))
return bytes(patched)
def op_update_loader(loader, wait, path, state_path, fuse_bytes):
"""Replace the resident loader with `path`, using the loader itself as
its own staging loader. Every phase is idempotent and keyed off the
actual flash state, so a re-run after any interruption resumes; the
state file carries the bytes the staging slot held."""
info = loader.info
image = load_image(path)
for warning in update_preflight(image, info, fuse_bytes):
print(f"note: {warning}")
staged = staging_content(image, info)
resident = bytes(image) + bytes([0xFF] * (SLOT - len(image)))
page = info.page
state = UpdateState(state_path)
state.load_or_save(loader)
# Install the staging copy. On a chip whose staging slot starts at
# address 0 (the 1 KB tiny13A), its first page carries the reset vector:
# written last, so any earlier interruption still resets into the old
# resident, and from then on resets enter the staging copy.
order = list(range(0, SLOT, page))
if info.stage == 0:
order = order[1:] + [0]
if write_differing(loader, info.stage, staged, order):
print(f"staging copy installed at {info.stage:#06x}")
# Enter it and let it rewrite the resident slot. Where a patched reset
# vector routes through the resident (a tiny with the staging slot away
# from page 0), word 0 is re-aimed at the staging copy around the
# rewrite, so a power failure mid-rewrite still resets into a loader.
loader.enter_copy(info.stage, wait)
redirect = info.patch_vector and info.stage != 0
if redirect:
patch_word0(loader, state.page0, info.stage)
if write_differing(loader, info.base, resident):
print(f"resident loader rewritten at {info.base:#06x}")
# Enter the new resident and put the staging region back: page 0 first
# where it lives in that region (word 0 then points at the new resident
# for the rest of the restore), the saved trampoline with the rest.
loader.enter_copy(info.base, wait)
if redirect:
write_differing(loader, 0, state.page0)
order = list(range(0, SLOT, page))
if info.stage == 0:
order = [0] + order[1:]
write_differing(loader, info.stage, state.staging, order)
state.discard()
print(f"loader updated: {len(image)} B at {info.base:#06x}, staging region restored")
def check_walk_region(pages, info, fuse_bytes, force):
"""With BOOTRST programmed but targeting below the loader, reset reaches
the loader only by walking across erased flash from the boot-section
start; application data in that span would divert reset into itself.
Only checkable when the fuses are known (--fuses or --assume-fuses)."""
if info.patch_vector or fuse_bytes is None:
return
bootrst, bls_start = mega_boot(fuse_bytes[3])
if not bootrst or bls_start >= info.base:
return
overlap = [a for a in sorted(pages) if a >= bls_start and pages[a].count(0xFF) != len(pages[a])]
if overlap and not force:
raise Error(
f"the image writes {overlap[0]:#06x}.. inside the reset walk region "
f"[{bls_start:#06x}, {info.base:#06x}) (BOOTRST programmed): reset could no "
f"longer reach the loader — --force to flash it anyway"
)
# ------------------------------------------------------------ operations ---
def op_erase_flash(loader):
"""0xff over the whole application area. Descending on a patched-vector
chip: page 0 — the patched reset vector — goes last, so an interrupted
erase still resets into the loader, and once it is gone the whole area
is erased and the reset walk reaches the loader anyway."""
blank = bytes([0xFF] * loader.info.page)
addresses = range(0, loader.info.base, loader.info.page)
for address in reversed(addresses) if loader.info.patch_vector else addresses:
loader.write_page(address, blank)
print(f"erase: {loader.info.base // loader.info.page} pages")
def op_erase_eeprom(loader):
loader.write_eeprom(0, bytes([0xFF] * loader.info.eeprom_size))
print(f"erase: {loader.info.eeprom_size} B of EEPROM")
def op_flash(loader, path, erase, verify, fuse_bytes=None, force=False):
image = load_image(path)
pages = plan_flash(image, loader.info)
check_walk_region(pages, loader.info, fuse_bytes, force)
if erase:
op_erase_flash(loader)
order = covered(pages, loader.info, skip_blank=erase)
for address in order:
loader.write_page(address, pages[address])
print(f"flash: {path}: {len(order)} pages")
if verify:
verify_pages(loader, pages)
def verify_pages(loader, pages):
for address in sorted(pages):
got = loader.read_flash(address, loader.info.page)
if got != pages[address]:
first = next(i for i in range(len(got)) if got[i] != pages[address][i])
raise Error(
f"verify failed at {address + first:#06x}: "
f"wrote {pages[address][first]:02x}, read {got[first]:02x}"
)
print(f"verify: {len(pages)} pages ok")
def op_verify_flash(loader, path):
verify_pages(loader, plan_flash(load_image(path), loader.info))
def op_read_flash(loader, path):
data = loader.read_flash(0, loader.info.base)
open(path, "wb").write(data)
print(f"read flash: {len(data)} B -> {path}")
def op_eeprom(loader, path, erase, verify):
image = load_image(path)
if len(image) > loader.info.eeprom_size:
raise Error(f"EEPROM image is {len(image)} B, device has {loader.info.eeprom_size}")
if erase:
op_erase_eeprom(loader)
loader.write_eeprom(0, image)
print(f"eeprom: {path}: {len(image)} B")
if verify:
got = loader.read_eeprom(0, len(image))
if got != image:
first = next(i for i in range(len(got)) if got[i] != image[i])
raise Error(f"verify failed at EEPROM {first:#06x}: wrote {image[first]:02x}, read {got[first]:02x}")
print(f"verify: {len(image)} B ok")
def op_verify_eeprom(loader, path):
image = load_image(path)
got = loader.read_eeprom(0, len(image))
if got != image:
first = next(i for i in range(len(got)) if got[i] != image[i])
raise Error(f"verify failed at EEPROM {first:#06x}: expected {image[first]:02x}, read {got[first]:02x}")
print(f"verify: {len(image)} B of EEPROM ok")
def op_read_eeprom(loader, path):
data = loader.read_eeprom(0, loader.info.eeprom_size)
open(path, "wb").write(data)
print(f"read EEPROM: {len(data)} B -> {path}")
def op_fuses(loader):
low, lock, extended, high = loader.read_fuses()
print(f"fuses: low {low:02x} high {high:02x} extended {extended:02x} lock {lock:02x}")
return bytes((low, lock, extended, high))
# -------------------------------------------------------------------- cli ---
def main():
parser = argparse.ArgumentParser(
description="pureboot host tool", epilog="operations run in the order listed above"
)
parser.add_argument("--port", required=True, help="serial device (or simavr pty)")
parser.add_argument("--baud", type=int, default=115200, help="115200 mega, 57600 tinies")
parser.add_argument("--wait", type=float, default=30.0, help="seconds to keep knocking")
parser.add_argument("--info", action="store_true", help="print the device info block")
parser.add_argument("--fuses", action="store_true", help="read the fuse and lock bytes")
parser.add_argument("--update-loader", metavar="FILE", help="replace the loader with this pureboot binary")
parser.add_argument("--state", metavar="FILE", help="update state file (default: FILE.pbstate)")
parser.add_argument("--assume-fuses", metavar="HEX8", help="fuse bytes low,lock,ext,high as 8 hex digits "
"(overrides reading them — e.g. under a simulator that cannot)")
parser.add_argument("--erase-flash", action="store_true", help="0xff over the application flash")
parser.add_argument("--flash", metavar="FILE", help="program an application (bin or ihex)")
parser.add_argument("--no-verify", action="store_true", help="skip read-back after writes")
parser.add_argument("--read-flash", metavar="FILE", help="dump the application flash")
parser.add_argument("--verify-flash", metavar="FILE", help="compare flash against an image")
parser.add_argument("--erase-eeprom", action="store_true", help="0xff over the EEPROM")
parser.add_argument("--eeprom", metavar="FILE", help="program the EEPROM (bin or ihex)")
parser.add_argument("--read-eeprom", metavar="FILE", help="dump the EEPROM")
parser.add_argument("--verify-eeprom", metavar="FILE", help="compare EEPROM against an image")
parser.add_argument("--force", action="store_true", help="override refusable safety checks")
parser.add_argument("--stay", action="store_true", help="leave the loader in its session")
args = parser.parse_args()
if args.update_loader and (args.flash or args.erase_flash):
parser.error("--update-loader does not combine with application flash operations")
fuse_override = None
if args.assume_fuses:
try:
fuse_override = bytes.fromhex(args.assume_fuses)
assert len(fuse_override) == 4
except (ValueError, AssertionError):
parser.error("--assume-fuses takes 8 hex digits: low,lock,extended,high")
port = Port(args.port, args.baud)
try:
loader = Loader(port)
info = loader.connect(args.wait)
if args.info:
print(f"device: {info.describe()}")
fuse_bytes = fuse_override
if args.fuses or (args.update_loader and not info.patch_vector and fuse_bytes is None):
read = op_fuses(loader)
if fuse_bytes is None:
fuse_bytes = read
if args.update_loader:
state = args.state or args.update_loader + ".pbstate"
op_update_loader(loader, args.wait, args.update_loader, state, fuse_bytes)
if args.flash:
op_flash(loader, args.flash, args.erase_flash, not args.no_verify, fuse_bytes, args.force)
elif args.erase_flash:
op_erase_flash(loader)
if args.read_flash:
op_read_flash(loader, args.read_flash)
if args.verify_flash:
op_verify_flash(loader, args.verify_flash)
if args.eeprom:
op_eeprom(loader, args.eeprom, args.erase_eeprom, not args.no_verify)
elif args.erase_eeprom:
op_erase_eeprom(loader)
if args.read_eeprom:
op_read_eeprom(loader, args.read_eeprom)
if args.verify_eeprom:
op_verify_eeprom(loader, args.verify_eeprom)
if args.stay:
print("loader stays in its session (reset to leave)")
else:
loader.run_application()
print("application running")
finally:
port.close()
if __name__ == "__main__":
try:
main()
except Error as error:
print(f"error: {error}", file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
sys.exit(130)