24 Commits

Author SHA1 Message Date
BlackMark
e85840fa39 pureboot: take the emitted loader HEX for --update-loader
The build emits an Intel HEX beside every loader image, but --update-loader
could not consume one: load_image() anchors every image at address zero, and
a loader HEX links at its base, so it decoded to a 32760-byte blob carrying
504 bytes of loader at the end. staging_content() then refused it as "loader
image is 32760 B, the slot holds 512" - an error naming neither the cause nor
the raw .bin the tool wanted instead.

Drop the blank below the base in the update path. The base comes from the
image's own info block rather than the device's, so an image built for
another target survives the slice intact and the preflight still reports it
as another target rather than failing to find an info block at all.

Verified on an ATmega328P: the full self-update flow driven straight from
pureboot_timeout-5s.hex, resident slot byte-for-byte against the image
afterwards, application preserved; both refusal paths unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 22:46:50 +02:00
BlackMark
045372a2bc pureboot: drive the serial port on Windows too
The host tool was standard-library-only but POSIX-only with it: termios and
select() bound the port layer, and importing termios failed outright on
Windows, so the module could not even load there.

Split Port into PosixPort (unchanged) and a WindowsPort over the Win32 serial
API through ctypes, picked by os.name; every call site keeps the Port name.
kernel32 only, so the standard-library constraint holds.

Windows has no select() for a COM handle, so the read deadlines move into the
driver as COMMTIMEOUTS, re-armed per read: read_available() ends on a gap
longer than a USB-serial latency timer coalesces (16 ms on FTDI parts),
read_exact() on the count or its deadline. Opening asserts DTR and RTS as a
POSIX open does, so a board wiring DTR to reset still pulses it. A failed
configuration closes the handle before raising - a COM handle is exclusive,
and the leak met the next open as "Access is denied". Win32 takes any integer
baud and a driver may accept one its hardware cannot produce (an FT232R
reports back a baud of 3 and keeps the old divisor), so obvious nonsense is
refused where termios' table would have.

Tested against an ATmega328P on COM6: info, fuses, both memories programmed
and verified, session reconnect, hand-over, the loader self-update, and the
write guard on its own slot. test_planner runs on Windows now as well.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 22:46:14 +02:00
19662d2386 build: emit an Intel-HEX beside every loader image
avrdude programs Intel-HEX, not ELF, and the build produced only ELFs — so
flashing a loader to a real chip meant running objcopy by hand. add_hex_output()
hangs a POST_BUILD objcopy on each loader image: the three tsb tiers through
add_tsb_variant, pureboot, and the re-timed pureboot9. .eeprom is dropped, being
its own avrdude update.

It uses the toolchain file's CMAKE_OBJCOPY rather than a hardcoded path, so
every chip preset emits hex, not just the mega. pbapp keeps its ELF alone: the
update test converts it to a raw binary itself, and it is not a flashing target.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 21:57:26 +02:00
f6598b0511 pureboot: PI lint, tightened gates, and the self-update test suite
check_pi.py asserts the two link-time facts position independence rests on
(no absolute jmp/call; the info block within the image's first 256 bytes);
the size gates drop to 510 on the tinies for the trampoline word.

New per-chip tests beside the reworked protocol test: the planner units
(programming orders and their recovery properties, the surgery, staging
composition, boot-fuse decode, and the update preflight's error/warning
matrix over synthetic fuse bytes), the relocated-copy sweep (the identical
image installed one slot lower serves the full command set — the PI
acceptance test, and the one that caught the temporary-buffer trap), and
the self-update end-to-end: --update-loader to a re-timed build
(pureboot9, byte-different by PUREBOOT_TIMEOUT alone), then every
power-fail phase killed mid-write, restarted from the runner's flash dump,
and completed by a re-run with the application intact throughout. The mega
rounds run the BOOTRST-unprogrammed profile: the fixture application's 'L'
jump is the application-owned loader entry that profile relies on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 19:37:57 +02:00
0604d3a0ad 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
62a548cc09 pureboot: harden the sim device runner
Cancel the GPIO bridge's cycle timers with the state they drive: avr_reset
drops the TX latch, whose falling edge starts a spurious decode before
bridge_reset runs, and the stale sampler then interleaves with the loader's
first real answer through the shared shift state — the first post-reset
replies came back corrupted and the knock retries burned the activation
window into the application.

Wrap the mega's registered flash ioctl to re-dispatch page erases with Z
masked to the page boundary: simavr's PGERS handler erases spm_pagesize
bytes from Z & ~1 (its PGWRT path masks correctly), wiping the neighbouring
page when Z sits past the page start, which hardware permits (§26.8.1).
Model the write-once temporary buffer in the tiny NVM module — silicon
refuses a second load per word until the buffer clears, and a last-write-
wins model masks real firmware bugs.

Optional arguments select the reset vector (the mega's fuse profiles) and a
raw flash image to resume from (power-fail tests re-enter a dumped state).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 19:37:10 +02:00
7d103ca957 pureboot: review-pass fixes to the host tool and device runner
pureboot.py: reject an empty image file with a clear error instead of
an IndexError deep in the vector-surgery planner; tighten the erase
docstring (order is irrelevant there — every target byte is the same
value, unlike a real flash where page 0 must go last).

pureboot_device.c: the GPIO bridge's bit_cycles used plain truncating
division where the firmware computes its own bit period with
round-to-nearest (uart.hpp: (Clock.hz + Baud.bd/2)/Baud.bd) — one
cycle off per bit on both tinies, harmless in practice but needless
drift against a firmware built to a different constant. Matched
exactly. Also clear the queued-bytes/decode-in-progress bridge state
on the test-only reset signal, so a future reset-mid-transfer scenario
can't feed a freshly reset chip bytes queued for its previous life.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AReSwkWkPX2A9Ym6grxRAh
2026-07-20 10:45:19 +02:00
eca7a41051 pureboot: gitignore python bytecode cache
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AReSwkWkPX2A9Ym6grxRAh
2026-07-20 10:43:56 +02:00
7314f7ab3b pureboot: stop tracking the python bytecode cache
A stray __pycache__/*.pyc from a local test run got swept into the
previous commit's git add. Untracked and gitignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AReSwkWkPX2A9Ym6grxRAh
2026-07-20 10:43:36 +02:00
5b361904ab pureboot: host tool and end-to-end protocol tests, all three chips
pureboot.py (Python stdlib only): images as raw binary or Intel HEX,
flash and EEPROM programming with read-back verify, erase composites,
fuse and info readout, activation-timeout configuration, and the
tinies' reset-vector surgery — the trampoline word below the loader,
page 0 written last.

The test spawns a simavr device (pureboot_device.c) — the mega's USART
as a pty; on the tinies a cycle-timed GPIO<->pty bridge for the polled
software UART plus the NVM module simavr's tiny cores lack (their SPM
opcode ioctls into a void and silently does nothing) — and drives it
with the real tool: knock from reset (erased-flash walk on the tinies),
program and verify both memories, timeout write, session reconnect, an
external reset through the patched vector, hand-over, and the fixture
application's banner. Results are cross-checked against ground-truth
memory dumps and an independent decode of the surgery's rjmp words,
red-verified against a sabotaged encoder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AReSwkWkPX2A9Ym6grxRAh
2026-07-20 05:54:15 +02:00
833e134e01 pureboot: the device — one pure C++ source, 512 bytes, every chip
No inline assembly, no global register variables; libavr does the
datasheet work. The device speaks primitives — flash read/page-program,
EEPROM read/write, fuse read, info block, EEPROM-resident activation
timeout, hand-over — and verify, erase, reset-vector surgery, and
timeout configuration live in the host tool. 490 B on the ATtiny13A,
510 B on the ATtiny85, 484 B on the ATmega328P, each linked into the
top 512 bytes of flash; per-chip size tests gate all three.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AReSwkWkPX2A9Ym6grxRAh
2026-07-20 05:33:28 +02:00
da730b7bb5 tsb: third size pass — restructure to the oracle's shape
The second pass concluded the 168 B tricks->asm gap was per-call ABI
cost. Most of it was structure. Rebuilt around the oracle's own shape —
argless noinline primitives over a whole-loader call-saved register
protocol (g_addr in Y, count r16, window r7, direction latch r6), a
top-down erase_below whose loop tests against zero and hands callers
g_addr = 0 for free, bounded rx everywhere (a silent host unwinds to
the app from any state, as the oracle does), and a named tsb_app entry
that --pmem-wrap-around=32k relaxes to the wrapped rjmp:

  tsb_asm    510 B in the 512 B section (oracle: 500), C++ except rx
             and the page-store loop — the two routines whose remaining
             cost is the calling convention itself (~30 asm lines, was
             ~280)
  tsb_tricks 526 B, no assembly at all (was 666)
  tsb_pure   836 B, still one readable function per command (was 842)

Every g_* update placement works around a GCC 16.1 wrong-code bug
(stores into global register variables deleted when only callees read
them — repro and rules in libavr dev/lessons.md). Also fixes two
latent hardware bugs all earlier tiers carried, masked by simavr's
zeroed register file: the crt-less entries never established
__zero_reg__ = 0, and the direction latch was read before written —
power-on registers are undefined.

All tiers full oracle feature parity, protocol tests green in both
libavr modes, .text byte-identical across modes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYufebsiWvGkAJ2fLAB1gT
2026-07-20 01:00:27 +02:00
c351bee257 tsb: beat the first-pass size floors (tricks 666, pure 842)
tricks 778->666: always_inline every single-call handler into the
[[noreturn]] reset entry (which pays no prologue, so their push/pop of
call-saved registers vanishes), walk the page pointer in Y (adiw, base
recovered as g_addr-page) instead of recomputing Z=base+offset, bring
the UART up in the two registers that are not already at their reset
value, and seed the activation counter as __uint24.

pure 896->842: TU-local internal linkage (proper hygiene, and it lets
the compiler inline the one-call handlers), a byte-wide activation
count, __uint24 timeout. Still one readable function per command.

asm unchanged at 498: its C++-expressible parts are already C++; the
core stays asm (the 666 B all-tricks tier is 168 B over — per-call ABI
tax, not a feature). All three cross-mode byte-identical, protocol green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHeP42XU3wf6RfhyuxBTE5
2026-07-19 20:15:49 +02:00
34e7f1be34 tsb: drive each tier to its size floor
asm 502->498 B (below the oracle's 500): the stack bring-up moves to plain C++,
and a register is reserved for the config-page high byte instead of reloading it
at each app-flash-boundary compare. tricks 808->778 B: shared erase/rww helpers
plus the libavr half-duplex W1C fix. pure 950->896 B and no SRAM: streams
rx->SPM/EEPROM instead of staging a 128 B page buffer. All three keep full oracle
feature parity and stay byte-identical across modes; protocol tests (round-trip +
password + emergency erase) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHeP42XU3wf6RfhyuxBTE5
2026-07-19 18:47:38 +02:00
445e187722 tsb: document the three tiers at full parity in the build file
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHeP42XU3wf6RfhyuxBTE5
2026-07-19 16:50:18 +02:00
250aba5cfb tsb: protocol test covers the password gate and emergency erase
Each scenario group now runs on its own freshly-reset device: the round-trip
on a blank config page, plus a password-config device that must be sent the
password after the knock to activate, and an emergency-erase device where a
0-byte + two confirms wipes flash, EEPROM and the config page (verified by
reading all three back as 0xff). All three tiers pass every group.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHeP42XU3wf6RfhyuxBTE5
2026-07-19 16:49:20 +02:00
5c900720e3 tsb: pure and tricks tiers reach full oracle feature parity
Both tiers gain the features the asm tier already carries — one-wire
half-duplex (via libavr's new .half_duplex), the config-page activation
timeout, and emergency erase (password \0 + double-confirm wipes flash,
EEPROM and the config page) — on top of the watchdog bail, password gate and
config/flash/EEPROM read-write they already had. pure stays idiomatic
(flash_table info block, one function per command) at 950 B; tricks keeps its
compiler trickery (call-saved global-register page walk, unified runtime-flag
paths pinned noinline/noclone, streaming stores, arithmetic command decode)
at 808 B. Both byte-identical across generated and reflect modes; the size
gradient across the three tiers is now 502 / 808 / 950 B.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHeP42XU3wf6RfhyuxBTE5
2026-07-19 16:47:21 +02:00
7d6ef959b2 tsb: asm tier reaches full oracle feature parity at 502 B
Rewrite the inline-asm tier so it matches the hand-written fixed-baud oracle's
feature set inside the 512 B boot section: watchdog-reset bail, one-wire
half-duplex (RXEN/TXEN toggled per direction, TX turnaround guard),
config-page activation timeout, the password gate (wrong byte hangs draining
the UART), emergency erase (password \0 + double-confirm wipes flash, EEPROM
and the config page), and config/flash/EEPROM read-write. Every geometry,
baud and info-block constant comes from libavr consteval; only the dense
control flow is hand-written. 502 B, byte-identical across generated and
reflect modes.

Test harness: seed the config page from TSB_CONFIG so the password and
emergency-erase paths are exercisable, and clear simavr's AVR_UART_FLAG_POLL_
SLEEP — a host-CPU-saving usleep(1)-per-idle-poll hack that models no hardware
and paces a one-wire loader (which releases TX between bytes) in real time,
distorting protocol timing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHeP42XU3wf6RfhyuxBTE5
2026-07-19 16:23:16 +02:00
11ffbce2e2 tsb: vendor the fixed-baud assembly oracle as the size/feature bar
The Seed Robotics native-UART fixed-baud TinySafeBoot (GPLv3), reference
only — not built. Assembles to 500 B with the full feature set, proving
≤512 B and full feature parity are simultaneously reachable. Also drops the
stale empty stk500v2/ leftover.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHeP42XU3wf6RfhyuxBTE5
2026-07-19 15:29:58 +02:00
f32a27ff15 tsb: use the named register surface
Direct register access now reads through the named surface
(hw::mcusr::wdrf.test(), hw::ucsr0b::write(...)) instead of the string form,
matching how libavr itself is written. Zero-overhead: pure 740 B, tricks 658 B,
asm 508 B unchanged, all byte-identical across modes, protocol green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 14:55:01 +02:00
57d94cf631 tsb: refactor the pure tier onto libavr sugar
The showcase tier now leans on the helpers it fed back instead of reaching under
them: the info block is an avr::flash_table (no raw [[gnu::progmem]]), a page is
filled with spm::fill(addr, span) (no hand-packed lo|hi<<8 loop), and the
WDT-reset bail reads field<"MCUSR","WDRF">::test() (no read() & {}(1).value).

Zero-overhead throughout: .text stays 740 B, byte-identical across generated and
reflect modes, protocol test green. The info block streams through the existing
address-based send_flash rather than a range-for over the flash_table — the
range-for is a distinct loop that cannot share the loader's one flash streamer,
so it would add 14 B for no functional gain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 13:33:53 +02:00
8203a24f33 tsb: slim the port branch to the libavr reimplementation
main carried the whole pre-libavr tree beside the port: the other-bootloader
directories (blink, stk500v2), the Atmel Studio solution/project, and — dead in
the tsb dir itself — four submodule links to the superseded io/flash/uart/type
libraries the libavr sources never include. None are build inputs; CMake drives
the three variants through FetchContent. master keeps the full legacy tree
untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 13:12:56 +02:00
2906da3272 tsb: drop the local -O3 strip, now handled by the libavr toolchain
The -O3 leak is fixed upstream (cmake/release-os.cmake via CMAKE_PROJECT_INCLUDE),
so the port no longer needs its own string(REPLACE); a Release build is -Os
through the toolchain file. Verified: all three variants build at their sizes
(508/658/740) and pass the size + protocol ctest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 10:52:13 +02:00
64c1e484b5 tsb: reimplement TinySafeBoot on libavr in three size tiers
The native-UART fixed-baud TinySafeBoot protocol, ported onto libavr as a
crt-free boot-section loader, in three variants that trade clarity for size:

  tsb_pure   740 B  idiomatic C++: SRAM page buffer, separate flash/EEPROM
                    leaves, shared framing; the polled `unused` guard posture.
  tsb_tricks 658 B  unified runtime-flag paths (noinline/noclone), call-saved
                    global-register page walk — attributes only, no asm.
  tsb_asm    508 B  streaming store + hand-rolled UART/SPM/EEPROM/erase loops;
                    fits the 512 B boot section (BOOTSZ=11). Trims the optional
                    password gate and WDT-reset bail — unreachable in C++ with
                    both (hand-asm is ~15 % denser). Tiers 1-2 keep them and
                    live in the 1 KB section they fit.

All three are .text byte-identical across libavr's generated and reflect modes.
The CMake build strips the leaked -O3 (a Release build is silently -O3, not the
-Os this loader is measured against) and gates each variant's size against its
section. A simavr harness (test/device.c + test/tsbtest.py) drives the real wire
protocol over a pty and flashes the device; the size and protocol tests run in
ctest. Verified byte-for-byte against the reference tsbloader_adv (C#/mono):
activate, read info, flash write + verify.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 05:00:51 +02:00
20 changed files with 598 additions and 3677 deletions

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "libavr"]
path = libavr
url = ../libavr.git

View File

@@ -8,9 +8,6 @@ include(FetchContent)
if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT}) if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT})
set(LIBAVR_ROOT $ENV{LIBAVR_ROOT}) set(LIBAVR_ROOT $ENV{LIBAVR_ROOT})
endif() endif()
if(NOT LIBAVR_ROOT)
set(LIBAVR_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/libavr)
endif()
if(LIBAVR_ROOT) if(LIBAVR_ROOT)
FetchContent_Declare(libavr SOURCE_DIR ${LIBAVR_ROOT}) FetchContent_Declare(libavr SOURCE_DIR ${LIBAVR_ROOT})
else() else()
@@ -54,18 +51,13 @@ if(PROJECT_IS_TOP_LEVEL)
endif() endif()
endif() endif()
# The ELF is only a container (symbols, section headers) and is never flashed — # avrdude programs Intel-HEX; the ELF is only a container (symbols, section
# and the host tool's load_image() dispatches on extension, so handing it one # headers) and is never flashed. Every loader image therefore gets a .hex beside
# would silently program the header bytes. Every loader image therefore gets # it at link time. .eeprom is dropped — EEPROM content is its own avrdude update.
# both flashable forms beside it at link time: .hex for avrdude, and .bin for function(add_hex_output name)
# the host tool's raw path (which is what the reloc and update tests convert to
# on the fly). .eeprom is dropped — EEPROM content is its own update.
function(add_image_outputs name)
add_custom_command(TARGET ${name} POST_BUILD add_custom_command(TARGET ${name} POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.hex $<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.hex)
COMMAND ${CMAKE_OBJCOPY} -O binary -R .eeprom
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
endfunction() endfunction()
# The TinySafeBoot protocol reimplemented on libavr in three variants that trade # The TinySafeBoot protocol reimplemented on libavr in three variants that trade
@@ -107,7 +99,7 @@ function(add_tsb_variant name bytes)
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${base_hex} target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${base_hex}
-Wl,--defsym=tsb_app=0 -Wl,--pmem-wrap-around=32k) -Wl,--defsym=tsb_app=0 -Wl,--pmem-wrap-around=32k)
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>) add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
add_image_outputs(${name}) add_hex_output(${name})
if(PROJECT_IS_TOP_LEVEL) if(PROJECT_IS_TOP_LEVEL)
add_test(NAME ${name}.size add_test(NAME ${name}.size
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}> COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}>
@@ -129,31 +121,70 @@ if(LIBAVR_MCU STREQUAL "atmega328p")
endif() endif()
# pureboot — the pure-constraint port (see pureboot/README.md): one source, # pureboot — the pure-constraint port (see pureboot/README.md): one source,
# no inline assembly, no global register variables, every libavr chip, # no inline assembly, no global register variables, every libavr chip, 512
# fitting each chip's smallest boot sector. The geometry and the # bytes each. The loader owns the top 512 bytes of flash on every chip; the
# pureboot_add_loader() deployment function live in pureboot/CMakeLists.txt — # application entry symbol is address 0 on the mega (reset re-vectors to the
# the unit a downstream project consumes; everything below is this port's # loader through BOOTRST, so word 0 stays the application's own vector) and
# own build: the stock loaders, their tests, and the size matrix. The # the trampoline word just below the loader on the tinies (host-side vector
# distinct binary dir keeps the `pureboot` target's output name free. # surgery points it at the application). --pmem-wrap-around models AVR's
add_subdirectory(pureboot pureboot-cmake) # modulo-flash PC where the flash is big enough to need it.
#
# The stock loader: the family-default deployment (crystal/RC clock, the # The image is position-independent (check_pi.py asserts the two link-time
# chip's natural link, default pins). The activation window stays a cache # facts that make it so), and on the tinies its budget is 510, not 512: the
# variable — re-timing a deployed loader is a self-update with a re-timed # slot's last word is the trampoline the host composes — the resident slot's
# build. pureboot9 is that re-timed build, and what the update test installs. # holds the application entry, and a staging copy's holds the jump through
# which it reaches the loader it installed. The activation window is a
# compile-time constant; a different PUREBOOT_TIMEOUT builds the re-timed
# binary a self-update then installs.
set(PUREBOOT_TIMEOUT 8 CACHE STRING "pureboot activation window, seconds") set(PUREBOOT_TIMEOUT 8 CACHE STRING "pureboot activation window, seconds")
pureboot_add_loader(pureboot TIMEOUT ${PUREBOOT_TIMEOUT}) if(LIBAVR_MCU STREQUAL "attiny13a")
if(PROJECT_IS_TOP_LEVEL) set(_pb_flash 1024)
get_target_property(_pb_stock_hz pureboot PUREBOOT_HZ) set(_pb_wrap "")
get_target_property(_pb_stock_baud pureboot PUREBOOT_BAUD) set(_pb_page 32)
set(_pb_hz 9600000)
set(_pb_baud 57600)
set(_pb_eeprom 64)
set(_pb_limit 510)
elseif(LIBAVR_MCU STREQUAL "attiny85")
set(_pb_flash 8192)
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
set(_pb_page 64)
set(_pb_hz 8000000)
set(_pb_baud 57600)
set(_pb_eeprom 512)
set(_pb_limit 510)
else()
set(_pb_flash 32768)
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
set(_pb_page 128)
set(_pb_hz 16000000)
set(_pb_baud 115200)
set(_pb_eeprom 1024)
set(_pb_limit 512)
endif()
math(EXPR _pb_base "${_pb_flash} - 512")
math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL)
if(LIBAVR_MCU STREQUAL "atmega328p")
set(_pb_app 0)
else()
math(EXPR _pb_app "${_pb_base} - 2")
endif()
add_executable(pureboot pureboot/pureboot.cpp)
target_link_libraries(pureboot PRIVATE libavr)
target_compile_definitions(pureboot PRIVATE PUREBOOT_TIMEOUT=${PUREBOOT_TIMEOUT})
target_link_options(pureboot PRIVATE -nostartfiles -Wl,--section-start=.text=${_pb_base_hex}
-Wl,--defsym=pureboot_app=${_pb_app} ${_pb_wrap})
add_custom_command(TARGET pureboot POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:pureboot>)
add_hex_output(pureboot)
if(PROJECT_IS_TOP_LEVEL)
add_test(NAME pureboot.size add_test(NAME pureboot.size
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot> COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot>
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake) -DLIMIT=${_pb_limit} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
if(Python3_FOUND) if(Python3_FOUND)
add_test(NAME pureboot.pi add_test(NAME pureboot.pi
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/check_pi.py COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/check_pi.py
${CMAKE_OBJDUMP} ${CMAKE_NM} $<TARGET_FILE:pureboot> ${PUREBOOT_BASE_HEX}) ${CMAKE_OBJDUMP} ${CMAKE_NM} $<TARGET_FILE:pureboot> ${_pb_base_hex})
add_test(NAME pureboot.planner add_test(NAME pureboot.planner
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_planner.py COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_planner.py
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py) ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
@@ -169,9 +200,9 @@ if(PROJECT_IS_TOP_LEVEL)
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:pbapp> $<TARGET_FILE:pbapp>.bin) COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:pbapp> $<TARGET_FILE:pbapp>.bin)
add_test(NAME pureboot.protocol add_test(NAME pureboot.protocol
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PB_DEVICE} $<TARGET_FILE:pureboot> ${LIBAVR_MCU} ${_pb_hz} ${_pb_base_hex}
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud} ${PUREBOOT_EEPROM} ${_pb_page} ${_pb_baud} ${_pb_eeprom} $<TARGET_FILE:pbapp>.bin
$<TARGET_FILE:pbapp>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
${CMAKE_BINARY_DIR}/pbtest-work) ${CMAKE_BINARY_DIR}/pbtest-work)
set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180) set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180)
@@ -179,143 +210,29 @@ if(PROJECT_IS_TOP_LEVEL)
# installed one slot lower, must serve the full command set. # installed one slot lower, must serve the full command set.
add_test(NAME pureboot.reloc add_test(NAME pureboot.reloc
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbreloc.py COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbreloc.py
${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PB_DEVICE} $<TARGET_FILE:pureboot> ${LIBAVR_MCU} ${_pb_hz} ${_pb_base_hex}
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud} ${_pb_page} ${_pb_baud} ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
${CMAKE_BINARY_DIR}/pbreloc-work) ${CMAKE_BINARY_DIR}/pbreloc-work)
set_tests_properties(pureboot.reloc PROPERTIES TIMEOUT 180 set_tests_properties(pureboot.reloc PROPERTIES TIMEOUT 180
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}") ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
# Entering the loader from a running application with no reset
# between, over a page buffer the application dirtied — the case the
# loader declines to guard and the host repairs. Hardware forbids the
# state here (SPM runs only from the boot section); simavr does not,
# which is what makes it constructible.
if(LIBAVR_MCU STREQUAL "atmega328p")
add_test(NAME pureboot.dirty
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbdirty.py
${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud}
$<TARGET_FILE:pbapp>.bin
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
${CMAKE_BINARY_DIR}/pbdirty-work)
set_tests_properties(pureboot.dirty PROPERTIES TIMEOUT 180)
endif()
# Re-homing: a loader mistakenly programmed at address 0 (a raw .bin
# handed to a programmer) or sitting in the staging slot must heal
# into the canonical slot through the ordinary --update-loader flow.
# Patched-vector behavior, so one representative chip carries it.
if(LIBAVR_MCU STREQUAL "attiny85")
add_test(NAME pureboot.rehome
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbrehome.py
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9>.bin
${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE}
${_pb_stock_baud} $<TARGET_FILE:pbapp>.bin
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
${CMAKE_BINARY_DIR}/pbrehome-work)
set_tests_properties(pureboot.rehome PROPERTIES TIMEOUT 180)
endif()
# The self-update end-to-end: the re-timed build (same source, only # The self-update end-to-end: the re-timed build (same source, only
# the timeout differs — a byte-different image) replaces the resident # PUREBOOT_TIMEOUT differs — a byte-different image) replaces the
# through --update-loader, with every power-fail phase rehearsed from # resident through --update-loader, with every power-fail phase
# the runner's flash dumps. # rehearsed from the runner's flash dumps.
pureboot_add_loader(pureboot9 TIMEOUT 9) add_executable(pureboot9 pureboot/pureboot.cpp)
target_link_libraries(pureboot9 PRIVATE libavr)
target_compile_definitions(pureboot9 PRIVATE PUREBOOT_TIMEOUT=9)
target_link_options(pureboot9 PRIVATE -nostartfiles -Wl,--section-start=.text=${_pb_base_hex}
-Wl,--defsym=pureboot_app=${_pb_app} ${_pb_wrap})
add_hex_output(pureboot9)
add_test(NAME pureboot.update add_test(NAME pureboot.update
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbupdate.py COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbupdate.py
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9> ${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9> ${LIBAVR_MCU}
${PUREBOOT_SIM_MCU} ${_pb_stock_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_baud} $<TARGET_FILE:pbapp>.bin
${_pb_stock_baud} $<TARGET_FILE:pbapp>.bin
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
${CMAKE_BINARY_DIR}/pbupdate-work) ${CMAKE_BINARY_DIR}/pbupdate-work)
set_tests_properties(pureboot.update PROPERTIES TIMEOUT 600 set_tests_properties(pureboot.update PROPERTIES TIMEOUT 600
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}") ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
endif() endif()
# The size matrix: every configuration axis that could move the image
# size — the serial backend (different code), the clock and its ladder
# baud (different constants and divisor shapes), the USART instance
# (different register class) — each combination must still fit the
# chip's slot budget. Pins are size-neutral (port and bit are immediate
# operands) and the timeout is a constant, so neither adds an axis. The
# stock build is one point of this matrix and already has its test.
function(pureboot_size_variant name)
pureboot_add_loader(${name} ${ARGN})
add_test(NAME ${name}.size
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}>
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
endfunction()
# Clock points: the shipped-fuse floor (CKDIV8), the calibrated RC, and
# the crystal the stock build assumes (the tiny13's ladder is its own RC
# menu — it has no crystal option).
if(LIBAVR_MCU MATCHES "^attiny13")
set(_matrix_clocks 1200000 4800000 9600000)
else()
set(_matrix_clocks 1000000 8000000 16000000)
endif()
foreach(_matrix_hz IN LISTS _matrix_clocks)
math(EXPR _matrix_khz "${_matrix_hz} / 1000")
if(PUREBOOT_HAS_USART OR NOT _matrix_hz EQUAL _pb_stock_hz)
pureboot_size_variant(pureboot_sw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL software)
endif()
if(PUREBOOT_HAS_USART AND NOT _matrix_hz EQUAL _pb_stock_hz)
pureboot_size_variant(pureboot_hw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL hardware)
endif()
endforeach()
if(PUREBOOT_HAS_USART1)
pureboot_size_variant(pureboot_usart1 USART 1)
endif()
# One configured deployment end to end — a real board's shape rather
# than the stock assumption: the ATmega328P on its shipped 1 MHz fuses,
# the software UART on hand-picked pins (TX = PB1, RX = PB5), the ladder
# baud (9600). The full protocol suite runs against it, fixture
# application included, over the runner's GPIO bridge — proving the
# configuration plumbing produces a working loader, not just one that
# fits.
if(LIBAVR_MCU STREQUAL "atmega328p" AND DEFINED PB_DEVICE)
pureboot_size_variant(pureboot_custom CLOCK 1000000 SERIAL software RX pb5 TX pb1)
get_target_property(_custom_hz pureboot_custom PUREBOOT_HZ)
get_target_property(_custom_baud pureboot_custom PUREBOOT_BAUD)
get_target_property(_custom_link pureboot_custom PUREBOOT_LINK)
add_executable(pbapp_custom test/pbapp.cpp)
target_link_libraries(pbapp_custom PRIVATE libavr)
target_compile_definitions(pbapp_custom PRIVATE PUREBOOT_CLOCK_HZ=${_custom_hz}
PUREBOOT_BAUD=${_custom_baud} PUREBOOT_SOFT_SERIAL PUREBOOT_TX=pb1)
add_custom_command(TARGET pbapp_custom POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O binary
$<TARGET_FILE:pbapp_custom> $<TARGET_FILE:pbapp_custom>.bin)
add_test(NAME pureboot.custom
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
${PB_DEVICE} $<TARGET_FILE:pureboot_custom> ${PUREBOOT_SIM_MCU} ${_custom_hz}
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_custom_baud} ${PUREBOOT_EEPROM}
$<TARGET_FILE:pbapp_custom>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
${CMAKE_BINARY_DIR}/pbcustom-work ${_custom_link})
set_tests_properties(pureboot.custom PROPERTIES TIMEOUT 180)
endif()
# The second USART, driven for real on one chip: instance selection is
# compile-checked everywhere, but only a live session proves the loader
# initialized and polls the USART it claims to. The fixture application
# banners on the same instance.
if(LIBAVR_MCU STREQUAL "atmega644a" AND DEFINED PB_DEVICE)
get_target_property(_usart1_hz pureboot_usart1 PUREBOOT_HZ)
get_target_property(_usart1_baud pureboot_usart1 PUREBOOT_BAUD)
add_executable(pbapp_usart1 test/pbapp.cpp)
target_link_libraries(pbapp_usart1 PRIVATE libavr)
target_compile_definitions(pbapp_usart1 PRIVATE PUREBOOT_CLOCK_HZ=${_usart1_hz}
PUREBOOT_BAUD=${_usart1_baud} PUREBOOT_USART=1)
add_custom_command(TARGET pbapp_usart1 POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O binary
$<TARGET_FILE:pbapp_usart1> $<TARGET_FILE:pbapp_usart1>.bin)
add_test(NAME pureboot.usart1
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
${PB_DEVICE} $<TARGET_FILE:pureboot_usart1> ${PUREBOOT_SIM_MCU} ${_usart1_hz}
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_usart1_baud} ${PUREBOOT_EEPROM}
$<TARGET_FILE:pbapp_usart1>.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
${CMAKE_BINARY_DIR}/pbusart1-work usart1)
set_tests_properties(pureboot.usart1 PROPERTIES TIMEOUT 180)
endif()
endif() endif()

File diff suppressed because it is too large Load Diff

1
libavr

Submodule libavr deleted from e81dad0131

View File

@@ -1,319 +0,0 @@
# pureboot as a consumable CMake unit: the per-chip geometry, the default
# baud ladder, and pureboot_add_loader() — the one way a loader target is
# created, both by this port's own build and by a downstream project. A
# downstream project brings its usual libavr setup (the `libavr` target and
# the LIBAVR_MCU toolchain preset), adds this directory, and states its
# deployment:
#
# add_subdirectory(bootloader/pureboot)
# pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
#
# Every argument is optional — CLOCK defaults to the family assumption
# below, BAUD to the fastest standard rate the clock reaches within 2.5 %
# (the ladder), SERIAL to the chip's hardware USART where it has one
# (`hardware`/`software` force a backend, USART 1 picks the second
# instance), RX/TX to pb0/pb1 for the software UART, TIMEOUT to 8 s.
# Infeasible picks fail the build by name: libavr's baud-error and
# software-UART cycle-floor static asserts re-check whatever is passed.
# Per-family geometry: flash/page/EEPROM sizes and the linker wrap the PC
# modulo needs, the loader slot (each chip's smallest boot sector — 1 KiB on
# the word-addressed 1284s), and the deployment defaults (crystal assumption
# on the megas, calibrated RC on the tinies). The USART flags mirror the
# hardware inventory the loader's own static asserts check (the plain 644 is
# the x4 family's one single-USART die, Atmel-2593).
set(_pb_has_usart 1)
set(_pb_has_usart1 0)
if(LIBAVR_MCU MATCHES "^attiny13a?$")
set(_pb_flash 1024)
set(_pb_wrap "")
set(_pb_page 32)
set(_pb_hz 9600000)
set(_pb_eeprom 64)
set(_pb_has_usart 0)
elseif(LIBAVR_MCU STREQUAL "attiny25")
set(_pb_flash 2048)
set(_pb_wrap "")
set(_pb_page 32)
set(_pb_hz 8000000)
set(_pb_eeprom 128)
set(_pb_has_usart 0)
elseif(LIBAVR_MCU STREQUAL "attiny45")
set(_pb_flash 4096)
set(_pb_wrap "")
set(_pb_page 64)
set(_pb_hz 8000000)
set(_pb_eeprom 256)
set(_pb_has_usart 0)
elseif(LIBAVR_MCU STREQUAL "attiny85")
set(_pb_flash 8192)
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
set(_pb_page 64)
set(_pb_hz 8000000)
set(_pb_eeprom 512)
set(_pb_has_usart 0)
elseif(LIBAVR_MCU MATCHES "^atmega48(a|p|pa)?$")
set(_pb_flash 4096)
set(_pb_wrap "")
set(_pb_page 64)
set(_pb_hz 16000000)
set(_pb_eeprom 256)
elseif(LIBAVR_MCU MATCHES "^atmega8a?$" OR LIBAVR_MCU MATCHES "^atmega88(a|p|pa)?$")
set(_pb_flash 8192)
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
set(_pb_page 64)
set(_pb_hz 16000000)
set(_pb_eeprom 512)
elseif(LIBAVR_MCU MATCHES "^atmega16a?$" OR LIBAVR_MCU MATCHES "^atmega168(a|p|pa)?$")
set(_pb_flash 16384)
set(_pb_wrap -Wl,--pmem-wrap-around=16k)
set(_pb_page 128)
set(_pb_hz 16000000)
set(_pb_eeprom 512)
elseif(LIBAVR_MCU MATCHES "^atmega164(a|p|pa)$")
set(_pb_flash 16384)
set(_pb_wrap -Wl,--pmem-wrap-around=16k)
set(_pb_page 128)
set(_pb_hz 16000000)
set(_pb_eeprom 512)
set(_pb_has_usart1 1)
elseif(LIBAVR_MCU MATCHES "^atmega32a?$" OR LIBAVR_MCU MATCHES "^atmega328p?$")
set(_pb_flash 32768)
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
set(_pb_page 128)
set(_pb_hz 16000000)
set(_pb_eeprom 1024)
elseif(LIBAVR_MCU MATCHES "^atmega324(a|p|pa)$")
set(_pb_flash 32768)
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
set(_pb_page 128)
set(_pb_hz 16000000)
set(_pb_eeprom 1024)
set(_pb_has_usart1 1)
elseif(LIBAVR_MCU MATCHES "^atmega644(a|p|pa)?$")
# 64 KiB is exactly the 16-bit byte space: plain LPM reaches everything,
# and the smallest boot section (1 KiB) holds the loader and its staging
# slot together (see README.md). The plain 644 is the family's one
# single-USART die.
set(_pb_flash 65536)
set(_pb_wrap -Wl,--pmem-wrap-around=64k)
set(_pb_page 256)
set(_pb_hz 16000000)
set(_pb_eeprom 2048)
if(NOT LIBAVR_MCU STREQUAL "atmega644")
set(_pb_has_usart1 1)
endif()
elseif(LIBAVR_MCU MATCHES "^atmega1284p?$")
# 128 KiB: wire flash addresses are word addresses, reads go through
# ELPM, and the PC's modulo wrap exceeds what --pmem-wrap-around models.
# The slot is 1 KiB — this chip's own smallest boot sector; the far
# machinery cannot fit 512 B (see README.md).
set(_pb_flash 131072)
set(_pb_wrap "")
set(_pb_page 256)
set(_pb_hz 16000000)
set(_pb_eeprom 4096)
set(_pb_slot 1024)
set(_pb_limit 1024)
set(_pb_has_usart1 1)
else()
message(FATAL_ERROR "pureboot: no geometry for ${LIBAVR_MCU}")
endif()
if(NOT DEFINED _pb_slot)
set(_pb_slot 512)
endif()
math(EXPR _pb_base "${_pb_flash} - ${_pb_slot}")
math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL)
# Patched-vector chips hand over through the trampoline word below the slot,
# which is also the slot's own last word — their budget is slot 2.
if(LIBAVR_MCU MATCHES "^atmega" AND NOT LIBAVR_MCU MATCHES "^atmega48")
set(_pb_app 0)
if(NOT DEFINED _pb_limit)
set(_pb_limit ${_pb_slot})
endif()
else()
math(EXPR _pb_app "${_pb_base} - 2")
math(EXPR _pb_limit "${_pb_slot} - 2")
endif()
# simavr names its cores after the base dies; the A revisions run on them
# (the 644PA on the 644P core).
set(_pb_sim_mcu ${LIBAVR_MCU})
if(LIBAVR_MCU MATCHES "^atmega(8|16|32|48|88|164|168|644)a$")
string(REGEX REPLACE "a$" "" _pb_sim_mcu ${LIBAVR_MCU})
elseif(LIBAVR_MCU STREQUAL "atmega644pa")
set(_pb_sim_mcu atmega644p)
endif()
# The function runs in its caller's scope, so everything it needs crosses
# scopes as global properties.
set_property(GLOBAL PROPERTY PUREBOOT_BASE_HEX ${_pb_base_hex})
set_property(GLOBAL PROPERTY PUREBOOT_APP ${_pb_app})
set_property(GLOBAL PROPERTY PUREBOOT_WRAP "${_pb_wrap}")
set_property(GLOBAL PROPERTY PUREBOOT_DEFAULT_HZ ${_pb_hz})
set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART ${_pb_has_usart})
set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART1 ${_pb_has_usart1})
# The port's own build (tests, the size matrix) reads the geometry from the
# parent scope; a downstream consumer gets the same variables for free.
set(PUREBOOT_BASE_HEX ${_pb_base_hex} PARENT_SCOPE)
set(PUREBOOT_PAGE ${_pb_page} PARENT_SCOPE)
set(PUREBOOT_SLOT ${_pb_slot} PARENT_SCOPE)
set(PUREBOOT_LIMIT ${_pb_limit} PARENT_SCOPE)
set(PUREBOOT_EEPROM ${_pb_eeprom} PARENT_SCOPE)
set(PUREBOOT_DEFAULT_HZ ${_pb_hz} PARENT_SCOPE)
set(PUREBOOT_HAS_USART ${_pb_has_usart} PARENT_SCOPE)
set(PUREBOOT_HAS_USART1 ${_pb_has_usart1} PARENT_SCOPE)
set(PUREBOOT_SIM_MCU ${_pb_sim_mcu} PARENT_SCOPE)
# The fastest standard rate the clock reaches within 2.5 % — the same
# best-of-U2X-and-plain divisor search libavr's solve_baud runs, so a
# default never trips the compile-time error it is checked against. A
# software build additionally requires the polled receiver's 100-cycles-a-bit
# floor (its own static assert): at low clocks the U2X divisor still reaches
# rates the bit-banged sampler cannot, so the backend gates the ladder.
function(pureboot_default_baud clock software outvar)
foreach(baud 115200 57600 38400 19200 9600)
math(EXPR _cycles "${clock} / ${baud}")
if(software AND _cycles LESS 100)
continue()
endif()
foreach(divisor 8 16)
math(EXPR _step "${divisor} * ${baud}")
math(EXPR _n "(${clock} + ${_step} / 2) / ${_step}")
if(_n LESS 1 OR _n GREATER 4096)
continue()
endif()
math(EXPR _actual "${clock} / (${divisor} * ${_n})")
math(EXPR _delta "${_actual} - ${baud}")
if(_delta LESS 0)
math(EXPR _delta "-(${_delta})")
endif()
math(EXPR _error_bp "${_delta} * 10000 / ${baud}")
if(_error_bp LESS_EQUAL 250)
set(${outvar} ${baud} PARENT_SCOPE)
return()
endif()
endforeach()
endforeach()
message(FATAL_ERROR "pureboot: no standard baud rate fits a ${clock} Hz clock within 2.5 %")
endfunction()
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
# [SERIAL auto|hardware|software] [USART <n>]
# [RX <pin>] [TX <pin>] [TIMEOUT <s>])
#
# Creates the loader target plus its flashable images (<name>.hex for a
# programmer, <name>.bin for --update-loader) and stamps the resolved
# deployment on the target: the PUREBOOT_HZ, PUREBOOT_BAUD and PUREBOOT_LINK
# properties (the link as usart0/usart1/sw:<RX>,<TX> — what a test harness
# needs to speak to the build).
function(pureboot_add_loader name)
cmake_parse_arguments(PB "" "CLOCK;BAUD;SERIAL;USART;RX;TX;TIMEOUT" "" ${ARGN})
if(PB_UNPARSED_ARGUMENTS)
message(FATAL_ERROR "pureboot_add_loader(${name}): unknown arguments ${PB_UNPARSED_ARGUMENTS}")
endif()
get_property(_hz GLOBAL PROPERTY PUREBOOT_DEFAULT_HZ)
get_property(_base_hex GLOBAL PROPERTY PUREBOOT_BASE_HEX)
get_property(_app GLOBAL PROPERTY PUREBOOT_APP)
get_property(_wrap GLOBAL PROPERTY PUREBOOT_WRAP)
get_property(_usart GLOBAL PROPERTY PUREBOOT_HAS_USART)
get_property(_usart1 GLOBAL PROPERTY PUREBOOT_HAS_USART1)
if(NOT PB_CLOCK)
set(PB_CLOCK ${_hz})
endif()
if(NOT PB_TIMEOUT)
set(PB_TIMEOUT 8)
endif()
if(NOT PB_SERIAL)
set(PB_SERIAL auto)
endif()
if(DEFINED PB_USART AND PB_SERIAL STREQUAL "software")
message(FATAL_ERROR "pureboot_add_loader(${name}): USART ${PB_USART} contradicts SERIAL software")
endif()
if(DEFINED PB_USART)
set(PB_SERIAL hardware)
elseif(PB_SERIAL STREQUAL "hardware")
set(PB_USART 0)
endif()
set(_serial_defines "")
if(PB_SERIAL STREQUAL "hardware")
if(PB_USART EQUAL 1 AND NOT _usart1)
message(FATAL_ERROR "pureboot_add_loader(${name}): ${LIBAVR_MCU} has no USART1")
elseif(NOT _usart)
message(FATAL_ERROR "pureboot_add_loader(${name}): ${LIBAVR_MCU} has no hardware USART")
endif()
set(_serial_defines PUREBOOT_USART=${PB_USART})
set(_link usart${PB_USART})
else()
if(PB_SERIAL STREQUAL "auto")
if(_usart AND (PB_RX OR PB_TX))
message(WARNING "pureboot_add_loader(${name}): RX/TX apply to the software UART, "
"which auto does not pick on ${LIBAVR_MCU} — SERIAL software to force it")
endif()
if(_usart)
set(_link usart0)
else()
set(PB_SERIAL software)
endif()
endif()
if(PB_SERIAL STREQUAL "software")
if(NOT PB_RX)
set(PB_RX pb0)
endif()
if(NOT PB_TX)
set(PB_TX pb1)
endif()
foreach(_pin ${PB_RX} ${PB_TX})
if(NOT _pin MATCHES "^p[a-h][0-7]$")
message(FATAL_ERROR "pureboot_add_loader(${name}): pin '${_pin}' is not of the form pb1")
endif()
endforeach()
set(_serial_defines PUREBOOT_SOFT_SERIAL PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
# The link spec a test harness drives a GPIO bridge with: sw:<RX>,<TX>
# as the port letter and bit, the loader's own pin naming upcased.
string(SUBSTRING ${PB_RX} 1 2 _rx_pin)
string(SUBSTRING ${PB_TX} 1 2 _tx_pin)
string(TOUPPER "sw:${_rx_pin},${_tx_pin}" _link)
string(REPLACE "SW" "sw" _link ${_link})
endif()
endif()
if(NOT PB_BAUD)
if(PB_SERIAL STREQUAL "software")
pureboot_default_baud(${PB_CLOCK} 1 PB_BAUD)
else()
pureboot_default_baud(${PB_CLOCK} 0 PB_BAUD)
endif()
endif()
set(_defines PUREBOOT_CLOCK_HZ=${PB_CLOCK} PUREBOOT_BAUD=${PB_BAUD} PUREBOOT_TIMEOUT=${PB_TIMEOUT}
${_serial_defines})
add_executable(${name} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/pureboot.cpp)
target_link_libraries(${name} PRIVATE libavr)
target_compile_definitions(${name} PRIVATE ${_defines})
# Codegen shaping for the loader TU only, worth ~40 B on every chip and
# what carries the far-flash 1284 build under 512. At -Os GCC otherwise
# rewrites the byte-stream loops' counters into end-pointer forms that
# cost registers (-fno-ivopts, -fno-split-wide-types), leaves register
# pressure on the table with the default allocator
# (-fira-algorithm=priority), and spends bytes on rewrites a
# straight-line loader gains nothing from.
target_compile_options(${name} PRIVATE
-fno-ivopts -fira-algorithm=priority -fno-expensive-optimizations -fno-split-wide-types)
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${_base_hex}
-Wl,--defsym=pureboot_app=${_app} ${_wrap})
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
# The ELF is a container (symbols, section headers), never flashed; the
# flashable forms sit beside it: .hex for a programmer, .bin (the slot's
# bare bytes) for the host tool's raw path and --update-loader.
add_custom_command(TARGET ${name} POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.hex
COMMAND ${CMAKE_OBJCOPY} -O binary -R .eeprom
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
set_target_properties(${name} PROPERTIES PUREBOOT_HZ ${PB_CLOCK} PUREBOOT_BAUD ${PB_BAUD}
PUREBOOT_LINK ${_link})
endfunction()

View File

@@ -2,115 +2,49 @@
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by
constraint: one C++ source, no inline assembly, no global register variables constraint: one C++ source, no inline assembly, no global register variables
(attributes and compiler flags allowed), built for **every chip libavr (attributes allowed), built for every chip libavr targets, **512 bytes on
targets — all 37 — in 512 bytes each**: 434 B on the tiny13s, 438442 B on each** — 488 B on the ATtiny13A, 502 B on the ATtiny85, 504 B on the
the tiny25/45/85, 412452 B across the megas, and 506 B on the ATmega328P. The device speaks primitives; every composite — verify, erase,
ATmega1284/1284P, whose far-flash machinery (ELPM reads, RAMPZ page commands, reset-vector surgery, updating the loader itself — lives in the host tool
word-addressed wire) is the heaviest. Those are the stock deployments; (`pureboot.py`).
choosing the software UART where the chip has a USART costs 846 B more (a
bit-bang against a peripheral), which every chip still absorbs inside its
slot — on the 1284s that means their 1 KiB boot sector, where the
software-serial image lands at 546 B. Bringing the 1284's default build
under 512 at all is what the loop-placement attributes on the byte streamers
(`pureboot.cpp`) and the codegen flags on the loader TU (`CMakeLists.txt`)
are for; measured against each chip's own budget the tightest is the
ATmega328P, 50 B spare. Clock, baud, serial backend and
pins are per-build configuration (below); the size matrix in the test suite
holds every combination inside its slot. The device speaks primitives; every
composite — verify, erase, reset-vector surgery, updating the loader itself —
lives in the host tool (`pureboot.py`).
The 1284s still *deploy* in a 1 KiB slot, their smallest boot sector being
512 words; at 506 B the image would also fit the 644's
two-512-byte-slots-per-boot-sector geometry.
The image is **position-independent**: control flow is PC-relative, the The image is **position-independent**: control flow is PC-relative, the
read/write paths take wire addresses, the write guard protects the slot the read/write paths take wire addresses, the write guard protects the 512-byte
code is *running* in (from the runtime return address), the info block is slot the code is *running* in (from the runtime return address), the info
addressed from that same anchor, and the application jump is an indirect block is addressed from that same anchor, and the application jump is an
call to an absolute entry. The identical binary therefore runs from any indirect call to an absolute entry. The identical binary therefore runs from
slot with every command intact — which makes pureboot **its own staging any 512-byte slot with every command intact — which makes pureboot **its own
loader**: the host installs the same binary one slot below the resident, staging loader**: the host installs the same binary one slot below the
jumps into it, and lets it rewrite the resident. The slot is 512 bytes resident, jumps into it, and lets it rewrite the resident. On the tinies the
(1 KiB on the word-addressed large chips, matching their boot-sector budget is 510, not 512: a slot's last word belongs to the host-managed
minimum); on the tinies the budget is 510, not 512: a slot's last word trampoline (below).
belongs to the host-managed trampoline (below).
## Configuration
Every deployment axis is a build parameter, resolved by the CMake function
`pureboot_add_loader()` (in `pureboot/CMakeLists.txt`) — the one way a
loader target is created, by this repo's own build and by a downstream
project alike:
| Argument | Meaning | Default |
|---|---|---|
| `CLOCK <hz>` | the clock the board runs | 16 MHz megas, 8 MHz t25/45/85, 9.6 MHz t13s |
| `BAUD <bd>` | the wire rate | the ladder below |
| `SERIAL auto\|hardware\|software` | the link backend | `auto`: the hardware USART where the chip has one |
| `USART <n>` | the USART instance (x4 megas carry two) | 0 |
| `RX <pin>`, `TX <pin>` | software-UART pins | `pb0`, `pb1` |
| `TIMEOUT <s>` | the activation window | 8 |
The default baud is the fastest of 115200/57600/38400/19200/9600 the clock
reaches within 2.5 % — the same U2X-included divisor search libavr's baud
solver runs — and on a software build additionally within the polled
receiver's 100-cycles-a-bit floor. 16 MHz lands 115200, 8 MHz 57600,
1 MHz 9600. Whatever is picked or overridden is re-checked in the compile:
an infeasible clock/baud/backend combination, or a USART the chip does not
have, fails with a named static assert.
A downstream project brings its usual libavr setup (the `libavr` target,
the chip via the `LIBAVR_MCU` toolchain preset), consumes this directory,
and states its deployment — for example an ATmega328P on its shipped
1 MHz fuses with the software UART on hand-picked pins:
```cmake
FetchContent_Declare(bootloader GIT_REPOSITORY git@git.blackmark.me:avr/bootloader.git GIT_TAG main)
FetchContent_MakeAvailable(bootloader)
add_subdirectory(${bootloader_SOURCE_DIR}/pureboot pureboot)
pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
```
The function emits the ELF plus `myboot.hex` (the programmer artifact) and
`myboot.bin` (the self-update image), prints the size, and stamps the
resolved deployment on the target as the `PUREBOOT_HZ`, `PUREBOOT_BAUD`
and `PUREBOOT_LINK` properties — what a flashing script or test harness
needs to speak to the build. This exact example deployment runs the full
protocol suite in CI (`pureboot.custom`).
## Link ## Link
The stock builds assume the family's natural deployment; any axis moves
per build (above).
| Chip | Serial | Baud | Clock assumed | | Chip | Serial | Baud | Clock assumed |
|---|---|---|---| |---|---|---|---|
| every ATmega | the hardware USART (USART0), RXD/TXD per pinout | 115200 8N1 | 16 MHz crystal | | ATmega328P | USART0, RXD/TXD = PD0/PD1 | 115200 8N1 | 16 MHz crystal |
| ATtiny25/45/85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC | | ATtiny85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC |
| ATtiny13/13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC | | ATtiny13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC |
The software-UART RX pin has its pull-up enabled; TX idles high. All The tiny RX pin has its pull-up enabled; TX idles high. All multi-byte
multi-byte quantities on the wire are little-endian. quantities on the wire are little-endian.
## Activation ## Activation
Reset enters the loader (BOOTRST on the boot-sectioned megas; the patched Reset enters the loader (BOOTRST on the mega, the patched reset vector on the
reset vector on the tinies and the boot-section-less m48s) — except a tinies) — except a watchdog reset, which hands straight to the application
watchdog reset, which hands straight to the application (the application (the application owns its watchdog; it must clear WDRF itself, which also
owns its watchdog; it must clear WDRF itself, which also releases the releases the WDRF-forced WDE).
WDRF-forced WDE).
The host then has one activation window per awaited byte to knock: `p` then The host then has one activation window per awaited byte to knock: `p` then
`b`. Each awaited byte gets a fresh window; any other byte is discarded and `b`. Each awaited byte gets a fresh window; any other byte is discarded and
awaited again (line noise cannot lock the loader, only delay it). A window awaited again (line noise cannot lock the loader, only delay it). A window
expiring with an idle line boots the application. expiring with an idle line boots the application.
The window length is a compile-time constant — 8 s by default, another The window length is a compile-time constant — 8 s by default, another value
value via `pureboot_add_loader(... TIMEOUT <s>)` (the stock target keeps via the `PUREBOOT_TIMEOUT` CMake cache variable — so the whole EEPROM belongs
the `PUREBOOT_TIMEOUT` cache variable) — so the whole EEPROM belongs to to the application; pureboot never uses it for its own state. Re-timing a
the application; pureboot never uses it for its own state. Re-timing a
deployed loader is a self-update with a re-timed build (below). deployed loader is a self-update with a re-timed build (below).
## Session ## Session
@@ -121,11 +55,6 @@ write to finish and sends the prompt `+` (0x2b) — the prompt is therefore
also the completion ack of the previous command. A session is: await `+`, also the completion ack of the previous command. A session is: await `+`,
send a command, read its reply, repeat. send a command, read its reply, repeat.
On chips whose flash exceeds 64 KiB (the 1284s — info-block flag bit 1) the
`R`/`W` flash addresses are **word** addresses; everywhere else they are byte
addresses (the 644s' 64 KiB is exactly the 16-bit byte space and stays
byte-addressed). EEPROM addresses are always bytes, counts always bytes.
| Cmd | Arguments | Reply | | Cmd | Arguments | Reply |
|---|---|---| |---|---|---|
| `b` | — | the 12-byte info block | | `b` | — | the 12-byte info block |
@@ -141,20 +70,7 @@ byte-addressed). EEPROM addresses are always bytes, counts always bytes.
then erases and programs; the address must be page-aligned. Pages inside the then erases and programs; the address must be page-aligned. Pages inside the
512-byte slot the loader is *running* in are drained but never programmed — a 512-byte slot the loader is *running* in are drained but never programmed — a
broken host cannot brick the running copy, and a staged copy may rewrite the broken host cannot brick the running copy, and a staged copy may rewrite the
resident slot. resident slot. `w` is host-paced: send the next byte only after the previous
The loader never clears the SPM buffer before a fill, so **one `W` may
program the wrong bytes, and the host is what fixes it**. The buffer is
write-once per word until cleared, and two things leave words in it: a
refused page (drained, never programmed) and — where SPM runs from anywhere,
the tinies and the m48s — an application that self-programmed before
entering. The next `W` takes those stale words, and clears them: a page write
auto-erases the buffer (§26.2.1; §19.2 on the tinies), so repeating it
programs correctly. The host therefore verifies every page it writes and
rewrites what comes back wrong (three retries, then it stops); a host that
programs without reading back cannot trust the first `W` after either event.
`w` is host-paced: send the next byte only after the previous
byte's `+`. `F` returns the bytes in the hardware's Z order; on a chip byte's `+`. `F` returns the bytes in the hardware's Z order; on a chip
without an extended fuse byte (the ATtiny13A) that slot carries no meaning. without an extended fuse byte (the ATtiny13A) that slot carries no meaning.
Fuse *writing* does not exist: SPM reaches flash (and, on the mega, lock Fuse *writing* does not exist: SPM reaches flash (and, on the mega, lock
@@ -170,70 +86,20 @@ The info block (`b`):
| Offset | Content | | Offset | Content |
|---|---| |---|---|
| 02 | `'P'`, `'B'`, pureboot version (2) | | 02 | `'P'`, `'B'`, protocol version (1) |
| 35 | device signature | | 35 | device signature |
| 6 | SPM page size in bytes (0 means 256) | | 6 | SPM page size in bytes |
| 78 | loader base — application flash ends here (a word address when bit 1 is set) | | 78 | loader base — application flash ends here |
| 910 | EEPROM size | | 910 | EEPROM size |
| 11 | bit 0: host must patch the reset vector (no hardware boot section); bit 1: flash wire addresses are word addresses | | 11 | bit 0 set: host must patch the reset vector (no hardware boot section) |
Composites are the host's job: verify = read back and compare, erase = Composites are the host's job: verify = read back and compare, erase =
write `0xff` (per page for flash, per byte for EEPROM). write `0xff` (per page for flash, per byte for EEPROM).
## Version
The third byte of the info block is the **pureboot version** — the loader's
one identity number, and the only way to tell what a deployed loader is.
Nothing else is numbered: the wire protocol has no version of its own, a
pureboot version implies its protocol, and the host tool is what holds that
map. It states the window of loader versions it speaks
(`OLDEST_LOADER`/`NEWEST_LOADER` in `pureboot.py`); a version that changes
the protocol becomes the new floor there. So far none has: pureboot 1 and 2
speak the identical session, and a loader newer than the tool is refused by
name rather than decoded on the assumption that nothing moved.
The tool carries its own version, free to drift from the loader's:
`--version` prints both it and the window.
## Deployment ## Deployment
The build leaves three artifacts per chip. The ELF is a container for the **ATmega328P**: program the loader at 0x7e00 with an external programmer.
tests and objcopy — never flashed. The **.hex is the programmer artifact**: Two fuse profiles, same binary:
it carries its own addresses and lands the loader in its top slot,
touching nothing else. The **.bin is the self-update image** — the slot's
bare bytes with no addressing, which a programmer would put at address 0.
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);
on the patched-vector chips it *runs* (the image is position-independent
and reset enters word 0), reports its canonical geometry, and the ordinary
`--update-loader` flow re-homes a build into the top slot from any
position — the staging install and the word-0 redirect execute from
copies outside page 0's slot, and a copy sitting in the staging slot
itself is recognized as the installed staging copy and left in place (it
streams the new resident like any staged copy, so an older build installs
a newer one). `pureboot.rehome` is the acceptance test for both
positions. Flashing the application afterwards overwrites the stale copy,
vector surgery included.
**Boot-sectioned megas**: program the loader at `flash slot` with an
external programmer. Every such mega has a BOOTSZ step whose boot section
is exactly the loader slot — 512 B, the second-smallest step on the 8 KiB
and 16 KiB chips (m8, m88, m16, m168, m164), the smallest on the 32 KiB
ones (m32, m328, m324); on the 1284s that step is the smallest, 512 words,
which is why their slot is 1 KiB — so the ATmega328P profiles below apply
to every one of them with its own addresses and slot size; the per-chip
BOOTSZ ladders live in the host tool (`BOOT_FUSE`). The 1284s' numbers:
standalone = BOOTSZ 512 words (reset at the loader base 0x1fc00);
self-update = 1024 words, covering both 1 KiB slots, the loader-first
reset landing at 0x1f800 — the staging slot, walked across when erased.
The **644s** are the geometry's sweet spot: their smallest boot section
(512 words = 1 KiB) is exactly *two* 512-byte slots, so the resident and
its staging slot both live inside the minimum section — self-update needs
no fuse step up, and the standalone profile does not exist (reset lands at
0xfc00, one erased slot below the loader: the loader-first walk built in).
ATmega328P profiles (addresses for its 32 KiB):
| BOOTSZ | BOOTRST | Behavior | | BOOTSZ | BOOTRST | Behavior |
|---|---|---| |---|---|---|
@@ -244,50 +110,34 @@ ATmega328P profiles (addresses for its 32 KiB):
Applications are flashed unmodified — word 0 stays the application's own Applications are flashed unmodified — word 0 stays the application's own
reset vector, and the hand-over jumps to 0. reset vector, and the hand-over jumps to 0.
**Patched-vector chips — the tinies and the m48s** (no boot section; the **Tinies** (no boot section): program the loader at `flash 512`; erased
m48s' SPM runs from the entire flash, Atmel-8271 §26): program the loader flash below it walks up into the loader, so a virgin chip activates. When
at `flash 512`; erased flash below it walks up into the loader, so a flashing an application the host performs reset-vector surgery: word 0 is
virgin chip activates. When flashing an application the host performs rewritten to `rjmp` to the loader base, and the application's own entry is
reset-vector surgery: word 0 is rewritten to `rjmp` to the loader base, and re-encoded as a trampoline `rjmp` in the word just below the loader
the application's own entry is re-encoded as a trampoline `rjmp` in the (`base 2`, where the hand-over jumps). Every other vector stays the
word just below the loader (`base 2`, where the hand-over jumps). Every application's. The patched page 0 and the trampoline page are written
other vector stays the application's. The patched page 0 and the trampoline *first*, so from the first write on an interrupted flash still resets into
page are written *first*, so from the first write on an interrupted flash the loader; an erase runs top-down for the same reason.
still resets into the loader; an erase runs top-down for the same reason.
The m48s speak this profile over their hardware USART — no fuse preflight,
BOOTRST does not exist there.
## Updating the loader ## Updating the loader
`pureboot.py --update-loader new_pureboot.bin` replaces the resident loader `pureboot.py --update-loader new_pureboot.bin` replaces the resident loader
with any pureboot build — a re-timed window, a newer version — using the with any pureboot build — a re-timed window, a newer protocol — using the
loader itself as its own staging loader. The image is the loader's own 512 loader itself as its own staging loader. The image is the loader's own 512
bytes as a raw binary, or the Intel HEX the build emits beside it, which bytes as a raw binary, or the Intel HEX the build emits beside it, which
links the loader at its base inside an otherwise blank flash image: links the loader at its base inside an otherwise blank flash image:
The preflight refuses an image built for another chip: the info block 1. The staging slot `[base512, base)` is saved to a host-side state file
embedded in every pureboot binary (signature, page size, loader base, (on the 1 KB tiny13A that is the whole application, vectors included).
EEPROM size, flags) must match the device's own, and the error names both. 2. The resident installs the identical update image there. On the tinies the
Die revisions share their base signature and geometry, so their images are host composes the slot's last word — the same address as the resident's
interchangeable — as the silicon is. `loader_image()` also accepts a trampoline — as a jump to the resident base, so even an abandoned staging
padded image (a raw .bin padded from 0, or a whole-flash read-back with copy times out into a loader, never into garbage.
the loader resident) and peels it to the slot content by the embedded base.
1. The staging slot `[baseslot, base)` is saved to a host-side state file
(on the 1 KB tiny13s that is the whole application, vectors included).
2. The resident installs the identical update image there. On the
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,
so even an abandoned staging copy times out into a loader, never into
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 t85 the host first re-aims word 0 at the staging copy, so a power loss
first re-aims word 0 at the staging copy, so a power loss mid-rewrite mid-rewrite still resets into a loader; on the t13a the staging slot
still resets into a loader; on the tiny13s the staging slot carries the carries the reset vector itself.
reset vector itself.
4. `J` enters the new resident, which restores the staging slot's saved 4. `J` enters the new resident, which restores the staging slot's saved
content (word 0 and the trampoline with it) and the state file is content (word 0 and the trampoline with it) and the state file is
discarded. discarded.
@@ -296,10 +146,9 @@ Every phase is idempotent and keyed off the actual flash state: re-running
the same command after any interruption resumes and completes. The state the same command after any interruption resumes and completes. The state
file carries the only bytes not recoverable from the device; if it is lost file carries the only bytes not recoverable from the device; if it is lost
mid-update the update still completes, and the staging region is restored by mid-update the update still completes, and the staging region is restored by
reflashing the application. A boot-sectioned mega needs its fuses for the reflashing the application. The mega needs its fuses for the preflight
preflight (BOOTSZ gate, profile notes) — read from the device, or supplied (BOOTSZ gate, profile notes) — read from the device, or supplied with
with `--assume-fuses` where reading is impossible (simulators); the `--assume-fuses` where reading is impossible (simulators).
patched-vector chips need none.
## Host tool ## Host tool
@@ -319,56 +168,25 @@ update, flash (erase / program / read / verify), EEPROM (erase / program /
read / verify) — then the loader hands over to the application; `--stay` read / verify) — then the loader hands over to the application; `--stay`
keeps the session alive instead, and a later invocation reconnects into it keeps the session alive instead, and a later invocation reconnects into it
(the knock converges there too). `--flash` and `--eeprom` verify by (the knock converges there too). `--flash` and `--eeprom` verify by
read-back unless `--no-verify`, and a flash page that reads back wrong is read-back unless `--no-verify`; images are raw binary, or Intel HEX by
rewritten up to three times before the run stops — the loader leaves one extension. `--force` overrides the refusable safety checks (today: flashing
recoverable way for a page to land wrong (see `W` above), and rewriting is
what clears it. `--verify-flash` only reports. Images are raw binary, or
Intel HEX by extension. `--force` overrides the refusable safety checks (today: flashing
application data into a mega's reset walk region). application data into a mega's reset walk region).
Readouts come one fact per line: `--info` prints the decoded info block
field by field, the loader's version first; `--fuses` each fuse byte on its
own line — plus, on a boot-sectioned mega, the decoded meaning (where the
BOOTSZ section starts, what BOOTRST does to reset). Transfers that take
wire time — programming, reading, erasing, verifying, the update phases —
draw a transient progress bar on stderr when it is a tty; logs and pipes
see only the summary lines.
`-v`/`--verbose` adds the decisions as they happen: knock counts, the
programming plan (vector-surgery targets, skipped blank pages), update
state handling and per-phase page counts.
## Tests ## Tests
`tools/check.sh` runs every chip's workflow (`tools/check.sh --full` adds Per chip preset, `ctest` runs:
the reflect-mode builds of libavr's spot set; `tools/make_presets.py`
regenerates the presets). Per chip preset, `ctest` runs:
- `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget; - `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget;
- `pureboot_*.size` — the size matrix: the serial backends × the clock
ladder (1/8/16 MHz; the t13s' own RC menu), plus the USART1 build on the
x4 chips — every configuration axis that could move the image, each
variant against the same slot budget (pins are immediate operands and the
timeout is a constant: size-neutral);
- `pureboot.custom` (328P) — the configured-deployment acceptance test: the
1 MHz software-serial TX=PB1/RX=PB5 build from the configuration example
drives the full protocol suite through the runner's GPIO bridge, fixture
application included;
- `pureboot.usart1` (644A) — the same protocol suite over the second
hardware USART: instance selection is compile-checked everywhere, but
only a live session proves the loader polls the USART it claims;
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call` - `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`
in the image, the info block within its first 256 bytes; in the image, the info block within its first 256 bytes;
- `pureboot.planner` — the host tool's pure logic: programming orders and - `pureboot.planner` — the host tool's pure logic: programming orders and
their recovery properties, the surgery, the staging composition, the their recovery properties, the surgery, the staging composition, the
boot-fuse decode, the update preflight's error/warning matrix over boot-fuse decode, and the update preflight's error/warning matrix over
synthetic fuse bytes, and the repairing verify against a fake device — one synthetic fuse bytes;
bad write repaired in a single rewrite, a page that never comes good
stopping after exactly three;
- `pureboot.protocol` — end to end against a simavr device - `pureboot.protocol` — end to end against a simavr device
(`test/pureboot_device.c`a hardware USART as a pty, or a cycle-timed (`test/pureboot_device.c`the mega's USART as a pty; on the tinies a
GPIO⇄pty bridge for a software-UART build, selected with `-l` to match cycle-timed GPIO⇄pty bridge for the software UART, plus the SPM/NVM module
the loader's link; plus the SPM/NVM module simavr's tiny cores lack) simavr's tiny cores lack) driven by the real host tool through
driven by the real host tool through
knock-from-reset, program + verify of both memories, session reconnect, an knock-from-reset, program + verify of both memories, session reconnect, an
external reset through the patched vector, and the hand-over to a fixture external reset through the patched vector, and the hand-over to a fixture
application whose banner proves the launch — cross-checked against the application whose banner proves the launch — cross-checked against the
@@ -377,19 +195,11 @@ regenerates the presets). Per chip preset, `ctest` runs:
- `pureboot.reloc` — the identical image installed one slot below the - `pureboot.reloc` — the identical image installed one slot below the
resident serves the complete command set from there (the resident serves the complete command set from there (the
position-independence acceptance test); position-independence acceptance test);
- `pureboot.dirty` (328P) — entering the loader from a running application
with no reset between, over an SPM page buffer the fixture deliberately
dirtied: the case the loader declines to guard against. A bare verify must
see the corruption, the repairing verify must fix it in one rewrite, and a
plain verify afterwards must pass. On the boot-sectioned megas hardware
forbids the state outright (SPM runs only from the boot section, and reset
erases the buffer), but simavr dispatches SPM from anywhere — which is what
makes the path constructible at all;
- `pureboot.update` — the full `--update-loader` flow to a re-timed build, - `pureboot.update` — the full `--update-loader` flow to a re-timed build,
then every power-fail phase: the device is killed mid-write, restarted then every power-fail phase: the device is killed mid-write, restarted
from its flash dump, and a re-run must complete the update with the from its flash dump, and a re-run must complete the update with the
application intact throughout. application intact throughout.
`size`, `pi`, and `planner` are host logic and run anywhere; the `size`, `pi`, and `planner` are host logic and run anywhere; the three
simulator-driven targets need simavr and a pty, so they are POSIX-only — simulator-driven targets need simavr and a pty, so they are POSIX-only —
on Windows the tool is exercised against real hardware. on Windows the tool is exercised against real hardware.

View File

@@ -16,10 +16,9 @@
// resident — how pureboot updates itself, host-driven, with no other // resident — how pureboot updates itself, host-driven, with no other
// firmware involved. // firmware involved.
// //
// Entry: reset lands in avr::startup::entry below (BOOTRST on the // Entry: reset lands in avr::startup::entry below (BOOTRST on the mega; the
// boot-sectioned megas; the patched reset vector — or erased flash walking // patched reset vector — or erased flash walking up into the loader — on the
// up into the loader — on the tinies and the boot-section-less m48s). A // tinies). A watchdog reset hands straight to the application. Otherwise the
// watchdog reset hands straight to the application. Otherwise the
// host has one activation window per awaited knock byte ("pb"); an idle line // host has one activation window per awaited knock byte ("pb"); an idle line
// boots the application. A session then stays in the command loop until 'J' // boots the application. A session then stays in the command loop until 'J'
// jumps away or the chip resets. // jumps away or the chip resets.
@@ -38,51 +37,38 @@ constexpr auto off = avr::irq::guard_policy::unused;
constexpr std::uint8_t ack = '+'; constexpr std::uint8_t ack = '+';
// Per-deployment personality, passed in by the build — pureboot_add_loader() // Per-chip personality, from the chip database: the clocks the dogfood
// (the CMake function next to this file) resolves the defaults: the clock the // boards run (16 MHz crystal on the mega, calibrated RC on the tinies) and
// board actually runs, the wire baud, the serial backend and its pins. The // the device signature (compile-time data — the tiny13A cannot even read its
// device signature needs no configuring — it comes from the chip database // signature row from code).
// (avr::hw::db.signature), the only universal source, since the tiny13A consteval avr::hertz_t clock()
// cannot even read its signature row from code.
#if !defined(PUREBOOT_CLOCK_HZ) || !defined(PUREBOOT_BAUD)
#error \
"PUREBOOT_CLOCK_HZ and PUREBOOT_BAUD select this build's clock and baud — create loader targets with pureboot_add_loader() (README.md)"
#endif
using dev = avr::device<{.clock = avr::hertz_t{PUREBOOT_CLOCK_HZ}}>;
constexpr avr::baud_t wire_baud{PUREBOOT_BAUD};
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
consteval std::int16_t wdrf_field()
{ {
auto reg = std::string_view{avr::hw::db.regs[static_cast<std::size_t>(avr::power::detail::reset_reg())].name}; if (avr::hw::db.name == "ATtiny13A")
return avr::hw::db.field_index(reg, "WDRF"); return 9.6_MHz;
if (avr::hw::db.name == "ATtiny85")
return 8_MHz;
return 16_MHz;
} }
// Geometry: the resident loader owns the top slot of flash — 512 bytes, consteval std::array<std::uint8_t, 3> signature()
// except on the >64 KiB chips whose own smallest boot sector is 1 KiB (the {
// 1284s): there the slot is 1 KiB, matching the hardware boundary the if (avr::hw::db.name == "ATtiny13A")
// 512-byte figure comes from everywhere else. The word below the slot is return {0x1e, 0x90, 0x07};
// the trampoline (the application's relocated reset vector) on chips if (avr::hw::db.name == "ATtiny85")
// without a hardware boot section — the tinies and the m48s, whose SPM return {0x1e, 0x93, 0x0b};
// runs from anywhere (Atmel-8271 §26). A boot section also means the CPU return {0x1e, 0x95, 0x0f};
// runs on while the RWW section programs; everywhere else it halts through }
// the operation.
constexpr std::uint16_t slot_bytes = spm::flash_bytes > 65536 ? 1024 : 512;
constexpr std::uint32_t base = spm::flash_bytes - slot_bytes;
constexpr std::uint16_t page = spm::page_bytes;
constexpr bool boot_section = avr::hw::curated::has_boot_section();
// Past 64 KiB a byte address no longer fits the wire's 16 bits, so on the using dev = avr::device<{.clock = clock()}>;
// large chips every flash address on the wire — and all slot arithmetic —
// is a word address instead ('J' always was one). A slot spans the same // Geometry: the resident loader owns the top 512 bytes of flash; the word
// wire-high-byte pair in either unit (512 B = 2 x 256 bytes, 1 KiB = // below it is the trampoline (the application's relocated reset vector) on
// 2 x 256 words), so the slot index is the high byte with its low bit // chips without a hardware boot section. The RWWSRE bit marks a separate
// dropped everywhere. // boot section — on classic AVR the two capabilities coincide.
constexpr bool word_flash = spm::flash_bytes > 65536; constexpr std::uint16_t boot_bytes = 512;
constexpr std::uint16_t wire_base = constexpr std::uint16_t base = static_cast<std::uint16_t>(spm::flash_bytes - boot_bytes);
word_flash ? static_cast<std::uint16_t>(base / 2) : static_cast<std::uint16_t>(base); constexpr std::uint16_t page = spm::page_bytes;
constexpr std::uint16_t wire_page_mask = word_flash ? (page / 2 - 1) : (page - 1); constexpr bool boot_section = avr::hw::db.field_index("SPMCSR", "RWWSRE") >= 0;
// The activation window, in seconds, is a compile-time constant (the build // The activation window, in seconds, is a compile-time constant (the build
// may override it): the whole EEPROM belongs to the application, and // may override it): the whole EEPROM belongs to the application, and
@@ -92,61 +78,52 @@ constexpr std::uint16_t wire_page_mask = word_flash ? (page / 2 - 1) : (page - 1
#endif #endif
constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT; constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
// The pureboot version: the loader's one identity number, carried in the info // The 12-byte info block the host reads with the 'b' command; flash-resident
// block so a host can tell a deployed loader apart from another. The wire // (there is no crt to copy a .data image).
// protocol has no number of its own — a version implies its protocol, and the inline constexpr std::array<std::uint8_t, 12> info_data = {
// host tool is what holds that map (README.md).
constexpr std::uint8_t version = 2;
// The 12-byte info block the host reads with the 'b' command, flash-resident
// through flash_table (there is no crt to copy a .data image, and its storage
// carries the word alignment 'b' needs to halve the address on the large
// chips). The page byte is the wire count convention: 0 means 256.
inline constexpr avr::flash_table<std::array<std::uint8_t, 12>{
'P', 'P',
'B', 'B',
version, // magic, then the loader's version 1, // magic, protocol version
avr::hw::db.signature[0], signature()[0],
avr::hw::db.signature[1], signature()[1],
avr::hw::db.signature[2], signature()[2],
static_cast<std::uint8_t>(page), static_cast<std::uint8_t>(page),
wire_base & 0xff, base & 0xff,
wire_base >> 8, // app flash ends here; resident loader base (a word address on large chips) base >> 8, // app flash ends here; resident loader base
avr::hw::db.mem.eeprom_size & 0xff, avr::hw::db.mem.eeprom_size & 0xff,
avr::hw::db.mem.eeprom_size >> 8, avr::hw::db.mem.eeprom_size >> 8,
// bit 0: host must patch the reset vector (no hardware boot section); boot_section ? 0 : 1, // bit 0: host must patch the reset vector (no hardware boot section)
// bit 1: flash wire addresses are word addresses };
static_cast<std::uint8_t>((boot_section ? 0 : 1) | (word_flash ? 2 : 0)), using info = avr::flash_table<info_data>;
}>
info_data;
// The serial link. PUREBOOT_USART forces a hardware USART instance, // The serial link: the hardware USART where the chip has one, the polled
// PUREBOOT_SOFT_SERIAL the polled software UART (no vector — the table // software UART (no vector — the table belongs to the application) on PB0/PB1
// belongs to the application) on PUREBOOT_RX/PUREBOOT_TX; with neither, the // elsewhere. Both are class templates on the clock so only the selected
// chip's first USART where it has one and the software UART elsewhere. Both // backend is ever instantiated. pending() is the cheap line test the
// are class templates on the clock so only the selected backend is ever // activation window polls; rx() then picks the byte up; drain() holds until
// instantiated. pending() is the cheap line test the activation window // the last transmitted frame is fully on the wire (the jump hand-over must
// polls; rx() then picks the byte up; drain() holds until the last // not let the target's re-init clip the ack).
// transmitted frame is fully on the wire (the jump hand-over must not let template <avr::hertz_t C>
// the target's re-init clip the ack). consteval std::int16_t rxc_field()
#if defined(PUREBOOT_SOFT_SERIAL) && defined(PUREBOOT_USART) {
#error "PUREBOOT_SOFT_SERIAL and PUREBOOT_USART select opposing serial backends" return avr::hw::db.field_index("UCSR0A", "RXC0");
#endif }
#if !defined(PUREBOOT_RX)
#define PUREBOOT_RX pb0 template <avr::hertz_t C>
#endif consteval std::int16_t txc_field()
#if !defined(PUREBOOT_TX) {
#define PUREBOOT_TX pb1 return avr::hw::db.field_index("UCSR0A", "TXC0");
#endif }
#if defined(PUREBOOT_USART)
constexpr char usart_digit = '0' + PUREBOOT_USART; template <avr::hertz_t C>
#else consteval std::int16_t status_reg()
constexpr char usart_digit = '0'; {
#endif return avr::hw::db.reg_index("UCSR0A");
}
template <avr::hertz_t C> template <avr::hertz_t C>
struct hardware_link { struct hardware_link {
using uart = avr::uart::usart<usart_digit, C, {.baud = wire_baud, .max_baud_error = 2.5_pct}>; using uart = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>;
// The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2), // The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2),
// sbiw + sbci + sbci + brne (6). // sbiw + sbci + sbci + brne (6).
@@ -159,7 +136,7 @@ struct hardware_link {
static bool pending() static bool pending()
{ {
return uart::rx_ready(); return avr::hw::field_impl<rxc_field<C>()>::test();
} }
static std::uint8_t rx() static std::uint8_t rx()
@@ -174,14 +151,22 @@ struct hardware_link {
static void drain() static void drain()
{ {
uart::drain(); // write() leaves the byte draining behind it. Clear a stale TXC0
// first (W1C by writing the sampled status back — the store a hand
// assembler writes, keeping U2X0), then wait for the fresh
// completion; with a byte still ahead in the shifter TXC0 cannot
// re-set until the last pending byte has fully left.
using status = avr::hw::reg_impl<status_reg<C>()>;
status::write(status::read());
while (!avr::hw::field_impl<txc_field<C>()>::test()) {
}
} }
}; };
template <avr::hertz_t C> template <avr::hertz_t C>
struct software_link { struct software_link {
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, wire_baud>; using rx_t = avr::uart::software_rx_polled<C, avr::pb0, 57600_Bd>;
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, wire_baud>; using tx_t = avr::uart::software_tx<C, avr::pb1, 57600_Bd>;
// The compiled idle poll: sbis skipping the exit (2), sbiw + sbci + // The compiled idle poll: sbis skipping the exit (2), sbiw + sbci +
// sbci + brne (6). // sbci + brne (6).
@@ -194,7 +179,7 @@ struct software_link {
static bool pending() static bool pending()
{ {
return rx_t::start_pending(); return !avr::io::input<avr::pb0>::read(); // a start bit has begun
} }
static std::uint8_t rx() static std::uint8_t rx()
@@ -213,15 +198,7 @@ struct software_link {
} }
}; };
#if defined(PUREBOOT_USART) using link = std::conditional_t<avr::hw::db.has_reg("UDR0"), hardware_link<dev::clock>, software_link<dev::clock>>;
static_assert(avr::uart::has_usart<usart_digit>(), "PUREBOOT_USART selects a hardware USART this chip does not have");
using link = hardware_link<dev::clock>;
#elif defined(PUREBOOT_SOFT_SERIAL)
using link = software_link<dev::clock>;
#else
using link =
std::conditional_t<avr::uart::has_usart<usart_digit>(), hardware_link<dev::clock>, software_link<dev::clock>>;
#endif
// The application's entry, an absolute address the linker pins (--defsym in // The application's entry, an absolute address the linker pins (--defsym in
// CMakeLists.txt): 0x0000 on the mega (word 0 stays the application's own // CMakeLists.txt): 0x0000 on the mega (word 0 stays the application's own
@@ -270,53 +247,27 @@ std::uint8_t rx_deadline()
return link::rx(); return link::rx();
} }
// Inlined into its call sites: reading two bytes across a call otherwise std::uint16_t rx16()
// strands the first in a call-saved register the caller must push/pop; folded
// into the (noreturn) command loop that cost disappears.
[[gnu::always_inline]] inline std::uint16_t rx16()
{ {
std::uint16_t low = link::rx(); std::uint16_t low = link::rx();
return static_cast<std::uint16_t>(low | (link::rx() << 8)); return static_cast<std::uint16_t>(low | (link::rx() << 8));
} }
const std::uint8_t *flash_ptr(std::uint16_t address)
{
return reinterpret_cast<const std::uint8_t *>(address);
}
// The streamers take the count in the wire's 8-bit form: 0 means 256. // The streamers take the count in the wire's 8-bit form: 0 means 256.
// // send_flash stays out of line: its two callers ('b' and 'R') otherwise each
// Two functions, because they want opposite placement and placement is an // inline a private copy of the loop.
// attribute: the byte-addressed loop is small enough to inline into both [[gnu::noinline]] void send_flash(std::uint16_t address, std::uint8_t count)
// callers, the word-addressed one stays out of line but flattened — a call to
// the transmit inside it would strand the 24-bit cursor in callee-saved
// registers. `word_flash` picks at the call site.
[[maybe_unused, gnu::always_inline]] inline void send_flash_near(std::uint16_t address, std::uint8_t count)
{ {
do do
link::tx(avr::flash_load(reinterpret_cast<const std::uint8_t *>(address++))); link::tx(avr::flash_load(flash_ptr(address++)));
while (--count); while (--count);
} }
// The 24-bit cursor as the machine holds it: the RAMPZ byte and a 16-bit Z,
// carried explicitly (the reassembled 32-bit address folds away inside the
// inlined far load).
[[maybe_unused, gnu::flatten, gnu::noinline]] void send_flash_far(std::uint16_t address, std::uint8_t count)
{
std::uint8_t rampz = static_cast<std::uint8_t>(address >> 15);
std::uint16_t z = static_cast<std::uint16_t>(address << 1);
do {
link::tx(avr::flash_load_far<std::uint8_t>((static_cast<std::uint32_t>(rampz) << 16) | z));
// The protocol never reads across 64 KiB, but carrying the wrap is
// smaller than the flat 32-bit cursor GCC builds without it.
if (++z == 0)
++rampz;
} while (--count);
}
[[gnu::always_inline]] inline void send_flash(std::uint16_t address, std::uint8_t count)
{
if constexpr (word_flash)
send_flash_far(address, count);
else
send_flash_near(address, count);
}
void send_eeprom(std::uint16_t address, std::uint8_t count) void send_eeprom(std::uint16_t address, std::uint8_t count)
{ {
do do
@@ -342,43 +293,20 @@ void store_eeprom(std::uint16_t address, std::uint8_t count)
// itself. `slot_high` is the high byte of that running slot's base (run() // itself. `slot_high` is the high byte of that running slot's base (run()
// derives it); a broken host thus cannot brick the running loader, and a // derives it); a broken host thus cannot brick the running loader, and a
// copy flashed one slot lower may rewrite the slot above it — how pureboot // copy flashed one slot lower may rewrite the slot above it — how pureboot
// updates itself. // updates itself. On the mega the RWW section is re-enabled so reads work
void program_flash(std::uint16_t wire_address, std::uint8_t slot_high) // immediately.
void program_flash(std::uint16_t address, std::uint8_t slot_high)
{ {
// No discard before the fill: the buffer is write-once per word // A buffer word cannot be loaded twice without an erase (§26.2.1), so a
// (§26.2.1), so filling over one a refused page or an application left // refused page's drained data must not linger for the next write:
// dirty programs stale words — but a page write auto-erases the buffer // discard the buffer up front — CTPB on the tinies; on the mega writing
// (§26.2.1; §19.2 on the tinies), so that write clears the condition and // RWWSRE aborts a pending load (§26.2.2).
// the host's read-back rewrites the page. if constexpr (boot_section)
spm::rww_enable<off>();
// One induction either way. On the byte-addressed chips the wire address else
// itself walks the page (aligned, so the offset bits wrap to zero); on spm::clear_buffer<off>();
// the word-addressed large chips the wire word address becomes a 32-bit // The address is the loop's only state: pages are aligned, so the walk
// byte cursor once, and their 256-byte page makes its low byte the whole // ends when the offset bits wrap back to zero.
// in-page offset. The slot index is one high byte of the wire address —
// two values on byte-addressed chips (the & ~1), bits 16:9 re-packed on
// the large ones.
spm::flash_address_t address;
std::uint8_t page_high;
if constexpr (word_flash) {
// Pages are aligned, so one page never crosses a 64 KiB boundary:
// RAMPZ is a per-page constant and the fill cursor is a 16-bit Z
// whose low byte is the whole in-page offset (256-byte pages). The
// slot index is simply the wire word address's high byte.
const std::uint8_t rampz = static_cast<std::uint8_t>(wire_address >> 15);
const std::uint16_t z0 = static_cast<std::uint16_t>(wire_address << 1);
std::uint16_t z = z0;
do {
std::uint8_t low = link::rx();
std::uint8_t high = link::rx();
spm::fill<off>((static_cast<spm::flash_address_t>(rampz) << 16) | z,
static_cast<std::uint16_t>(low | (high << 8)));
z += 2;
} while (static_cast<std::uint8_t>(z));
address = (static_cast<spm::flash_address_t>(rampz) << 16) | z0;
page_high = static_cast<std::uint8_t>(wire_address >> 8) & 0xfe;
} else {
address = static_cast<spm::flash_address_t>(wire_address);
do { do {
std::uint8_t low = link::rx(); std::uint8_t low = link::rx();
std::uint8_t high = link::rx(); std::uint8_t high = link::rx();
@@ -386,24 +314,19 @@ void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
address += 2; address += 2;
} while (static_cast<std::uint8_t>(address) & (page - 1)); } while (static_cast<std::uint8_t>(address) & (page - 1));
address -= 2; // back inside the page — erase and write ignore the word bits address -= 2; // back inside the page — erase and write ignore the word bits
page_high = static_cast<std::uint8_t>(address >> 8) & 0xfe; const std::uint8_t page_high = static_cast<std::uint8_t>(address >> 8) & 0xfe;
}
if (page_high != slot_high) { if (page_high != slot_high) {
// The tinies and the m48s halt the CPU through the erase and the // The tinies halt the CPU through the erase and the write, so only
// write, so only the boot-sectioned megas — running on while their // the mega — running on while its RWW section programs — waits.
// RWW section programs — wait.
spm::erase_page<off>(address); spm::erase_page<off>(address);
if constexpr (boot_section) if constexpr (boot_section)
spm::wait(); spm::wait();
spm::write_page<off>(address); spm::write_page<off>(address);
if constexpr (boot_section) if constexpr (boot_section) {
spm::wait(); spm::wait();
}
// The megas program with their RWW section disabled; reads need it back
// on. The same store discards the buffer (§26.2.2), so they never meet
// the stale-word case above. boot_section implies an RWW section.
if constexpr (boot_section)
spm::rww_enable<off>(); spm::rww_enable<off>();
}
}
} }
// The four fuse/lock bytes in the hardware's own Z order: low, lock, // The four fuse/lock bytes in the hardware's own Z order: low, lock,
@@ -421,23 +344,18 @@ void send_fuses()
{ {
// A watchdog reset belongs to the application (whose watchdog stays // A watchdog reset belongs to the application (whose watchdog stays
// forced on until it clears WDRF) — no activation window in its way. // forced on until it clears WDRF) — no activation window in its way.
// The flag register is MCUSR, or the classic megas' MCUCSR. if (avr::hw::mcusr::wdrf.test())
if (avr::hw::field_impl<wdrf_field()>::test())
run_app(); run_app();
link::init(); link::init();
// The high byte of the 512-byte-aligned base this copy runs at: the // The high byte of the 512-byte-aligned base this copy runs at: the word
// return address is a word address, whose high byte is the 256-word slot // return address's high byte is the byte address >> 9 (the slot index),
// index — on byte-addressed chips doubled back into byte terms. // doubled back into address terms. program_flash refuses this one slot
// program_flash refuses this one slot and the info block is addressed // and the info block is addressed from it, so both follow wherever the
// from it, so both follow wherever the code was flashed. The high byte is // code was flashed.
// spelled as byteswap's low byte: the builtin's value is itself built by const std::uint8_t slot_high =
// swapping the two stacked bytes, and the double swap folds to the single static_cast<std::uint8_t>((reinterpret_cast<std::uint16_t>(__builtin_return_address(0)) >> 8) << 1);
// byte pick a hand assembler writes — `>> 8` leaves the swap materialized.
const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
const std::uint8_t ra_high = static_cast<std::uint8_t>(std::byteswap(ra_words));
const std::uint8_t slot_high = word_flash ? ra_high & 0xfe : static_cast<std::uint8_t>(ra_high << 1);
// The knock: 'p' then 'b', each under a fresh window; any other byte is // The knock: 'p' then 'b', each under a fresh window; any other byte is
// line noise and waits again. Falling out of a window runs the app. // line noise and waits again. Falling out of a window runs the app.
@@ -454,15 +372,11 @@ void send_fuses()
case 'b': { // info block, read relative to the running slot case 'b': { // info block, read relative to the running slot
// The block sits in the image's first 256 bytes (the build lint // The block sits in the image's first 256 bytes (the build lint
// asserts it), and slots are 512-aligned — so the low byte of its // asserts it), and slots are 512-aligned — so the low byte of its
// link address (in wire units: bytes, or words on the large // link address is its offset in any slot, and the high byte of
// chips) is its offset in any slot, and the high byte of its // its runtime address is the running slot's. Built as a byte
// runtime address is the running slot's. Composed from the two // pair so no absolute 16-bit address is ever materialized.
// bytes — the high half is runtime data, so no absolute address const std::uint8_t low = static_cast<std::uint8_t>(reinterpret_cast<std::uint16_t>(info::storage.data()));
// is ever materialized. send_flash(std::bit_cast<std::uint16_t>(std::array{low, slot_high}), info::size());
const auto link_low = reinterpret_cast<std::uint16_t>(info_data.storage.data());
const std::uint8_t low =
word_flash ? static_cast<std::uint8_t>(link_low >> 1) : static_cast<std::uint8_t>(link_low);
send_flash(static_cast<std::uint16_t>(low | (slot_high << 8)), static_cast<std::uint8_t>(info_data.size()));
break; break;
} }
case 'J': { // jump to a wire word address: hand-over and staging transfer case 'J': { // jump to a wire word address: hand-over and staging transfer

View File

@@ -35,68 +35,14 @@ else:
import termios import termios
PROMPT = b"+" PROMPT = b"+"
VERSION = 2 # this tool's own version — free to drift from a loader's PROTOCOL_VERSION = 1
# The loader versions this tool speaks to. A pureboot version implies its wire SLOT = 512 # the loader slot size; also the self-update staging distance
# protocol — the protocol carries no number of its own — so knowing which
# versions speak what is the tool's job, and this window is where it says so:
# every pureboot so far speaks this protocol, and a version that changes it
# becomes the new floor here.
OLDEST_LOADER = 1
NEWEST_LOADER = 2
SLOT = 512 # the loader slot on byte-addressed chips; word-addressed ones (>64 KiB) use 1 KiB — their own smallest boot sector
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
VERBOSE = False
def verbose(message):
"""Detail printed only under --verbose: decisions and derived facts, not
per-byte chatter — the progress bar carries the bulk transfers."""
if VERBOSE:
print(f" {message}")
class Error(Exception): class Error(Exception):
pass pass
class Progress:
"""A transient in-place bar on stderr for the operations that take wire
time. Drawn only when stderr is a tty — logs, pipes and the test harness
see nothing — and erased once done; the summary line each operation
prints afterwards is the persistent record. A zero total (or no label)
disables it, so callers can pass one through unconditionally."""
def __init__(self, label, total, unit="pages"):
self.label, self.total, self.unit = label, total, unit
self.done = 0
self.width = 0
self.live = bool(label) and total > 0 and sys.stderr.isatty()
self._draw()
def __enter__(self):
return self
def __exit__(self, *exc):
if self.live:
sys.stderr.write("\r" + " " * self.width + "\r")
sys.stderr.flush()
def step(self, n=1):
self.done += n
self._draw()
def _draw(self):
if not self.live:
return
bar = 24 * self.done // self.total
line = (f"{self.label:<16} [{'#' * bar}{'-' * (24 - bar)}] "
f"{100 * self.done // self.total:3d}% {self.done}/{self.total} {self.unit}")
self.width = max(self.width, len(line))
sys.stderr.write("\r" + line)
sys.stderr.flush()
# ---------------------------------------------------------------- serial --- # ---------------------------------------------------------------- serial ---
@@ -320,25 +266,16 @@ class Info:
def __init__(self, raw): def __init__(self, raw):
if len(raw) != 12 or raw[0:2] != b"PB": if len(raw) != 12 or raw[0:2] != b"PB":
raise Error(f"bad info block: {raw.hex()}") raise Error(f"bad info block: {raw.hex()}")
self.version = raw[2] if raw[2] != PROTOCOL_VERSION:
if not OLDEST_LOADER <= self.version <= NEWEST_LOADER: raise Error(f"protocol version {raw[2]}, tool speaks {PROTOCOL_VERSION}")
raise Error(
f"pureboot {self.version}: this tool (version {VERSION}) speaks pureboot "
f"{OLDEST_LOADER}..{NEWEST_LOADER} — a newer loader needs a newer tool"
)
self.raw = bytes(raw) self.raw = bytes(raw)
self.signature = raw[3:6] self.signature = raw[3:6]
self.page = raw[6] or 256 # the wire count convention: 0 means 256 self.page = raw[6]
self.patch_vector = bool(raw[11] & 1) self.base = raw[7] | (raw[8] << 8)
# Large chips speak word addresses for flash (bit 1); the host keeps
# every address in bytes and converts at the wire.
self.word_flash = bool(raw[11] & 2)
scale = 2 if self.word_flash else 1
self.base = (raw[7] | (raw[8] << 8)) * scale
self.eeprom_size = raw[9] | (raw[10] << 8) self.eeprom_size = raw[9] | (raw[10] << 8)
self.slot = 1024 if self.word_flash else SLOT self.patch_vector = bool(raw[11] & 1)
self.flash_size = self.base + self.slot self.flash_size = self.base + SLOT
self.stage = self.base - self.slot # where a staging copy of the loader goes 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 # The hand-over target, as the word address 'J' takes: the trampoline
# below the loader (tinies), or word 0 (mega — the application's own # below the loader (tinies), or word 0 (mega — the application's own
# reset vector; BOOTRST re-vectors a reset into the loader instead). # reset vector; BOOTRST re-vectors a reset into the loader instead).
@@ -353,24 +290,6 @@ class Info:
f"EEPROM {self.eeprom_size} B, {vector}" f"EEPROM {self.eeprom_size} B, {vector}"
) )
def lines(self):
"""The info block as one fact per line — what --info prints."""
if self.patch_vector:
hand_over = f"host-patched reset vector, trampoline at {self.base - 2:#06x}"
else:
hand_over = "hardware boot section, jump to word 0"
return (
f"version pureboot {self.version}",
f"signature {' '.join(f'{b:02x}' for b in self.signature)}",
f"flash {self.flash_size} B, {self.page} B pages"
+ (", word-addressed wire" if self.word_flash else ""),
f"application 0x0000..{self.base - 1:#06x} ({self.base} B)",
f"loader {self.base:#06x} ({self.slot} B slot)",
f"staging {self.stage:#06x}",
f"EEPROM {self.eeprom_size} B",
f"hand-over {hand_over}",
)
class Loader: class Loader:
"""A pureboot session. Between commands the loader has prompted `+` and """A pureboot session. Between commands the loader has prompted `+` and
@@ -388,10 +307,8 @@ class Loader:
absorbs whatever they produced.""" absorbs whatever they produced."""
self.port.flush_input() self.port.flush_input()
deadline = time.monotonic() + wait deadline = time.monotonic() + wait
knocks = 0
while True: while True:
self.port.write(b"pb") self.port.write(b"pb")
knocks += 1
if PROMPT in self.port.read_available(0.4): if PROMPT in self.port.read_available(0.4):
break break
if time.monotonic() > deadline: if time.monotonic() > deadline:
@@ -401,7 +318,6 @@ class Loader:
self.port.write(b"b") self.port.write(b"b")
self.info = Info(self.port.read_exact(12, 2.0)) self.info = Info(self.port.read_exact(12, 2.0))
self._expect_prompt() self._expect_prompt()
verbose(f"loader answered knock {knocks}; info block read")
return self.info return self.info
def _expect_prompt(self, timeout=2.0): def _expect_prompt(self, timeout=2.0):
@@ -415,44 +331,28 @@ class Loader:
self._expect_prompt(timeout) self._expect_prompt(timeout)
return reply return reply
def _stream_read(self, command, address, count, address_scale=1): def _stream_read(self, command, address, count):
data = b"" data = b""
while count: while count:
chunk = min(count, 256) chunk = min(count, 256)
wire = address // address_scale head = bytes((ord(command), address & 0xFF, address >> 8, chunk & 0xFF))
head = bytes((ord(command), wire & 0xFF, wire >> 8, chunk & 0xFF))
data += self._command(head, chunk, 5.0) data += self._command(head, chunk, 5.0)
address += chunk address += chunk
count -= chunk count -= chunk
return data return data
def read_flash(self, address, count): def read_flash(self, address, count):
if not self.info.word_flash:
return self._stream_read("R", address, count) return self._stream_read("R", address, count)
# Word-addressed wire: widen to even bounds and never let one read
# cross a 64 KiB boundary (the device holds RAMPZ for a whole run).
start = address & ~1
span = (address + count + 1 & ~1) - start
data = b""
at = start
remaining = span
while remaining:
chunk = min(remaining, 0x10000 - (at & 0xFFFF))
data += self._stream_read("R", at, chunk, address_scale=2)
at += chunk
remaining -= chunk
return data[address - start : address - start + count]
def read_eeprom(self, address, count): def read_eeprom(self, address, count):
return self._stream_read("r", address, count) return self._stream_read("r", address, count)
def write_page(self, address, data): def write_page(self, address, data):
assert len(data) == self.info.page and address % self.info.page == 0 assert len(data) == self.info.page and address % self.info.page == 0
wire = address // (2 if self.info.word_flash else 1) head = bytes((ord("W"), address & 0xFF, address >> 8))
head = bytes((ord("W"), wire & 0xFF, wire >> 8))
self._command(head + data, 0, 2.0) self._command(head + data, 0, 2.0)
def write_eeprom(self, address, data, progress=None): def write_eeprom(self, address, data):
offset = 0 offset = 0
while offset < len(data): while offset < len(data):
chunk = data[offset : offset + 256] chunk = data[offset : offset + 256]
@@ -461,8 +361,6 @@ class Loader:
for byte in chunk: for byte in chunk:
self.port.write(bytes((byte,))) self.port.write(bytes((byte,)))
self._expect_prompt() # per-byte ack: the write has begun self._expect_prompt() # per-byte ack: the write has begun
if progress:
progress.step()
self._expect_prompt() # the next command prompt self._expect_prompt() # the next command prompt
address += len(chunk) address += len(chunk)
offset += len(chunk) offset += len(chunk)
@@ -568,8 +466,6 @@ def plan_flash(image, info):
final += bytearray([0xFF] * (trampoline_page + page - len(final))) final += bytearray([0xFF] * (trampoline_page + page - len(final)))
jump = rjmp_to(trampoline_word, entry, flash_words) jump = rjmp_to(trampoline_word, entry, flash_words)
final[info.base - 2], final[info.base - 1] = jump & 0xFF, jump >> 8 final[info.base - 2], final[info.base - 1] = jump & 0xFF, jump >> 8
verbose(f"vector surgery: word 0 -> loader {info.base:#06x}, "
f"trampoline {info.base - 2:#06x} -> entry word {entry:#06x}")
pages = {a: bytes(final[a : a + page]) for a in range(0, len(final), page)} pages = {a: bytes(final[a : a + page]) for a in range(0, len(final), page)}
return pages return pages
@@ -599,66 +495,23 @@ def covered(pages, info, skip_blank):
# ----------------------------------------------------------------- fuses --- # ----------------------------------------------------------------- fuses ---
# Per-chip boot fuse geometry, keyed by the signature's family/part bytes: def mega_boot(high_fuse):
# which byte of the 'F' reply (low, lock, extended, high) carries BOOTSZ/ """Decode the ATmega328P high fuse's boot configuration (DS40002061B
# BOOTRST, and the BOOTSZ->words ladder. A die revision shares its base §27.3, Table 27-13/27-16): BOOTSZ1:0 in bits 2:1 select the boot-section
# signature, so one row covers it. The m48s have no boot section and no words, BOOTRST in bit 0 (programmed = 0) re-vectors reset to its start.
# row — their info block says patch-vector and this table is never Returns (bootrst_programmed, boot_section_start_byte)."""
# consulted. Sources: Atmel-2486/2466/2503 (HIGH fuse), Atmel-2545/8271/ bootsz = (high_fuse >> 1) & 0x03
# DS40002065 (x8: EXTENDED, except the m328s' HIGH), Atmel-8272/8011/2593/ words = {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}[bootsz]
# 42719 (x4: HIGH). return (high_fuse & 1) == 0, 0x8000 - words * 2
_LADDER_128 = {0b11: 128, 0b10: 256, 0b01: 512, 0b00: 1024}
_LADDER_256 = {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}
_LADDER_512 = {0b11: 512, 0b10: 1024, 0b01: 2048, 0b00: 4096}
BOOT_FUSE = {
bytes((0x93, 0x07)): (3, _LADDER_128), # m8/8A
bytes((0x94, 0x03)): (3, _LADDER_128), # m16/16A
bytes((0x95, 0x02)): (3, _LADDER_256), # m32/32A
bytes((0x93, 0x0A)): (2, _LADDER_128), # m88/88A
bytes((0x93, 0x0F)): (2, _LADDER_128), # m88P/88PA
bytes((0x94, 0x06)): (2, _LADDER_128), # m168/168A
bytes((0x94, 0x0B)): (2, _LADDER_128), # m168P/168PA
bytes((0x95, 0x14)): (3, _LADDER_256), # m328
bytes((0x95, 0x0F)): (3, _LADDER_256), # m328P
bytes((0x94, 0x0F)): (3, _LADDER_128), # m164A
bytes((0x94, 0x0A)): (3, _LADDER_128), # m164P/164PA
bytes((0x95, 0x15)): (3, _LADDER_256), # m324A
bytes((0x95, 0x08)): (3, _LADDER_256), # m324P
bytes((0x95, 0x11)): (3, _LADDER_256), # m324PA
bytes((0x96, 0x09)): (3, _LADDER_512), # m644/644A
bytes((0x96, 0x0A)): (3, _LADDER_512), # m644P/644PA
bytes((0x97, 0x06)): (3, _LADDER_512), # m1284
bytes((0x97, 0x05)): (3, _LADDER_512), # m1284P
}
def mega_boot(info, fuse_bytes):
"""Decode a mega's boot configuration from its fuses (the byte and the
BOOTSZ ladder are per chip): 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)."""
entry = BOOT_FUSE.get(bytes(info.signature[1:3]))
if entry is None:
raise Error(f"unknown mega signature {info.signature.hex()} — no boot fuse map")
which, ladder = entry
fuse = fuse_bytes[which]
words = ladder[(fuse >> 1) & 0x03]
return (fuse & 1) == 0, info.flash_size - words * 2
# ---------------------------------------------------------- loader update --- # ---------------------------------------------------------- loader update ---
def image_info(image): def image_info(image):
"""The info block embedded in a pureboot binary, or None. Searched per """The info block embedded in a pureboot binary, or None."""
known loader version, so the magic stays three selective bytes rather than at = image.find(b"PB" + bytes((PROTOCOL_VERSION,)))
two that code could carry by chance — and a binary this tool does not know return Info(image[at : at + 12]) if 0 <= at <= len(image) - 12 else None
the version of reads as no block at all, which is what it is to the tool."""
for version in range(OLDEST_LOADER, NEWEST_LOADER + 1):
at = image.find(b"PB" + bytes((version,)))
if 0 <= at <= len(image) - 12:
return Info(image[at : at + 12])
return None
def loader_image(path): def loader_image(path):
@@ -681,13 +534,12 @@ def staging_content(image, info):
that word, which for a staging copy is the slot's own last word: an rjmp 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 to the resident base. The staging copy's fall-through and 'J'-free exit
both land in a loader instead of garbage.""" both land in a loader instead of garbage."""
slot = info.slot if len(image) > (SLOT - 2 if info.patch_vector else SLOT):
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}")
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)))
content = bytearray(image) + bytearray([0xFF] * (slot - len(image)))
if info.patch_vector: if info.patch_vector:
through = rjmp_to((info.base - 2) // 2, info.base // 2, info.flash_size // 2) through = rjmp_to((info.base - 2) // 2, info.base // 2, info.flash_size // 2)
content[slot - 2], content[slot - 1] = through & 0xFF, through >> 8 content[SLOT - 2], content[SLOT - 1] = through & 0xFF, through >> 8
return bytes(content) return bytes(content)
@@ -695,10 +547,7 @@ def update_preflight(image, info, fuse_bytes):
"""Errors and warnings before any flash is touched. Returns warnings.""" """Errors and warnings before any flash is touched. Returns warnings."""
embedded = image_info(image) embedded = image_info(image)
if embedded is None: if embedded is None:
raise Error( raise Error("no pureboot info block in the update image — not a pureboot binary?")
"no pureboot info block in the update image — not a pureboot binary, "
f"or a version this tool ({VERSION}) does not know"
)
if embedded.raw[3:] != info.raw[3:]: if embedded.raw[3:] != info.raw[3:]:
raise Error( raise Error(
f"update image is for another target: it declares " f"update image is for another target: it declares "
@@ -708,13 +557,14 @@ def update_preflight(image, info, fuse_bytes):
if not info.patch_vector: if not info.patch_vector:
if fuse_bytes is None: if fuse_bytes is None:
raise Error("a loader update on this chip needs its fuses — unreadable? pass --assume-fuses") raise Error("a loader update on this chip needs its fuses — unreadable? pass --assume-fuses")
bootrst, bls_start = mega_boot(info, fuse_bytes) high = fuse_bytes[3]
bootrst, bls_start = mega_boot(high)
if info.stage < bls_start: if info.stage < bls_start:
raise Error( raise Error(
f"cannot self-update: the staging slot {info.stage:#06x} lies below the " f"cannot self-update: the staging slot {info.stage:#06x} lies below the "
f"boot section ({bls_start:#06x}) where SPM is disabled " f"boot section ({bls_start:#06x}, high fuse {high:#04x}) where SPM is disabled "
f"— a boot section of at least two slots ({2 * info.slot} B, BOOTSZ) is " f"— a boot section of at least 1 KB (BOOTSZ) is required, and only an "
f"required, and only an external programmer can change fuses" f"external programmer can change fuses"
) )
if not bootrst: if not bootrst:
warnings.append( warnings.append(
@@ -755,7 +605,7 @@ class UpdateState:
self.data = { self.data = {
"signature": info.signature.hex(), "signature": info.signature.hex(),
"base": info.base, "base": info.base,
"staging": loader.read_flash(info.stage, info.slot).hex(), "staging": loader.read_flash(info.stage, SLOT).hex(),
"page0": loader.read_flash(0, info.page).hex() if info.patch_vector else "", "page0": loader.read_flash(0, info.page).hex() if info.patch_vector else "",
} }
with open(self.path, "w") as f: with open(self.path, "w") as f:
@@ -773,42 +623,20 @@ class UpdateState:
os.unlink(self.path) os.unlink(self.path)
def write_differing(loader, base, content, order=None, label=None): def write_differing(loader, base, content, order=None):
"""Program the pages of `content` at `base` that differ from flash — """Program the pages of `content` at `base` that differ from flash —
idempotent, so a resumed phase redoes only what an interruption left. idempotent, so a resumed phase redoes only what an interruption left."""
A label puts the compare-and-program loop on the progress bar."""
page = loader.info.page page = loader.info.page
offsets = list(order) if order is not None else list(range(0, len(content), page)) offsets = order if order is not None else range(0, len(content), page)
written = 0 written = 0
with Progress(label, len(offsets)) as bar:
for offset in offsets: for offset in offsets:
want = content[offset : offset + page] want = content[offset : offset + page]
if loader.read_flash(base + offset, page) != want: if loader.read_flash(base + offset, page) != want:
loader.write_page(base + offset, want) loader.write_page(base + offset, want)
written += 1 written += 1
bar.step() for at in range(0, len(content), 256):
if label: if loader.read_flash(base + at, min(256, len(content) - at)) != content[at : at + 256]:
verbose(f"{label}: {written} of {len(offsets)} pages differed") raise Error(f"verify failed at {base + at:#06x} after programming")
# Page-wise read-back with the same bounded repair as verify_pages: this
# is the loader-update path, where a page left wrong is a half-written
# loader slot.
for retry in range(RETRIES + 1):
bad = [
offset
for offset in range(0, len(content), page)
if loader.read_flash(base + offset, len(content[offset : offset + page])) != content[offset : offset + page]
]
if not bad:
break
if retry == RETRIES:
raise Error(
f"verify failed at {base + bad[0]:#06x} after programming "
f"(still wrong after {RETRIES} retries)"
)
for offset in bad:
verbose(f"rewriting page {base + offset:#06x} (retry {retry + 1})")
loader.write_page(base + offset, content[offset : offset + page])
written += 1
return written return written
@@ -832,75 +660,47 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
image = loader_image(path) image = loader_image(path)
for warning in update_preflight(image, info, fuse_bytes): for warning in update_preflight(image, info, fuse_bytes):
print(f"note: {warning}") print(f"note: {warning}")
update = image_info(image) # the preflight proved it is there
verbose(f"installing pureboot {update.version} over pureboot {info.version}")
staged = staging_content(image, info) staged = staging_content(image, info)
resident = bytes(image) + bytes([0xFF] * (info.slot - len(image))) resident = bytes(image) + bytes([0xFF] * (SLOT - len(image)))
page = info.page page = info.page
state = UpdateState(state_path) state = UpdateState(state_path)
if os.path.exists(state_path):
verbose(f"resuming the update recorded in {state_path}")
else:
verbose(f"saving the staging slot to {state_path}")
state.load_or_save(loader) state.load_or_save(loader)
# Install the staging copy — unless a loader already sits whole in the # Install the staging copy. On a chip whose staging slot starts at
# staging slot (a build programmed there by hand): that copy IS the # address 0 (the 1 KB tiny13A), its first page carries the reset vector:
# installed staging copy, and rewriting it would only trip its own # written last, so any earlier interruption still resets into the old
# running-slot guard on the composed through-word. Any pureboot with # resident, and from then on resets enter the staging copy.
# the device's own info block serves — the staged copy just streams order = list(range(0, SLOT, page))
# 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("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))
if info.stage == 0: if info.stage == 0:
order = order[1:] + [0] order = order[1:] + [0]
if write_differing(loader, info.stage, staged, order, label="staging copy"): if write_differing(loader, info.stage, staged, order):
print(f"staging copy installed at {info.stage:#06x}") print(f"staging copy installed at {info.stage:#06x}")
# Enter it and let it rewrite the resident slot. Where a patched reset # 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 # 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 # 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. # rewrite, so a power failure mid-rewrite still resets into a loader.
verbose(f"entering the staging copy at {info.stage:#06x}")
loader.enter_copy(info.stage, wait) loader.enter_copy(info.stage, wait)
redirect = info.patch_vector and info.stage != 0 redirect = info.patch_vector and info.stage != 0
if redirect: if redirect:
verbose("word 0 re-aimed at the staging copy for the rewrite")
patch_word0(loader, state.page0, info.stage) patch_word0(loader, state.page0, info.stage)
if write_differing(loader, info.base, resident, label="resident"): if write_differing(loader, info.base, resident):
print(f"resident loader rewritten at {info.base:#06x}") print(f"resident loader rewritten at {info.base:#06x}")
# Enter the new resident and put the staging region back: page 0 first # 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 # 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. # for the rest of the restore), the saved trampoline with the rest.
verbose(f"entering the new resident at {info.base:#06x}")
loader.enter_copy(info.base, wait) loader.enter_copy(info.base, wait)
if redirect: if redirect:
verbose("word 0 restored")
write_differing(loader, 0, state.page0) write_differing(loader, 0, state.page0)
order = list(range(0, info.slot, page)) order = list(range(0, SLOT, page))
if info.stage == 0: if info.stage == 0:
order = [0] + order[1:] order = [0] + order[1:]
write_differing(loader, info.stage, state.staging, order, label="staging restore") write_differing(loader, info.stage, state.staging, order)
state.discard() state.discard()
print(f"loader updated: pureboot {update.version}, {len(image)} B at {info.base:#06x}, staging region restored") print(f"loader updated: {len(image)} B at {info.base:#06x}, staging region restored")
def check_walk_region(pages, info, fuse_bytes, force): def check_walk_region(pages, info, fuse_bytes, force):
@@ -910,7 +710,7 @@ def check_walk_region(pages, info, fuse_bytes, force):
Only checkable when the fuses are known (--fuses or --assume-fuses).""" Only checkable when the fuses are known (--fuses or --assume-fuses)."""
if info.patch_vector or fuse_bytes is None: if info.patch_vector or fuse_bytes is None:
return return
bootrst, bls_start = mega_boot(info, fuse_bytes) bootrst, bls_start = mega_boot(fuse_bytes[3])
if not bootrst or bls_start >= info.base: if not bootrst or bls_start >= info.base:
return return
overlap = [a for a in sorted(pages) if a >= bls_start and pages[a].count(0xFF) != len(pages[a])] overlap = [a for a in sorted(pages) if a >= bls_start and pages[a].count(0xFF) != len(pages[a])]
@@ -932,85 +732,48 @@ def op_erase_flash(loader):
is erased and the reset walk reaches the loader anyway.""" is erased and the reset walk reaches the loader anyway."""
blank = bytes([0xFF] * loader.info.page) blank = bytes([0xFF] * loader.info.page)
addresses = range(0, loader.info.base, loader.info.page) addresses = range(0, loader.info.base, loader.info.page)
with Progress("erase", len(addresses)) as bar:
for address in reversed(addresses) if loader.info.patch_vector else addresses: for address in reversed(addresses) if loader.info.patch_vector else addresses:
loader.write_page(address, blank) loader.write_page(address, blank)
bar.step()
print(f"erase: {loader.info.base // loader.info.page} pages") print(f"erase: {loader.info.base // loader.info.page} pages")
def op_erase_eeprom(loader): def op_erase_eeprom(loader):
with Progress("erase EEPROM", loader.info.eeprom_size, "B") as bar: loader.write_eeprom(0, bytes([0xFF] * loader.info.eeprom_size))
loader.write_eeprom(0, bytes([0xFF] * loader.info.eeprom_size), progress=bar)
print(f"erase: {loader.info.eeprom_size} B of EEPROM") print(f"erase: {loader.info.eeprom_size} B of EEPROM")
def op_flash(loader, path, erase, verify, fuse_bytes=None, force=False): def op_flash(loader, path, erase, verify, fuse_bytes=None, force=False):
image = load_image(path) image = load_image(path)
verbose(f"{path}: {len(image)} B image")
pages = plan_flash(image, loader.info) pages = plan_flash(image, loader.info)
check_walk_region(pages, loader.info, fuse_bytes, force) check_walk_region(pages, loader.info, fuse_bytes, force)
if erase: if erase:
op_erase_flash(loader) op_erase_flash(loader)
order = covered(pages, loader.info, skip_blank=erase) order = covered(pages, loader.info, skip_blank=erase)
if len(order) != len(pages):
verbose(f"{len(pages) - len(order)} blank pages skipped (erased flash underneath)")
with Progress("flash", len(order)) as bar:
for address in order: for address in order:
loader.write_page(address, pages[address]) loader.write_page(address, pages[address])
bar.step()
print(f"flash: {path}: {len(order)} pages") print(f"flash: {path}: {len(order)} pages")
if verify: if verify:
verify_pages(loader, pages, repair=True) verify_pages(loader, pages)
def verify_pages(loader, pages, repair=False): def verify_pages(loader, pages):
"""Read every page back and compare. With `repair`, a mismatched page is
rewritten and re-read, up to RETRIES times before it is raised: a page
filled over a dirty SPM buffer takes stale words, and the write that took
them cleared the buffer, so one rewrite settles it. Anything still wrong
after three is not that, and stops the run."""
repaired = 0
with Progress("verify", len(pages)) as bar:
for address in sorted(pages): for address in sorted(pages):
for retry in range(RETRIES + 1):
got = loader.read_flash(address, loader.info.page) got = loader.read_flash(address, loader.info.page)
if got == pages[address]: if got != pages[address]:
break
first = next(i for i in range(len(got)) if got[i] != pages[address][i]) first = next(i for i in range(len(got)) if got[i] != pages[address][i])
detail = ( raise Error(
f"verify failed at {address + first:#06x}: " f"verify failed at {address + first:#06x}: "
f"wrote {pages[address][first]:02x}, read {got[first]:02x}" f"wrote {pages[address][first]:02x}, read {got[first]:02x}"
) )
if not repair: print(f"verify: {len(pages)} pages ok")
raise Error(detail)
if retry == RETRIES:
raise Error(f"{detail} (still wrong after {RETRIES} retries)")
verbose(f"{detail} — rewriting page {address:#06x} (retry {retry + 1})")
loader.write_page(address, pages[address])
repaired += 1
bar.step()
note = f", {repaired} page rewrite(s)" if repaired else ""
print(f"verify: {len(pages)} pages ok{note}")
def op_verify_flash(loader, path): def op_verify_flash(loader, path):
verify_pages(loader, plan_flash(load_image(path), loader.info)) verify_pages(loader, plan_flash(load_image(path), loader.info))
def read_progress(reader, total, label):
"""A bulk read in 256-byte wire chunks under a progress bar."""
data = b""
with Progress(label, total, "B") as bar:
while len(data) < total:
chunk = min(256, total - len(data))
data += reader(len(data), chunk)
bar.step(chunk)
return data
def op_read_flash(loader, path): def op_read_flash(loader, path):
data = read_progress(loader.read_flash, loader.info.base, "read flash") data = loader.read_flash(0, loader.info.base)
open(path, "wb").write(data) open(path, "wb").write(data)
print(f"read flash: {len(data)} B -> {path}") print(f"read flash: {len(data)} B -> {path}")
@@ -1021,11 +784,10 @@ def op_eeprom(loader, path, erase, verify):
raise Error(f"EEPROM image is {len(image)} B, device has {loader.info.eeprom_size}") raise Error(f"EEPROM image is {len(image)} B, device has {loader.info.eeprom_size}")
if erase: if erase:
op_erase_eeprom(loader) op_erase_eeprom(loader)
with Progress("eeprom", len(image), "B") as bar: loader.write_eeprom(0, image)
loader.write_eeprom(0, image, progress=bar)
print(f"eeprom: {path}: {len(image)} B") print(f"eeprom: {path}: {len(image)} B")
if verify: if verify:
got = read_progress(loader.read_eeprom, len(image), "verify EEPROM") got = loader.read_eeprom(0, len(image))
if got != image: if got != image:
first = next(i for i in range(len(got)) if got[i] != image[i]) 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}") raise Error(f"verify failed at EEPROM {first:#06x}: wrote {image[first]:02x}, read {got[first]:02x}")
@@ -1034,7 +796,7 @@ def op_eeprom(loader, path, erase, verify):
def op_verify_eeprom(loader, path): def op_verify_eeprom(loader, path):
image = load_image(path) image = load_image(path)
got = read_progress(loader.read_eeprom, len(image), "verify EEPROM") got = loader.read_eeprom(0, len(image))
if got != image: if got != image:
first = next(i for i in range(len(got)) if got[i] != image[i]) 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}") raise Error(f"verify failed at EEPROM {first:#06x}: expected {image[first]:02x}, read {got[first]:02x}")
@@ -1042,30 +804,15 @@ def op_verify_eeprom(loader, path):
def op_read_eeprom(loader, path): def op_read_eeprom(loader, path):
data = read_progress(loader.read_eeprom, loader.info.eeprom_size, "read EEPROM") data = loader.read_eeprom(0, loader.info.eeprom_size)
open(path, "wb").write(data) open(path, "wb").write(data)
print(f"read EEPROM: {len(data)} B -> {path}") print(f"read EEPROM: {len(data)} B -> {path}")
def op_fuses(loader): def op_fuses(loader):
low, lock, extended, high = loader.read_fuses() low, lock, extended, high = loader.read_fuses()
print("fuses:") print(f"fuses: low {low:02x} high {high:02x} extended {extended:02x} lock {lock:02x}")
print(f" low 0x{low:02x}") return bytes((low, lock, extended, high))
print(f" high 0x{high:02x}")
print(f" extended 0x{extended:02x}")
print(f" lock 0x{lock:02x}")
fuse_bytes = bytes((low, lock, extended, high))
# On a boot-sectioned mega the BOOTSZ/BOOTRST decode is the fuse fact the
# loader's whole deployment hangs on — say it in words.
if not loader.info.patch_vector:
try:
bootrst, bls_start = mega_boot(loader.info, fuse_bytes)
reset = "reset enters it" if bootrst else "reset boots the application"
print(f" boot section at {bls_start:#06x} ({loader.info.flash_size - bls_start} B), "
f"BOOTRST {'programmed' if bootrst else 'unprogrammed'}{reset}")
except Error:
pass # unknown signature: the raw bytes above still stand
return fuse_bytes
# -------------------------------------------------------------------- cli --- # -------------------------------------------------------------------- cli ---
@@ -1075,8 +822,6 @@ def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="pureboot host tool", epilog="operations run in the order listed above" description="pureboot host tool", epilog="operations run in the order listed above"
) )
parser.add_argument("--version", action="version", version=f"%(prog)s {VERSION} "
f"(speaks pureboot {OLDEST_LOADER}..{NEWEST_LOADER})")
parser.add_argument("--port", required=True, help="serial device: COM6, /dev/ttyUSB0, or a simavr pty") parser.add_argument("--port", required=True, help="serial device: COM6, /dev/ttyUSB0, or a simavr pty")
parser.add_argument("--baud", type=int, default=115200, help="115200 mega, 57600 tinies") 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("--wait", type=float, default=30.0, help="seconds to keep knocking")
@@ -1097,11 +842,7 @@ def main():
parser.add_argument("--verify-eeprom", metavar="FILE", help="compare EEPROM against an image") 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("--force", action="store_true", help="override refusable safety checks")
parser.add_argument("--stay", action="store_true", help="leave the loader in its session") parser.add_argument("--stay", action="store_true", help="leave the loader in its session")
parser.add_argument("-v", "--verbose", action="store_true",
help="print decisions and derived facts as operations run")
args = parser.parse_args() args = parser.parse_args()
global VERBOSE
VERBOSE = args.verbose
if args.update_loader and (args.flash or args.erase_flash): if args.update_loader and (args.flash or args.erase_flash):
parser.error("--update-loader does not combine with application flash operations") parser.error("--update-loader does not combine with application flash operations")
@@ -1114,14 +855,11 @@ def main():
parser.error("--assume-fuses takes 8 hex digits: low,lock,extended,high") parser.error("--assume-fuses takes 8 hex digits: low,lock,extended,high")
port = Port(args.port, args.baud) port = Port(args.port, args.baud)
verbose(f"{args.port}: {args.baud} Bd 8N1, DTR/RTS asserted")
try: try:
loader = Loader(port) loader = Loader(port)
info = loader.connect(args.wait) info = loader.connect(args.wait)
if args.info: if args.info:
print("device:") print(f"device: {info.describe()}")
for line in info.lines():
print(f" {line}")
fuse_bytes = fuse_override fuse_bytes = fuse_override
if args.fuses or (args.update_loader and not info.patch_vector and fuse_bytes is None): if args.fuses or (args.update_loader and not info.patch_vector and fuse_bytes is None):
read = op_fuses(loader) read = op_fuses(loader)

View File

@@ -41,8 +41,7 @@ def main():
if len(info) != 1: if len(info) != 1:
print(f"FAIL: expected one info-block storage symbol, found {len(info)}") print(f"FAIL: expected one info-block storage symbol, found {len(info)}")
sys.exit(1) sys.exit(1)
address = int(info[0].split()[0], 16) offset = int(info[0].split()[0], 16) - text_start
offset = address - text_start
if not 0 <= offset < 256: if not 0 <= offset < 256:
print(f"FAIL: info block at image offset {offset:#x}, must sit in the first 256 bytes") print(f"FAIL: info block at image offset {offset:#x}, must sit in the first 256 bytes")
sys.exit(1) sys.exit(1)

View File

@@ -4,16 +4,11 @@
// surgery, actually launched it. Linked normally (crt, vectors at 0); on // surgery, actually launched it. Linked normally (crt, vectors at 0); on
// the tinies its reset vector is the rjmp the host re-homes. // the tinies its reset vector is the rjmp the host re-homes.
// //
// On the hardware-USART link it then listens, and an 'L' makes it jump into // On the mega it then listens, and an 'L' makes it jump into the resident
// the resident loader — the application-owned loader entry a // loader — the application-owned loader entry a BOOTRST-unprogrammed mega
// BOOTRST-unprogrammed mega relies on (reset always boots the application // relies on (reset always boots the application there), exercised by the
// there), exercised by the self-update tests. The software link idles: // self-update tests. The tinies idle: reset reaches their loader through
// reset reaches those loaders through the patched vector (or the runner // the patched vector, so the application owes it nothing.
// models BOOTRST), so the application owes them nothing.
//
// The fixture speaks the deployment its loader was built for: the same
// PUREBOOT_* defines configure it, and without them it assumes the stock
// deployment (the crystal/RC clock table below, the chip's natural link).
#include <libavr/libavr.hpp> #include <libavr/libavr.hpp>
using namespace avr::literals; using namespace avr::literals;
@@ -22,76 +17,33 @@ namespace {
consteval avr::hertz_t clock() consteval avr::hertz_t clock()
{ {
#if defined(PUREBOOT_CLOCK_HZ) if (avr::hw::db.name == "ATtiny13A")
return avr::hertz_t{PUREBOOT_CLOCK_HZ};
#else
auto name = std::string_view{avr::hw::db.name};
if (name.starts_with("ATtiny13"))
return 9.6_MHz; return 9.6_MHz;
if (name.starts_with("ATtiny")) if (avr::hw::db.name == "ATtiny85")
return 8_MHz; return 8_MHz;
return 16_MHz; return 16_MHz;
#endif
}
#if !defined(PUREBOOT_TX)
#define PUREBOOT_TX pb1
#endif
#if !defined(PUREBOOT_USART)
#define PUREBOOT_USART 0
#endif
consteval bool use_hardware()
{
#if defined(PUREBOOT_SOFT_SERIAL)
return false;
#else
return avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART");
#endif
} }
using dev = avr::device<{.clock = clock()}>; using dev = avr::device<{.clock = clock()}>;
template <avr::hertz_t C, bool Hardware = use_hardware()> template <avr::hertz_t C, bool Hardware = avr::hw::db.has_reg("UDR0")>
struct link { struct link {
#if defined(PUREBOOT_BAUD) using tx_t = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>;
static constexpr avr::baud_t baud{PUREBOOT_BAUD};
#else
static constexpr avr::baud_t baud{115200};
#endif
using tx_t = avr::uart::usart<'0' + PUREBOOT_USART, C, {.baud = baud, .max_baud_error = 2.5_pct}>;
static void tx(char c) static void tx(char c)
{ {
tx_t::write(static_cast<std::uint8_t>(c)); tx_t::write(static_cast<std::uint8_t>(c));
} }
[[noreturn]] static void idle() [[noreturn]] static void idle()
{ {
// 'L' hands back to the loader at the top slot — 512 bytes, or the for (;;)
// 1 KiB the >64 KiB chips use. if (tx_t::read_blocking() == 'L')
constexpr std::uint32_t slot = avr::hw::db.mem.flash_size > 65536 ? 1024 : 512; reinterpret_cast<void (*)()>((avr::hw::db.mem.flash_size - 512) / 2)();
for (;;) {
auto command = tx_t::read_blocking();
if (command == 'L')
reinterpret_cast<void (*)()>(static_cast<std::uint16_t>((avr::hw::db.mem.flash_size - slot) / 2))();
// 'D' leaves every word of the SPM page buffer dirty, so that a
// following 'L' enters the loader with the buffer it never clears.
if (command == 'D') {
for (std::uint16_t at = 0; at < avr::spm::page_bytes; at += 2)
avr::spm::fill(at, 0xdead);
tx('D');
}
}
} }
}; };
template <avr::hertz_t C> template <avr::hertz_t C>
struct link<C, false> { struct link<C, false> {
#if defined(PUREBOOT_BAUD) using tx_t = avr::uart::software_tx<C, avr::pb1, 57600_Bd>;
static constexpr avr::baud_t baud{PUREBOOT_BAUD};
#else
static constexpr avr::baud_t baud{57600};
#endif
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, baud>;
static void tx(char c) static void tx(char c)
{ {
tx_t::write(static_cast<std::uint8_t>(c)); tx_t::write(static_cast<std::uint8_t>(c));

View File

@@ -1,90 +0,0 @@
#!/usr/bin/env python3
"""Dirty-page-buffer acceptance test: the loader carries no buffer discard,
so a page filled over words an earlier writer left behind programs those
instead. This asserts the whole contract — the corruption is real and a bare
verify sees it, the repairing verify fixes it in one rewrite (the write that
took the stale words auto-erased the buffer), and it stays fixed.
The state is reached the way the loader cannot prevent: an application
dirties the buffer and jumps in with no reset between. Real boot-sectioned
megas forbid that outright — SPM executes only from the boot section
(Atmel-8271 §26.2) — but simavr dispatches SPM from anywhere, which is what
makes the path constructible at all.
Usage: pbdirty.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
<baud> <app_bin> <tool_py> <workdir>
"""
import os
import sys
def fail(message):
print(f"FAIL: {message}")
sys.exit(1)
def main():
device_bin, elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir = sys.argv[1:]
page, baud = 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)
dump = os.path.join(workdir, "dump.bin")
# Reset boots the application on a BOOTRST-unprogrammed mega; its 'L' is
# the loader entry this test needs, reached without a reset.
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, reset_hex="0")
try:
port = pb.Port(device.pty, baud)
loader = pb.Loader(port)
loader.connect(25)
# Install the application and hand over to it.
pb.op_flash(loader, app_bin, erase=False, verify=True)
loader.run_application()
if port.read_exact(3, 5.0) != b"APP":
fail("the application did not start")
port.write(b"D")
if port.read_exact(1, 5.0) != b"D":
fail("the application did not acknowledge dirtying the page buffer")
port.write(b"L")
loader = pb.Loader(port)
loader.connect(25)
# Program by hand, so the corruption is observable before anything
# repairs it.
pages = pb.plan_flash(open(app_bin, "rb").read(), loader.info)
for address in sorted(pages):
loader.write_page(address, pages[address])
try:
pb.verify_pages(loader, pages)
except pb.Error as error:
if "verify failed" not in str(error):
fail(f"the read-back failed, but not at verify: {error}")
else:
# Either the fixture no longer dirties the buffer, or the loader
# clears it again — in which case this test's premise is gone.
fail("programming over a dirty page buffer came back clean")
# What the programming path uses: one rewrite settles it, and it stays
# settled.
pb.verify_pages(loader, pages, repair=True)
pb.verify_pages(loader, pages)
# Ground truth beyond the loader's own read-back.
loader.run_application()
if port.read_exact(3, 5.0) != b"APP":
fail("the application did not start after the recovered write")
port.close()
finally:
device.stop()
print("pbdirty: a dirty page buffer is caught by verify and cleared by the retry")
if __name__ == "__main__":
main()

View File

@@ -1,102 +0,0 @@
#!/usr/bin/env python3
"""Re-homing acceptance test: a pureboot image programmed somewhere other
than its canonical top slot must still be a working loader —
position-independent, guarding its accidental slot — and the ordinary
--update-loader flow must put a build into the top slot from there.
Two positions are exercised. Address 0 (a raw .bin handed to a programmer,
which defaults to offset 0): the staging install and the word-0 redirect
both run from copies whose slots are not page 0's, so the running-slot
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>
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
"""
import os
import sys
def fail(message):
print(f"FAIL: {message}")
sys.exit(1)
def rehome_from(pbsim, pb, device_bin, elf, place_hex, guard_probe, update_bin, base, page, baud, app_bin, workdir,
mcu, hz):
"""Place the loader at `place_hex`, heal through --update-loader, flash
the application, expect the banner."""
dump = os.path.join(workdir, f"dump-{place_hex}.bin")
state = os.path.join(workdir, f"rehome-{place_hex}.pbstate")
if os.path.exists(state):
os.unlink(state)
device = pbsim.Device(device_bin, elf, mcu, hz, place_hex, page, baud, dump, reset_hex="0")
try:
port = pb.Port(device.pty, baud)
loader = pb.Loader(port)
info = loader.connect(25)
if info.base != base:
fail(f"the misplaced copy reports base {info.base:#06x} — the info block must stay canonical")
# The accidental slot still guards itself; re-homing rides on the
# canonical slots being writable from it.
probe = int(guard_probe, 0)
before = loader.read_flash(probe, info.page)
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")
# The ordinary update flow puts the build into the top slot.
pb.op_update_loader(loader, 25, update_bin, state, None)
update = open(update_bin, "rb").read()
if loader.read_flash(base, len(update)) != update:
fail("the canonical slot does not hold the update image")
# An application flashed through the healed resident overwrites the
# stale copy (surgery included) and launches.
pages = pb.plan_flash(open(app_bin, "rb").read(), loader.info)
for address in pb.covered(pages, loader.info, skip_blank=False):
loader.write_page(address, pages[address])
pb.verify_pages(loader, pages)
loader.run_application()
if port.read_exact(3, 5.0) != b"APP":
fail(f"application does not banner after the re-home from {place_hex}")
port.close()
finally:
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")
if __name__ == "__main__":
main()

View File

@@ -24,7 +24,7 @@ def fail(message):
def main(): def main():
device_bin, elf, mcu, hz, base_hex, page, baud, tool, workdir = sys.argv[1:] device_bin, elf, mcu, hz, base_hex, page, baud, tool, workdir = sys.argv[1:]
base, page, baud = int(base_hex, 0), int(page), int(baud) base, page, baud = int(base_hex, 0), int(page), int(baud)
stage = None # derived from the device's own info (slot-sized) below stage = base - 512
sys.path.insert(0, os.path.dirname(os.path.abspath(tool))) sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pbsim import pbsim
@@ -46,7 +46,6 @@ def main():
resident_info = info.raw resident_info = info.raw
# Install the staging copy exactly as the update flow would. # Install the staging copy exactly as the update flow would.
stage = info.stage
staged = pb.staging_content(image, info) staged = pb.staging_content(image, info)
pb.write_differing(loader, stage, staged) pb.write_differing(loader, stage, staged)
@@ -66,10 +65,8 @@ def main():
if loader.read_eeprom(0, len(pattern)) != pattern: if loader.read_eeprom(0, len(pattern)) != pattern:
fail("EEPROM round-trip through the staged copy") fail("EEPROM round-trip through the staged copy")
# The guard, both ways: its own slot refused (drained, unchanged), the # The guard, both ways: its own slot refused (drained, unchanged),
# resident slot writable. The refusal leaves its drained words in the # the resident slot writable.
# SPM buffer, so the write that follows may take them — and clears
# them by writing, so the retry must not.
before = loader.read_flash(stage, page) before = loader.read_flash(stage, page)
loader.write_page(stage, bytes(page)) loader.write_page(stage, bytes(page))
if loader.read_flash(stage, page) != before: if loader.read_flash(stage, page) != before:
@@ -77,13 +74,11 @@ def main():
marker = bytes((i * 3) & 0xFF for i in range(page)) marker = bytes((i * 3) & 0xFF for i in range(page))
loader.write_page(base, marker) loader.write_page(base, marker)
if loader.read_flash(base, page) != marker: if loader.read_flash(base, page) != marker:
loader.write_page(base, marker) fail("the staged copy could not write the resident slot")
if loader.read_flash(base, page) != marker:
fail("the staged copy could not write the resident slot, even on retry")
# Restore the resident image through the staged copy, then 'J' back # Restore the resident image through the staged copy, then 'J' back
# into it and prove it lives. # into it and prove it lives.
resident = image + b"\xff" * (info.slot - len(image)) resident = image + b"\xff" * (512 - len(image))
pb.write_differing(loader, base, resident) pb.write_differing(loader, base, resident)
back_info = loader.enter_copy(base, 25) back_info = loader.enter_copy(base, 25)
if back_info.raw != resident_info: if back_info.raw != resident_info:

View File

@@ -8,17 +8,10 @@ import subprocess
class Device: class Device:
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None, link=None): def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None):
cmd = [binary] cmd = [binary, elf, mcu, hz, base_hex, str(page), str(baud), dump]
if link:
cmd += ["-l", link]
cmd += [elf, mcu, hz, base_hex, str(page), str(baud), dump]
if reset_hex is not None or resume is not None: if reset_hex is not None or resume is not None:
# Chips without a hardware boot section — the tinies and the cmd.append(reset_hex if reset_hex is not None else ("0" if mcu != "atmega328p" else base_hex))
# m48s — reset to address 0 like silicon; the boot-sectioned
# megas re-vector to the loader base (BOOTRST).
patch = not mcu.startswith("atmega") or mcu.startswith("atmega48")
cmd.append(reset_hex if reset_hex is not None else ("0" if patch else base_hex))
if resume is not None: if resume is not None:
cmd.append(resume) cmd.append(resume)
self.log = open(dump + ".log", "a") self.log = open(dump + ".log", "a")

View File

@@ -5,15 +5,15 @@ through flash + EEPROM + fuse + hand-over scenarios, and cross-check
the tool's view against the simulator's ground-truth memory dumps. the tool's view against the simulator's ground-truth memory dumps.
Usage: pbtest.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page> Usage: pbtest.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
<baud> <eeprom_size> <app_bin> <tool_py> <workdir> [link] <baud> <eeprom_size> <app_bin> <tool_py> <workdir>
The optional link is the runner's -l spec (usart1, sw:B5,B1, ...) for a
loader built off the chip's natural serial default.
Exits 0 if every scenario passes. Exits 0 if every scenario passes.
""" """
import os import os
import signal
import subprocess
import sys import sys
import time
def fail(message): def fail(message):
@@ -33,14 +33,53 @@ def rjmp_decode(word, at, flash_words):
return (at + 1 + offset) % flash_words return (at + 1 + offset) % flash_words
class Device:
def __init__(self, binary, elf, mcu, hz, base, page, baud, dump):
self.proc = subprocess.Popen(
[binary, elf, mcu, hz, base, str(page), str(baud), dump],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
self.dump = dump
self.pty = None
deadline = time.time() + 5
while time.time() < deadline:
line = self.proc.stdout.readline()
if not line:
break
if line.startswith("PB_PTY"):
self.pty = line.split()[1]
break
if not self.pty:
self.stop()
raise RuntimeError("device did not report a pty")
def stop(self):
self.proc.terminate()
try:
self.proc.wait(timeout=3)
except subprocess.TimeoutExpired:
self.proc.kill()
def run_tool(tool, pty, baud, *args):
result = subprocess.run(
[sys.executable, tool, "--port", pty, "--baud", str(baud), "--wait", "20", *args],
capture_output=True,
text=True,
timeout=120,
)
print(result.stdout, end="")
if result.returncode != 0:
fail(f"tool exited {result.returncode}: {result.stderr.strip()}")
return result.stdout
def main(): def main():
args = sys.argv[1:] (device_bin, elf, mcu, hz, base_hex, page, baud, eeprom_size, app_bin, tool, workdir) = sys.argv[1:]
link = args.pop() if len(args) == 12 else None
(device_bin, elf, mcu, hz, base_hex, page, baud, eeprom_size, app_bin, tool, workdir) = args
base, page, baud, eeprom_size = int(base_hex, 0), int(page), int(baud), int(eeprom_size) base, page, baud, eeprom_size = int(base_hex, 0), int(page), int(baud), int(eeprom_size)
sys.path.insert(0, os.path.dirname(os.path.abspath(tool))) 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 import pureboot as pb
os.makedirs(workdir, exist_ok=True) os.makedirs(workdir, exist_ok=True)
@@ -51,33 +90,25 @@ def main():
read_flash = os.path.join(workdir, "readback_flash.bin") read_flash = os.path.join(workdir, "readback_flash.bin")
read_eeprom = os.path.join(workdir, "readback_eeprom.bin") read_eeprom = os.path.join(workdir, "readback_eeprom.bin")
# The geometry the host will discover, for computing the expected image: # The geometry the host will discover, for computing the expected image.
# the boot-sectioned megas need no vector surgery (the tinies and the
# boot-section-less m48s do), the large chips speak word addresses, and
# the page byte is the wire's 0-means-256.
mega = mcu.startswith("atmega")
patch = not mega or mcu.startswith("atmega48")
word_flash = base + 512 > 0x10000
wire_base = base // 2 if word_flash else base
flags = (1 if patch else 0) | (2 if word_flash else 0)
info = pb.Info( info = pb.Info(
bytes([ord("P"), ord("B"), pb.NEWEST_LOADER, 0, 0, 0, page & 0xFF]) bytes([ord("P"), ord("B"), 1, 0, 0, 0, page])
+ bytes([wire_base & 0xFF, wire_base >> 8, eeprom_size & 0xFF, eeprom_size >> 8]) + bytes([base & 0xFF, base >> 8, eeprom_size & 0xFF, eeprom_size >> 8])
+ bytes([flags]) + bytes([0 if mcu == "atmega328p" else 1])
) )
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, link=link) device = Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump)
try: try:
# Session 1: knock from reset, identify, program everything, stay. # Session 1: knock from reset, identify, program everything, stay.
out = pbsim.run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin, out = run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin,
"--eeprom", ee_path, "--stay") "--eeprom", ee_path, "--stay")
for needed in ("version", "signature", "fuses", "verify:", "stays"): for needed in ("device: signature", "fuses:", "verify:", "stays"):
if needed not in out: if needed not in out:
fail(f"session 1 output lacks {needed!r}") fail(f"session 1 output lacks {needed!r}")
# Session 2: reconnect into the live session, verify, dump, hand over # Session 2: reconnect into the live session, verify, dump, hand over
# is deferred — the pty must be reopened for the APP banner first. # is deferred — the pty must be reopened for the APP banner first.
out = pbsim.run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path, out = run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path,
"--read-flash", read_flash, "--read-eeprom", read_eeprom, "--stay") "--read-flash", read_flash, "--read-eeprom", read_eeprom, "--stay")
if out.count("verify:") != 2: if out.count("verify:") != 2:
fail("session 2 did not verify both memories") fail("session 2 did not verify both memories")
@@ -97,16 +128,11 @@ def main():
# runner resets them to address 0 like silicon) or BOOTRST (mega). # runner resets them to address 0 like silicon) or BOOTRST (mega).
# The loader must answer a fresh knock, and the 'J' hand-over must # The loader must answer a fresh knock, and the 'J' hand-over must
# land in the application, which banners on the same link. # land in the application, which banners on the same link.
device.reset() device.proc.send_signal(signal.SIGUSR1)
port = pb.Port(device.pty, baud) port = pb.Port(device.pty, baud)
try: try:
loader = pb.Loader(port) loader = pb.Loader(port)
live = loader.connect(15) loader.connect(15)
# The loader built from this tree and the tool beside it must
# agree on where the version numbering stands: a bump the tool
# was never told about is a loader it would refuse to speak to.
if live.version != pb.NEWEST_LOADER:
fail(f"loader reports pureboot {live.version}, the tool's newest is {pb.NEWEST_LOADER}")
loader.run_application() loader.run_application()
banner = port.read_exact(3, 5.0) banner = port.read_exact(3, 5.0)
if banner != b"APP": if banner != b"APP":
@@ -124,9 +150,8 @@ def main():
fail("loader region looks erased in the ground-truth dump") fail("loader region looks erased in the ground-truth dump")
# The surgery, decoded independently: the patched vector must land on the # The surgery, decoded independently: the patched vector must land on the
# loader, the trampoline on the application's own entry (patched-vector # loader, the trampoline on the application's own entry.
# chips only — a boot-sectioned mega's word 0 stays the application's). if mcu != "atmega328p":
if patch:
flash_words = (base + 512) // 2 flash_words = (base + 512) // 2
app = open(app_bin, "rb").read() app = open(app_bin, "rb").read()
word0 = flash_true[0] | (flash_true[1] << 8) word0 = flash_true[0] | (flash_true[1] << 8)

View File

@@ -5,13 +5,12 @@ replaces itself with a re-timed build through the host tool's
killing the simulated device mid-write, restarting it from its flash dump, killing the simulated device mid-write, restarting it from its flash dump,
and letting a re-run complete the update. and letting a re-run complete the update.
The boot-sectioned megas run the BOOTRST-unprogrammed profile (reset boots The mega runs the BOOTRST-unprogrammed profile (reset boots the application;
the application; the fixture application's 'L' jump is the application-owned the fixture application's 'L' jump is the application-owned loader entry),
loader entry), with --assume-fuses standing in for the fuse read simavr with --assume-fuses standing in for the fuse read simavr cannot model. The
cannot model. The patched-vector chips — the tinies and the m48s — reset tinies reset into a loader at every phase by construction — the t13a because
into a loader at every phase by construction: the t13a because its staging its staging slot carries the reset vector itself, the t85 through the word-0
slot carries the reset vector itself, the others through the word-0 redirect redirect the tool plants around the resident rewrite.
the tool plants around the resident rewrite.
Usage: pbupdate.py <device_bin> <pureboot_elf> <update_elf> <mcu> <hz> Usage: pbupdate.py <device_bin> <pureboot_elf> <update_elf> <mcu> <hz>
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir> <base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
@@ -42,21 +41,10 @@ class PowerFail(Exception):
pass pass
def assumed_fuses(pb, image): MEGA_FUSES = "ffffffdd" # high 0xdd: BOOTSZ = 1 KB, BOOTRST unprogrammed
"""Synthetic 'F' bytes for --assume-fuses: the smallest boot section
covering both the resident and the staging slot (two slots — what a
self-update needs), BOOTRST unprogrammed — the per-chip BOOTSZ ladder
and fuse byte come from the tool's own table, keyed by the update
image's embedded signature."""
info = pb.image_info(image)
which, ladder = pb.BOOT_FUSE[bytes(info.signature[1:3])]
bits = min((b for b in ladder if ladder[b] * 2 >= 2 * info.slot), key=lambda b: ladder[b])
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
fuses[which] = 0xF8 | (bits << 1) | 1
return bytes(fuses)
def make_fault_loader(pb, base, slot, kill_region, kill_hits, device): def make_fault_loader(pb, base, kill_region, kill_hits, device):
"""A Loader whose write_page kills the device (or, with device=None, """A Loader whose write_page kills the device (or, with device=None,
just the host) at the Nth write into a region; the sequence just the host) at the Nth write into a region; the sequence
stage->resident->stage distinguishes the install from the restore.""" stage->resident->stage distinguishes the install from the restore."""
@@ -71,7 +59,7 @@ def make_fault_loader(pb, base, slot, kill_region, kill_hits, device):
if address >= base: if address >= base:
phase = "resident" phase = "resident"
self.seen_resident = True self.seen_resident = True
elif address >= base - slot: elif address >= base - 512:
phase = "stage_restore" if self.seen_resident else "stage" phase = "stage_restore" if self.seen_resident else "stage"
else: else:
phase = "app" phase = "app"
@@ -89,15 +77,8 @@ def make_fault_loader(pb, base, slot, kill_region, kill_hits, device):
def main(): def main():
(device_bin, elf, update_elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:] (device_bin, elf, update_elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:]
base, page, baud = int(base_hex, 0), int(page), int(baud) base, page, baud = int(base_hex, 0), int(page), int(baud)
mega = mcu.startswith("atmega") mega = mcu == "atmega328p"
# The m48s are megas without a boot section: patched vector, no fuse reset_hex = "0" if mega else None # the mega runs BOOTRST-unprogrammed here
# preflight, and the same reset-to-0 the tinies get.
patch = not mega or mcu.startswith("atmega48")
# Word-addressed (>64 KiB) chips use the 1 KiB slot; their loader base
# itself sits beyond the 16-bit byte space — the 644's base + slot only
# touches the 64 KiB boundary and stays byte-addressed.
slot = 1024 if base >= 0x10000 and mega else 512
reset_hex = "0" if mega else None # the boot-sectioned mega runs BOOTRST-unprogrammed here
sys.path.insert(0, os.path.dirname(os.path.abspath(tool))) sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pbsim import pbsim
@@ -114,7 +95,7 @@ def main():
fail("the update image is byte-identical to the resident build") fail("the update image is byte-identical to the resident build")
dump = os.path.join(workdir, "dump.bin") dump = os.path.join(workdir, "dump.bin")
state = os.path.join(workdir, "update.pbstate") state = os.path.join(workdir, "update.pbstate")
fuses = assumed_fuses(pb, images["v0"]) if mega and not patch else None fuses = bytes.fromhex(MEGA_FUSES) if mega else None
def connect(device): def connect(device):
port = pb.Port(device.pty, baud) port = pb.Port(device.pty, baud)
@@ -128,16 +109,16 @@ def main():
return port, loader return port, loader
def padded(image): def padded(image):
return image + b"\xff" * (slot - len(image)) return image + b"\xff" * (512 - len(image))
def resident_bytes(loader): def resident_bytes(loader):
return loader.read_flash(base, slot) return loader.read_flash(base, 256) + loader.read_flash(base + 256, 256)
def assert_state(loader, image, app_pages): def assert_state(loader, image, app_pages):
if resident_bytes(loader) != padded(image): if resident_bytes(loader) != padded(image):
fail("resident loader does not match the update image") fail("resident loader does not match the update image")
stage = base - slot stage = base - 512
got = loader.read_flash(stage, slot) got = loader.read_flash(stage, 256) + loader.read_flash(stage + 256, 256)
for address, data in app_pages.items(): for address, data in app_pages.items():
if stage <= address < base: if stage <= address < base:
if got[address - stage : address - stage + page] != data: if got[address - stage : address - stage + page] != data:
@@ -154,8 +135,8 @@ def main():
# A clean CLI update, resident -> v9. # A clean CLI update, resident -> v9.
args = ["--update-loader", os.path.join(workdir, "v9.bin"), "--state", state, "--stay"] args = ["--update-loader", os.path.join(workdir, "v9.bin"), "--state", state, "--stay"]
if fuses: if mega:
args += ["--assume-fuses", fuses.hex()] args += ["--assume-fuses", MEGA_FUSES]
out = pbsim.run_tool(tool, device.pty, baud, *args) out = pbsim.run_tool(tool, device.pty, baud, *args)
if "loader updated" not in out: if "loader updated" not in out:
fail("update did not report success") fail("update did not report success")
@@ -179,14 +160,14 @@ def main():
# cost of that profile (README). # cost of that profile (README).
for kill_region, kill_hits, kill_device in ( for kill_region, kill_hits, kill_device in (
("stage", 2, True), ("stage", 2, True),
("resident", 1, patch), ("resident", 1, not mega),
("stage_restore", 2, True), ("stage_restore", 2, True),
): ):
device.reset() # the previous round left the application running device.reset() # the previous round left the application running
port, loader = connect(device) port, loader = connect(device)
target = "v9" if resident_bytes(loader) == padded(images["v0"]) else "v0" target = "v9" if resident_bytes(loader) == padded(images["v0"]) else "v0"
image_path = os.path.join(workdir, target + ".bin") image_path = os.path.join(workdir, target + ".bin")
injected = make_fault_loader(pb, base, slot, kill_region, kill_hits, device if kill_device else None)(port) injected = make_fault_loader(pb, base, kill_region, kill_hits, device if kill_device else None)(port)
injected.info = loader.info injected.info = loader.info
try: try:
pb.op_update_loader(injected, 25, image_path, state, fuses) pb.op_update_loader(injected, 25, image_path, state, fuses)
@@ -210,12 +191,12 @@ def main():
device.stop() device.stop()
# Ground truth: the simulator's own flash against the final state, and # Ground truth: the simulator's own flash against the final state, and
# on the patched-vector chips an independent decode of the reset routing. # on the tinies an independent decode of the reset routing.
flash = open(dump, "rb").read() flash = open(dump, "rb").read()
if flash[base : base + slot] != padded(images[final]): if flash[base : base + 512] != padded(images[final]):
fail("ground-truth resident region does not match the final image") fail("ground-truth resident region does not match the final image")
if patch: if not mega:
flash_words = (base + slot) // 2 flash_words = (base + 512) // 2
word0 = flash[0] | (flash[1] << 8) word0 = flash[0] | (flash[1] << 8)
if rjmp_decode(word0, 0, flash_words) != base // 2: if rjmp_decode(word0, 0, flash_words) != base // 2:
fail("ground-truth reset vector does not land on the loader") fail("ground-truth reset vector does not land on the loader")

View File

@@ -1,16 +1,12 @@
// simavr "device" for the pureboot protocol tests, every chip. Loads the // simavr "device" for the pureboot protocol tests, all three chips. Loads
// boot-linked ELF at the loader base, starts execution there (BOOTRST / the // the boot-linked ELF at the loader base, starts execution there (BOOTRST /
// patched vector are not what is under test), and exposes the loader's // the patched vector are not what is under test), and exposes the loader's
// serial link as a pty for the real host tool: // serial link as a pty for the real host tool:
// //
// - Hardware USART builds: simavr's uart_pty on the selected instance. // - ATmega328P: the hardware USART0 through simavr's uart_pty.
// - Software UART builds: an 8N1 bridge between a pty and the GPIO pins, // - Tinies: an 8N1 bridge between a pty and the GPIO software UART
// timed against the simulated cycle counter (drives the loader's RX, // (drives PB0, the loader's RX; decodes PB1, its TX), timed against the
// decodes its TX). // simulated cycle counter.
//
// The link follows the chip's natural default (USART0 on the megas, the
// software UART on PB0/PB1 elsewhere) unless -l overrides it: `-l usart1`
// for the second instance, `-l sw:B5,B1` for a software build's RX,TX pins.
// //
// simavr's tiny cores decode the SPM opcode but attach no NVM module — SPM // simavr's tiny cores decode the SPM opcode but attach no NVM module — SPM
// is a silent no-op (the mega's boot section has one, avr_flash). The // is a silent no-op (the mega's boot section has one, avr_flash). The
@@ -42,44 +38,17 @@
static avr_t *avr; static avr_t *avr;
static uart_pty_t uart_pty; static uart_pty_t uart_pty;
static int link_software; static int use_uart_pty;
static char uart_digit = '0';
static char sw_rx_port = 'B', sw_tx_port = 'B';
static int sw_rx_bit = 0, sw_tx_bit = 1;
static const char *dump_path; static const char *dump_path;
static uint32_t reset_pc; static uint32_t reset_pc;
static volatile sig_atomic_t reset_requested; static volatile sig_atomic_t reset_requested;
static int parse_link(const char *spec)
{
if (strcmp(spec, "usart0") == 0 || strcmp(spec, "usart1") == 0) {
link_software = 0;
uart_digit = spec[5];
return 0;
}
if (strncmp(spec, "sw", 2) == 0) {
link_software = 1;
if (spec[2] == '\0')
return 0;
if (sscanf(spec + 2, ":%c%d,%c%d", &sw_rx_port, &sw_rx_bit, &sw_tx_port, &sw_tx_bit) == 4)
return 0;
}
return -1;
}
// simavr 1.6's avr_flash PGERS handler erases spm_pagesize bytes starting at // simavr 1.6's avr_flash PGERS handler erases spm_pagesize bytes starting at
// Z & ~1 instead of the page containing Z (its PGWRT path masks correctly) — // Z & ~1 instead of the page containing Z (its PGWRT path masks correctly) —
// hardware ignores the in-page bits (§26.8.1), so an erase issued with Z // hardware ignores the in-page bits (§26.8.1), so an erase issued with Z
// anywhere inside the page wipes half the neighbouring page in simulation // anywhere inside the page wipes half the neighbouring page in simulation
// only. Wrap the mega's registered flash ioctl and re-dispatch page erases // only. Wrap the mega's registered flash ioctl and re-dispatch page erases
// with Z forced to the page boundary; everything else passes through. // with Z forced to the page boundary; everything else passes through.
//
// A second gap on the boot-section-less m48s: their RWWSRE bit is the
// temporary-buffer discard (Atmel-8271 §26.2/§26.3.1), but the stock model
// gates its RWWSRE branch on AVR_SELFPROG_HAVE_RWW — absent on the m48
// core — so the discard store falls through into the buffer-fill branch and
// plants whatever Z/R1:R0 happen to hold. Perform the silicon's discard
// here instead.
static avr_flash_t *mega_flash; static avr_flash_t *mega_flash;
static int (*mega_flash_ioctl)(avr_io_t *io, uint32_t ctl, void *param); static int (*mega_flash_ioctl)(avr_io_t *io, uint32_t ctl, void *param);
@@ -95,15 +64,6 @@ static int fixed_flash_ioctl(avr_io_t *io, uint32_t ctl, void *param)
io->avr->data[31] = (uint8_t)(z >> 8); io->avr->data[31] = (uint8_t)(z >> 8);
return result; return result;
} }
if (ctl == AVR_IOCTL_FLASH_SPM && !(mega_flash->flags & AVR_SELFPROG_HAVE_RWW) &&
(io->avr->data[mega_flash->r_spm] & 0x11) == 0x11) { // RWWSRE|SELFPRGEN: the m48 buffer discard
for (int i = 0; i < mega_flash->spm_pagesize / 2; i++) {
mega_flash->tmppage[i] = 0xffff;
mega_flash->tmppage_used[i] = 0;
}
avr_regbit_clear(io->avr, mega_flash->selfprgen);
return 0;
}
return mega_flash_ioctl(io, ctl, param); return mega_flash_ioctl(io, ctl, param);
} }
@@ -297,42 +257,29 @@ static void finish(int sig)
} }
} }
} }
if (!link_software) if (use_uart_pty)
uart_pty_stop(&uart_pty); uart_pty_stop(&uart_pty);
_exit(0); _exit(0);
} }
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
int link_given = 0; if (argc < 8 || argc > 10) {
for (int opt; (opt = getopt(argc, argv, "l:")) != -1;) {
if (opt != 'l' || parse_link(optarg) != 0) {
fprintf(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)\n");
return 2;
}
link_given = 1;
}
int args = argc - optind;
if (args < 7 || args > 9) {
fprintf(stderr, fprintf(stderr,
"usage: %s [-l link] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>" "usage: %s <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
" [reset_hex] [resume_flash]\n" " [reset_hex] [resume_flash]\n"
" -l link: usart0 | usart1 | sw[:B0,B1] (RX,TX); default: the chip's own\n" " reset_hex: reset vector (default: base on the mega, 0 on the tinies)\n"
" reset_hex: reset vector (default: base with a boot section, else 0)\n"
" resume_flash: raw full-flash image loaded instead of the ELF — a prior\n" " resume_flash: raw full-flash image loaded instead of the ELF — a prior\n"
" run's dump, for power-fail resume tests\n", " run's dump, for power-fail resume tests\n",
argv[0]); argv[0]);
return 2; return 2;
} }
argv += optind - 1; // argv[1] is the ELF again, whatever was parsed
const char *mcu_name = argv[2]; const char *mcu_name = argv[2];
uint32_t base = (uint32_t)strtoul(argv[4], NULL, 0); uint32_t base = (uint32_t)strtoul(argv[4], NULL, 0);
unsigned page = (unsigned)atoi(argv[5]); unsigned page = (unsigned)atoi(argv[5]);
unsigned baud = (unsigned)atoi(argv[6]); unsigned baud = (unsigned)atoi(argv[6]);
dump_path = argv[7]; dump_path = argv[7];
int is_mega = strncmp(mcu_name, "atmega", 6) == 0; use_uart_pty = strcmp(mcu_name, "atmega328p") == 0;
if (!link_given)
link_software = !is_mega; // the chips' natural links: USART0, or PB0/PB1
avr = avr_make_mcu_by_name(mcu_name); avr = avr_make_mcu_by_name(mcu_name);
if (!avr) { if (!avr) {
@@ -343,7 +290,7 @@ int main(int argc, char *argv[])
avr->frequency = (uint32_t)strtoul(argv[3], NULL, 0); avr->frequency = (uint32_t)strtoul(argv[3], NULL, 0);
memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased
if (args > 8) { if (argc > 9) {
// Resume: the full flash image of an interrupted prior run. // Resume: the full flash image of an interrupted prior run.
FILE *f = fopen(argv[9], "rb"); FILE *f = fopen(argv[9], "rb");
if (!f || fread(avr->flash, 1, avr->flashend + 1, f) == 0) { if (!f || fread(avr->flash, 1, avr->flashend + 1, f) == 0) {
@@ -359,13 +306,11 @@ int main(int argc, char *argv[])
} }
memcpy(avr->flash + base, fw.flash, fw.flashsize); memcpy(avr->flash + base, fw.flash, fw.flashsize);
} }
// The boot-sectioned megas enter the loader in hardware (BOOTRST, not // The mega enters the loader in hardware (BOOTRST, not modeled — the
// modeled — the argument picks the modeled fuse's target); the tinies // argument picks the modeled fuse's target); the tinies reset to word 0
// and the boot-section-less m48s reset to word 0 like silicon — erased // like silicon — erased flash walks up into the loader, and after the
// flash walks up into the loader, and after the host's surgery the // host's surgery the patched vector routes there.
// patched vector routes there. reset_pc = argc > 8 ? (uint32_t)strtoul(argv[8], NULL, 0) : (use_uart_pty ? base : 0);
int boot_section = is_mega && strncmp(mcu_name, "atmega48", 8) != 0;
reset_pc = args > 7 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? base : 0);
avr->pc = reset_pc; avr->pc = reset_pc;
avr->codeend = avr->flashend; avr->codeend = avr->flashend;
@@ -378,34 +323,27 @@ int main(int argc, char *argv[])
avr_ioctl(avr, AVR_IOCTL_EEPROM_SET, &seed); avr_ioctl(avr, AVR_IOCTL_EEPROM_SET, &seed);
} }
// The megas carry simavr's avr_flash module (and its two gaps the wrap if (use_uart_pty) {
// above fixes); the tinies get the NVM module simavr lacks. Which serial
// bridge runs is the link's business, not the chip class's.
if (is_mega) {
fix_mega_flash_erase(); fix_mega_flash_erase();
// POLL_SLEEP paces an idle-polling loader in host real time (a
// no-hardware CPU-saving hack); clear it so cycles run free.
uint32_t flags = 0;
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &flags);
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &flags);
uart_pty_init(avr, &uart_pty);
uart_pty_connect(&uart_pty, '0');
printf("PB_PTY %s\n", uart_pty.pty.slavename);
} else { } else {
nvm.page = page; nvm.page = page;
memset(nvm.buffer, 0xff, sizeof(nvm.buffer)); memset(nvm.buffer, 0xff, sizeof(nvm.buffer));
nvm.io.kind = "tiny_nvm"; nvm.io.kind = "tiny_nvm";
nvm.io.ioctl = nvm_ioctl; nvm.io.ioctl = nvm_ioctl;
avr_register_io(avr, &nvm.io); avr_register_io(avr, &nvm.io);
}
if (!link_software) {
// POLL_SLEEP paces an idle-polling loader in host real time (a
// no-hardware CPU-saving hack); clear it so cycles run free.
uint32_t flags = 0;
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
uart_pty_init(avr, &uart_pty);
uart_pty_connect(&uart_pty, uart_digit);
printf("PB_PTY %s\n", uart_pty.pty.slavename);
} else {
bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly
rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_rx_port), (unsigned)sw_rx_bit); rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), 0);
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_tx_port), (unsigned)sw_tx_bit), tx_hook, avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), 1), tx_hook, NULL);
NULL);
avr_raise_irq(rx_pin, 1); // idle line avr_raise_irq(rx_pin, 1); // idle line
int slave; int slave;
@@ -433,27 +371,18 @@ int main(int argc, char *argv[])
reset_requested = 0; reset_requested = 0;
avr_reset(avr); avr_reset(avr);
avr->pc = reset_pc; avr->pc = reset_pc;
if (!link_software) { // reset restores the pacing hack; re-clear it if (use_uart_pty) { // reset restores the pacing hack; re-clear it
uint32_t flags = 0; uint32_t flags = 0;
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags); avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &flags);
flags &= ~AVR_UART_FLAG_POLL_SLEEP; flags &= ~AVR_UART_FLAG_POLL_SLEEP;
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags); avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &flags);
} else { } else {
bridge_reset(); bridge_reset();
} }
} }
if (link_software && ++since_poll >= 2000) { if (!use_uart_pty && ++since_poll >= 2000) {
since_poll = 0; since_poll = 0;
poll_pty(); poll_pty();
// An unthrottled idle simulation runs the activation window out
// from under the host's real-time knock cadence: a 1 MHz build's
// 8 s window is 8 M cycles — tens of wall milliseconds — so a
// first knock lost to an in-flight reset misses the window
// entirely. Pace the simulation only while the bridge is fully
// quiet (nothing decoding, nothing queued); transfers keep full
// speed, and a quiet window stretches toward real time.
if (!rx_active && !tx_active && rx_head == rx_tail)
usleep(200);
} }
} }
finish(0); finish(0);

View File

@@ -27,12 +27,9 @@ def expect_error(what, fn, *needles):
fail(f"{what}: no error raised") fail(f"{what}: no error raised")
def info_of(pb, base, page, patch, flash, signature=(0x1E, 0x93, 0x0B), word_flash=False, version=None): def info_of(pb, base, page, patch, flash):
scale = 2 if word_flash else 1 raw = bytes((0x50, 0x42, 1, 0x1E, 0x93, 0x0B, page, base & 0xFF, base >> 8,
wire_base = base // scale 0, 2, 1 if patch else 0))
flags = (1 if patch else 0) | (2 if word_flash else 0)
raw = bytes((0x50, 0x42, pb.NEWEST_LOADER if version is None else version,
*signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8, 0, 2, flags))
info = pb.Info(raw) info = pb.Info(raw)
assert info.flash_size == flash assert info.flash_size == flash
return info return info
@@ -53,67 +50,16 @@ def main():
import pureboot as pb import pureboot as pb
tiny = info_of(pb, 0x1E00, 64, True, 0x2000) tiny = info_of(pb, 0x1E00, 64, True, 0x2000)
mega = info_of(pb, 0x7E00, 128, False, 0x8000, signature=(0x1E, 0x95, 0x0F)) mega = info_of(pb, 0x7E00, 128, False, 0x8000)
# Versioning: the block's third byte is the loader's version, and the tool # mega_boot: BOOTSZ words and the BOOTRST sense, DS40002061B §27.
# speaks a window of them. Every version in the window decodes, so an older for bits, start in ((0b11, 0x7E00), (0b10, 0x7C00), (0b01, 0x7800), (0b00, 0x7000)):
# deployed loader stays usable; one above the window is refused by name, prog, at = pb.mega_boot((0xF8 | (bits << 1)) & ~1)
# since which version changed the protocol is knowledge only the tool
# holds, and it holds none about a version it has never heard of.
for version in range(pb.OLDEST_LOADER, pb.NEWEST_LOADER + 1):
if info_of(pb, 0x1E00, 64, True, 0x2000, version=version).version != version:
fail(f"pureboot {version} does not decode")
expect_error(
"unknown loader version",
lambda: info_of(pb, 0x1E00, 64, True, 0x2000, version=pb.NEWEST_LOADER + 1),
f"pureboot {pb.NEWEST_LOADER + 1}",
"newer tool",
)
# mega_boot: BOOTSZ words and the BOOTRST sense per chip — the fuse byte
# index (EXTENDED on the x8 line except the m328s' HIGH, HIGH elsewhere)
# and the per-family ladders (Atmel-2486/2466/2503/2545/8271/DS40002065/
# 8272/8011/2593/42719). Synthetic 'F' replies: only the boot byte
# carries meaning.
cases = (
((0x1E, 0x93, 0x07), 0x2000, 3, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m8
((0x1E, 0x94, 0x03), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m16
((0x1E, 0x95, 0x02), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m32
((0x1E, 0x93, 0x0A), 0x2000, 2, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m88
((0x1E, 0x93, 0x0F), 0x2000, 2, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m88P
((0x1E, 0x94, 0x06), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168/168A
((0x1E, 0x94, 0x0B), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168P
((0x1E, 0x95, 0x14), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m328
((0x1E, 0x95, 0x0F), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m328P
((0x1E, 0x94, 0x0F), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m164A
((0x1E, 0x94, 0x0A), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m164P
((0x1E, 0x95, 0x15), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m324A
((0x1E, 0x96, 0x09), 0x10000, 3, {0b11: 0xFC00, 0b10: 0xF800, 0b01: 0xF000, 0b00: 0xE000}), # m644
((0x1E, 0x96, 0x0A), 0x10000, 3, {0b11: 0xFC00, 0b10: 0xF800, 0b01: 0xF000, 0b00: 0xE000}), # m644P
((0x1E, 0x97, 0x06), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284
((0x1E, 0x97, 0x05), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284P
)
for signature, flash, which, ladder in cases:
# Word-addressed chips carry the 1 KiB slot (their smallest boot sector).
slot = 1024 if flash > 0x10000 else 512
chip = info_of(pb, flash - slot, 128 if flash < 0x20000 else 0, False, flash,
signature=signature, word_flash=flash > 0x10000)
for bits, start in ladder.items():
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
fuses[which] = (0xF8 | (bits << 1)) & ~1
prog, at = pb.mega_boot(chip, bytes(fuses))
if not prog or at != start: if not prog or at != start:
fail(f"mega_boot {signature[1]:02x}{signature[2]:02x} BOOTSZ={bits:02b} programmed: {prog} {at:#07x}") fail(f"mega_boot BOOTSZ={bits:02b} programmed: {prog} {at:#06x}")
fuses[which] |= 1 prog, at = pb.mega_boot(0xF8 | (bits << 1) | 1)
prog, at = pb.mega_boot(chip, bytes(fuses))
if prog or at != start: if prog or at != start:
fail(f"mega_boot {signature[1]:02x}{signature[2]:02b} unprogrammed: {prog} {at:#07x}") fail(f"mega_boot BOOTSZ={bits:02b} unprogrammed: {prog} {at:#06x}")
# Word-addressed info decode: the 1284P's base/page ride the wire scaled,
# and its slot is 1 KiB.
big = info_of(pb, 0x1FC00, 0, False, 0x20000, signature=(0x1E, 0x97, 0x05), word_flash=True)
if big.page != 256 or big.base != 0x1FC00 or big.stage != 0x1F800 or big.slot != 1024:
fail(f"word-addressed info decode: page {big.page}, base {big.base:#x}, stage {big.stage:#x}")
# Surgery: word 0 lands on the loader, the trampoline on the original # Surgery: word 0 lands on the loader, the trampoline on the original
# entry — checked with an independent decoder. # entry — checked with an independent decoder.
@@ -169,27 +115,6 @@ def main():
fail("image_info misses the embedded block") fail("image_info misses the embedded block")
if pb.image_info(bytes((0xAA,)) * 40) is not None: if pb.image_info(bytes((0xAA,)) * 40) is not None:
fail("image_info invents a block") fail("image_info invents a block")
# An older loader's image stays readable, so a deployed build can be
# identified and installed like any other.
old = info_of(pb, 0x1E00, 64, True, 0x2000, version=pb.OLDEST_LOADER)
found_old = pb.image_info(bytes((0xAA,)) * 10 + old.raw)
if found_old is None or found_old.version != pb.OLDEST_LOADER:
fail("image_info misses an older loader's block")
# loader_image must peel a padded image down to the slot content: a raw
# .bin padded from address 0 (or a whole-flash read-back with the loader
# resident at base) yields the same bytes as the bare slot image.
import tempfile
slot_image = bytes((0xAA,)) * 10 + tiny.raw + bytes((0xCC,)) * 40
padded = bytes((0xFF,)) * tiny.base + slot_image
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
f.write(padded)
padded_path = f.name
try:
if pb.loader_image(padded_path) != slot_image:
fail("loader_image does not peel a padded image to the slot content")
finally:
os.unlink(padded_path)
# Update preflight: the full fuse matrix, plus target mismatch. # Update preflight: the full fuse matrix, plus target mismatch.
other = info_of(pb, 0x1E00, 32, True, 0x2000) other = info_of(pb, 0x1E00, 32, True, 0x2000)
@@ -225,52 +150,6 @@ def main():
pb.check_walk_region({0x7800: bytes((0xFF,)) * 128}, mega, fuses(0xFA), False) pb.check_walk_region({0x7800: bytes((0xFF,)) * 128}, mega, fuses(0xFA), False)
pb.check_walk_region(deep, mega, None, False) # fuses unknown: no check pb.check_walk_region(deep, mega, None, False) # fuses unknown: no check
# The repairing verify: a mismatched page is rewritten rather than raised,
# bounded so a fault that is not self-clearing cannot spin.
class FakeLoader:
"""A device whose first `bad` writes of any page land wrong."""
def __init__(self, info, bad):
self.info = info
self.bad = bad
self.flash = {}
self.writes = 0
def write_page(self, address, data):
self.writes += 1
self.flash[address] = bytes(len(data)) if self.bad > 0 else bytes(data)
self.bad -= 1
def read_flash(self, address, count):
return self.flash.get(address, bytes(count))
want = {0: bytes((i * 5) & 0xFF for i in range(128))}
# One bad write, then good: repaired in place, and the caller never sees
# an error. The rewrite is counted, so a silent no-op cannot pass.
device = FakeLoader(info_of(pb, 0x7E00, 128, False, 0x8000), bad=1)
device.write_page(0, want[0])
pb.verify_pages(device, want, repair=True)
if device.writes != 2:
fail(f"repairing verify made {device.writes} writes, expected 2")
# Without repair the same state raises, so the repair is what fixed it.
device = FakeLoader(info_of(pb, 0x7E00, 128, False, 0x8000), bad=1)
device.write_page(0, want[0])
expect_error("verify without repair", lambda: pb.verify_pages(device, want), "verify failed")
# A page that never comes good stops after RETRIES rewrites, and says so.
device = FakeLoader(info_of(pb, 0x7E00, 128, False, 0x8000), bad=99)
device.write_page(0, want[0])
expect_error(
"unrepairable page",
lambda: pb.verify_pages(device, want, repair=True),
"verify failed",
f"after {pb.RETRIES} retries",
)
if device.writes != pb.RETRIES + 1:
fail(f"unrepairable page took {device.writes} writes, expected {pb.RETRIES + 1}")
print("test_planner: all planner and policy checks pass") print("test_planner: all planner and policy checks pass")

View File

@@ -1,37 +0,0 @@
#!/bin/bash
# The port's gate: every chip's generated workflow — build, size matrix, and
# the simulator-driven protocol suites. --full adds the reflect-spot builds
# (libavr's rule: reflect compiles are bounded to its spot set, never the
# full matrix). LIBAVR_ROOT must point at the libavr checkout.
set -e
cd "$(dirname "$0")/.."
full=0
[[ "$1" == "--full" ]] && { full=1; shift; }
CHIPS=(attiny13 attiny13a attiny25 attiny45 attiny85
atmega8 atmega8a atmega16 atmega16a atmega32 atmega32a
atmega48 atmega48a atmega48p atmega48pa
atmega88 atmega88a atmega88p atmega88pa
atmega168 atmega168a atmega168p atmega168pa
atmega328 atmega328p
atmega164a atmega164p atmega164pa
atmega324a atmega324p atmega324pa
atmega644 atmega644a atmega644p atmega644pa
atmega1284 atmega1284p)
REFLECT_SPOT=(attiny13a attiny85 atmega8 atmega16a atmega32a atmega48pa
atmega88 atmega168pa atmega328p atmega164a atmega644p atmega1284)
for chip in "${CHIPS[@]}"; do
echo "==== $chip ===="
cmake --workflow --preset "$chip-generated" "$@"
done
if ((full)); then
for chip in "${REFLECT_SPOT[@]}"; do
echo "==== $chip reflect ===="
cmake --workflow --preset "$chip-reflect" "$@"
done
fi
echo "check: every chip green"

View File

@@ -1,90 +0,0 @@
#!/usr/bin/env python3
"""Regenerate CMakePresets.json — one uniform pipeline per chip.
Every chip gets generated-mode configure/build/test presets and a workflow
running all three. Reflect-mode presets (configure + build, no tests — the
port's TUs compile identically; the sims prove nothing new there) exist for
libavr's reflect spot set only, mirroring its rule: the full reflect matrix
is never built, one chip per hardware class and pack vintage is.
Run from the repo root: tools/make_presets.py
"""
import json
import os
CHIPS = [
"attiny13", "attiny13a", "attiny25", "attiny45", "attiny85",
"atmega8", "atmega8a", "atmega16", "atmega16a", "atmega32", "atmega32a",
"atmega48", "atmega48a", "atmega48p", "atmega48pa",
"atmega88", "atmega88a", "atmega88p", "atmega88pa",
"atmega168", "atmega168a", "atmega168p", "atmega168pa",
"atmega328", "atmega328p",
"atmega164a", "atmega164p", "atmega164pa",
"atmega324a", "atmega324p", "atmega324pa",
"atmega644", "atmega644a", "atmega644p", "atmega644pa",
"atmega1284", "atmega1284p",
]
# libavr's REFLECT_SPOT (tools/check.sh): one chip per hardware class and
# pack vintage.
REFLECT_SPOT = [
"attiny13a", "attiny85", "atmega8", "atmega16a", "atmega32a",
"atmega48pa", "atmega88", "atmega168pa", "atmega328p", "atmega164a",
"atmega644p", "atmega1284",
]
def main():
configure = [{
"name": "base",
"hidden": True,
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"toolchainFile": "$env{LIBAVR_ROOT}/cmake/avr-toolchain.cmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
"CMAKE_COLOR_DIAGNOSTICS": "ON",
},
}]
build, test, workflows = [], [], []
def add(chip, mode):
name = f"{chip}-{mode}"
configure.append({
"name": name,
"inherits": "base",
"cacheVariables": {
"LIBAVR_MCU": chip,
"LIBAVR_REFLECT": "ON" if mode == "reflect" else "OFF",
},
})
build.append({"name": name, "configurePreset": name})
steps = [{"type": "configure", "name": name}, {"type": "build", "name": name}]
if mode == "generated":
test.append({"name": name, "configurePreset": name, "output": {"outputOnFailure": True}})
steps.append({"type": "test", "name": name})
workflows.append({"name": name, "steps": steps})
for chip in CHIPS:
add(chip, "generated")
for chip in REFLECT_SPOT:
add(chip, "reflect")
presets = {
"version": 8,
"configurePresets": configure,
"buildPresets": build,
"testPresets": test,
"workflowPresets": workflows,
}
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "CMakePresets.json")
with open(path, "w") as f:
json.dump(presets, f, indent=1)
f.write("\n")
print(f"{len(CHIPS)} chips, {len(REFLECT_SPOT)} reflect: {os.path.normpath(path)}")
if __name__ == "__main__":
main()