pureboot: a loader in the staging slot is the staging copy — leave it there
The update flow's first step wrote the staging content over whatever the staging slot held; with a loader running right there (programmed by hand onto erased flash), that write met the copy's own running-slot guard on the composed through-word and the tool stopped at its verify — although the copy is exactly an installed staging copy, able to stream the new resident like any other. The install is now skipped when the slot holds a complete loader: its info block where every image carries it, matching the device's byte for byte, and the slot unchanged since the update began (the state file's snapshot) — so a resumed half-written install still differs from its snapshot and takes the install path, which completes it. pbrehome gains the staging-slot position (an older build at stage streaming a newer resident in); the README's wrong "cannot re-home from the staging slot" claim is corrected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -120,14 +120,14 @@ On a boot-sectioned mega a copy at 0 is dead weight (SPM only executes
|
|||||||
from the boot section, so it cannot even heal itself — reflash the .hex);
|
from the boot section, so it cannot even heal itself — reflash the .hex);
|
||||||
on the patched-vector chips it *runs* (the image is position-independent
|
on the patched-vector chips it *runs* (the image is position-independent
|
||||||
and reset enters word 0), reports its canonical geometry, and the ordinary
|
and reset enters word 0), reports its canonical geometry, and the ordinary
|
||||||
`--update-loader` flow re-homes a build into the top slot — the staging
|
`--update-loader` flow re-homes a build into the top slot from any
|
||||||
install and the word-0 redirect both execute from copies outside page 0's
|
position — the staging install and the word-0 redirect execute from
|
||||||
slot, so the running-slot guard never blocks the flow (`pureboot.rehome`
|
copies outside page 0's slot, and a copy sitting in the staging slot
|
||||||
is the acceptance test). Flashing the application afterwards overwrites
|
itself is recognized as the installed staging copy and left in place (it
|
||||||
the stale copy, vector surgery included. The one position that cannot
|
streams the new resident like any staged copy, so an older build installs
|
||||||
re-home itself is the staging slot: installing the staging copy would hit
|
a newer one). `pureboot.rehome` is the acceptance test for both
|
||||||
the running copy's own guard and the update stops at its verify — flash
|
positions. Flashing the application afterwards overwrites the stale copy,
|
||||||
the .hex instead.
|
vector surgery included.
|
||||||
|
|
||||||
**Boot-sectioned megas**: program the loader at `flash − slot` with an
|
**Boot-sectioned megas**: program the loader at `flash − slot` with an
|
||||||
external programmer. Every such mega has a BOOTSZ step whose boot section
|
external programmer. Every such mega has a BOOTSZ step whose boot section
|
||||||
@@ -193,7 +193,10 @@ the loader resident) and peels it to the slot content by the embedded base.
|
|||||||
patched-vector chips the host composes the slot's last word — the same
|
patched-vector chips the host composes the slot's last word — the same
|
||||||
address as the resident's trampoline — as a jump to the resident base,
|
address as the resident's trampoline — as a jump to the resident base,
|
||||||
so even an abandoned staging copy times out into a loader, never into
|
so even an abandoned staging copy times out into a loader, never into
|
||||||
garbage.
|
garbage. A loader already sitting whole in the staging slot (its info
|
||||||
|
block in place, the slot unchanged since the update began) is left as
|
||||||
|
the staging copy instead — rewriting it would only meet its own
|
||||||
|
running-slot guard.
|
||||||
3. `J` enters the staging copy, which rewrites the resident slot. On the
|
3. `J` enters the staging copy, which rewrites the resident slot. On the
|
||||||
patched-vector chips whose staging slot sits away from page 0 the host
|
patched-vector chips whose staging slot sits away from page 0 the host
|
||||||
first re-aims word 0 at the staging copy, so a power loss mid-rewrite
|
first re-aims word 0 at the staging copy, so a power loss mid-rewrite
|
||||||
|
|||||||
@@ -725,10 +725,28 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
|||||||
state = UpdateState(state_path)
|
state = UpdateState(state_path)
|
||||||
state.load_or_save(loader)
|
state.load_or_save(loader)
|
||||||
|
|
||||||
# Install the staging copy. On a chip whose staging slot starts at
|
# Install the staging copy — unless a loader already sits whole in the
|
||||||
# address 0 (the 1 KB tiny13A), its first page carries the reset vector:
|
# staging slot (a build programmed there by hand): that copy IS the
|
||||||
# written last, so any earlier interruption still resets into the old
|
# installed staging copy, and rewriting it would only trip its own
|
||||||
# resident, and from then on resets enter the staging copy.
|
# running-slot guard on the composed through-word. Any pureboot with
|
||||||
|
# the device's own info block serves — the staged copy just streams
|
||||||
|
# pages, so an older build installs a newer resident all the same. Two
|
||||||
|
# checks make "already a loader" mean a *complete* one: the block must
|
||||||
|
# sit where every image carries it (within the slot's first 256 bytes
|
||||||
|
# — the build's position lint), matching the device's block byte for
|
||||||
|
# byte, and the slot must be unchanged since this update began (the
|
||||||
|
# state file's snapshot) — a resumed, half-written install differs
|
||||||
|
# from its snapshot and takes the install path below, which completes
|
||||||
|
# it page by page.
|
||||||
|
current = loader.read_flash(info.stage, info.slot)
|
||||||
|
staged_loader = image_info(current[:268])
|
||||||
|
if staged_loader is not None and staged_loader.raw == info.raw and current == state.staging:
|
||||||
|
print(f"staging slot already holds a loader — left in place")
|
||||||
|
else:
|
||||||
|
# On a chip whose staging slot starts at address 0 (the 1 KB
|
||||||
|
# tiny13s), 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, info.slot, page))
|
order = list(range(0, info.slot, page))
|
||||||
if info.stage == 0:
|
if info.stage == 0:
|
||||||
order = order[1:] + [0]
|
order = order[1:] + [0]
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Re-homing acceptance test: a pureboot image mistakenly programmed at
|
"""Re-homing acceptance test: a pureboot image programmed somewhere other
|
||||||
address 0 of a patched-vector chip (a raw .bin handed to a programmer, which
|
than its canonical top slot must still be a working loader —
|
||||||
defaults to offset 0) must still be a working loader — position-independent,
|
position-independent, guarding its accidental slot — and the ordinary
|
||||||
guarding its accidental slot — and the ordinary --update-loader flow must
|
--update-loader flow must put a build into the top slot from there.
|
||||||
re-home a build into the canonical top slot: the staging install and the
|
|
||||||
word-0 redirect both run from copies whose slots are not page 0's, so the
|
Two positions are exercised. Address 0 (a raw .bin handed to a programmer,
|
||||||
running-slot guard never blocks the flow. Flashing an application through
|
which defaults to offset 0): the staging install and the word-0 redirect
|
||||||
the healed resident then overwrites the stale copy, vector surgery included,
|
both run from copies whose slots are not page 0's, so the running-slot
|
||||||
and the banner proves the launch.
|
guard never blocks the flow. The staging slot itself: a loader already
|
||||||
|
sitting there IS the installed staging copy — the tool recognizes it by
|
||||||
|
its embedded info block and leaves it in place instead of tripping the
|
||||||
|
copy's own guard on the composed through-word — and that (older) copy
|
||||||
|
streams the new resident like any staged copy. In both cases flashing an
|
||||||
|
application through the healed resident overwrites the stale copy, vector
|
||||||
|
surgery included, and the banner proves the launch.
|
||||||
|
|
||||||
Usage: pbrehome.py <device_bin> <pureboot_elf> <update_bin> <mcu> <hz>
|
Usage: pbrehome.py <device_bin> <pureboot_elf> <update_bin> <mcu> <hz>
|
||||||
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
|
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
|
||||||
@@ -22,23 +28,15 @@ def fail(message):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def rehome_from(pbsim, pb, device_bin, elf, place_hex, guard_probe, update_bin, base, page, baud, app_bin, workdir,
|
||||||
(device_bin, elf, update_bin, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:]
|
mcu, hz):
|
||||||
base, baud = int(base_hex, 0), int(baud)
|
"""Place the loader at `place_hex`, heal through --update-loader, flash
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
the application, expect the banner."""
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
dump = os.path.join(workdir, f"dump-{place_hex}.bin")
|
||||||
import pbsim
|
state = os.path.join(workdir, f"rehome-{place_hex}.pbstate")
|
||||||
import pureboot as pb
|
|
||||||
|
|
||||||
os.makedirs(workdir, exist_ok=True)
|
|
||||||
dump = os.path.join(workdir, "dump.bin")
|
|
||||||
state = os.path.join(workdir, "rehome.pbstate")
|
|
||||||
if os.path.exists(state):
|
if os.path.exists(state):
|
||||||
os.unlink(state)
|
os.unlink(state)
|
||||||
|
device = pbsim.Device(device_bin, elf, mcu, hz, place_hex, page, baud, dump, reset_hex="0")
|
||||||
# The runner's base argument places the firmware: 0 is the misplaced
|
|
||||||
# flash, and the patched-vector reset at word 0 lands in it directly.
|
|
||||||
device = pbsim.Device(device_bin, elf, mcu, hz, "0x0", int(page), baud, dump, reset_hex="0")
|
|
||||||
try:
|
try:
|
||||||
port = pb.Port(device.pty, baud)
|
port = pb.Port(device.pty, baud)
|
||||||
loader = pb.Loader(port)
|
loader = pb.Loader(port)
|
||||||
@@ -46,14 +44,15 @@ def main():
|
|||||||
if info.base != base:
|
if info.base != base:
|
||||||
fail(f"the misplaced copy reports base {info.base:#06x} — the info block must stay canonical")
|
fail(f"the misplaced copy reports base {info.base:#06x} — the info block must stay canonical")
|
||||||
|
|
||||||
# The accidental slot still guards itself; the canonical base slot
|
# The accidental slot still guards itself; re-homing rides on the
|
||||||
# must be writable from it (that is what re-homing rides on).
|
# canonical slots being writable from it.
|
||||||
before = loader.read_flash(0, info.page)
|
probe = int(guard_probe, 0)
|
||||||
loader.write_page(0, bytes(info.page))
|
before = loader.read_flash(probe, info.page)
|
||||||
if loader.read_flash(0, info.page) != before:
|
loader.write_page(probe, bytes(info.page))
|
||||||
|
if loader.read_flash(probe, info.page) != before:
|
||||||
fail("the misplaced copy's guard let its own slot change")
|
fail("the misplaced copy's guard let its own slot change")
|
||||||
|
|
||||||
# The ordinary update flow re-homes the build into the top slot.
|
# The ordinary update flow puts the build into the top slot.
|
||||||
pb.op_update_loader(loader, 25, update_bin, state, None)
|
pb.op_update_loader(loader, 25, update_bin, state, None)
|
||||||
update = open(update_bin, "rb").read()
|
update = open(update_bin, "rb").read()
|
||||||
if loader.read_flash(base, len(update)) != update:
|
if loader.read_flash(base, len(update)) != update:
|
||||||
@@ -67,10 +66,35 @@ def main():
|
|||||||
pb.verify_pages(loader, pages)
|
pb.verify_pages(loader, pages)
|
||||||
loader.run_application()
|
loader.run_application()
|
||||||
if port.read_exact(3, 5.0) != b"APP":
|
if port.read_exact(3, 5.0) != b"APP":
|
||||||
fail("application does not banner after the re-home")
|
fail(f"application does not banner after the re-home from {place_hex}")
|
||||||
port.close()
|
port.close()
|
||||||
finally:
|
finally:
|
||||||
device.stop()
|
device.stop()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
(device_bin, elf, update_bin, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:]
|
||||||
|
base, page, baud = int(base_hex, 0), int(page), int(baud)
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import pbsim
|
||||||
|
import pureboot as pb
|
||||||
|
|
||||||
|
os.makedirs(workdir, exist_ok=True)
|
||||||
|
|
||||||
|
# Address 0: the raw-.bin-to-a-programmer accident. The guard probe is
|
||||||
|
# the copy's own page 0.
|
||||||
|
rehome_from(pbsim, pb, device_bin, elf, "0x0", "0x0", update_bin, base, page, baud, app_bin, workdir, mcu, hz)
|
||||||
|
print("re-home from address 0: converged")
|
||||||
|
|
||||||
|
# The staging slot: erased flash with the loader sitting exactly where
|
||||||
|
# a staging copy would — the tool must leave it in place and let it
|
||||||
|
# stream the (different) update build into the resident slot.
|
||||||
|
stage = base - 512
|
||||||
|
rehome_from(pbsim, pb, device_bin, elf, hex(stage), hex(stage), update_bin, base, page, baud, app_bin, workdir,
|
||||||
|
mcu, hz)
|
||||||
|
print("re-home from the staging slot: converged")
|
||||||
|
|
||||||
print("pbrehome: a misplaced loader re-homes through the ordinary update flow")
|
print("pbrehome: a misplaced loader re-homes through the ordinary update flow")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user