build: the libavr pin advances past phase 6, at byte parity everywhere
The pin crosses libavr's phase 6 - the renamed system surface, the named serial configs, the receiver-tolerance table, the paged SPM receipts - and every loader image comes out size-identical: the full matrix on six representative chips (the exhaustive cross product on three of them), the stock and autobaud columns untouched, the four tsb tiers back on their recorded floors at 510/526/638/836. Byte parity was not free, and the two libavr defects it surfaced were fixed there rather than absorbed here. The EEPROM write procedure's step 2 - the SPMEN spin - had landed unconditionally and cost every build six bytes for a wait a polled loader can never take; it is scoped now, and the loaders state the datasheet's own omission clause (spm_interlock::omitted, DS40002061B 8.6.3). The blocking page erase/write grew an internal wait the tiers' settle() already provides, so the tiers issue the command form and pureboot keeps its host-driven sp_spm path. What the port states rather than inherits: the stock 115200 at 16 MHz sits +2.1 % past the receiver-tolerance table libavr now holds rates to, so the hardware links say .allow_baud_error = true - the same 2.5 % envelope pureboot_baud_feasible() has always enforced, proven on silicon across the fleet. rx_ready() reads readable() now. Alongside the pin: rule 33's ASCII sweep over every source (docs keep their typography), rule 34's InsertBraces in .clang-format with the tree reformatted, std::array over the simavr runners' raw buffers, and the stale Studio size in ide/README.md replaced by the claim its check-flags gate actually holds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,9 @@ TabWidth: 4
|
||||
UseTab: ForIndentation
|
||||
AlignEscapedNewlines: DontAlign
|
||||
AllowShortFunctionsOnASingleLine: Empty
|
||||
AlwaysBreakTemplateDeclarations: true
|
||||
BreakTemplateDeclarations: Yes
|
||||
BreakBeforeBraces: Custom
|
||||
BraceWrapping:
|
||||
AfterFunction: true
|
||||
InsertBraces: true
|
||||
...
|
||||
|
||||
@@ -12,7 +12,7 @@ if(NOT LIBAVR_ROOT)
|
||||
set(LIBAVR_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/libavr)
|
||||
endif()
|
||||
if(NOT EXISTS ${LIBAVR_ROOT}/CMakeLists.txt)
|
||||
message(FATAL_ERROR "libavr not found at ${LIBAVR_ROOT} — run: git submodule update --init libavr")
|
||||
message(FATAL_ERROR "libavr not found at ${LIBAVR_ROOT} - run: git submodule update --init libavr")
|
||||
endif()
|
||||
add_subdirectory(${LIBAVR_ROOT} libavr-build)
|
||||
|
||||
@@ -22,7 +22,7 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
|
||||
# The behavioral tests drive the real wire protocols over a simavr pty
|
||||
# (as the host tools do) and actually flash the device. The runners are
|
||||
# host programs built at configure time against libsimavr (C++23 — what
|
||||
# host programs built at configure time against libsimavr (C++23 - what
|
||||
# the distribution's compiler speaks in full); if they or Python are
|
||||
# missing, only the size tests run.
|
||||
find_program(_host_cxx NAMES c++ g++)
|
||||
@@ -36,7 +36,7 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
-lsimavr -lsimavrparts -lelf -lutil
|
||||
RESULT_VARIABLE _pbdev_res ERROR_VARIABLE _pbdev_err)
|
||||
if(NOT _pbdev_res EQUAL 0)
|
||||
message(STATUS "pureboot_device not built (${_pbdev_err}) — protocol tests skipped")
|
||||
message(STATUS "pureboot_device not built (${_pbdev_err}) - protocol tests skipped")
|
||||
unset(PB_DEVICE)
|
||||
endif()
|
||||
if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||
@@ -48,19 +48,19 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
-lsimavr -lsimavrparts -lelf
|
||||
RESULT_VARIABLE _dev_res ERROR_VARIABLE _dev_err)
|
||||
if(NOT _dev_res EQUAL 0)
|
||||
message(STATUS "tsb_device not built (${_dev_err}) — protocol tests skipped")
|
||||
message(STATUS "tsb_device not built (${_dev_err}) - protocol tests skipped")
|
||||
unset(TSB_DEVICE)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# The ELF is only a container (symbols, section headers) and is never flashed —
|
||||
# The ELF is only a container (symbols, section headers) and is never flashed -
|
||||
# and the host tool's load_image() dispatches on extension, so handing it one
|
||||
# would silently program the header bytes. Every loader image therefore gets
|
||||
# both flashable forms beside it at link time: .hex for avrdude, and .bin for
|
||||
# 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.
|
||||
# on the fly). .eeprom is dropped - EEPROM content is its own update.
|
||||
function(add_image_outputs name)
|
||||
add_custom_command(TARGET ${name} POST_BUILD
|
||||
COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom
|
||||
@@ -71,9 +71,9 @@ endfunction()
|
||||
|
||||
# The TinySafeBoot protocol reimplemented on libavr in variants that trade
|
||||
# clarity for size. Each links into the ATmega328P boot section (BOOTSZ selects
|
||||
# its size; BOOTRST vectors a reset to its base) with -nostartfiles — a polled
|
||||
# its size; BOOTRST vectors a reset to its base) with -nostartfiles - a polled
|
||||
# loader has no use for the crt or the vector table. The entry sits in
|
||||
# .vectors, laid first, and runs — avr::startup::entry on the policy tier,
|
||||
# .vectors, laid first, and runs - avr::startup::entry on the policy tier,
|
||||
# the experiment tiers' own naked stubs elsewhere, each documented in its
|
||||
# source. The boot base is FLASHEND+1 minus the section size; the linker
|
||||
# section-start and the source's boot_bytes agree. tsb_app is
|
||||
@@ -83,26 +83,26 @@ endfunction()
|
||||
# All four implement the full oracle feature set (see oracle/README.md):
|
||||
# watchdog bail, one-wire half-duplex, config-page activation timeout, password
|
||||
# gate, emergency erase, config/flash/EEPROM read-write. They differ only in how,
|
||||
# and the size gradient is the cost of that "how" — see dev/lessons.md.
|
||||
# tsb_asm — the tricks tier's C++ with exactly two routines in asm (the
|
||||
# bounded rx and the page-store loop — the two whose remaining
|
||||
# and the size gradient is the cost of that "how" - see dev/lessons.md.
|
||||
# tsb_asm - the tricks tier's C++ with exactly two routines in asm (the
|
||||
# bounded rx and the page-store loop - the two whose remaining
|
||||
# cost is the C ABI itself): 510 B in the 512 B section the
|
||||
# hand-written 500 B oracle occupies. Everything else, from
|
||||
# bring-up to dispatch, is C++ on libavr.
|
||||
# tsb_tricks — no asm at all: the whole-loader register allocation lives in
|
||||
# tsb_tricks - no asm at all: the whole-loader register allocation lives in
|
||||
# global register variables (Y walks the page pointer), every
|
||||
# helper is a tiny noinline primitive placed by the
|
||||
# global-register store rules, pages stream straight to
|
||||
# SPM/EEPROM, and the bring-up is the two reset-non-default
|
||||
# registers only. 526 B in the 1 KB section (BOOTSZ=10) — 14
|
||||
# registers only. 526 B in the 1 KB section (BOOTSZ=10) - 14
|
||||
# over the oracle's section, from 168 over at this tier's first
|
||||
# floor.
|
||||
# tsb_pure — pure idiomatic libavr, one function per command, TU-local
|
||||
# tsb_pure - pure idiomatic libavr, one function per command, TU-local
|
||||
# (internal linkage), streaming (no SRAM page buffer): 836 B in
|
||||
# the 1 KB section.
|
||||
# tsb_policy — the policy floor: pureboot's rules (no asm, no register
|
||||
# tsb_policy - the policy floor: pureboot's rules (no asm, no register
|
||||
# variables) with every pureboot lesson applied. 638 B in the
|
||||
# 1 KB section — the measured evidence that the 512 B fit is a
|
||||
# 1 KB section - the measured evidence that the 512 B fit is a
|
||||
# property of the mechanisms philosophy #5 bans.
|
||||
#
|
||||
# add_tsb_variant(<name> <boot-section-bytes>)
|
||||
@@ -136,14 +136,14 @@ if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||
add_tsb_variant(tsb_tricks 1024)
|
||||
# The policy tier's floor is measured with the loop flags pureboot's size
|
||||
# work found (a loader's loop bodies all contain calls); the other tiers
|
||||
# keep the flag set their recorded floors were measured with — none.
|
||||
# keep the flag set their recorded floors were measured with - none.
|
||||
target_compile_options(tsb_policy PRIVATE -fno-move-loop-invariants -fno-tree-ter)
|
||||
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,
|
||||
# fitting each chip's smallest boot sector. The geometry and the
|
||||
# pureboot_add_loader() deployment function live in pureboot/CMakeLists.txt —
|
||||
# pureboot_add_loader() deployment function live in pureboot/CMakeLists.txt -
|
||||
# the unit a downstream project consumes; everything below is this port's
|
||||
# own build: the stock loaders, their tests, and the size matrix. The
|
||||
# distinct binary dir keeps the `pureboot` target's output name free.
|
||||
@@ -151,7 +151,7 @@ add_subdirectory(pureboot pureboot-cmake)
|
||||
|
||||
# The stock loader: the family-default deployment (crystal/RC clock, the
|
||||
# chip's natural link, default pins). The activation window stays a cache
|
||||
# variable — re-timing a deployed loader is a self-update with a re-timed
|
||||
# variable - re-timing a deployed loader is a self-update with a re-timed
|
||||
# build. pureboot9 is that re-timed build, and what the update test installs.
|
||||
set(PUREBOOT_TIMEOUT 8 CACHE STRING "pureboot activation window, seconds")
|
||||
pureboot_add_loader(pureboot TIMEOUT ${PUREBOOT_TIMEOUT})
|
||||
@@ -203,8 +203,8 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180)
|
||||
|
||||
# The activation window as a measured duration: application installed,
|
||||
# line idle, the first transmit is the application's banner — its
|
||||
# cycle is the window the source declares, held to ±2 % (one
|
||||
# line idle, the first transmit is the application's banner - its
|
||||
# cycle is the window the source declares, held to +/-2 % (one
|
||||
# mis-counted cycle per poll is a 10 % shift).
|
||||
add_test(NAME pureboot.window
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbwindow.py
|
||||
@@ -218,8 +218,8 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
set_tests_properties(pureboot.window PROPERTIES TIMEOUT 300)
|
||||
|
||||
# The half-duplex loader's window, same gate: its poll runs through
|
||||
# rx_ready()'s release-line test, whose outlined call re-shapes the
|
||||
# whole loop — a per-class cycle count (poll_cost() in pureboot.cpp)
|
||||
# readable()'s release-line test, whose outlined call re-shapes the
|
||||
# whole loop - a per-class cycle count (poll_cost() in pureboot.cpp)
|
||||
# that only the built image can prove, chip by chip.
|
||||
if(PUREBOOT_HAS_USART)
|
||||
add_test(NAME pureboot.window.halfduplex
|
||||
@@ -268,7 +268,7 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
set_tests_properties(pureboot.glitch PROPERTIES TIMEOUT 180)
|
||||
|
||||
# Entering the loader from a running application with no reset
|
||||
# between, over a page buffer the application dirtied — the case the
|
||||
# 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.
|
||||
@@ -299,7 +299,7 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
endif()
|
||||
|
||||
# The self-update end-to-end: the re-timed build (same source, only
|
||||
# the timeout differs — a byte-different image) replaces the resident
|
||||
# the timeout differs - a byte-different image) replaces the resident
|
||||
# through --update-loader, with every power-fail phase rehearsed from
|
||||
# the runner's flash dumps.
|
||||
pureboot_add_loader(pureboot9 TIMEOUT 9)
|
||||
@@ -315,11 +315,11 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
endif()
|
||||
|
||||
# The size matrix: every configuration axis that could move the image
|
||||
# size — the serial backend (different code), the USART instance
|
||||
# size - the serial backend (different code), the USART instance
|
||||
# (different registers), the clock (different constants), the baud
|
||||
# through the shapes its bit timing takes, and the pins through the one
|
||||
# thing they decide (whether a bit-banged link has to release the USART
|
||||
# that owns them) — each combination must still fit the chip's slot
|
||||
# that owns them) - each combination must still fit the chip's slot
|
||||
# budget. The timeout is a constant and adds no axis. The stock build is
|
||||
# one point of this matrix and already has its test.
|
||||
function(pureboot_size_variant name)
|
||||
@@ -330,7 +330,7 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
endfunction()
|
||||
|
||||
# The autobaud loader: one clock-agnostic image per chip, so it has no
|
||||
# clock x baud axis of its own — the matrix below sweeps those for the
|
||||
# clock x baud axis of its own - the matrix below sweeps those for the
|
||||
# fixed-baud builds, and this one binary has to serve all of them at run
|
||||
# time. Size-tested against the same per-chip budget as every other variant.
|
||||
pureboot_add_loader(pureboot_autobaud SERIAL autobaud)
|
||||
@@ -386,7 +386,7 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
|
||||
# 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).
|
||||
# menu - it has no crystal option).
|
||||
if(LIBAVR_MCU MATCHES "^attiny13")
|
||||
set(_matrix_clocks 1200000 4800000 9600000)
|
||||
set(_full_clocks 128000 600000 1200000 4800000 9600000)
|
||||
@@ -397,20 +397,20 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
endif()
|
||||
|
||||
# The exhaustive cross product: every clock a deployment plausibly runs
|
||||
# — the internal oscillators, the shipped CKDIV8 floor, the plain
|
||||
# crystals and the UART crystals — against every rate, against every
|
||||
# - the internal oscillators, the shipped CKDIV8 floor, the plain
|
||||
# crystals and the UART crystals - against every rate, against every
|
||||
# backend. Beyond the ladder the list carries the slow rates a
|
||||
# sub-megahertz oscillator is left with, which no ladder rate reaches
|
||||
# (16000 Bd is the only rate the 128 kHz oscillator holds exactly); at
|
||||
# the fast clocks those same rates also select the software UART's
|
||||
# 16-bit _delay_loop_2 bit spin (two words more setup at each of its five
|
||||
# sites), the largest image the space produces and a shape the ladder
|
||||
# default — always the *fastest* rate a clock reaches — never picks.
|
||||
# default - always the *fastest* rate a clock reaches - never picks.
|
||||
#
|
||||
# Every chip runs the full cross product: the size-bearing classes (flash
|
||||
# addressing, hand-over shape, page size, USART inventory) are what make
|
||||
# the image differ, and a chip outside them is expected to match its class
|
||||
# — but "expected" is what a matrix is for, and the whole sweep is cheap
|
||||
# - but "expected" is what a matrix is for, and the whole sweep is cheap
|
||||
# enough to run rather than reason about. PUREBOOT_FULL_MATRIX is what
|
||||
# selects it; the compact matrix below is the per-commit default.
|
||||
get_property(_full_bauds GLOBAL PROPERTY PUREBOOT_BAUD_LADDER)
|
||||
@@ -449,9 +449,9 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
endforeach()
|
||||
list(GET _matrix_clocks -1 _matrix_top_hz)
|
||||
pureboot_size_variant(pureboot_sw_wide CLOCK ${_matrix_top_hz} BAUD 9600 SERIAL software)
|
||||
# The pin axis at the widest software image — the slowest ladder rate
|
||||
# The pin axis at the widest software image - the slowest ladder rate
|
||||
# against the fastest clock, whose bit spin needs the 16-bit delay
|
||||
# loop — with the USART release on top of it. The exhaustive sweep
|
||||
# loop - with the USART release on top of it. The exhaustive sweep
|
||||
# above carries the same axis across its whole cross product.
|
||||
if(PUREBOOT_HAS_USART)
|
||||
pureboot_size_variant(pureboot_sw_wide_on_usart0 CLOCK ${_matrix_top_hz} BAUD 9600
|
||||
@@ -468,7 +468,7 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
|
||||
# The pin axis at its fixed points, in both matrix modes. The autobaud
|
||||
# loader carries no clock and no baud, so the sweep has nothing to vary
|
||||
# for it — yet it is the tightest image in the space, and on a USART's
|
||||
# for it - yet it is the tightest image in the space, and on a USART's
|
||||
# own pins it pays the release too: that combination is the one that
|
||||
# overflowed the 1284's slot. The software build on those pins is the
|
||||
# same deployment the mute test drives.
|
||||
@@ -486,7 +486,7 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
endif()
|
||||
|
||||
# The OSCCAL axis at its fixed points: the stock shape, and the tightest
|
||||
# image in the space with the trim on top — the axis adds one register
|
||||
# image in the space with the trim on top - the axis adds one register
|
||||
# write, and these points hold both of its addressing encodings to every
|
||||
# chip's budget.
|
||||
pureboot_size_variant(pureboot_osccal OSCCAL 0x9c)
|
||||
@@ -499,9 +499,9 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
# The one-wire axis at its fixed points, in both matrix modes (the
|
||||
# exhaustive sweep carries the same shapes across its cross product):
|
||||
# the software link folded onto one pin, the tightest autobaud image
|
||||
# likewise — on the default pin and on the USART's own RXD, whose
|
||||
# likewise - on the default pin and on the USART's own RXD, whose
|
||||
# release the now-driven shared pin needs where a receive-only link
|
||||
# would not — and the hardware USART's half-duplex turn-around, stock
|
||||
# would not - and the hardware USART's half-duplex turn-around, stock
|
||||
# and at the widest fixed-baud shape.
|
||||
# The two spellings deliberately split across the two points: HALF_DUPLEX
|
||||
# folds TX onto RX, RX == TX states the same thing directly.
|
||||
@@ -520,10 +520,10 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
pureboot_size_variant(pureboot_usart1_hd USART 1 HALF_DUPLEX)
|
||||
endif()
|
||||
|
||||
# The trim byte, observed through the wire from the first prompt — one
|
||||
# The trim byte, observed through the wire from the first prompt - one
|
||||
# chip per OSCCAL addressing class: extended I/O on the 328P (data 0x66,
|
||||
# an sts — DS40002061B §36), plain I/O on the 85 (data 0x51, an out —
|
||||
# Atmel-2586 §21).
|
||||
# an sts - DS40002061B section 36), plain I/O on the 85 (data 0x51, an out -
|
||||
# Atmel-2586 section 21).
|
||||
if(LIBAVR_MCU MATCHES "^(atmega328p|attiny85)$" AND DEFINED PB_DEVICE)
|
||||
if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||
set(_osccal_addr 0x66)
|
||||
@@ -541,11 +541,11 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
set_tests_properties(pureboot.osccal PROPERTIES TIMEOUT 120)
|
||||
endif()
|
||||
|
||||
# One configured deployment end to end — a real board's shape rather
|
||||
# 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
|
||||
# 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)
|
||||
@@ -569,7 +569,7 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
set_tests_properties(pureboot.custom PROPERTIES TIMEOUT 180)
|
||||
endif()
|
||||
|
||||
# Hand-over with the USART that owns the loader's pins left enabled — the
|
||||
# Hand-over with the USART that owns the loader's pins left enabled - the
|
||||
# state an application reaches by jumping in without a reset, and the one
|
||||
# that made a bit-banged loader on PD0/PD1 (where the Uno's USB bridge
|
||||
# lands) receive and obey while answering nothing. Run where it was found
|
||||
@@ -634,7 +634,7 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
set_tests_properties(pureboot.onewire PROPERTIES TIMEOUT 180)
|
||||
|
||||
# The hardware USART's half-duplex turn-around, end to end: every
|
||||
# reply byte runs drive-line, TXC-hold, release — against simavr's
|
||||
# reply byte runs drive-line, TXC-hold, release - against simavr's
|
||||
# RXEN-gated receiver, which drops input to a disabled receiver the
|
||||
# way silicon does. The pty is a two-wire transport, so the host
|
||||
# needs no echo discard here; the off-chip tie itself is the
|
||||
@@ -674,7 +674,7 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
# The autobaud loader driven end to end over the software-UART bridge:
|
||||
# the host sends the 0xC0 calibration pulse, the loader times it, locks,
|
||||
# and programs. Run on the near-flash 328P
|
||||
# and the word-addressed 1284P — the two flash-addressing classes — and each
|
||||
# and the word-addressed 1284P - the two flash-addressing classes - and each
|
||||
# at two clocks with the one binary, which is the clock-agnostic property
|
||||
# autobaud exists for (test/pbautobaud.py). The fixture application banners
|
||||
# over the same software link at the first clock's rate.
|
||||
@@ -696,7 +696,7 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
|
||||
# The tightest deployment in the space, end to end: the autobaud
|
||||
# loader folded onto the USART's own RXD with the OSCCAL trim baked
|
||||
# — one-wire calibration, the receive-side release, and the host's
|
||||
# - one-wire calibration, the receive-side release, and the host's
|
||||
# echo discard, over the same two-clock sweep. One chip carries it;
|
||||
# the shape is chip-independent.
|
||||
if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
`master` carries `bootloader.atsln`, so this branch does too: `ide/bootloader.atsln`
|
||||
builds the loaders from the same sources Ninja does, to a **byte-identical
|
||||
`.text`** — 390 B for the 328P pureboot loader, 510 B for the `tsb_asm` tier in
|
||||
its 512-byte section. CMake remains the build system; the solution is here so the
|
||||
port opens in Studio as its predecessor did.
|
||||
`.text`** — the stock 328P pureboot deployment and the `tsb_asm` tier in its
|
||||
512-byte section (`check-flags.py` below is what holds the flag sets equal, so
|
||||
the sizes are Ninja's own). CMake remains the build system; the solution is
|
||||
here so the port opens in Studio as its predecessor did.
|
||||
|
||||
## The two projects, and why two
|
||||
|
||||
|
||||
2
libavr
2
libavr
Submodule libavr updated: b719ed74d8...26109e172b
@@ -1,5 +1,5 @@
|
||||
# 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.
|
||||
# ladder, and pureboot_add_loader() - the one way a loader target is created.
|
||||
# A downstream project brings its usual libavr setup (the `libavr` target and
|
||||
# the LIBAVR_MCU toolchain preset), adds this directory, and states its
|
||||
# deployment; every argument is optional (README.md):
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
# Per-family geometry, deployment defaults, and the linker wrap the PC modulo
|
||||
# needs. The slot is 512 bytes on every chip. The USART flags mirror the
|
||||
# hardware inventory the loader's own static asserts check — the plain 644 is
|
||||
# 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)
|
||||
@@ -107,7 +107,7 @@ set(_pb_slot 512)
|
||||
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.
|
||||
# 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)
|
||||
set(_pb_limit ${_pb_slot})
|
||||
@@ -118,7 +118,7 @@ endif()
|
||||
|
||||
# The pins each USART owns. A bit-banged link deployed on them has to release
|
||||
# that USART before it can drive the line, and those instructions are the one
|
||||
# way the choice of pins moves the image — so a size matrix needs them as an
|
||||
# way the choice of pins moves the image - so a size matrix needs them as an
|
||||
# axis even though pins are otherwise immediate operands. Uniform across every
|
||||
# mega libavr covers: USART0 (the classics' un-numbered USART included) on
|
||||
# PD0/PD1, USART1 on PD2/PD3.
|
||||
@@ -233,7 +233,7 @@ function(pureboot_default_baud clock software outvar)
|
||||
endif()
|
||||
endforeach()
|
||||
message(FATAL_ERROR "pureboot: no standard baud rate fits a ${clock} Hz clock within 2.5 % "
|
||||
"— pass BAUD <rate> to deploy a non-standard one")
|
||||
" - pass BAUD <rate> to deploy a non-standard one")
|
||||
endfunction()
|
||||
|
||||
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
||||
@@ -245,12 +245,12 @@ endfunction()
|
||||
# <name>.bin for --update-loader). The resolved deployment is stamped on the
|
||||
# target as PUREBOOT_HZ / PUREBOOT_BAUD / PUREBOOT_LINK (the link spelled
|
||||
# usart0, usart1, or sw:<RX>,<TX> with a trailing @<n> where those pins are a
|
||||
# USART's own) — what a test harness speaks to it with.
|
||||
# USART's own) - what a test harness speaks to it with.
|
||||
#
|
||||
# HALF_DUPLEX is the one-wire deployment, per backend: on the hardware USART
|
||||
# it enables the library's .half_duplex turn-around (RXD and TXD tied
|
||||
# together off-chip); on a software or autobaud link it puts both directions
|
||||
# on the RX pin — the same thing RX == TX spells directly.
|
||||
# on the RX pin - the same thing RX == TX spells directly.
|
||||
#
|
||||
# SERIAL autobaud measures the host's bit timing at run time, so the image
|
||||
# carries no clock and no baud: CLOCK and BAUD are not build parameters there,
|
||||
@@ -260,7 +260,7 @@ endfunction()
|
||||
#
|
||||
# OSCCAL bakes a measured oscillator trim into the loader (README.md: the
|
||||
# RC-oscillator deployment answer): the byte is written at the top of run(),
|
||||
# so every reset path — the watchdog hand-over included — runs on the
|
||||
# so every reset path - the watchdog hand-over included - runs on the
|
||||
# corrected clock. Orthogonal to the backend: an autobaud build may carry it
|
||||
# purely for the application's benefit, its own link being clock-free. No
|
||||
# value, no code.
|
||||
@@ -310,7 +310,7 @@ function(pureboot_add_loader name)
|
||||
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")
|
||||
"which auto does not pick on ${LIBAVR_MCU} - SERIAL software to force it")
|
||||
endif()
|
||||
if(_usart)
|
||||
set(_link usart0)
|
||||
@@ -347,7 +347,7 @@ function(pureboot_add_loader name)
|
||||
else()
|
||||
set(_serial_defines PUREBOOT_SOFT_SERIAL PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
|
||||
endif()
|
||||
# sw:<RX>,<TX> as port letter and bit, upcased — with @<n> where
|
||||
# sw:<RX>,<TX> as port letter and bit, upcased - with @<n> where
|
||||
# the TX pin is a USART's own TXD, since a harness driving that
|
||||
# link has to know the USART owns the pin until the loader
|
||||
# releases it.
|
||||
@@ -365,8 +365,8 @@ function(pureboot_add_loader name)
|
||||
set(_link "${_link}@1")
|
||||
elseif(PB_TX STREQUAL PB_RX AND _usart AND PB_RX STREQUAL _rx0)
|
||||
# One-wire on a USART's RXD: RXEN forces that pin's direction
|
||||
# (§20.7.3), so the driven shared pin is held exactly like a
|
||||
# TXD — the harness models the hold either way.
|
||||
# (section 20.7.3), so the driven shared pin is held exactly like a
|
||||
# TXD - the harness models the hold either way.
|
||||
set(_link "${_link}@0")
|
||||
elseif(PB_TX STREQUAL PB_RX AND _usart1 AND PB_RX STREQUAL _rx1)
|
||||
set(_link "${_link}@1")
|
||||
@@ -403,12 +403,12 @@ function(pureboot_add_loader name)
|
||||
# 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 keeps
|
||||
# expression temporaries in registers (-fno-tree-ter) — but every loop body
|
||||
# expression temporaries in registers (-fno-tree-ter) - but every loop body
|
||||
# here contains a call, so a register held across it costs more than the
|
||||
# load-immediate it saves. The set is fitted to the loader's body and has to
|
||||
# be re-measured when that body changes: -fno-move-loop-invariants belonged
|
||||
# here while the command loop carried four transfer bodies and costs bytes
|
||||
# now that it carries one, and -fno-ivopts is fitted per backend — an
|
||||
# now that it carries one, and -fno-ivopts is fitted per backend - an
|
||||
# autobaud body needs ivopts to keep the calibration countdown a single
|
||||
# induction variable (without it the counter is duplicated and the
|
||||
# measurement loop runs 9 cycles instead of its contracted 7), while the
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// pureboot — a serial bootloader on libavr: one C++ source, no inline
|
||||
// pureboot - a serial bootloader on libavr: one C++ source, no inline
|
||||
// assembly, no global register variables, a 512-byte slot on every chip
|
||||
// libavr targets. The device speaks primitives; every composite (verify, erase,
|
||||
// reset-vector surgery, self-update) lives in the host tool. Protocol,
|
||||
// deployment and configuration: README.md next to this file.
|
||||
//
|
||||
// The image is position-independent — PC-relative control flow and wire
|
||||
// addresses in, no absolute address formed anywhere — so the identical binary
|
||||
// The image is position-independent - PC-relative control flow and wire
|
||||
// addresses in, no absolute address formed anywhere - so the identical binary
|
||||
// runs from any slot. That is what makes a copy one slot below able to rewrite
|
||||
// the resident one, and every change here has to keep it (test/check_pi.py).
|
||||
// It does not need to know *which* slot it is in: nothing here refuses an
|
||||
@@ -25,12 +25,20 @@ namespace {
|
||||
// Purely polled: every interrupt guard folds to nothing.
|
||||
constexpr auto off = avr::irq::guard_policy::unused;
|
||||
|
||||
// The EEPROM procedure's step 2 - wait until SPMEN clears - guards a flash
|
||||
// operation still in flight, and this loader never has one when it touches
|
||||
// the EEPROM: commit() waits every boot-sectioned page operation out before
|
||||
// the ack, and everywhere else the CPU halts through the operation itself.
|
||||
// The posture states the omission the datasheet grants for exactly that
|
||||
// (DS40002061B section 8.6.3).
|
||||
constexpr auto no_spm = ee::spm_interlock::omitted;
|
||||
|
||||
constexpr std::uint8_t ack = '+';
|
||||
|
||||
// The refusal, which is the ack inverted: on a link whose whole problem is
|
||||
// flipped bits, the byte saying "nothing happened" should be as far as a byte
|
||||
// can be from the one saying "it did", and the complement is all eight bits.
|
||||
// It is also the only spelling that needs no justifying — every other value
|
||||
// It is also the only spelling that needs no justifying - every other value
|
||||
// would be a choice.
|
||||
constexpr std::uint8_t nak = static_cast<std::uint8_t>(~ack);
|
||||
|
||||
@@ -41,7 +49,7 @@ constexpr std::uint8_t nak = static_cast<std::uint8_t>(~ack);
|
||||
constexpr std::uint8_t seal = 0x5a;
|
||||
|
||||
// The opcode, as bits rather than letters. Each is a one-instruction skip,
|
||||
// where a set of arbitrary values costs a compare and a branch apiece — and
|
||||
// where a set of arbitrary values costs a compare and a branch apiece - and
|
||||
// with the seal deciding what is a command at all, there is nothing left for a
|
||||
// readable spelling to buy. A transfer is the absence of the other three, and
|
||||
// its direction is the low bit.
|
||||
@@ -50,7 +58,7 @@ constexpr std::uint8_t seal = 0x5a;
|
||||
// the knock. Two things follow that no other assignment gives. A host cannot
|
||||
// know which generation it is talking to until something has answered, so the
|
||||
// command reporting the version has to mean the same thing before the version
|
||||
// is known — 'b' still asks it. And a knock aimed at a loader that is
|
||||
// is known - 'b' still asks it. And a knock aimed at a loader that is
|
||||
// *already* in session has to stay harmless: with no opcode reserved as
|
||||
// invalid, every byte now starts a command, so a knock that meant nothing to
|
||||
// earlier generations would otherwise consume the five header bytes behind it
|
||||
@@ -60,11 +68,11 @@ enum : std::uint8_t { op_write = 1, op_fill = 4, op_jump = 8, op_identify = 0x20
|
||||
|
||||
// Deployment parameters come from the build (pureboot_add_loader()). The
|
||||
// signature is not one of them: the chip database is the only universal
|
||||
// source — a tiny13A cannot read its own signature row from code. An autobaud
|
||||
// source - a tiny13A cannot read its own signature row from code. An autobaud
|
||||
// build carries no clock and no baud at all; it measures both.
|
||||
#if !defined(PUREBOOT_AUTOBAUD) && (!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(), or PUREBOOT_AUTOBAUD for a clock-free one (README.md)"
|
||||
"PUREBOOT_CLOCK_HZ and PUREBOOT_BAUD select this build's clock and baud - create loader targets with pureboot_add_loader(), or PUREBOOT_AUTOBAUD for a clock-free one (README.md)"
|
||||
#endif
|
||||
|
||||
#if !defined(PUREBOOT_AUTOBAUD)
|
||||
@@ -73,8 +81,8 @@ constexpr avr::baud_t wire_baud{PUREBOOT_BAUD};
|
||||
#endif
|
||||
|
||||
// The loader owns the top 512 bytes; a staging copy goes in the slot below.
|
||||
// Chips without a hardware boot section — the tinies and the m48s, whose SPM
|
||||
// runs from anywhere (Atmel-8271 §26) — keep the application's relocated
|
||||
// Chips without a hardware boot section - the tinies and the m48s, whose SPM
|
||||
// runs from anywhere (Atmel-8271 section 26) - keep the application's relocated
|
||||
// reset vector in the word under the slot. The size itself is the linker's and
|
||||
// the host's business: nothing in here needs to know where the slot ends.
|
||||
constexpr std::uint16_t page = spm::page_bytes;
|
||||
@@ -101,23 +109,23 @@ constexpr avr::uint24_t autobaud_budget = PUREBOOT_AUTOBAUD_POLLS;
|
||||
|
||||
// A build may bake a measured oscillator trim (README.md: the RC-oscillator
|
||||
// deployment answer); the byte is applied at the top of run(). Orthogonal to
|
||||
// the serial backend — an autobaud build may carry it for the application's
|
||||
// the serial backend - an autobaud build may carry it for the application's
|
||||
// benefit alone.
|
||||
#if defined(PUREBOOT_OSCCAL)
|
||||
static_assert(PUREBOOT_OSCCAL >= 0 && PUREBOOT_OSCCAL <= 0xff, "PUREBOOT_OSCCAL is one OSCCAL byte");
|
||||
#endif
|
||||
|
||||
// The loader's one identity number. The protocol carries none of its own —
|
||||
// The loader's one identity number. The protocol carries none of its own -
|
||||
// a version implies it, and the host tool holds that map (README.md).
|
||||
constexpr std::uint8_t version = 9;
|
||||
|
||||
// The image's identity stamp, for the host tool rather than for the wire: an
|
||||
// update image is a bare 512-byte slot, and without this nothing in it says
|
||||
// which chip it was built for. The tool refuses to install an image whose
|
||||
// stamp does not match the device — flashing a foreign loader bricks the
|
||||
// stamp does not match the device - flashing a foreign loader bricks the
|
||||
// target, and the loader itself cannot check what has already replaced it.
|
||||
//
|
||||
// Never read from flash by the loader — 'b' answers out of this array, but at
|
||||
// Never read from flash by the loader - 'b' answers out of this array, but at
|
||||
// constant indices, so those fold to immediates and no runtime address of it
|
||||
// is ever formed. `used` keeps the compiler from dropping the copy the host
|
||||
// needs and `retain` keeps --gc-sections from collecting it.
|
||||
@@ -141,7 +149,7 @@ constexpr std::uint8_t stamp_identity = 2;
|
||||
// SPMCSR and fires the instruction at the transfer's address, which is how
|
||||
// page erase, page write and RWW re-enable reach the wire without the loader
|
||||
// carrying a command for each. The hardware's four-cycle store-to-SPM window
|
||||
// is why this is one fused primitive and not a poke of SPMCSR — no host can
|
||||
// is why this is one fused primitive and not a poke of SPMCSR - no host can
|
||||
// hit that window across a serial link.
|
||||
//
|
||||
// It is the one space with no direction: the opcode's write bit is not
|
||||
@@ -151,7 +159,7 @@ constexpr std::uint8_t stamp_identity = 2;
|
||||
// trade the running-slot guard lost.
|
||||
enum : std::uint8_t { sp_flash = 0, sp_eeprom = 1, sp_data = 2, sp_fuse = 3, sp_spm = 4 };
|
||||
|
||||
// A selector's high nibble is the flash bank — the address bits above the
|
||||
// A selector's high nibble is the flash bank - the address bits above the
|
||||
// 16-bit wire address, RAMPZ on the chips that have one. Keeping it here
|
||||
// rather than widening the wire address is what lets one 16-bit cursor serve
|
||||
// every space: a 24-bit cursor would pay its extra byte on EEPROM and data
|
||||
@@ -195,9 +203,9 @@ constexpr int usart_unit = 0;
|
||||
#endif
|
||||
|
||||
// One-wire on the hardware USART (PUREBOOT_HALF_DUPLEX): RXD and TXD tied
|
||||
// together off-chip, exactly one direction enabled at a time — the library's
|
||||
// together off-chip, exactly one direction enabled at a time - the library's
|
||||
// .half_duplex turn-around. The activation window is unchanged; only its
|
||||
// poll grows the release-line test rx_ready() carries in this mode.
|
||||
// poll grows the release-line test readable() carries in this mode.
|
||||
constexpr bool hw_half_duplex =
|
||||
#if defined(PUREBOOT_HALF_DUPLEX)
|
||||
true;
|
||||
@@ -207,19 +215,24 @@ constexpr bool hw_half_duplex =
|
||||
|
||||
template <avr::hertz_t C, avr::baud_t B>
|
||||
struct hardware_link {
|
||||
using uart = avr::uart::usart<usart_unit, C, {.baud = B, .max_baud_error = 2.5_pct, .half_duplex = hw_half_duplex}>;
|
||||
// The deployment envelope is the build's: pureboot_baud_feasible() holds
|
||||
// every configured rate within 2.5 %, which the bench has proven across
|
||||
// the fleet, and the datasheet's stricter per-frame tolerance table would
|
||||
// refuse the stock 115200 at 16 MHz (+2.1 %) that every deployed board
|
||||
// runs. .allow_baud_error states that this is meant.
|
||||
using uart = avr::uart::usart<usart_unit, C, {.baud = B, .allow_baud_error = true, .half_duplex = hw_half_duplex}>;
|
||||
|
||||
// The compiled idle poll around the window's narrow (uint24_t) countdown:
|
||||
// the RXC test, then sbiw + sbci + brne (5). The test's cost follows the
|
||||
// status register's home — a 2-cycle bit-skip where UCSRnA sits in
|
||||
// status register's home - a 2-cycle bit-skip where UCSRnA sits in
|
||||
// bit-addressable I/O (the classic megas), lds + skip (4) in extended
|
||||
// I/O. Half-duplex polls through rx_ready()'s release-line test, which
|
||||
// I/O. Half-duplex polls through readable()'s release-line test, which
|
||||
// -Os outlines: the rcall (3), the UCSR#B read and not-taken skip with
|
||||
// the jump over the write (I/O 3, extended 5), the ret (4) — and the
|
||||
// the jump over the write (I/O 3, extended 5), the ret (4) - and the
|
||||
// call in the loop body pushes the countdown into call-saved registers,
|
||||
// where the uint24_t step is ldi+sub+sbc+sbc (4) instead of sbiw+sbci
|
||||
// (3). Measured off the built loops: 18 a poll in bit-addressable I/O,
|
||||
// 22 in extended. A uint32_t countdown pays one more sbci —
|
||||
// 22 in extended. A uint32_t countdown pays one more sbci -
|
||||
// window_polls() adds it where the count forces the wide type. Held per
|
||||
// chip by the pureboot.window gates. The lookup rides the baud parameter
|
||||
// so it stays dependent: the trait is an incomplete type on the
|
||||
@@ -228,8 +241,9 @@ struct hardware_link {
|
||||
template <avr::baud_t Baud, typename U = avr::hw::usart_of<usart_unit>>
|
||||
static consteval std::uint8_t poll_cost()
|
||||
{
|
||||
if (hw_half_duplex)
|
||||
if (hw_half_duplex) {
|
||||
return U::ucsra::addr < 0x40 ? 18 : 22;
|
||||
}
|
||||
return U::ucsra::addr < 0x40 ? 7 : 9;
|
||||
}
|
||||
static constexpr std::uint8_t poll_cycles = poll_cost<B>();
|
||||
@@ -241,7 +255,7 @@ struct hardware_link {
|
||||
|
||||
static bool pending()
|
||||
{
|
||||
return uart::rx_ready();
|
||||
return uart::readable();
|
||||
}
|
||||
|
||||
static std::uint8_t rx()
|
||||
@@ -256,7 +270,7 @@ struct hardware_link {
|
||||
|
||||
static void drain()
|
||||
{
|
||||
// A drain here always follows this link's own write — the frame is
|
||||
// A drain here always follows this link's own write - the frame is
|
||||
// in flight by construction, so the completion the wait needs is
|
||||
// guaranteed and the bounded default's countdown would be dead bytes.
|
||||
uart::drain_unbounded();
|
||||
@@ -273,7 +287,7 @@ struct software_link {
|
||||
|
||||
// The compiled idle poll around the window's narrow (uint24_t) countdown:
|
||||
// sbis skipping the exit (2), sbiw + sbci + brne (5). A uint32_t
|
||||
// countdown pays one more sbci — window_polls() adds it where the count
|
||||
// countdown pays one more sbci - window_polls() adds it where the count
|
||||
// forces the wide type. Held by the pureboot.window gate.
|
||||
static constexpr std::uint8_t poll_cycles = 7;
|
||||
|
||||
@@ -305,12 +319,12 @@ struct software_link {
|
||||
|
||||
// The clock-free link: the bit period is measured from the host's calibration
|
||||
// pulse instead of derived from a clock, so one image serves every F_CPU and
|
||||
// every rate. Activation differs in kind from the other two — there is no
|
||||
// clock to time a window against — so this backend brings its own, below.
|
||||
// every rate. Activation differs in kind from the other two - there is no
|
||||
// clock to time a window against - so this backend brings its own, below.
|
||||
struct autobaud_link {
|
||||
// The unit in GPIOR2:GPIOR1 where the chip has them: the loader owns the
|
||||
// whole chip while it runs, and the pair costs one word per access where
|
||||
// the RAM word costs two — six words across the image.
|
||||
// the RAM word costs two - six words across the image.
|
||||
using uart = avr::uart::software_autobaud<avr::PUREBOOT_RX, avr::PUREBOOT_TX, avr::uart::unit_home::gpior>;
|
||||
|
||||
static void init()
|
||||
@@ -330,7 +344,7 @@ struct autobaud_link {
|
||||
|
||||
static void drain()
|
||||
{
|
||||
// A drain here always follows this link's own write — the frame is
|
||||
// A drain here always follows this link's own write - the frame is
|
||||
// in flight by construction, so the completion the wait needs is
|
||||
// guaranteed and the bounded default's countdown would be dead bytes.
|
||||
uart::drain_unbounded();
|
||||
@@ -350,7 +364,7 @@ using link = std::conditional_t<avr::uart::has_usart<usart_unit>(), hardware_lin
|
||||
#endif
|
||||
|
||||
// The application's entry, pinned by the linker (--defsym): word 0 on a
|
||||
// boot-sectioned mega, the trampoline at base − 2 elsewhere. Reaching it must
|
||||
// boot-sectioned mega, the trampoline at base - 2 elsewhere. Reaching it must
|
||||
// not depend on where this copy runs, so the jump goes through a pointer, and
|
||||
// [[gnu::noipa]] keeps the constant from folding back into a relative call.
|
||||
extern "C" [[noreturn]] void pureboot_app();
|
||||
@@ -368,36 +382,38 @@ extern "C" [[noreturn]] void pureboot_app();
|
||||
|
||||
// Activation: a bounded wait for the host, then the knock. Both forms boot the
|
||||
// application when the window closes on an idle line, and both bound *every*
|
||||
// wait — a knock awaited without a deadline would let one stray edge hold an
|
||||
// wait - a knock awaited without a deadline would let one stray edge hold an
|
||||
// unattended device in the loader forever.
|
||||
#if defined(PUREBOOT_AUTOBAUD)
|
||||
// The window is a fixed poll budget: with no clock, whole seconds cannot be
|
||||
// timed. A uint24_t holds it — a fourth byte would cost two words at every
|
||||
// timed. A uint24_t holds it - a fourth byte would cost two words at every
|
||||
// countdown step for range never used.
|
||||
void await_host()
|
||||
{
|
||||
for (;;) {
|
||||
if (!link::uart::calibrate(autobaud_budget))
|
||||
if (!link::uart::calibrate(autobaud_budget)) {
|
||||
run_app();
|
||||
}
|
||||
// The calibration pulse has already proven a host is there, so one
|
||||
// byte activates. A knock that never arrives falls back to calibrate(),
|
||||
// whose own budget then boots the application.
|
||||
if (link::uart::template read<off>(autobaud_budget) == 'p')
|
||||
if (link::uart::template read<off>(autobaud_budget) == 'p') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
// The window as one countdown, divided by the backend's counted poll-loop
|
||||
// cycles. Whole seconds is all it promises. The per-poll cost depends on the
|
||||
// countdown's own width (a uint32_t decrement chain is one sbci longer), and
|
||||
// the width depends on the poll count — solved narrow-first: a count that
|
||||
// the width depends on the poll count - solved narrow-first: a count that
|
||||
// fits 24 bits at the narrow cost keeps the narrow loop, anything else takes
|
||||
// the wide loop at its own cost. A count fitting 24 bits only at the wide
|
||||
// cost stays wide, so the choice cannot oscillate on the boundary.
|
||||
consteval std::uint32_t polls_at(std::uint32_t per_poll)
|
||||
{
|
||||
// Whole-window cycles first, then the per-poll division: one truncation
|
||||
// instead of one per second. Same instructions either way — only the
|
||||
// instead of one per second. Same instructions either way - only the
|
||||
// countdown's immediate moves.
|
||||
return static_cast<std::uint32_t>(dev::cycles_for<std::chrono::seconds{timeout_seconds}>() / per_poll);
|
||||
}
|
||||
@@ -421,8 +437,9 @@ bool pending_before_deadline()
|
||||
{
|
||||
window_t polls = window_polls();
|
||||
do {
|
||||
if (link::pending())
|
||||
if (link::pending()) {
|
||||
return true;
|
||||
}
|
||||
} while (--polls);
|
||||
return false;
|
||||
}
|
||||
@@ -431,8 +448,9 @@ bool pending_before_deadline()
|
||||
// application runs.
|
||||
std::uint8_t rx_deadline()
|
||||
{
|
||||
if (!pending_before_deadline())
|
||||
if (!pending_before_deadline()) {
|
||||
run_app();
|
||||
}
|
||||
return link::rx();
|
||||
}
|
||||
|
||||
@@ -452,7 +470,7 @@ void await_host()
|
||||
return static_cast<std::uint16_t>(low | (link::rx() << 8));
|
||||
}
|
||||
|
||||
// The wire's byte pair as the word it is — AVR is little-endian too, so the
|
||||
// The wire's byte pair as the word it is - AVR is little-endian too, so the
|
||||
// cast is the identity a shift-and-or spelling makes the compiler rediscover.
|
||||
// Callers read into named variables first: the wire order is a sequence of
|
||||
// reads, not an argument order.
|
||||
@@ -471,10 +489,11 @@ void await_host()
|
||||
// A wire address and its selector's bank as the flash address they name.
|
||||
[[gnu::always_inline]] inline spm::flash_address_t flash_address([[maybe_unused]] std::uint8_t bank, std::uint16_t at)
|
||||
{
|
||||
if constexpr (banked_flash)
|
||||
if constexpr (banked_flash) {
|
||||
return (static_cast<spm::flash_address_t>(bank) << 16) | at;
|
||||
else
|
||||
} else {
|
||||
return at;
|
||||
}
|
||||
}
|
||||
|
||||
// One byte out of any space. Every accessor shares the transfer's cursor, its
|
||||
@@ -483,20 +502,24 @@ void await_host()
|
||||
[[gnu::always_inline]] inline std::uint8_t load(std::uint8_t space, [[maybe_unused]] std::uint8_t bank,
|
||||
std::uint16_t at)
|
||||
{
|
||||
if (space == sp_eeprom)
|
||||
return ee::read(at);
|
||||
if (space == sp_data)
|
||||
if (space == sp_eeprom) {
|
||||
return ee::read<no_spm>(at);
|
||||
}
|
||||
if (space == sp_data) {
|
||||
return *reinterpret_cast<volatile std::uint8_t *>(at);
|
||||
if (space == sp_fuse)
|
||||
}
|
||||
if (space == sp_fuse) {
|
||||
return spm::read_fuse<off>(static_cast<spm::fuse>(at));
|
||||
if constexpr (banked_flash)
|
||||
}
|
||||
if constexpr (banked_flash) {
|
||||
return avr::flash_load_far<std::uint8_t>(flash_address(bank, at));
|
||||
else
|
||||
} else {
|
||||
return avr::flash_load(reinterpret_cast<const std::uint8_t *>(at));
|
||||
}
|
||||
}
|
||||
|
||||
// One byte into a writable space. Flash is not one of them — it arrives a
|
||||
// page at a time through 'W' and is committed by the sealed SPM command — and
|
||||
// One byte into a writable space. Flash is not one of them - it arrives a
|
||||
// page at a time through 'W' and is committed by the sealed SPM command - and
|
||||
// the fuses are not writable at all: SPM reaches flash and boot lock bits only.
|
||||
[[gnu::always_inline]] inline void store(std::uint8_t space, std::uint16_t at, std::uint8_t value)
|
||||
{
|
||||
@@ -506,7 +529,7 @@ void await_host()
|
||||
}
|
||||
// Host-paced: the ack goes out once the write has begun, so the next byte
|
||||
// arrives while it completes and nothing is missed without a buffer.
|
||||
ee::write<off>(at, value);
|
||||
ee::write<off, no_spm>(at, value);
|
||||
}
|
||||
|
||||
// The irreversible half of the protocol, and the whole of it: page erase, page
|
||||
@@ -517,8 +540,8 @@ void await_host()
|
||||
//
|
||||
// Nothing here refuses an address. A loader that will not write its own slot
|
||||
// cannot plant anything in it either, and a resident copy able to rewrite its
|
||||
// own trailing page is what lets a 512-byte boot section — where no staging
|
||||
// copy can run SPM at all — carry an SPM primitive for an application-side
|
||||
// own trailing page is what lets a 512-byte boot section - where no staging
|
||||
// copy can run SPM at all - carry an SPM primitive for an application-side
|
||||
// installer to drive. The protection that made the guard look necessary is the
|
||||
// seal: a wire fault can no longer name an address, only a host can, and a host
|
||||
// that names this one means it.
|
||||
@@ -528,17 +551,18 @@ void await_host()
|
||||
// Only a boot-sectioned mega runs on while its RWW section programs;
|
||||
// everywhere else the CPU halts through erase and write, so the wait
|
||||
// is already over by the time it returns.
|
||||
if constexpr (boot_section)
|
||||
if constexpr (boot_section) {
|
||||
spm::wait();
|
||||
}
|
||||
}
|
||||
|
||||
// One page into the SPM buffer, and only that: the erase and the write that
|
||||
// commit it are host-issued sp_spm stores, which reach the same fused
|
||||
// store-and-SPM pair through the transfer path's own address and data.
|
||||
//
|
||||
// Nothing discards the buffer first: it is write-once per word (§26.2.1), so
|
||||
// Nothing discards the buffer first: it is write-once per word (section 26.2.1), so
|
||||
// filling over a refused page or an application's leavings programs stale
|
||||
// words — but a page write auto-erases it (§26.2.1; §19.2 on the tinies), so
|
||||
// words - but a page write auto-erases it (section 26.2.1; section 19.2 on the tinies), so
|
||||
// that write clears the condition and the host's read-back rewrites the page.
|
||||
void fill_page(std::uint8_t bank, std::uint16_t at)
|
||||
{
|
||||
@@ -546,10 +570,14 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
|
||||
// starts at the page base; the low byte of the cursor is the whole in-page
|
||||
// offset, since a page is aligned and never crosses a bank.
|
||||
std::uint16_t z = at & ~static_cast<std::uint16_t>(page - 1);
|
||||
// The receipt is the erase-first contract's token and costs nothing here:
|
||||
// the erase is the host's own sealed SPM command, before or after the fill
|
||||
// as it chooses (the page write clears a stale buffer either way, above).
|
||||
const auto open = spm::page::begin<spm::from::boot_section, off>(flash_address(bank, z));
|
||||
do {
|
||||
std::uint8_t low = link::rx();
|
||||
std::uint8_t high = link::rx();
|
||||
spm::fill<off>(flash_address(bank, z), word_of({low, high}));
|
||||
spm::fill<off>(open, flash_address(bank, z), word_of({low, high}));
|
||||
z += 2;
|
||||
} while (static_cast<std::uint8_t>(z) & (page - 1));
|
||||
}
|
||||
@@ -557,23 +585,24 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
|
||||
[[noreturn]] void run()
|
||||
{
|
||||
#if defined(PUREBOOT_OSCCAL)
|
||||
// The build's oscillator trim, ahead of everything — the WDRF bail
|
||||
// included — so every path out of reset, the watchdog hand-over to the
|
||||
// The build's oscillator trim, ahead of everything - the WDRF bail
|
||||
// included - so every path out of reset, the watchdog hand-over to the
|
||||
// application first among them, runs on the corrected clock.
|
||||
avr::clock::calibrate(PUREBOOT_OSCCAL);
|
||||
#endif
|
||||
// A watchdog reset belongs to the application, whose watchdog stays forced
|
||||
// on until it clears WDRF — no activation window in its way.
|
||||
if (avr::power::peek_reset_cause().watchdog)
|
||||
// on until it clears WDRF - no activation window in its way.
|
||||
if (avr::power::peek_reset_cause().watchdog) {
|
||||
run_app();
|
||||
}
|
||||
|
||||
link::init();
|
||||
await_host();
|
||||
|
||||
for (;;) {
|
||||
// No prompt while an EEPROM write runs: it blocks SPM and fuse reads
|
||||
// (§26.2.1), and the prompt is the previous command's completion ack.
|
||||
ee::wait();
|
||||
// (section 26.2.1), and the prompt is the previous command's completion ack.
|
||||
ee::wait<no_spm>();
|
||||
tx_ack();
|
||||
const std::uint8_t command = link::rx();
|
||||
if (command & op_identify) {
|
||||
@@ -582,15 +611,16 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
|
||||
// about what this loader is. The indices are constant and the array
|
||||
// is constexpr, so these are immediates, not flash reads: nothing
|
||||
// here needs the stamp's runtime address. Unsealed, because it
|
||||
// takes no argument and changes nothing — and because a command
|
||||
// takes no argument and changes nothing - and because a command
|
||||
// that cannot be got wrong is what a lost host resynchronises on.
|
||||
for (std::uint8_t at = stamp_identity; at != sizeof identity_stamp; ++at)
|
||||
for (std::uint8_t at = stamp_identity; at != sizeof identity_stamp; ++at) {
|
||||
link::tx(identity_stamp[at]);
|
||||
}
|
||||
} else {
|
||||
// One decode, one cursor and one loop for every space, both
|
||||
// directions and the jump: a command per memory would carry a copy
|
||||
// of all three each. The jump — the hand-over and staging transfer
|
||||
// — carries a selector it ignores so its address rides the same two
|
||||
// of all three each. The jump - the hand-over and staging transfer
|
||||
// - carries a selector it ignores so its address rides the same two
|
||||
// reads as everything else; the page fill joins the same decode
|
||||
// rather than keeping an address form of its own, so flash
|
||||
// addressing is uniform across every command that names it. Both
|
||||
@@ -610,8 +640,8 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
|
||||
// whole point. An ack reports a command that has already run, and
|
||||
// for the one command that cannot be taken back a report is not a
|
||||
// defence. Once the running-slot guard is gone the address is as
|
||||
// fatal as the command byte — a wrong one reaches the loader's own
|
||||
// page — so the seal covers the act and the place together, and a
|
||||
// fatal as the command byte - a wrong one reaches the loader's own
|
||||
// page - so the seal covers the act and the place together, and a
|
||||
// stream that lost or mangled either cannot produce it.
|
||||
//
|
||||
// Folded here, after the last read, and never accumulated across
|
||||
@@ -619,7 +649,7 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
|
||||
// command needs it anyway, so the fold costs one xor each and no
|
||||
// register. An accumulator would have to survive four calls, and
|
||||
// paying for that in call-saved registers costs more than the whole
|
||||
// check costs in arithmetic — measured at fourteen bytes, on a
|
||||
// check costs in arithmetic - measured at fourteen bytes, on a
|
||||
// budget of ten.
|
||||
std::uint8_t fold = command;
|
||||
fold ^= selector;
|
||||
@@ -628,15 +658,15 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
|
||||
fold ^= count;
|
||||
fold ^= sealed;
|
||||
// The verdict, and it is not a courtesy. Every command whose
|
||||
// payload the host sends without waiting — a page fill, a write
|
||||
// burst — would otherwise be handed to a loader that has already
|
||||
// payload the host sends without waiting - a page fill, a write
|
||||
// burst - would otherwise be handed to a loader that has already
|
||||
// gone back to reading commands, so a *detected* error would
|
||||
// become the desync the seal exists to prevent: a 128-byte page
|
||||
// read as command headers is twenty-one more chances at the one in
|
||||
// two hundred and fifty-six. Answering the seal before the payload
|
||||
// is what keeps a refusal local to the command that earned it.
|
||||
//
|
||||
// An unknown opcode lands here too — every bit pattern is now some
|
||||
// An unknown opcode lands here too - every bit pattern is now some
|
||||
// command, so it is the seal, not a table of valid letters, that
|
||||
// rejects noise, and the host hears about it either way.
|
||||
if (fold != seal) {
|
||||
@@ -650,7 +680,7 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
|
||||
fill_page(bank, at);
|
||||
} else if (space == sp_spm) {
|
||||
// An SPM command is the whole of what this loader can do
|
||||
// that doing again will not undo, and it is one byte — so
|
||||
// that doing again will not undo, and it is one byte - so
|
||||
// it rides the count field, inside the seal, rather than
|
||||
// arriving as data after the seal has been checked. Which
|
||||
// is also what makes a deliberate lock-bit write
|
||||
@@ -663,8 +693,9 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
|
||||
if (command & op_write) {
|
||||
store(space, at, link::rx());
|
||||
tx_ack();
|
||||
} else
|
||||
} else {
|
||||
link::tx(load(space, bank, at));
|
||||
}
|
||||
++at;
|
||||
} while (--count);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""pureboot host tool — the smart half of the protocol (README.md).
|
||||
"""pureboot host tool - the smart half of the protocol (README.md).
|
||||
|
||||
The device exposes primitives; everything composite is here: HEX/raw images,
|
||||
programming with repairing read-back verification, the reset-vector surgery
|
||||
@@ -29,10 +29,10 @@ PROMPT = b"+"
|
||||
# The refusal, from pureboot 9: the prompt inverted, so no single flipped bit
|
||||
# turns "nothing happened" into "it did".
|
||||
NAK = bytes((~PROMPT[0] & 0xFF,))
|
||||
VERSION = 10 # this tool's own version — free to drift from a loader's
|
||||
VERSION = 10 # this tool's own version - free to drift from a loader's
|
||||
# The loader versions this tool can drive. A pureboot version implies its wire
|
||||
# protocol, which carries no number of its own, so this window is where that
|
||||
# map lives: the tool keeps a decoder for every generation in it (1–4 speak
|
||||
# map lives: the tool keeps a decoder for every generation in it (1-4 speak
|
||||
# the per-memory commands, 5 the unified pair; 6 marks the OSCCAL-carrying
|
||||
# builds and changes nothing on the wire; 8 the one-wire deployments, whose
|
||||
# only host-side trace is the --one-wire echo discard; 9 seals every command
|
||||
@@ -46,20 +46,20 @@ RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
||||
# pureboot 5 replaced the four per-memory commands with one pair: 'G' reads and
|
||||
# 'g' writes, each taking a selector byte, a 16-bit address and a count, over
|
||||
# the spaces below. The loader carries one transfer loop instead of four bodies
|
||||
# — which is what buys the data space and the host-issued SPM operations.
|
||||
# - which is what buys the data space and the host-issued SPM operations.
|
||||
# 6 marks the builds that may carry a baked OSCCAL trim, nothing on the wire;
|
||||
# 7 gives 'J' a selector byte (older loaders take the bare address — jump()
|
||||
# 7 gives 'J' a selector byte (older loaders take the bare address - jump()
|
||||
# sends each form to the version that speaks it) and re-homes the autobaud
|
||||
# unit into the GPIOR pair where the chip has one; 8 marks the builds whose
|
||||
# deployment may be one-wire (hardware half-duplex, or a software link folded
|
||||
# onto one pin) — nothing on the wire either, but a shared line makes the
|
||||
# onto one pin) - nothing on the wire either, but a shared line makes the
|
||||
# host read its own bytes back, which is what --one-wire consumes.
|
||||
UNIFIED_LOADER = 5
|
||||
SP_FLASH, SP_EEPROM, SP_RAM, SP_FUSE, SP_SPM = 0, 1, 2, 3, 4
|
||||
|
||||
# pureboot 9 replaces the command letters with bits and seals every command.
|
||||
# The header is one shape for all of them — opcode, selector, address, count,
|
||||
# seal — and the loader answers the seal *before* it acts: PROMPT accepts, NAK
|
||||
# The header is one shape for all of them - opcode, selector, address, count,
|
||||
# seal - and the loader answers the seal *before* it acts: PROMPT accepts, NAK
|
||||
# refuses and nothing happened. That verdict is what lets a refusal stay local
|
||||
# to its own command: the payload of a fill or a write burst only goes out
|
||||
# after the header has been accepted, so a rejected header never leaves the
|
||||
@@ -69,12 +69,12 @@ SP_FLASH, SP_EEPROM, SP_RAM, SP_FUSE, SP_SPM = 0, 1, 2, 3, 4
|
||||
# the low bit. Identify is bit 5 because both knock bytes carry it: 'b' has to
|
||||
# still ask the version (the host cannot know which generation it is talking to
|
||||
# until something answers), and 'p' has to stay harmless against a loader
|
||||
# already in session — pureboot 9 reserves no invalid opcode, so a knock that
|
||||
# already in session - pureboot 9 reserves no invalid opcode, so a knock that
|
||||
# meant nothing before would otherwise eat the five bytes behind it.
|
||||
SEALED_LOADER = 9
|
||||
OP_WRITE, OP_FILL, OP_JUMP, OP_IDENTIFY = 1, 4, 8, 0x20
|
||||
# What a sealed header's fields must fold to. Non-zero, so a run of one
|
||||
# repeated byte — a stuck line, a page of erased flash read as a header —
|
||||
# repeated byte - a stuck line, a page of erased flash read as a header -
|
||||
# cannot satisfy it.
|
||||
SEAL = 0x5A
|
||||
# An SPM command carries its SPMCSR byte in the count field, where the seal
|
||||
@@ -82,16 +82,16 @@ SEAL = 0x5A
|
||||
# the seal had already been checked, which is the hole the seal exists to close.
|
||||
|
||||
# An autobaud loader keeps its measured bit period readable, encoded as
|
||||
# delay-loop counts: (bit cycles − UNIT_DISCOUNT) / UNIT_LOOP_CYCLES,
|
||||
# floored — the spin granule and per-bit overhead of libavr's software UART.
|
||||
# delay-loop counts: (bit cycles - UNIT_DISCOUNT) / UNIT_LOOP_CYCLES,
|
||||
# floored - the spin granule and per-bit overhead of libavr's software UART.
|
||||
# v5/v6 keep it at ram_start; v7 moves it into GPIOR2:GPIOR1 on the chips
|
||||
# that have the pair (their data addresses are in the geometry) and keeps
|
||||
# ram_start only where they do not exist. --info undoes the encoding to
|
||||
# report the true clock, which therefore sits within one granule below it.
|
||||
UNIT_LOOP_CYCLES, UNIT_DISCOUNT = 4, 8
|
||||
|
||||
# A selector's high nibble is the flash bank — the address bits above the 16-bit
|
||||
# wire address — so a transfer names a byte address within one 64 KiB bank and
|
||||
# A selector's high nibble is the flash bank - the address bits above the 16-bit
|
||||
# wire address - so a transfer names a byte address within one 64 KiB bank and
|
||||
# no command has to speak word addresses. No single transfer may cross a bank
|
||||
# boundary; the host chunks to keep that true.
|
||||
def selector(space, address):
|
||||
@@ -104,7 +104,7 @@ def selector(space, address):
|
||||
SPM_ERASE, SPM_WRITE, SPM_RWWSRE = 0x03, 0x05, 0x11
|
||||
|
||||
# Calibration byte for an autobaud loader: 0xC0 is a start bit plus six zero
|
||||
# data bits — one low pulse of seven bit-times, which the loader times into its
|
||||
# data bits - one low pulse of seven bit-times, which the loader times into its
|
||||
# per-bit unit. Sent at whatever baud the host chose; the loader locks to it.
|
||||
CALIBRATE = 0xC0
|
||||
|
||||
@@ -119,7 +119,7 @@ CHIP_GEOMETRY = {
|
||||
# ram_start is where SRAM begins in data space: the classic megas and the
|
||||
# tinies keep it right after the plain I/O registers (0x60), the x8/x4
|
||||
# generations past their extended I/O file (0x100). gpior1 is GPIOR1's
|
||||
# data address — 0x32 on the t25/45/85, 0x4A from the x8 generation on,
|
||||
# data address - 0x32 on the t25/45/85, 0x4A from the x8 generation on,
|
||||
# None where the chip has no pair (t13, classic megas). A v7 autobaud
|
||||
# loader's measured bit period lives in GPIOR2:GPIOR1 where they exist
|
||||
# and at exactly ram_start elsewhere (its only RAM object; the loader's
|
||||
@@ -164,7 +164,7 @@ class Error(Exception):
|
||||
|
||||
|
||||
class Progress:
|
||||
"""A transient bar on stderr, drawn only for a tty and erased when done —
|
||||
"""A transient bar on stderr, drawn only for a tty and erased when done -
|
||||
logs and pipes see only the summary line each operation prints. No label
|
||||
or a zero total disables it, so callers can pass one unconditionally."""
|
||||
|
||||
@@ -203,7 +203,7 @@ class Progress:
|
||||
|
||||
class PosixPort:
|
||||
"""A raw serial port with deadline-based reads, over termios. A rate with
|
||||
no B-constant — the off-nominal probes `--scan` walks — goes through
|
||||
no B-constant - the off-nominal probes `--scan` walks - goes through
|
||||
Linux's termios2 BOTHER; a platform without that ioctl refuses the rate
|
||||
by name."""
|
||||
|
||||
@@ -252,7 +252,7 @@ class PosixPort:
|
||||
raise
|
||||
|
||||
def set_baud(self, baud):
|
||||
"""Retune the port without closing it — the fd stays open, so no DTR
|
||||
"""Retune the port without closing it - the fd stays open, so no DTR
|
||||
pulse and no reset. That matters: the only caller is mid-session with a
|
||||
loader copy that a reset would throw away."""
|
||||
self._apply_baud(termios.tcgetattr(self.fd), baud)
|
||||
@@ -285,7 +285,7 @@ class PosixPort:
|
||||
|
||||
|
||||
if os.name == "nt":
|
||||
# The same port, over the Win32 serial API — kernel32 through ctypes, so
|
||||
# The same port, over the Win32 serial API - kernel32 through ctypes, so
|
||||
# the tool stays standard-library only. Timeouts live in the driver
|
||||
# (COMMTIMEOUTS) rather than in a readiness call: Windows has no select()
|
||||
# for a COM handle, so each read asks the driver for its own deadline.
|
||||
@@ -350,7 +350,7 @@ if os.name == "nt":
|
||||
|
||||
def __init__(self, path, baud):
|
||||
# Win32 takes the rate as a plain integer, so unlike termios any
|
||||
# rate the hardware can divide down to is available — but a driver
|
||||
# rate the hardware can divide down to is available - but a driver
|
||||
# may also accept one it cannot produce (an FT232R takes a baud of
|
||||
# 3, reports it back, and goes on using the previous divisor).
|
||||
# Only obvious nonsense is refusable; the rest is the driver's word.
|
||||
@@ -375,14 +375,14 @@ if os.name == "nt":
|
||||
# fBinary, and DTR/RTS asserted (fDtrControl and fRtsControl,
|
||||
# two bits each, = _ENABLE); every other flag clear, so no
|
||||
# parity and no flow control. Raising both matches what opening
|
||||
# a POSIX tty does — including the reset pulse on the boards
|
||||
# a POSIX tty does - including the reset pulse on the boards
|
||||
# that wire DTR to it.
|
||||
dcb.fBits = 0x1 | (1 << 4) | (1 << 12)
|
||||
if not _k32.SetCommState(self.handle, ctypes.byref(dcb)):
|
||||
_fail(f"cannot configure {path} for {baud} baud 8N1")
|
||||
# Arm them once here too: reads re-arm per call, but the write
|
||||
# timeout would otherwise stay at the driver's default — which
|
||||
# may be "wait forever" — until the first read.
|
||||
# timeout would otherwise stay at the driver's default - which
|
||||
# may be "wait forever" - until the first read.
|
||||
self._deadline(_GAP_MS, 1000)
|
||||
self.baud = baud
|
||||
except Error:
|
||||
@@ -393,7 +393,7 @@ if os.name == "nt":
|
||||
raise
|
||||
|
||||
def set_baud(self, baud):
|
||||
"""Retune the port on its live handle — SetCommState only, so the
|
||||
"""Retune the port on its live handle - SetCommState only, so the
|
||||
handle is never reopened and DTR never drops. That matters: the only
|
||||
caller is mid-session with a loader copy a reset would throw away."""
|
||||
if baud < 50:
|
||||
@@ -444,7 +444,7 @@ if os.name == "nt":
|
||||
def read_available(self, wait):
|
||||
"""Everything that arrives within `wait` seconds of quiet start."""
|
||||
# A zero total means *no* timeout to the driver, so never round
|
||||
# down to it — the same trap on the deadline below.
|
||||
# down to it - the same trap on the deadline below.
|
||||
self._deadline(_GAP_MS, max(1, round(wait * 1000)))
|
||||
return self._read(4096)
|
||||
|
||||
@@ -467,15 +467,15 @@ Port = WindowsPort if os.name == "nt" else PosixPort
|
||||
|
||||
class OneWirePort:
|
||||
"""The host side of a shared line (--one-wire): an FTDI-style adapter on
|
||||
a one-wire link reads back every byte it transmits — its RX is tied to
|
||||
a one-wire link reads back every byte it transmits - its RX is tied to
|
||||
its own TX through the line. Consume that echo at each write and verify
|
||||
it, which doubles as a wiring check: an echo that never comes is an RX
|
||||
not on the line, and is reported as itself instead of decoding as a
|
||||
device reply.
|
||||
|
||||
The device's reply may interleave with the echo of a multi-byte write —
|
||||
The device's reply may interleave with the echo of a multi-byte write -
|
||||
a loader already in session re-prompts after the knock's first byte
|
||||
while the second is still queued behind that reply — so the echo is
|
||||
while the second is still queued behind that reply - so the echo is
|
||||
matched byte for byte and anything else arriving in between is device
|
||||
traffic, held for the next read."""
|
||||
|
||||
@@ -491,11 +491,11 @@ class OneWirePort:
|
||||
"""Put `data` on the line and consume its echo.
|
||||
|
||||
`blind` marks the protocol's one multi-byte write with no ack between
|
||||
its bytes — the knock. Aimed at a loader already in session, its first
|
||||
its bytes - the knock. Aimed at a loader already in session, its first
|
||||
byte draws a prompt while the second is still going out, and on real
|
||||
wiring the device's push-pull ack **wins the line** against the host's
|
||||
1 k series resistor: that second byte is *destroyed, not delayed*, and
|
||||
its echo never comes. Measured on an ATtiny13A at 57600 — the loader
|
||||
its echo never comes. Measured on an ATtiny13A at 57600 - the loader
|
||||
answers a single byte perfectly and loses the knock's second every
|
||||
time. So on a blind write a missing echo is a property of the wiring
|
||||
rather than a fault in it, and the caller's retry is what deals with
|
||||
@@ -504,7 +504,7 @@ class OneWirePort:
|
||||
"""
|
||||
data = bytes(data)
|
||||
self._port.write(data)
|
||||
# The echo arrives at line rate — 10 bits a byte — plus adapter
|
||||
# The echo arrives at line rate - 10 bits a byte - plus adapter
|
||||
# latency; a generous floor keeps slow rates and USB scheduling out
|
||||
# of the error path.
|
||||
deadline = time.monotonic() + 10 * len(data) / self._port.baud + 0.5
|
||||
@@ -513,7 +513,7 @@ class OneWirePort:
|
||||
# Speculative, so it cannot be read_exact, whose contract is to
|
||||
# raise: doing that made the diagnosis below unreachable on every
|
||||
# quiet line and surfaced a bare "timeout: got 0 of 1 bytes" in
|
||||
# its place — the one message this class exists to replace.
|
||||
# its place - the one message this class exists to replace.
|
||||
for byte in self._port.read_available(0.02):
|
||||
if remaining and byte == remaining[0]:
|
||||
remaining = remaining[1:]
|
||||
@@ -523,17 +523,17 @@ class OneWirePort:
|
||||
return
|
||||
if not blind:
|
||||
raise Error(f"one-wire echo missing after {len(data) - len(remaining)} of "
|
||||
f"{len(data)} byte(s) — is the adapter's RX tied to the line?")
|
||||
f"{len(data)} byte(s) - is the adapter's RX tied to the line?")
|
||||
self.lost_echoes += len(remaining)
|
||||
# Which loss this is matters, and the count says it. *Some* bytes lost is
|
||||
# the device's ack winning the line against the host's series resistor —
|
||||
# the device's ack winning the line against the host's series resistor -
|
||||
# ordinary, and what the retry absorbs. *Every* byte lost is nothing
|
||||
# coming back at all, which is a line that is not free: an application
|
||||
# holding the shared pin low (this rig's LED demo ends that way), a
|
||||
# wedge, or an RX that is not on the line. Same retry either way, but
|
||||
# blaming an ack that never happened sends the reader to the wrong place.
|
||||
if len(remaining) == len(data):
|
||||
verbose(f"one-wire: none of {len(data)} byte(s) echoed — the line is not "
|
||||
verbose(f"one-wire: none of {len(data)} byte(s) echoed - the line is not "
|
||||
f"coming back. Held low by something? (a pin driven low, a wedge, "
|
||||
f"or an RX not on the line)")
|
||||
else:
|
||||
@@ -567,7 +567,7 @@ class Info:
|
||||
@classmethod
|
||||
def from_identity(cls, raw):
|
||||
"""pureboot 5's reply: the version and the chip signature. The rest of
|
||||
the geometry is looked up from the signature — the loader derived the
|
||||
the geometry is looked up from the signature - the loader derived the
|
||||
same facts from its chip database at build time, so nothing is guessed,
|
||||
it is simply not sent. Reconstructs a block in the older layout, so
|
||||
every derived attribute below is shared with the loaders that do send
|
||||
@@ -584,7 +584,7 @@ class Info:
|
||||
geometry = CHIP_GEOMETRY.get(signature)
|
||||
if geometry is None:
|
||||
sig = " ".join(f"{b:02x}" for b in signature)
|
||||
raise Error(f"unknown signature {sig} — this tool has no geometry for it")
|
||||
raise Error(f"unknown signature {sig} - this tool has no geometry for it")
|
||||
flash, page, eeprom, patch, _, _ = geometry
|
||||
base = flash - SLOT
|
||||
word_flash = flash > 0x10000
|
||||
@@ -601,7 +601,7 @@ class Info:
|
||||
if not OLDEST_LOADER <= self.version <= NEWEST_LOADER:
|
||||
raise Error(
|
||||
f"pureboot {self.version}: this tool (version {VERSION}) speaks pureboot "
|
||||
f"{OLDEST_LOADER}..{NEWEST_LOADER} — a newer loader needs a newer tool"
|
||||
f"{OLDEST_LOADER}..{NEWEST_LOADER} - a newer loader needs a newer tool"
|
||||
)
|
||||
self.raw = bytes(raw)
|
||||
self.signature = raw[3:6]
|
||||
@@ -621,8 +621,8 @@ class Info:
|
||||
# The hand-over target as 'J' takes it: the trampoline below the
|
||||
# loader, or word 0 where BOOTRST re-vectors reset in hardware.
|
||||
self.app_entry_word = (self.base - 2) // 2 if self.patch_vector else 0
|
||||
# Where SRAM begins, from the signature — None only for a chip this
|
||||
# tool has no geometry row for, which the wire-block path (v1–4)
|
||||
# Where SRAM begins, from the signature - None only for a chip this
|
||||
# tool has no geometry row for, which the wire-block path (v1-4)
|
||||
# permits where from_identity refuses.
|
||||
geometry = CHIP_GEOMETRY.get(tuple(self.signature))
|
||||
self.ram = geometry[4] if geometry else None
|
||||
@@ -642,7 +642,7 @@ class Info:
|
||||
)
|
||||
|
||||
def lines(self):
|
||||
"""One fact per line — what --info prints."""
|
||||
"""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:
|
||||
@@ -662,7 +662,7 @@ class Info:
|
||||
|
||||
class Loader:
|
||||
"""A session. Between commands the loader has prompted and awaits a
|
||||
command byte; every method restores that, except jump() — after which the
|
||||
command byte; every method restores that, except jump() - after which the
|
||||
target must be knocked afresh."""
|
||||
|
||||
def __init__(self, port):
|
||||
@@ -673,7 +673,7 @@ class Loader:
|
||||
self.autobaud = False
|
||||
# The pre-knock drain runs once per port: the bytes it exists for are
|
||||
# leftovers from before this process opened the port. Re-knocks later
|
||||
# in the same session must not pay it — a fresh activation window is
|
||||
# in the same session must not pay it - a fresh activation window is
|
||||
# already burning while they wait.
|
||||
self._line_drained = False
|
||||
# The link this session is speaking. It moves when the host follows a
|
||||
@@ -689,7 +689,7 @@ class Loader:
|
||||
|
||||
The timeout is short on purpose: a real answer follows the prompt
|
||||
within a frame time or two, so half a second is dozens of times the
|
||||
worst case — while a *false* prompt match (a stale byte, reset
|
||||
worst case - while a *false* prompt match (a stale byte, reset
|
||||
garbage) makes this read collect noise, and every second spent on it
|
||||
comes out of the activation window the retry needs."""
|
||||
head = self.port.read_exact(4, 0.5)
|
||||
@@ -701,7 +701,7 @@ class Loader:
|
||||
"""Ask a live session who it is.
|
||||
|
||||
The identity command takes no argument and changes nothing, which makes
|
||||
it the one question whose answer is known in advance — so it doubles as
|
||||
it the one question whose answer is known in advance - so it doubles as
|
||||
the host's check that the stream is still in step, and as the byte a
|
||||
lost host resynchronises on."""
|
||||
self.port.write(b"b")
|
||||
@@ -711,7 +711,7 @@ class Loader:
|
||||
|
||||
def _handshake(self, wait, knock, what):
|
||||
"""One activation, retried until the loader answers or the window
|
||||
closes. The identity reply is what proves the loader is listening — a
|
||||
closes. The identity reply is what proves the loader is listening - a
|
||||
prompt byte alone does not, since one left over from a previous session
|
||||
can still be in the pipeline while the port opening resets the device
|
||||
into a fresh window, where a command without its knock is discarded.
|
||||
@@ -722,7 +722,7 @@ class Loader:
|
||||
Before the port's first knock ever, the line is drained until quiet: a
|
||||
prompt from a previous session (`--stay`) can still be in the USB
|
||||
pipeline when the port opens, where a flush cannot clear what has not
|
||||
arrived yet — and on a board that resets when its port opens, trusting
|
||||
arrived yet - and on a board that resets when its port opens, trusting
|
||||
that stale byte would spend the fresh activation window reading noise
|
||||
from a device that never heard the knock. Once only, and bounded:
|
||||
later re-knocks in this session face no foreign leftovers, and their
|
||||
@@ -738,7 +738,7 @@ class Loader:
|
||||
refusal = None
|
||||
# The knock is the only write in the protocol with no ack between its
|
||||
# bytes, so on a shared line it is the only one whose echo may
|
||||
# legitimately not come back — the device's ack collides with it and
|
||||
# legitimately not come back - the device's ack collides with it and
|
||||
# wins (OneWirePort.write). Losing a byte here is what the retry below
|
||||
# is for; raising instead aborted the loop before it ever ran, which on
|
||||
# real wiring made every reconnect into a live session fail.
|
||||
@@ -750,8 +750,8 @@ class Loader:
|
||||
if PROMPT in self.port.read_available(0.4):
|
||||
# Settle: absorb a real loader's trailing bytes before asking
|
||||
# for the identity. Bounded by the deadline so a target that
|
||||
# never falls quiet — a board stuck in a reset loop, whose
|
||||
# garbage carries a stray prompt — cannot spin here forever.
|
||||
# never falls quiet - a board stuck in a reset loop, whose
|
||||
# garbage carries a stray prompt - cannot spin here forever.
|
||||
while self.port.read_available(0.3):
|
||||
if time.monotonic() > deadline:
|
||||
break
|
||||
@@ -775,8 +775,8 @@ class Loader:
|
||||
return self.info
|
||||
if time.monotonic() > deadline:
|
||||
if refusal is not None:
|
||||
raise Error(f"no usable answer — the last identity reply failed: {refusal}")
|
||||
raise Error("no answer — reset the device within its activation window")
|
||||
raise Error(f"no usable answer - the last identity reply failed: {refusal}")
|
||||
raise Error("no answer - reset the device within its activation window")
|
||||
|
||||
def connect(self, wait):
|
||||
"""Knock 'p' then 'b' and read the identity."""
|
||||
@@ -784,8 +784,8 @@ class Loader:
|
||||
|
||||
def connect_autobaud(self, wait):
|
||||
"""The autobaud handshake. In place of the p+b knock the host sends the
|
||||
calibration pulse — one seven-bit-time low pulse at the host's chosen
|
||||
baud, which the loader times into its per-bit unit — then a single 'p'
|
||||
calibration pulse - one seven-bit-time low pulse at the host's chosen
|
||||
baud, which the loader times into its per-bit unit - then a single 'p'
|
||||
the loader decodes at the rate it just measured. A lost pulse, or a
|
||||
knock landing while the loader is mid-frame, simply fails to answer and
|
||||
leaves the measurement loop waiting for the next pulse, so the retry in
|
||||
@@ -808,7 +808,7 @@ class Loader:
|
||||
"""Send a sealed header and take the loader's verdict on it.
|
||||
|
||||
Returning normally means the loader has accepted the command and not
|
||||
yet done it — which is the whole point of the verdict, and why the
|
||||
yet done it - which is the whole point of the verdict, and why the
|
||||
payload of a fill or a write burst is sent only after this returns."""
|
||||
head = bytes((op, selector(space, address), address & 0xFF, (address >> 8) & 0xFF, count & 0xFF))
|
||||
seal = SEAL
|
||||
@@ -819,7 +819,7 @@ class Loader:
|
||||
if answer == NAK:
|
||||
raise Error(
|
||||
f"the loader refused the command (opcode {op:#04x}, {address:#06x}): the "
|
||||
f"seal did not match, so nothing was done — the link mangled the header"
|
||||
f"seal did not match, so nothing was done - the link mangled the header"
|
||||
)
|
||||
if answer != PROMPT:
|
||||
raise Error(f"expected a verdict on the seal, got {answer.hex()}")
|
||||
@@ -864,7 +864,7 @@ class Loader:
|
||||
return data
|
||||
|
||||
def _write_space(self, space, address, data, progress=None):
|
||||
"""A run into any space. Each byte is acked as its write begins — an
|
||||
"""A run into any space. Each byte is acked as its write begins - an
|
||||
EEPROM cell and an SPM operation both need that pacing, and the ack is
|
||||
what the loader sends in place of a completion status."""
|
||||
offset = 0
|
||||
@@ -885,7 +885,7 @@ class Loader:
|
||||
offset += len(chunk)
|
||||
|
||||
def spm(self, operation, address):
|
||||
"""One SPM operation at a flash address — the erase, write and RWW
|
||||
"""One SPM operation at a flash address - the erase, write and RWW
|
||||
re-enable that pureboot 4 ran inside 'W' and pureboot 5 leaves here.
|
||||
|
||||
From pureboot 9 the operation rides the header's count field instead of
|
||||
@@ -935,13 +935,13 @@ class Loader:
|
||||
if self.unified:
|
||||
# The fill loads the page buffer and stops there; the erase and the
|
||||
# write are host-issued SPM operations. Only a chip with a boot
|
||||
# section has RWW to re-enable — on the others bit 4 of SPMCSR
|
||||
# section has RWW to re-enable - on the others bit 4 of SPMCSR
|
||||
# means something else entirely, so it must not be sent.
|
||||
if self.sealed:
|
||||
# The page goes out only once the header is accepted. It is the
|
||||
# protocol's one unacked burst, so a header refused after the
|
||||
# host had already started sending it would leave the page
|
||||
# being read as commands — which is exactly what the verdict
|
||||
# being read as commands - which is exactly what the verdict
|
||||
# is placed here to prevent.
|
||||
self._header(OP_FILL, SP_FLASH, address, len(data) & 0xFF)
|
||||
self.port.write(data)
|
||||
@@ -985,7 +985,7 @@ class Loader:
|
||||
"""The device acks, then execution continues at the word address.
|
||||
From v7 the jump rides the unified decode, so it carries a selector
|
||||
byte the loader ignores; older loaders take the bare address. From v9
|
||||
the ack is the verdict on its seal — a jump to a mangled address is a
|
||||
the ack is the verdict on its seal - a jump to a mangled address is a
|
||||
jump into arbitrary code, so it is sealed like everything else."""
|
||||
if self.sealed:
|
||||
self._header(OP_JUMP, 0, word_address, 0)
|
||||
@@ -997,7 +997,7 @@ class Loader:
|
||||
self._expect_prompt()
|
||||
|
||||
def enter_copy(self, byte_address, wait, link=None):
|
||||
"""Jump into the loader copy at `byte_address` and knock it — a slot
|
||||
"""Jump into the loader copy at `byte_address` and knock it - a slot
|
||||
base is that copy's entry stub, so it can only land there.
|
||||
|
||||
`link` is that copy's own `(baud, autobaud)`, for when it is not this
|
||||
@@ -1005,7 +1005,7 @@ class Loader:
|
||||
backend it was built for; the host has to be told which, because 512
|
||||
bytes of position-independent code carry no header to read it from.
|
||||
Retuning goes through the open port, so no DTR pulse resets the copy that
|
||||
is now running — and the session keeps the new link afterwards, since
|
||||
is now running - and the session keeps the new link afterwards, since
|
||||
every later jump lands in the same image.
|
||||
"""
|
||||
baud, autobaud = link if link is not None else (self.baud, self.autobaud)
|
||||
@@ -1028,7 +1028,7 @@ class Loader:
|
||||
raise Error(
|
||||
f"the copy at {byte_address:#06x} did not answer on this session's "
|
||||
f"link ({baud} Bd, {'autobaud' if autobaud else 'fixed baud'}). An "
|
||||
f"image built for another baud or backend speaks that one instead — "
|
||||
f"image built for another baud or backend speaks that one instead - "
|
||||
f"say which with --staged-baud / --staged-autobaud"
|
||||
) from unheard
|
||||
|
||||
@@ -1099,13 +1099,13 @@ def plan_flash(image, info):
|
||||
word0 = final[0] | (final[1] << 8)
|
||||
if word0 & 0xF000 != 0xC000:
|
||||
raise Error(
|
||||
"the image's reset vector is not an rjmp — pureboot's vector "
|
||||
"the image's reset vector is not an rjmp - pureboot's vector "
|
||||
"surgery cannot re-home it (crt-less entry at address 0?)"
|
||||
)
|
||||
entry = rjmp_target(0, word0, flash_words)
|
||||
if entry >= info.base // 2:
|
||||
raise Error(
|
||||
"the image's reset vector already targets the loader — this "
|
||||
"the image's reset vector already targets the loader - this "
|
||||
"is a read-back of a patched image; flash the original"
|
||||
)
|
||||
trampoline_word = (info.base - 2) // 2
|
||||
@@ -1129,7 +1129,7 @@ def covered(pages, info, skip_blank):
|
||||
|
||||
A patched vector puts page 0 first and the trampoline page second, so from
|
||||
the first write on a reset lands in the loader and its fall-through on the
|
||||
application entry — every interruption point recoverable. A hardware boot
|
||||
application entry - every interruption point recoverable. A hardware boot
|
||||
section re-vectors reset regardless; page 0 goes last there, which
|
||||
maximizes what an interrupted image retains."""
|
||||
trampoline_page = info.base - info.page if info.patch_vector else None
|
||||
@@ -1150,7 +1150,7 @@ def covered(pages, info, skip_blank):
|
||||
# which byte of the 'F' reply (low, lock, extended, high) carries BOOTSZ/
|
||||
# BOOTRST, and the BOOTSZ->words ladder. A die revision shares its base
|
||||
# signature, so one row covers it. The m48s have no boot section and no
|
||||
# row — their info block says patch-vector and this table is never
|
||||
# row - their info block says patch-vector and this table is never
|
||||
# consulted. Sources: Atmel-2486/2466/2503 (HIGH fuse), Atmel-2545/8271/
|
||||
# DS40002065 (x8: EXTENDED, except the m328s' HIGH), Atmel-8272/8011/2593/
|
||||
# 42719 (x4: HIGH).
|
||||
@@ -1186,7 +1186,7 @@ def mega_boot(info, fuse_bytes):
|
||||
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")
|
||||
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]
|
||||
@@ -1200,7 +1200,7 @@ def image_info(image):
|
||||
"""What a pureboot binary says about itself, or None.
|
||||
|
||||
An update image is a bare slot: nothing about it names the chip it was
|
||||
built for, and installing a foreign one bricks the target — so every
|
||||
built for, and installing a foreign one bricks the target - so every
|
||||
loader carries a stamp for this. Through pureboot 4 the stamp is the
|
||||
12-byte info block the device also serves; pureboot 5 serves its identity
|
||||
from immediates and carries a 6-byte stamp (magic, version, signature)
|
||||
@@ -1234,8 +1234,8 @@ def loader_image(path):
|
||||
|
||||
|
||||
def staging_content(image, info):
|
||||
"""The staging slot's content: the image, padding, and — where the
|
||||
hand-over jumps through the word below the resident — that word, which for
|
||||
"""The staging slot's content: the image, padding, and - where the
|
||||
hand-over jumps through the word below the resident - that word, which for
|
||||
a staging copy is its own last one. Composed as an rjmp to the resident,
|
||||
so an abandoned staging copy still falls through into a loader."""
|
||||
budget = SLOT - 2 if info.patch_vector else SLOT
|
||||
@@ -1253,7 +1253,7 @@ def update_preflight(image, info, fuse_bytes):
|
||||
embedded = image_info(image)
|
||||
if embedded is None:
|
||||
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:]:
|
||||
@@ -1264,13 +1264,13 @@ def update_preflight(image, info, fuse_bytes):
|
||||
warnings = []
|
||||
if not info.patch_vector:
|
||||
if fuse_bytes is None:
|
||||
raise Error("a loader update on this chip needs its fuses — unreadable? pass --assume-fuses")
|
||||
raise Error("a loader update on this chip needs its fuses - unreadable? pass --assume-fuses")
|
||||
bootrst, bls_start = mega_boot(info, fuse_bytes)
|
||||
if info.stage < bls_start:
|
||||
raise Error(
|
||||
f"cannot self-update: the staging slot {info.stage:#06x} lies below the "
|
||||
f"boot section ({bls_start:#06x}) where SPM is disabled "
|
||||
f"— a boot section of at least two slots ({2 * SLOT} B, BOOTSZ) is "
|
||||
f" - a boot section of at least two slots ({2 * SLOT} B, BOOTSZ) is "
|
||||
f"required, and only an external programmer can change fuses"
|
||||
)
|
||||
if not bootrst:
|
||||
@@ -1294,7 +1294,7 @@ def update_preflight(image, info, fuse_bytes):
|
||||
class UpdateState:
|
||||
"""The host-side memory of an update in flight: what the staging slot
|
||||
held (and page 0, where the update repoints it). Losing this file after
|
||||
the staging slot was overwritten loses those saved bytes — the update
|
||||
the staging slot was overwritten loses those saved bytes - the update
|
||||
still completes, but the staging region can then only be restored by
|
||||
reflashing the application."""
|
||||
|
||||
@@ -1307,7 +1307,7 @@ class UpdateState:
|
||||
if os.path.exists(self.path):
|
||||
self.data = json.load(open(self.path))
|
||||
if bytes.fromhex(self.data["signature"]) != info.signature or self.data["base"] != info.base:
|
||||
raise Error(f"{self.path} belongs to a different device — remove it to start over")
|
||||
raise Error(f"{self.path} belongs to a different device - remove it to start over")
|
||||
return
|
||||
self.data = {
|
||||
"signature": info.signature.hex(),
|
||||
@@ -1368,7 +1368,7 @@ def write_differing(loader, base, content, order=None, label=None):
|
||||
|
||||
|
||||
def patch_word0(loader, page0, target_base):
|
||||
"""Re-aim word 0 at `target_base` — the resume insurance around
|
||||
"""Re-aim word 0 at `target_base` - the resume insurance around
|
||||
rewriting a loader slot the reset path goes through."""
|
||||
info = loader.info
|
||||
patched = bytearray(page0)
|
||||
@@ -1384,7 +1384,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes, staged_link=Non
|
||||
so a re-run resumes; the state file carries what the staging slot held.
|
||||
|
||||
`staged_link` is the new image's own `(baud, autobaud)` where it differs from
|
||||
this session's — the copies the host enters *are* that image, so they answer
|
||||
this session's - the copies the host enters *are* that image, so they answer
|
||||
on its link and not the resident's. Note what this does to the idempotence
|
||||
above: once the staging copy is installed, the resumable state is only
|
||||
reachable on the new link, so a re-run has to name it too."""
|
||||
@@ -1408,7 +1408,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes, staged_link=Non
|
||||
# A loader already sitting whole in the staging slot IS the staging copy:
|
||||
# rewriting it in place would be a copy overwriting itself as it runs. Any pureboot
|
||||
# with the device's info block serves, since a staged copy only streams
|
||||
# pages. "Whole" needs both checks — the block where every image carries
|
||||
# pages. "Whole" needs both checks - the block where every image carries
|
||||
# it and matching byte for byte, and the slot unchanged since this update
|
||||
# began, so a half-written install takes the path below instead.
|
||||
current = loader.read_flash(info.stage, SLOT)
|
||||
@@ -1417,7 +1417,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes, staged_link=Non
|
||||
# before that.
|
||||
staged_loader = image_info(current)
|
||||
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")
|
||||
print("staging slot already holds a loader - left in place")
|
||||
else:
|
||||
# Where the staging slot starts at address 0 (the 1 KB tiny13s) its
|
||||
# first page carries the reset vector, so it goes last: until then a
|
||||
@@ -1471,7 +1471,7 @@ def check_walk_region(pages, info, fuse_bytes, force):
|
||||
raise Error(
|
||||
f"the image writes {overlap[0]:#06x}.. inside the reset walk region "
|
||||
f"[{bls_start:#06x}, {info.base:#06x}) (BOOTRST programmed): reset could no "
|
||||
f"longer reach the loader — --force to flash it anyway"
|
||||
f"longer reach the loader - --force to flash it anyway"
|
||||
)
|
||||
|
||||
|
||||
@@ -1481,7 +1481,7 @@ def check_walk_region(pages, info, fuse_bytes, force):
|
||||
def op_erase_flash(loader):
|
||||
"""0xff over the application area, descending where the reset vector is
|
||||
patched: page 0 goes last, so an interrupted erase still resets into the
|
||||
loader — and once it is gone, the erased walk reaches it anyway."""
|
||||
loader - and once it is gone, the erased walk reaches it anyway."""
|
||||
blank = bytes([0xFF] * loader.info.page)
|
||||
addresses = range(0, loader.info.base, loader.info.page)
|
||||
with Progress("erase", len(addresses)) as bar:
|
||||
@@ -1537,7 +1537,7 @@ def verify_pages(loader, pages, repair=False):
|
||||
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})")
|
||||
verbose(f"{detail} - rewriting page {address:#06x} (retry {retry + 1})")
|
||||
loader.write_page(address, pages[address])
|
||||
repaired += 1
|
||||
bar.step()
|
||||
@@ -1604,7 +1604,7 @@ def _require_unified(loader, what):
|
||||
|
||||
|
||||
def _peek_spec(spec):
|
||||
"""ADDR[:N] — addresses and counts in any Python integer base."""
|
||||
"""ADDR[:N] - addresses and counts in any Python integer base."""
|
||||
address, _, count = spec.partition(":")
|
||||
return int(address, 0), int(count, 0) if count else 1
|
||||
|
||||
@@ -1638,13 +1638,13 @@ def op_fuses(loader):
|
||||
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.
|
||||
# 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}")
|
||||
f"BOOTRST {'programmed' if bootrst else 'unprogrammed'} - {reset}")
|
||||
except Error:
|
||||
pass # unknown signature: the raw bytes above still stand
|
||||
return fuse_bytes
|
||||
@@ -1652,7 +1652,7 @@ def op_fuses(loader):
|
||||
|
||||
def scan_ratios():
|
||||
"""The probe walk, in percent of the built rate: the built rate itself
|
||||
first, then ±10 % in 2 % steps nearest-first — a drifted oscillator near
|
||||
first, then +/-10 % in 2 % steps nearest-first - a drifted oscillator near
|
||||
its trim is the common case, and each probe costs a reset."""
|
||||
return [0] + [sign * step for step in (2, 4, 6, 8, 10) for sign in (-1, 1)]
|
||||
|
||||
@@ -1664,9 +1664,9 @@ def scan_rate(baud, pct):
|
||||
def scan_report(baud, pct, version, clock=None):
|
||||
"""The findings, one per line: the found rate is the session workaround,
|
||||
its ratio to the built rate is the oscillator's offset, and the fixes are
|
||||
the OSCCAL bake (≈1 %/step, opposing the drift) or the autobaud build."""
|
||||
the OSCCAL bake (~1 %/step, opposing the drift) or the autobaud build."""
|
||||
rate = scan_rate(baud, pct)
|
||||
lines = [f"scan: answered at {rate} Bd ({pct:+d} % of the built rate) — pureboot {version}",
|
||||
lines = [f"scan: answered at {rate} Bd ({pct:+d} % of the built rate) - pureboot {version}",
|
||||
f" session --baud {rate}"]
|
||||
if clock:
|
||||
lines.append(f" clock ~{clock * (100 + pct) // 100} Hz (built for {clock})")
|
||||
@@ -1675,12 +1675,12 @@ def scan_report(baud, pct, version, clock=None):
|
||||
lines.append(f" fix rebuild with OSCCAL ~{abs(pct)} steps {direction} (~1 %/step), "
|
||||
"or the autobaud build")
|
||||
else:
|
||||
lines.append(" fix none — the built rate answers; check the earlier wiring instead")
|
||||
lines.append(" fix none - the built rate answers; check the earlier wiring instead")
|
||||
return lines
|
||||
|
||||
|
||||
def op_scan(port_path, baud, wait, clock=None, one_wire=False):
|
||||
"""A fixed-baud loader whose oscillator drifted still answers — at the
|
||||
"""A fixed-baud loader whose oscillator drifted still answers - at the
|
||||
drifted ratio, since its rate scales with its clock. One probe per
|
||||
activation window, and with an application resident the window opens
|
||||
exactly once per reset, so each probe announces itself and expects a
|
||||
@@ -1688,11 +1688,11 @@ def op_scan(port_path, baud, wait, clock=None, one_wire=False):
|
||||
everything else; undiscarded they would answer every rate."""
|
||||
for pct in scan_ratios():
|
||||
rate = scan_rate(baud, pct)
|
||||
print(f"scan: {rate} Bd ({pct:+d} %) — reset the target", flush=True)
|
||||
print(f"scan: {rate} Bd ({pct:+d} %) - reset the target", flush=True)
|
||||
try:
|
||||
port = Port(port_path, rate)
|
||||
except Error as unmakeable:
|
||||
print(f"scan: {rate} Bd skipped — {unmakeable}")
|
||||
print(f"scan: {rate} Bd skipped - {unmakeable}")
|
||||
continue
|
||||
if one_wire:
|
||||
port = OneWirePort(port)
|
||||
@@ -1705,7 +1705,7 @@ def op_scan(port_path, baud, wait, clock=None, one_wire=False):
|
||||
for line in scan_report(baud, pct, info.version, clock):
|
||||
print(line)
|
||||
return
|
||||
raise Error("no answer within ±10 % of the built rate — check the wiring, or deploy the "
|
||||
raise Error("no answer within +/-10 % of the built rate - check the wiring, or deploy the "
|
||||
"autobaud build, which has no rate to miss (README.md)")
|
||||
|
||||
|
||||
@@ -1728,10 +1728,10 @@ def main():
|
||||
help="drive an autobaud loader: send the 0xC0 calibration pulse and a single "
|
||||
"knock, and take geometry from the signature (no clock/baud baked in)")
|
||||
parser.add_argument("--scan", action="store_true",
|
||||
help="walk ±10%% around --baud for a fixed-baud loader gone silent — one "
|
||||
help="walk +/-10%% around --baud for a fixed-baud loader gone silent - one "
|
||||
"reset per probe, standalone (README.md: deployment)")
|
||||
parser.add_argument("--clock", type=int, metavar="HZ",
|
||||
help="the clock the loader was built for — lets --scan and an autobaud "
|
||||
help="the clock the loader was built for - lets --scan and an autobaud "
|
||||
"--info state drift in absolute terms")
|
||||
parser.add_argument("--info", action="store_true", help="print the device info block")
|
||||
parser.add_argument("--fuses", action="store_true", help="read the fuse and lock bytes")
|
||||
@@ -1747,7 +1747,7 @@ def main():
|
||||
help="whether that image is an autobaud build, where it differs "
|
||||
"from --autobaud")
|
||||
parser.add_argument("--assume-fuses", metavar="HEX8", help="fuse bytes low,lock,ext,high as 8 hex digits "
|
||||
"(overrides reading them — e.g. under a simulator that cannot)")
|
||||
"(overrides reading them - e.g. under a simulator that cannot)")
|
||||
parser.add_argument("--erase-flash", action="store_true", help="0xff over the application flash")
|
||||
parser.add_argument("--flash", metavar="FILE", help="program an application (bin or ihex)")
|
||||
parser.add_argument("--no-verify", action="store_true", help="skip read-back after writes")
|
||||
@@ -1758,8 +1758,8 @@ def main():
|
||||
parser.add_argument("--read-eeprom", metavar="FILE", help="dump the EEPROM")
|
||||
parser.add_argument("--verify-eeprom", metavar="FILE", help="compare EEPROM against an image")
|
||||
parser.add_argument("--peek", metavar="ADDR[:N]", help="read N bytes of data space (SRAM, registers, "
|
||||
"I/O) — pureboot 5 and later")
|
||||
parser.add_argument("--poke", metavar="ADDR:HEX", help="write hex bytes into data space — "
|
||||
"I/O) - pureboot 5 and later")
|
||||
parser.add_argument("--poke", metavar="ADDR:HEX", help="write hex bytes into data space - "
|
||||
"pureboot 5 and later")
|
||||
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")
|
||||
@@ -1800,7 +1800,7 @@ def main():
|
||||
if args.autobaud and info.unit_home is not None:
|
||||
# The measured bit period, from wherever this version keeps it
|
||||
# (unit_home); decoded and times the rate this session drives,
|
||||
# that is the true clock — the number to hold an OSCCAL bake
|
||||
# that is the true clock - the number to hold an OSCCAL bake
|
||||
# or a fixed-baud build against (README.md: deployment). The
|
||||
# autobaud identity path refuses unknown signatures, so the
|
||||
# home is always known here; the guard states that dependency.
|
||||
@@ -1808,7 +1808,7 @@ def main():
|
||||
cycles = unit * UNIT_LOOP_CYCLES + UNIT_DISCOUNT
|
||||
clock = cycles * args.baud
|
||||
offset = f", {(clock / args.clock - 1) * 100:+.1f} % of {args.clock}" if args.clock else ""
|
||||
print(f" measured {clock} Hz ({cycles} cycles/bit × {args.baud} Bd{offset})")
|
||||
print(f" measured {clock} Hz ({cycles} cycles/bit x {args.baud} Bd{offset})")
|
||||
fuse_bytes = fuse_override
|
||||
if args.fuses or (args.update_loader and not info.patch_vector and fuse_bytes is None):
|
||||
read = op_fuses(loader)
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
"""Position-independence lint: the property that lets the identical image run
|
||||
from any slot, asserted from the built ELF and its object.
|
||||
|
||||
1. No absolute jmp/call — -mrelax normally guarantees it, but a branch that
|
||||
1. No absolute jmp/call - -mrelax normally guarantees it, but a branch that
|
||||
grows out of relaxation range would break it silently.
|
||||
2. Nothing flash-resident to address: the image is .text alone, so there is
|
||||
no table whose runtime address has to be reconstructed.
|
||||
3. The image is byte-identical when linked at a different base. This is
|
||||
position independence itself rather than a proxy for it — an absolute
|
||||
position independence itself rather than a proxy for it - an absolute
|
||||
address anywhere in the image would move with the link and show up as a
|
||||
differing byte.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Asserts the autobaud loader's measured unit sits where the host will read
|
||||
# it (--info's measured clock — the address is wire contract). Two homes: on
|
||||
# it (--info's measured clock - the address is wire contract). Two homes: on
|
||||
# a chip with the GPIOR pair the unit lives there and the image must carry no
|
||||
# RAM word for it at all; elsewhere it is the first RAM object at SRAM start.
|
||||
# Run as
|
||||
@@ -16,21 +16,21 @@ string(REGEX MATCH "\n0*([0-9a-f]+)[^\n]+[ \t][^ \t\n]*unit_E\n" _line "${_syms}
|
||||
|
||||
if(GPIOR)
|
||||
if(_line)
|
||||
message(FATAL_ERROR "unit_ RAM symbol present although the unit's home is GPIOR ${GPIOR} — "
|
||||
message(FATAL_ERROR "unit_ RAM symbol present although the unit's home is GPIOR ${GPIOR} - "
|
||||
"the host peeks the pair, and a RAM copy would be dead weight")
|
||||
endif()
|
||||
message(STATUS "no unit_ RAM object — the unit lives in the GPIOR pair at ${GPIOR}")
|
||||
message(STATUS "no unit_ RAM object - the unit lives in the GPIOR pair at ${GPIOR}")
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(NOT _line)
|
||||
message(FATAL_ERROR "no unit_ symbol in ${ELF} — is this the autobaud loader?")
|
||||
message(FATAL_ERROR "no unit_ symbol in ${ELF} - is this the autobaud loader?")
|
||||
endif()
|
||||
|
||||
# AVR data-space symbols carry the 0x800000 VMA offset.
|
||||
math(EXPR _want "0x800000 + ${RAM_START}" OUTPUT_FORMAT HEXADECIMAL)
|
||||
math(EXPR _have "0x${CMAKE_MATCH_1}" OUTPUT_FORMAT HEXADECIMAL)
|
||||
if(NOT _have STREQUAL _want)
|
||||
message(FATAL_ERROR "unit_ sits at ${_have}, ram_start is ${_want} — the host peeks ram_start")
|
||||
message(FATAL_ERROR "unit_ sits at ${_have}, ram_start is ${_want} - the host peeks ram_start")
|
||||
endif()
|
||||
message(STATUS "unit_ at ${_have} == ram_start")
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// SPM genuinely writes avr->flash on the mega cores, so on exit (or SIGTERM)
|
||||
// we dump the flash image to a file for a ground-truth cross-check against
|
||||
// what the client read back through the bootloader.
|
||||
#include <array>
|
||||
#include <csignal>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -17,7 +18,7 @@
|
||||
#include <unistd.h>
|
||||
|
||||
// The parts headers (uart_pty.h) carry no C++ linkage guards of their own,
|
||||
// unlike simavr's core headers — the block covers both harmlessly.
|
||||
// unlike simavr's core headers - the block covers both harmlessly.
|
||||
extern "C" {
|
||||
#include "avr_uart.h"
|
||||
#include "sim_avr.h"
|
||||
@@ -75,11 +76,11 @@ int main(int argc, char *argv[])
|
||||
return 1;
|
||||
}
|
||||
// An image that runs past flash end cannot execute on hardware, and a
|
||||
// naive copy of it would smash the heap beyond avr->flash — after which
|
||||
// naive copy of it would smash the heap beyond avr->flash - after which
|
||||
// the simulation misbehaves in ways that point everywhere but here.
|
||||
// Refuse it loudly instead.
|
||||
if (boot_base + fw.flashsize > avr->flashend + 1) {
|
||||
std::println(stderr, "device: {} B at {:#x} runs past flash end {:#x} — image does not fit its slot",
|
||||
std::println(stderr, "device: {} B at {:#x} runs past flash end {:#x} - image does not fit its slot",
|
||||
fw.flashsize, boot_base, avr->flashend);
|
||||
return 1;
|
||||
}
|
||||
@@ -94,13 +95,13 @@ int main(int argc, char *argv[])
|
||||
if (cfg) {
|
||||
std::uint32_t app_end = boot_base - 128; // config page sits directly below the boot code
|
||||
for (int i = 0; cfg[i] && cfg[i + 1]; i += 2) {
|
||||
char b[3] = {cfg[i], cfg[i + 1], 0};
|
||||
avr->flash[app_end + i / 2] = static_cast<std::uint8_t>(std::strtoul(b, nullptr, 16));
|
||||
const std::array<char, 3> pair = {cfg[i], cfg[i + 1], 0};
|
||||
avr->flash[app_end + i / 2] = static_cast<std::uint8_t>(std::strtoul(pair.data(), nullptr, 16));
|
||||
}
|
||||
}
|
||||
|
||||
// POLL_SLEEP makes simavr usleep(1) on every status-register read while the
|
||||
// UART is idle — a host-CPU-saving hack that models no hardware and paces a
|
||||
// UART is idle - a host-CPU-saving hack that models no hardware and paces a
|
||||
// tight-polling loader (one that releases TX between bytes, as one-wire does)
|
||||
// in real time, distorting protocol timing. Clear it so the loader runs at
|
||||
// true cycle speed.
|
||||
@@ -119,8 +120,9 @@ int main(int argc, char *argv[])
|
||||
|
||||
for (;;) {
|
||||
int state = avr_run(avr);
|
||||
if (state == cpu_Done || state == cpu_Crashed)
|
||||
if (state == cpu_Done || state == cpu_Crashed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
finish(0);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
// Test-fixture application for the pureboot protocol tests: prints "APP" on
|
||||
// the chip's serial link (the same link the loader uses) — the proof that
|
||||
// the chip's serial link (the same link the loader uses) - the proof that
|
||||
// the loader's hand-over, and on the tinies the host's reset-vector
|
||||
// surgery, actually launched it. Linked normally (crt, vectors at 0); on
|
||||
// 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
|
||||
// the resident loader — the application-owned loader entry a
|
||||
// the resident loader - the application-owned loader entry a
|
||||
// BOOTRST-unprogrammed mega relies on (reset always boots the application
|
||||
// there), exercised by the self-update tests. The software link idles:
|
||||
// reset reaches those loaders through the patched vector (or the runner
|
||||
// models BOOTRST), so the application owes them nothing.
|
||||
//
|
||||
// PUREBOOT_HANDOVER drops the listening and jumps straight in, leaving the
|
||||
// USART enabled behind it — the hand-over state a loader bit-banging on that
|
||||
// USART enabled behind it - the hand-over state a loader bit-banging on that
|
||||
// USART's own pins has to survive.
|
||||
//
|
||||
// The fixture speaks the deployment its loader was built for: the same
|
||||
@@ -30,10 +30,12 @@ consteval avr::hertz_t clock()
|
||||
return avr::hertz_t{PUREBOOT_CLOCK_HZ};
|
||||
#else
|
||||
auto name = std::string_view{avr::hw::db.name};
|
||||
if (name.starts_with("ATtiny13"))
|
||||
if (name.starts_with("ATtiny13")) {
|
||||
return 9.6_MHz;
|
||||
if (name.starts_with("ATtiny"))
|
||||
}
|
||||
if (name.starts_with("ATtiny")) {
|
||||
return 8_MHz;
|
||||
}
|
||||
return 16_MHz;
|
||||
#endif
|
||||
}
|
||||
@@ -66,7 +68,10 @@ struct link {
|
||||
#else
|
||||
static constexpr avr::baud_t baud{115200};
|
||||
#endif
|
||||
using tx_t = avr::uart::usart<PUREBOOT_USART, C, {.baud = baud, .max_baud_error = 2.5_pct}>;
|
||||
// The fixture speaks whatever rate the loader was built for, stock
|
||||
// 115200 at 16 MHz included, which sits past the receiver-tolerance
|
||||
// table's bound - the same deployment envelope the loader itself states.
|
||||
using tx_t = avr::uart::usart<PUREBOOT_USART, C, {.baud = baud, .allow_baud_error = true}>;
|
||||
static void init()
|
||||
{
|
||||
avr::init<tx_t>();
|
||||
@@ -75,7 +80,7 @@ struct link {
|
||||
{
|
||||
tx_t::write(static_cast<std::uint8_t>(c));
|
||||
}
|
||||
// The loader sits in the top slot — 512 bytes on every chip. The jump
|
||||
// The loader sits in the top slot - 512 bytes on every chip. The jump
|
||||
// takes a word address, which is what makes the >64 KiB chips' entry
|
||||
// reachable through a 16-bit pointer at all.
|
||||
static void enter_loader()
|
||||
@@ -87,7 +92,7 @@ struct link {
|
||||
[[noreturn]] static void idle()
|
||||
{
|
||||
#if defined(PUREBOOT_HANDOVER)
|
||||
// Hand back at once, with this USART still enabled — the state that
|
||||
// Hand back at once, with this USART still enabled - the state that
|
||||
// leaves a bit-banged loader on its pins mute unless the loader
|
||||
// releases it. Unconditional because there is no command wire to
|
||||
// wait on: that loader's link is the pins, not this peripheral.
|
||||
@@ -96,13 +101,20 @@ struct link {
|
||||
#else
|
||||
for (;;) {
|
||||
auto command = tx_t::read_blocking();
|
||||
if (command == 'L')
|
||||
if (command == 'L') {
|
||||
enter_loader();
|
||||
}
|
||||
// 'D' leaves every word of the SPM page buffer dirty, so that a
|
||||
// following 'L' enters the loader with the buffer it never clears.
|
||||
// Hardware refuses application-section SPM on a boot-sectioned
|
||||
// part; simavr dispatches it anyway, which is the whole reason the
|
||||
// state is constructible - the stated section is the compilable
|
||||
// fiction that matches what the simulator runs.
|
||||
if (command == 'D') {
|
||||
for (std::uint16_t at = 0; at < avr::spm::page_bytes; at += 2)
|
||||
avr::spm::fill(at, 0xdead);
|
||||
const auto open = avr::spm::page::begin<avr::spm::from::boot_section>(0);
|
||||
for (std::uint16_t at = 0; at < avr::spm::page_bytes; at += 2) {
|
||||
avr::spm::fill(open, at, 0xdead);
|
||||
}
|
||||
tx('D');
|
||||
}
|
||||
}
|
||||
@@ -119,19 +131,20 @@ struct link<C, false> {
|
||||
#endif
|
||||
// A shared-pin deployment (RX == TX) banners as a guest on its own line:
|
||||
// the pull-up input is the released line, the transmitter takes the pin
|
||||
// for exactly one frame per byte — the shape a real one-wire application
|
||||
// for exactly one frame per byte - the shape a real one-wire application
|
||||
// beside this loader uses.
|
||||
static constexpr bool one_wire = avr::PUREBOOT_RX == avr::PUREBOOT_TX;
|
||||
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, baud, one_wire>;
|
||||
static void init()
|
||||
{
|
||||
// The guest transmitter configures no pin; the released line — the
|
||||
// pull-up input a receiver would own — is established here.
|
||||
if constexpr (one_wire)
|
||||
// The guest transmitter configures no pin; the released line - the
|
||||
// pull-up input a receiver would own - is established here.
|
||||
if constexpr (one_wire) {
|
||||
avr::init<avr::io::input<avr::PUREBOOT_TX, avr::io::pull::up>, tx_t>();
|
||||
else
|
||||
} else {
|
||||
avr::init<tx_t>();
|
||||
}
|
||||
}
|
||||
static void tx(char c)
|
||||
{
|
||||
tx_t::write(static_cast<std::uint8_t>(c));
|
||||
@@ -143,7 +156,7 @@ struct link<C, false> {
|
||||
// cycles-per-bit transmitter: `tools/pbrig.py rate` sweeps the host rate
|
||||
// against it to find the part's true bit rate, and from that the clock
|
||||
// its RC oscillator is really running at. Only the *bit* timing carries
|
||||
// the measurement — the delay merely spaces the lines out, so its own
|
||||
// the measurement - the delay merely spaces the lines out, so its own
|
||||
// error does not matter. Software link only: the hardware-link idle owes
|
||||
// the self-update tests a command loop, and a crystal deployment has
|
||||
// nothing to measure.
|
||||
@@ -173,7 +186,7 @@ int main()
|
||||
link<dev::clock>::tx('P');
|
||||
#endif
|
||||
// The hand-over fixture stays silent: nothing is listening on the USART it
|
||||
// brings up — the loader it hands to speaks those pins directly — so its
|
||||
// brings up - the loader it hands to speaks those pins directly - so its
|
||||
// banner would be a write into a peer that does not exist.
|
||||
link<dev::clock>::idle();
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
"""End-to-end autobaud test: drive an autobaud loader in simavr through the
|
||||
calibration handshake and a flash + EEPROM + fuse round-trip, cross-checked
|
||||
against the simulator's ground-truth memory — then repeat at a second F_CPU with
|
||||
against the simulator's ground-truth memory - then repeat at a second F_CPU with
|
||||
the *same* loader binary, which is the property autobaud exists for: one
|
||||
clock-agnostic image that locks onto whatever rate the host sends.
|
||||
|
||||
Usage: pbautobaud.py <device_bin> <loader_elf> <mcu> <base_hex> <page>
|
||||
<app_bin> <app_hz> <app_baud> <tool_py> <workdir> [link]
|
||||
|
||||
The loader is a software-serial build, driven over the GPIO⇄pty bridge; the
|
||||
optional link overrides the default -l sw:B0,B1 — RX == TX in it is the
|
||||
The loader is a software-serial build, driven over the GPIO<->pty bridge; the
|
||||
optional link overrides the default -l sw:B0,B1 - RX == TX in it is the
|
||||
one-wire deployment, and every session then runs with the host's echo
|
||||
discard on. The app fixture is built for (app_hz, app_baud); the hand-over
|
||||
is checked at that point, and a second point at half the clock proves the
|
||||
@@ -45,7 +45,7 @@ def main():
|
||||
open(ee_path, "wb").write(ee_image)
|
||||
|
||||
# The geometry the surgery planner needs, from the chip class the runner is
|
||||
# told — the same derivation pbtest.py makes: the boot-sectioned megas need
|
||||
# told - the same derivation pbtest.py makes: the boot-sectioned megas need
|
||||
# no vector surgery, the tinies and the boot-section-less m48s do, and the
|
||||
# large chips speak word addresses.
|
||||
mega = mcu.startswith("atmega")
|
||||
@@ -74,7 +74,7 @@ def main():
|
||||
# must land inside the
|
||||
# encoding's own envelope: the loader floors the bit period to
|
||||
# 4-cycle spin granules after an 8-cycle discount, and the edge
|
||||
# poll can shave a few cycles more — one granule of slack below
|
||||
# poll can shave a few cycles more - one granule of slack below
|
||||
# the true clock, none above (in cycles per bit, times the rate).
|
||||
measured = re.search(r"measured\s+(\d+) Hz", out)
|
||||
if not measured:
|
||||
@@ -96,8 +96,8 @@ def main():
|
||||
if hand_over:
|
||||
# Regression: a calibration pulse with no knock behind it must
|
||||
# not wedge the loader. The knock's edge wait used to be
|
||||
# unbudgeted, so one stray low pulse — EMI, or a host that opens
|
||||
# the port and never knocks — held the loader forever and the
|
||||
# unbudgeted, so one stray low pulse - EMI, or a host that opens
|
||||
# the port and never knocks - held the loader forever and the
|
||||
# application never ran. The whole activation is bounded now, so
|
||||
# the window closes and the app boots; the banner is the proof.
|
||||
# (The pause lets the loader reach its measurement loop, so the
|
||||
@@ -111,13 +111,13 @@ def main():
|
||||
port.write(bytes((pb.CALIBRATE,)))
|
||||
# Accumulate rather than match exactly: the reset leaves the
|
||||
# idle line a framing artefact ahead of the banner, which is
|
||||
# noise here — the question is only whether the app ran.
|
||||
# noise here - the question is only whether the app ran.
|
||||
seen = b""
|
||||
deadline = time.monotonic() + 180.0
|
||||
while b"APP" not in seen and time.monotonic() < deadline:
|
||||
seen += port.read_available(1.0)
|
||||
if b"APP" not in seen:
|
||||
fail(f"{label}: lone calibration pulse wedged the loader — app never bannered, saw {seen!r}")
|
||||
fail(f"{label}: lone calibration pulse wedged the loader - app never bannered, saw {seen!r}")
|
||||
print(f" {label}: lone calibration pulse does not wedge the loader")
|
||||
finally:
|
||||
port.close()
|
||||
@@ -165,7 +165,7 @@ def main():
|
||||
+ (", hand-over ok" if hand_over else ""))
|
||||
|
||||
def must_lock(hz, baud, label):
|
||||
"""The calibration alone, at a tight bit period. Nothing is programmed —
|
||||
"""The calibration alone, at a tight bit period. Nothing is programmed -
|
||||
the question is only whether the loader can still measure the pulse."""
|
||||
dump = os.path.join(workdir, f"flash_{label}.bin")
|
||||
device = pbsim.Device(device_bin, elf, mcu, str(hz), base_hex, page, baud, dump,
|
||||
@@ -186,7 +186,7 @@ def main():
|
||||
|
||||
# The app fixture is built for one clock; the hand-over banners there. A
|
||||
# second point at double that clock, same loader binary, proves the lock is
|
||||
# measured, not baked in — the whole point of autobaud. (Doubling keeps the
|
||||
# measured, not baked in - the whole point of autobaud. (Doubling keeps the
|
||||
# bit period healthy; halving would drop it below the software UART's floor.)
|
||||
round_trip(app_hz, app_baud, "clock-a", hand_over=True)
|
||||
round_trip(app_hz * 2, app_baud, "clock-b", hand_over=False)
|
||||
@@ -194,7 +194,7 @@ def main():
|
||||
# Both points above sit near 100 cycles a bit, which is comfortable. The
|
||||
# calibration's real floor is far tighter, and it is worth a gate: measured
|
||||
# here, the lock is solid down to ~36 cycles a bit and fails outright by ~31
|
||||
# — a sharp edge, not a fraying one. This pins the tightest standard rate the
|
||||
# - a sharp edge, not a fraying one. This pins the tightest standard rate the
|
||||
# fixture's clock reaches, so a change that raises the floor is caught.
|
||||
#
|
||||
# It does *not* bound what a real deployment can use. On silicon the
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Dirty-page-buffer acceptance test: with no discard in the loader, a page
|
||||
filled over words an earlier writer left takes those instead. The whole
|
||||
contract is asserted — a bare verify sees the corruption, the repairing
|
||||
contract is asserted - a bare verify sees the corruption, the repairing
|
||||
verify fixes it in one rewrite, and it stays fixed.
|
||||
|
||||
The state is reached the one way the loader cannot prevent: an application
|
||||
dirties the buffer and jumps in with no reset between. Boot-sectioned megas
|
||||
forbid that outright (SPM runs only from the boot section, Atmel-8271 §26.2),
|
||||
forbid that outright (SPM runs only from the boot section, Atmel-8271 section 26.2),
|
||||
but simavr dispatches SPM from anywhere, which is what makes it constructible.
|
||||
|
||||
Usage: pbdirty.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||
@@ -66,7 +66,7 @@ def main():
|
||||
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.
|
||||
# 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
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
that exists for it.
|
||||
|
||||
The board this was written for loses and mangles bytes on its own serial path,
|
||||
and the failure that made it matter — a page-fill byte lost, the stream one
|
||||
byte out, a page-address byte arriving where an SPMCSR value belongs — is not
|
||||
and the failure that made it matter - a page-fill byte lost, the stream one
|
||||
byte out, a page-address byte arriving where an SPMCSR value belongs - is not
|
||||
reachable by asking a healthy link nicely. So the damage is injected here, at
|
||||
a named byte index rather than a probability: a failing case is a case that
|
||||
fails again.
|
||||
@@ -13,7 +13,7 @@ Every check is a pair. The same bit flipped in the same field is applied on
|
||||
one side of the seal and then the other: *after* the host seals the header,
|
||||
which is a mangled command and must be refused, and *before*, which is a
|
||||
well-formed command for something else and must be obeyed. Only the pair
|
||||
proves anything — a test that showed the refusal alone would pass against a
|
||||
proves anything - a test that showed the refusal alone would pass against a
|
||||
loader that had simply stopped doing SPM, and one that showed the corruption
|
||||
alone would not say what caught it.
|
||||
|
||||
@@ -35,7 +35,7 @@ def frame(pb, op, space, address, count, damage=None, before_seal=False):
|
||||
|
||||
`damage` is (index, mask). Applied before the seal is computed it produces
|
||||
a valid command for whatever the damaged fields now say; applied after, a
|
||||
command whose seal no longer matches its own body — which is the shape a
|
||||
command whose seal no longer matches its own body - which is the shape a
|
||||
link fault actually has."""
|
||||
head = bytearray((op, pb.selector(space, address), address & 0xFF,
|
||||
(address >> 8) & 0xFF, count & 0xFF))
|
||||
@@ -76,7 +76,7 @@ def main():
|
||||
fail("the marker page did not survive an undamaged write")
|
||||
|
||||
# Every field of the header, one bit each. A damaged seal must be
|
||||
# refused, the loader must re-prompt, and the page must be untouched —
|
||||
# refused, the loader must re-prompt, and the page must be untouched -
|
||||
# and it is the erase being aimed at it, so a single escape is visible.
|
||||
for index in range(6):
|
||||
bad = frame(pb, pb.OP_WRITE, pb.SP_SPM, 0, pb.SPM_ERASE, damage=(index, 0x01))
|
||||
@@ -87,13 +87,13 @@ def main():
|
||||
if port.read_exact(1, 5.0) != pb.PROMPT:
|
||||
fail(f"no prompt after refusing a header damaged in byte {index}")
|
||||
if loader.read_flash(0, page) != marker:
|
||||
fail(f"damage in byte {index} reached flash — the marker page changed")
|
||||
fail(f"damage in byte {index} reached flash - the marker page changed")
|
||||
|
||||
# The fill, whose payload is the protocol's one unacked burst: a
|
||||
# refused fill must be refused *before* the page is sent, or the host
|
||||
# is left pushing 128 bytes into a loader reading commands. Nothing is
|
||||
# sent after the verdict here, and the very next command must be
|
||||
# understood — that is the whole claim.
|
||||
# understood - that is the whole claim.
|
||||
bad = frame(pb, pb.OP_FILL, pb.SP_FLASH, 0, page, damage=(3, 0x80))
|
||||
port.write(bad)
|
||||
if port.read_exact(1, 5.0) != pb.NAK:
|
||||
@@ -105,7 +105,7 @@ def main():
|
||||
|
||||
# The pair's other half. The identical flip, applied before the seal:
|
||||
# a well-formed erase of the page one bit away from the one intended.
|
||||
# It must be obeyed — otherwise the refusals above prove nothing about
|
||||
# It must be obeyed - otherwise the refusals above prove nothing about
|
||||
# the seal and only that this loader stopped erasing.
|
||||
port.write(frame(pb, pb.OP_WRITE, pb.SP_SPM, 0, pb.SPM_ERASE,
|
||||
damage=(2, 0x01), before_seal=True))
|
||||
@@ -114,7 +114,7 @@ def main():
|
||||
if port.read_exact(1, 5.0) != pb.PROMPT:
|
||||
fail("no prompt after a correctly sealed erase")
|
||||
if loader.read_flash(0, page) != b"\xff" * page:
|
||||
fail("the sealed erase did not reach flash — the marker page is intact")
|
||||
fail("the sealed erase did not reach flash - the marker page is intact")
|
||||
port.close()
|
||||
finally:
|
||||
device.stop()
|
||||
|
||||
@@ -6,7 +6,7 @@ application hands over with that USART still enabled: TXEN keeps the USART
|
||||
owning the pin, so the bit-banged transmitter's port writes go nowhere and the
|
||||
loader receives and obeys while answering nothing. The link's init releases it.
|
||||
|
||||
The state is reached the way silicon reaches it — an application that sets up
|
||||
The state is reached the way silicon reaches it - an application that sets up
|
||||
its USART and jumps in with no reset between, so nothing clears UCSRnB for it.
|
||||
The pin ownership itself is modelled by the device runner: simavr wires a
|
||||
USART through IRQs alone and never takes the pin from the port, so without
|
||||
@@ -35,7 +35,7 @@ def main():
|
||||
import pureboot as pb
|
||||
|
||||
if "@" not in link:
|
||||
fail(f"the link {link} names no owning USART — nothing would be under test")
|
||||
fail(f"the link {link} names no owning USART - nothing would be under test")
|
||||
# A shared line (RX == TX) echoes the host's own bytes; discard them the
|
||||
# way the shipped --one-wire mode does.
|
||||
one_wire = re.fullmatch(r"sw:([A-H][0-7]),\1@[01]", link) is not None
|
||||
@@ -63,7 +63,7 @@ def main():
|
||||
try:
|
||||
loader.connect(25)
|
||||
except pb.Error as error:
|
||||
fail(f"the loader never answered after the hand-over — the USART still owns its TX pin ({error})")
|
||||
fail(f"the loader never answered after the hand-over - the USART still owns its TX pin ({error})")
|
||||
if loader.info.version != resident:
|
||||
fail(f"identity changed across the hand-over: {resident} then {loader.info.version}")
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The build-time OSCCAL trim, observed through the wire: a loader built with
|
||||
the OSCCAL axis holds the trim register at the built byte from its first
|
||||
prompt on — the write sits at the top of run(), ahead of the WDRF bail, so
|
||||
prompt on - the write sits at the top of run(), ahead of the WDRF bail, so
|
||||
every path out of reset runs on the corrected clock. simavr's clock does not
|
||||
follow OSCCAL, which is what makes the value assertable at all: the register
|
||||
is plain state there, and the peek must return exactly what the build
|
||||
|
||||
@@ -4,7 +4,7 @@ canonical slot must still be a working loader, and the ordinary
|
||||
--update-loader flow must put a build into the top slot from there.
|
||||
|
||||
Two positions. Address 0, a raw .bin handed to a programmer. And the staging
|
||||
slot itself, where a loader already sitting there IS the staging copy —
|
||||
slot itself, where a loader already sitting there IS the staging copy -
|
||||
recognized by its embedded block and left in place, then streaming the new
|
||||
resident like any staged copy.
|
||||
|
||||
@@ -35,7 +35,7 @@ def rehome_from(pbsim, pb, device_bin, elf, place_hex, update_bin, base, page, b
|
||||
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")
|
||||
fail(f"the misplaced copy reports base {info.base:#06x} - the info block must stay canonical")
|
||||
|
||||
# The ordinary update flow puts the build into the top slot.
|
||||
pb.op_update_loader(loader, 25, update_bin, state, None)
|
||||
@@ -72,7 +72,7 @@ def main():
|
||||
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
|
||||
# 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 - pb.SLOT
|
||||
rehome_from(pbsim, pb, device_bin, elf, hex(stage), update_bin, base, page, baud, app_bin, workdir,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"""Position-independence acceptance test: the identical binary, flashed one
|
||||
slot below the resident, must serve the complete command set from there. The
|
||||
info block must come back byte-identical, and the staged copy must be able to
|
||||
rewrite the resident verbatim — which is the whole of what relocation is for.
|
||||
rewrite the resident verbatim - which is the whole of what relocation is for.
|
||||
|
||||
Usage: pbreloc.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||
<baud> <tool_py> <workdir>
|
||||
@@ -63,7 +63,7 @@ def main():
|
||||
if loader.read_eeprom(0, len(pattern)) != pattern:
|
||||
fail("EEPROM round-trip through the staged copy")
|
||||
|
||||
# The resident slot, written from the copy standing beside it — the
|
||||
# The resident slot, written from the copy standing beside it - the
|
||||
# whole point of relocating. pureboot 9 dropped the running-slot guard
|
||||
# that used to sit behind this, so the probe that used to accompany it
|
||||
# (aim a write at the copy's *own* slot and watch it be refused) is
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
loader is executing from.
|
||||
|
||||
pureboot 9 dropped the running-slot write guard, so this command is now
|
||||
permitted — that is what lets a resident copy plant something in its own slot,
|
||||
permitted - that is what lets a resident copy plant something in its own slot,
|
||||
which on a chip whose boot section *is* the loader slot is the only route a
|
||||
self-update has. Permitted means the loader must actually do it, and the only
|
||||
honest proof is the flash afterwards.
|
||||
@@ -72,12 +72,12 @@ def main():
|
||||
fail("the loader did not re-prompt after refusing the erase")
|
||||
alive = loader.read_flash(base, 8)
|
||||
if alive == b"\xff" * 8:
|
||||
fail("the refused erase happened anyway — the running page reads erased")
|
||||
fail("the refused erase happened anyway - the running page reads erased")
|
||||
|
||||
# Green: the identical command, correctly sealed. Nothing is required
|
||||
# of the link from here on. The verdict is *issued* before the SPM, but
|
||||
# the erase takes the code that would have finished saying it, and how
|
||||
# much of it survives is the chip's business — an erase removes one page
|
||||
# much of it survives is the chip's business - an erase removes one page
|
||||
# and nothing else, so a loader whose command loop lives past the page
|
||||
# erased will prompt as usual where one with 128-byte pages goes with
|
||||
# the stub. None of that is the claim. The claim is that the erase
|
||||
@@ -95,7 +95,7 @@ def main():
|
||||
# Ground truth: the simulator's flash, not the loader's opinion of it.
|
||||
flash = open(dump, "rb").read()
|
||||
if flash[base : base + page] != b"\xff" * page:
|
||||
fail("the sealed erase did not reach flash — the running page is intact")
|
||||
fail("the sealed erase did not reach flash - the running page is intact")
|
||||
print("pbselfwrite: the running slot is refused unsealed and erased sealed")
|
||||
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ class Device:
|
||||
cmd.append("-w") # report the first-transmit cycle, free-run idle
|
||||
cmd += [elf, mcu, hz, base_hex, str(page), str(baud), dump]
|
||||
if reset_hex is not None or resume is not None:
|
||||
# Chips without a hardware boot section — the tinies and the
|
||||
# m48s — reset to address 0 like silicon; the boot-sectioned
|
||||
# Chips without a hardware boot section - the tinies and the
|
||||
# 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))
|
||||
@@ -44,7 +44,7 @@ class Device:
|
||||
self.proc.send_signal(signal.SIGUSR1)
|
||||
|
||||
def power_fail(self):
|
||||
"""SIGTERM: the runner dumps its flash and exits — the image a
|
||||
"""SIGTERM: the runner dumps its flash and exits - the image a
|
||||
restart resumes from."""
|
||||
self.stop()
|
||||
return self.dump
|
||||
|
||||
@@ -21,7 +21,7 @@ def fail(message):
|
||||
|
||||
|
||||
def rjmp_decode(word, at, flash_words):
|
||||
"""Where an rjmp word at word-address `at` lands — deliberately written
|
||||
"""Where an rjmp word at word-address `at` lands - deliberately written
|
||||
against the instruction-set definition (12-bit signed offset), not with
|
||||
the host tool's encoder, so an encoding bug cannot verify itself."""
|
||||
if word & 0xF000 != 0xC000:
|
||||
@@ -86,7 +86,7 @@ def main():
|
||||
fail(f"session 1 output lacks {needed!r}")
|
||||
|
||||
# Session 2: reconnect into the live session, verify, dump, exercise
|
||||
# the data space; hand over is deferred — the pty must be reopened for
|
||||
# the data space; hand over is deferred - the pty must be reopened for
|
||||
# the APP banner first.
|
||||
probe = "c0ffee"
|
||||
out = pbsim.run_tool(tool, device.pty, baud, *extra, "--verify-flash", app_bin, "--verify-eeprom", ee_path,
|
||||
@@ -123,7 +123,7 @@ def main():
|
||||
loader = pb.Loader(port)
|
||||
live = loader.connect(15)
|
||||
# The loader built from this tree must report a version the tool
|
||||
# beside it speaks — a bump the tool was never told about is a
|
||||
# beside it speaks - a bump the tool was never told about is a
|
||||
# loader it would refuse to talk to. Not equality with the newest:
|
||||
# the tool now spans two loader generations, the fixed-baud one
|
||||
# here and the unified autobaud loader that follows it.
|
||||
@@ -139,7 +139,7 @@ def main():
|
||||
# rather than through write_page(), which would follow the fill
|
||||
# with its erase and write; the point here is that the fill alone
|
||||
# consumes exactly one page whatever the address's low bits say.
|
||||
# Hand-sealed too — a protocol probe that borrowed the tool's own
|
||||
# Hand-sealed too - a protocol probe that borrowed the tool's own
|
||||
# frame builder could not tell a wrong frame from a wrong loader.
|
||||
wire = base + 1
|
||||
head = bytes((pb.OP_FILL, pb.selector(pb.SP_FLASH, wire), wire & 0xFF,
|
||||
@@ -156,8 +156,8 @@ def main():
|
||||
|
||||
# And the seal itself, red: one wrong bit in the address of that
|
||||
# same frame must be refused outright. The verdict has to arrive
|
||||
# *before* the page would have been sent — that ordering is what
|
||||
# keeps a refusal from turning into a desync — so the probe sends
|
||||
# *before* the page would have been sent - that ordering is what
|
||||
# keeps a refusal from turning into a desync - so the probe sends
|
||||
# no payload at all and expects the loader straight back at the
|
||||
# command level.
|
||||
broken = bytearray(head + bytes((seal,)))
|
||||
@@ -186,7 +186,7 @@ def main():
|
||||
|
||||
# The surgery, decoded independently: the patched vector must land on the
|
||||
# loader, the trampoline on the application's own entry (patched-vector
|
||||
# chips only — a boot-sectioned mega's word 0 stays the application's).
|
||||
# chips only - a boot-sectioned mega's word 0 stays the application's).
|
||||
if patch:
|
||||
flash_words = (base + pb.SLOT) // 2
|
||||
app = open(app_bin, "rb").read()
|
||||
|
||||
@@ -4,8 +4,8 @@ itself with a re-timed build, and every power-fail phase is rehearsed by
|
||||
killing the device mid-write, restarting it from its flash dump, and letting
|
||||
a re-run complete the update.
|
||||
|
||||
The boot-sectioned megas run the BOOTRST-unprogrammed profile — reset boots
|
||||
the application, whose 'L' is the application-owned loader entry — with
|
||||
The boot-sectioned megas run the BOOTRST-unprogrammed profile - reset boots
|
||||
the application, whose 'L' is the application-owned loader entry - with
|
||||
--assume-fuses standing in for the fuse read simavr cannot model.
|
||||
|
||||
Usage: pbupdate.py <device_bin> <pureboot_elf> <update_elf> <mcu> <hz>
|
||||
@@ -39,8 +39,8 @@ class PowerFail(Exception):
|
||||
|
||||
def assumed_fuses(pb, image):
|
||||
"""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
|
||||
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)
|
||||
@@ -138,7 +138,7 @@ def main():
|
||||
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=reset_hex)
|
||||
final = "v0"
|
||||
try:
|
||||
# The application first — its planner output is the restore truth.
|
||||
# The application first - its planner output is the restore truth.
|
||||
pbsim.run_tool(tool, device.pty, baud, "--flash", app_bin, "--stay")
|
||||
port, loader = connect(device)
|
||||
app_pages = pb.plan_flash(open(app_bin, "rb").read(), loader.info)
|
||||
@@ -167,7 +167,7 @@ def main():
|
||||
# direction so the flash is never already at its target. The mega's
|
||||
# mid-resident-rewrite loss is exercised as a host crash instead:
|
||||
# with BOOTRST unprogrammed and the resident mid-erase, a power loss
|
||||
# there has no reset path into the staging copy — the documented
|
||||
# there has no reset path into the staging copy - the documented
|
||||
# cost of that profile (README).
|
||||
for kill_region, kill_hits, kill_device in (
|
||||
("stage", 2, True),
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
"""The activation window as a behavioral duration gate.
|
||||
|
||||
The loader's window is a counted poll loop whose per-poll cost is hand-counted
|
||||
in the source (`link::poll_cycles`) — but the loop compiles in consumer
|
||||
in the source (`link::poll_cycles`) - but the loop compiles in consumer
|
||||
context, so only the running image can prove the count. This test installs a
|
||||
real application beside the loader (the host tool's own `plan_flash` supplies
|
||||
the reset-vector surgery), starts the simulator with the line idle, and reads
|
||||
the cycle of the first transmit activity: nothing talks until the window
|
||||
closes and the application banners, so that cycle *is* the window, give or
|
||||
take a banner lead measured in microseconds. Asserted at ±2 % — one
|
||||
take a banner lead measured in microseconds. Asserted at +/-2 % - one
|
||||
mis-counted cycle per poll shifts a window by 10 % and more.
|
||||
|
||||
Fixed-baud loaders declare their window in seconds (--seconds, the build's
|
||||
TIMEOUT). The autobaud loader's window is its calibration poll budget
|
||||
(--autobaud-polls); the seconds it amounts to are budget × 9 / f_cpu, the
|
||||
(--autobaud-polls); the seconds it amounts to are budget x 9 / f_cpu, the
|
||||
measured cost of the calibrate() wait loop this gate pins.
|
||||
"""
|
||||
import argparse
|
||||
@@ -26,7 +26,7 @@ import time
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
|
||||
from pbsim import Device
|
||||
|
||||
# The calibrate() budget loop's cycles per poll in the built image — what the
|
||||
# The calibrate() budget loop's cycles per poll in the built image - what the
|
||||
# README's window arithmetic rests on, verified here. A measured fact, not a
|
||||
# design constant: the wait's exit branches land where the compiler's block
|
||||
# layout puts them, and the bounded-calibration rework moved the loop from
|
||||
@@ -43,7 +43,7 @@ def load_tool(path):
|
||||
|
||||
def compose_flash(pb, loader_bytes, app_bytes, mcu, base, page):
|
||||
"""The flash image a completed programming session leaves: application
|
||||
(with the tinies' vector surgery), loader at base — built through the
|
||||
(with the tinies' vector surgery), loader at base - built through the
|
||||
host tool's own planner so the surgery is the shipped one, not a copy."""
|
||||
flash_size = base + pb.SLOT
|
||||
patch = not mcu.startswith("atmega") or mcu.startswith("atmega48")
|
||||
@@ -129,7 +129,7 @@ def main():
|
||||
error = (measured - expected) / expected
|
||||
ok = abs(error) <= 0.02
|
||||
print(f" [{'PASS' if ok else 'FAIL'}] window {measured:.3f} s vs declared "
|
||||
f"{expected:.3f} s ({error:+.1%}, gate ±2%)")
|
||||
f"{expected:.3f} s ({error:+.1%}, gate +/-2%)")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
|
||||
@@ -11,18 +11,19 @@
|
||||
// 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,
|
||||
// and `-l sw:D0,D1@0` where those pins are a USART's own — see the pin
|
||||
// and `-l sw:D0,D1@0` where those pins are a USART's own - see the pin
|
||||
// ownership the bridge models below.
|
||||
//
|
||||
// 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
|
||||
// missing module is supplied here: the SPM ioctl reads SPMCSR/Z/r1:r0 and
|
||||
// implements buffer fill, page erase, page write, and CTPB, completing
|
||||
// instantly. RFLB's LPM diversion (fuse readout) stays unmodeled, so the
|
||||
// 'F' command answers with flash bytes — the tests assert transport only.
|
||||
// 'F' command answers with flash bytes - the tests assert transport only.
|
||||
//
|
||||
// On exit (or SIGTERM) the flash and EEPROM are dumped to files for a
|
||||
// ground-truth cross-check against what the host read back.
|
||||
#include <array>
|
||||
#include <csignal>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -37,7 +38,7 @@
|
||||
#include <unistd.h>
|
||||
|
||||
// The parts headers (uart_pty.h) carry no C++ linkage guards of their own,
|
||||
// unlike simavr's core headers — the block covers both harmlessly.
|
||||
// unlike simavr's core headers - the block covers both harmlessly.
|
||||
extern "C" {
|
||||
#include "avr_eeprom.h"
|
||||
#include "avr_flash.h"
|
||||
@@ -64,7 +65,7 @@ std::uint32_t reset_pc;
|
||||
volatile std::sig_atomic_t reset_requested;
|
||||
|
||||
// -w: report the cycle of the first transmit activity, once. What the
|
||||
// activation-window gate reads — with an idle line and an application
|
||||
// activation-window gate reads - with an idle line and an application
|
||||
// installed, the first thing that ever talks is the application's banner,
|
||||
// so this cycle *is* the loader's window plus a banner lead measured in
|
||||
// microseconds. Idle pacing is skipped in this mode: there is no real-time
|
||||
@@ -74,8 +75,9 @@ bool window_tx_seen;
|
||||
|
||||
void window_first_tx()
|
||||
{
|
||||
if (!window_report || window_tx_seen)
|
||||
if (!window_report || window_tx_seen) {
|
||||
return;
|
||||
}
|
||||
window_tx_seen = true;
|
||||
std::println("PB_WINDOW_TX {}", avr->cycle);
|
||||
std::fflush(stdout);
|
||||
@@ -91,7 +93,7 @@ void window_uart_hook(avr_irq_t *, std::uint32_t, void *)
|
||||
// direction the way the real wiring does: it drives only while the
|
||||
// firmware's DDR bit reads input, decodes transitions as the firmware's
|
||||
// transmit only while the firmware owns the line, ignores its own raises
|
||||
// coming back through the shared irq — and echoes every byte it drives back
|
||||
// coming back through the shared irq - and echoes every byte it drives back
|
||||
// to the pty, which is what the host-side FTDI tie does and what the host
|
||||
// tool's --one-wire mode reads back and discards.
|
||||
bool link_one_wire;
|
||||
@@ -107,8 +109,9 @@ int parse_link(std::string_view spec)
|
||||
}
|
||||
if (spec.starts_with("sw")) {
|
||||
link_software = true;
|
||||
if (spec.size() == 2)
|
||||
if (spec.size() == 2) {
|
||||
return 0;
|
||||
}
|
||||
char owner = 0;
|
||||
int fields =
|
||||
std::sscanf(spec.data() + 2, ":%c%d,%c%d@%c", &sw_rx_port, &sw_rx_bit, &sw_tx_port, &sw_tx_bit, &owner);
|
||||
@@ -122,16 +125,16 @@ int parse_link(std::string_view spec)
|
||||
}
|
||||
|
||||
// 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) —
|
||||
// hardware ignores the in-page bits (§26.8.1), so an erase issued with Z
|
||||
// Z & ~1 instead of the page containing Z (its PGWRT path masks correctly) -
|
||||
// hardware ignores the in-page bits (section 26.8.1), so an erase issued with Z
|
||||
// anywhere inside the page wipes half the neighbouring page in simulation
|
||||
// only. Wrap the mega's registered flash ioctl and re-dispatch page erases
|
||||
// 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
|
||||
// temporary-buffer discard (Atmel-8271 section 26.2/section 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.
|
||||
avr_flash_t *mega_flash;
|
||||
@@ -171,7 +174,7 @@ void fix_mega_flash_erase()
|
||||
return;
|
||||
}
|
||||
}
|
||||
std::println(stderr, "device: no flash module to fix — SPM page erases may misalign");
|
||||
std::println(stderr, "device: no flash module to fix - SPM page erases may misalign");
|
||||
}
|
||||
|
||||
void request_reset(int)
|
||||
@@ -183,8 +186,8 @@ void request_reset(int)
|
||||
|
||||
struct tiny_nvm_t {
|
||||
avr_io_t io;
|
||||
std::uint8_t buffer[128];
|
||||
std::uint8_t used[128]; // a buffer word loads once until erased — like silicon
|
||||
std::array<std::uint8_t, 128> buffer;
|
||||
std::array<std::uint8_t, 128> used; // a buffer word loads once until erased - like silicon
|
||||
unsigned page;
|
||||
};
|
||||
|
||||
@@ -192,8 +195,9 @@ tiny_nvm_t nvm;
|
||||
|
||||
int nvm_ioctl(avr_io_t *io, std::uint32_t ctl, void *)
|
||||
{
|
||||
if (ctl != AVR_IOCTL_FLASH_SPM)
|
||||
if (ctl != AVR_IOCTL_FLASH_SPM) {
|
||||
return -1;
|
||||
}
|
||||
auto *n = reinterpret_cast<tiny_nvm_t *>(io);
|
||||
avr_t *mcu = io->avr;
|
||||
std::uint8_t command = mcu->data[0x57] & 0x1f; // SPMCSR, both tinies
|
||||
@@ -209,13 +213,14 @@ int nvm_ioctl(avr_io_t *io, std::uint32_t ctl, void *)
|
||||
} else if (command == 0x03) { // PGERS
|
||||
std::memset(mcu->flash + page_base, 0xff, n->page);
|
||||
} else if (command == 0x05) { // PGWRT: programming only clears bits
|
||||
for (unsigned i = 0; i < n->page; i++)
|
||||
for (unsigned i = 0; i < n->page; i++) {
|
||||
mcu->flash[page_base + i] &= n->buffer[i];
|
||||
std::memset(n->buffer, 0xff, n->page);
|
||||
std::memset(n->used, 0, n->page);
|
||||
}
|
||||
std::memset(n->buffer.data(), 0xff, n->page);
|
||||
std::memset(n->used.data(), 0, n->page);
|
||||
} else if (command == 0x11) { // CTPB
|
||||
std::memset(n->buffer, 0xff, n->page);
|
||||
std::memset(n->used, 0, n->page);
|
||||
std::memset(n->buffer.data(), 0xff, n->page);
|
||||
std::memset(n->used.data(), 0, n->page);
|
||||
}
|
||||
mcu->data[0x57] &= static_cast<std::uint8_t>(~0x1f); // the operation completes instantly
|
||||
return 0;
|
||||
@@ -234,9 +239,9 @@ avr_cycle_count_t tx_sample(avr_t *, avr_cycle_count_t when, void *)
|
||||
{
|
||||
if (tx_bit < 0) {
|
||||
// Half a bit into the start bit: a real receiver re-samples here and
|
||||
// abandons a false start. The device's own init produces one — DDR
|
||||
// abandons a false start. The device's own init produces one - DDR
|
||||
// drives the pin low for the instructions until the idle level is
|
||||
// written — and without this check that glitch decodes as a stray
|
||||
// written - and without this check that glitch decodes as a stray
|
||||
// byte (and would read as first transmit activity under -w).
|
||||
if (tx_level) {
|
||||
tx_active = 0;
|
||||
@@ -248,23 +253,25 @@ avr_cycle_count_t tx_sample(avr_t *, avr_cycle_count_t when, void *)
|
||||
}
|
||||
if (tx_bit < 8) {
|
||||
tx_shift = static_cast<std::uint8_t>((tx_shift >> 1) | (tx_level ? 0x80 : 0));
|
||||
if (++tx_bit < 8)
|
||||
if (++tx_bit < 8) {
|
||||
return when + bit_cycles;
|
||||
}
|
||||
// The byte is delivered at the stop bit's sampling point (9.5 bit
|
||||
// times), where a hardware receiver raises its RXC — not sooner: a
|
||||
// times), where a hardware receiver raises its RXC - not sooner: a
|
||||
// host answering before the stop bit would put its start bit on the
|
||||
// wire while the device is still driving, which the device,
|
||||
// transmitting, is not watching for.
|
||||
return when + bit_cycles;
|
||||
}
|
||||
if (write(pty_master, &tx_shift, 1) != 1)
|
||||
if (write(pty_master, &tx_shift, 1) != 1) {
|
||||
std::println(stderr, "device: pty write lost a byte");
|
||||
}
|
||||
tx_active = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// A USART owns its TxD pin whenever its transmitter is enabled, and the port
|
||||
// register cannot drive it (§20.2 / Atmel-8271 §19.2) — which is why a
|
||||
// register cannot drive it (section 20.2 / Atmel-8271 section 19.2) - which is why a
|
||||
// bit-banged link deployed on those pins is mute until it clears UCSRnB.
|
||||
// simavr wires a USART entirely through IRQs and never touches the port pin
|
||||
// model, so the ownership does not exist there and the mute cannot happen:
|
||||
@@ -274,36 +281,40 @@ avr_uart_t *tx_owner;
|
||||
|
||||
bool tx_pin_taken()
|
||||
{
|
||||
if (!tx_owner)
|
||||
if (!tx_owner) {
|
||||
return false;
|
||||
if (avr_regbit_get(avr, tx_owner->txen))
|
||||
}
|
||||
if (avr_regbit_get(avr, tx_owner->txen)) {
|
||||
return true;
|
||||
}
|
||||
// One-wire on the USART's RXD: RXEN forces the shared pin's direction to
|
||||
// input (§20.7.3), so the firmware's drive goes nowhere until the
|
||||
// release — the receive-side twin of the TXD hold.
|
||||
// input (section 20.7.3), so the firmware's drive goes nowhere until the
|
||||
// release - the receive-side twin of the TXD hold.
|
||||
return link_one_wire && avr_regbit_get(avr, tx_owner->rxen);
|
||||
}
|
||||
|
||||
// simavr leaves TXEN set in UCSRnB out of reset, where silicon clears the
|
||||
// whole register (§20.11.3) — which would hand the pin to a USART no code has
|
||||
// whole register (section 20.11.3) - which would hand the pin to a USART no code has
|
||||
// enabled, making a freshly reset chip mute for reasons hardware does not
|
||||
// have. Reset it the way the datasheet does, so the ownership starts from
|
||||
// nobody's and only an application that really enables the USART takes it.
|
||||
void reset_tx_owner()
|
||||
{
|
||||
if (tx_owner)
|
||||
if (tx_owner) {
|
||||
avr_regbit_clear(avr, tx_owner->txen);
|
||||
}
|
||||
}
|
||||
|
||||
void find_tx_owner()
|
||||
{
|
||||
for (avr_io_t *io = avr->io_port; io; io = io->next)
|
||||
for (avr_io_t *io = avr->io_port; io; io = io->next) {
|
||||
if (io->kind && std::string_view{io->kind} == "uart" &&
|
||||
reinterpret_cast<avr_uart_t *>(io)->name == sw_tx_owner) {
|
||||
tx_owner = reinterpret_cast<avr_uart_t *>(io);
|
||||
reset_tx_owner();
|
||||
return;
|
||||
}
|
||||
}
|
||||
std::println(stderr, "device: no USART{} to own the software link's TX pin", sw_tx_owner);
|
||||
}
|
||||
|
||||
@@ -311,7 +322,7 @@ void tx_hook(avr_irq_t *, std::uint32_t value, void *)
|
||||
{
|
||||
if (link_one_wire && (self_drive || !mcu_owns_line)) {
|
||||
// The bridge's own drive coming back through the shared irq, or a
|
||||
// transition while the line is the bridge's — either way not the
|
||||
// transition while the line is the bridge's - either way not the
|
||||
// firmware talking: the decoder sees an idle line.
|
||||
tx_level = 1;
|
||||
return;
|
||||
@@ -329,7 +340,7 @@ void tx_hook(avr_irq_t *, std::uint32_t value, void *)
|
||||
tx_level = level;
|
||||
}
|
||||
|
||||
std::uint8_t rx_queue[8192];
|
||||
std::array<std::uint8_t, 8192> rx_queue;
|
||||
unsigned rx_head, rx_tail; // ring: head = next to send
|
||||
int rx_active, rx_bit;
|
||||
std::uint8_t rx_byte;
|
||||
@@ -355,10 +366,11 @@ avr_cycle_count_t rx_step(avr_t *, avr_cycle_count_t when, void *)
|
||||
if (rx_bit == 8) { // stop bit, plus one idle bit of margin
|
||||
bridge_drive(1);
|
||||
// The host-side tie: an FTDI adapter on a one-wire line reads every
|
||||
// byte it transmits — supply that echo, which the host tool's
|
||||
// byte it transmits - supply that echo, which the host tool's
|
||||
// --one-wire mode consumes as its wiring check.
|
||||
if (link_one_wire && write(pty_master, &rx_byte, 1) != 1)
|
||||
if (link_one_wire && write(pty_master, &rx_byte, 1) != 1) {
|
||||
std::println(stderr, "device: pty echo lost a byte");
|
||||
}
|
||||
rx_bit++;
|
||||
return when + 2 * bit_cycles;
|
||||
}
|
||||
@@ -369,15 +381,17 @@ avr_cycle_count_t rx_step(avr_t *, avr_cycle_count_t when, void *)
|
||||
|
||||
void rx_start_next()
|
||||
{
|
||||
if (rx_active || rx_head == rx_tail)
|
||||
if (rx_active || rx_head == rx_tail) {
|
||||
return;
|
||||
// The firmware is answering on the shared line: hold the byte — a real
|
||||
}
|
||||
// The firmware is answering on the shared line: hold the byte - a real
|
||||
// host's transmission waits out the reply on the wire too. The next
|
||||
// poll_pty tick retries once the line is handed back.
|
||||
if (link_one_wire && mcu_owns_line)
|
||||
if (link_one_wire && mcu_owns_line) {
|
||||
return;
|
||||
}
|
||||
rx_byte = rx_queue[rx_head];
|
||||
rx_head = (rx_head + 1) % sizeof(rx_queue);
|
||||
rx_head = (rx_head + 1) % rx_queue.size();
|
||||
rx_active = 1;
|
||||
rx_bit = 0;
|
||||
bridge_drive(0); // start bit
|
||||
@@ -389,11 +403,12 @@ void rx_start_next()
|
||||
void on_ddr(avr_irq_t *, std::uint32_t value, void *)
|
||||
{
|
||||
const bool owns = (value >> sw_rx_bit) & 1;
|
||||
if (mcu_owns_line && !owns)
|
||||
if (mcu_owns_line && !owns) {
|
||||
bridge_drive(1); // hand-back: a turn-based host idles here, and the cache stays truthful
|
||||
}
|
||||
mcu_owns_line = owns;
|
||||
// A byte held back while the firmware answered starts from the next
|
||||
// poll_pty tick, never from inside the DDR write itself — the port
|
||||
// poll_pty tick, never from inside the DDR write itself - the port
|
||||
// model's own pull-up re-derivation runs right after this notify and
|
||||
// would erase a start edge raised here.
|
||||
}
|
||||
@@ -416,7 +431,7 @@ void bridge_reset()
|
||||
mcu_owns_line = false; // avr_reset zeroed DDR: every pin reads input again
|
||||
// Re-drive the idle line through a forced transition: ioport pin irqs are
|
||||
// IRQ_FLAG_FILTERED, and avr_reset zeroes the port latch while the irq
|
||||
// keeps its pre-reset cached value — so a plain raise(1) against a cached
|
||||
// keeps its pre-reset cached value - so a plain raise(1) against a cached
|
||||
// 1 is dropped and the device reads the line stuck low. A loader entering
|
||||
// calibration on that line measures reset-to-first-edge as one giant
|
||||
// pulse and mis-locks or boots the application on the first real knock.
|
||||
@@ -428,12 +443,13 @@ void bridge_reset()
|
||||
|
||||
void poll_pty()
|
||||
{
|
||||
std::uint8_t chunk[256];
|
||||
ssize_t got = read(pty_master, chunk, sizeof(chunk));
|
||||
std::array<std::uint8_t, 256> chunk;
|
||||
ssize_t got = read(pty_master, chunk.data(), chunk.size());
|
||||
for (ssize_t i = 0; i < got; i++) {
|
||||
unsigned next = (rx_tail + 1) % sizeof(rx_queue);
|
||||
if (next == rx_head)
|
||||
unsigned next = (rx_tail + 1) % rx_queue.size();
|
||||
if (next == rx_head) {
|
||||
break; // full: the host will retry on timeout
|
||||
}
|
||||
rx_queue[rx_tail] = chunk[i];
|
||||
rx_tail = next;
|
||||
}
|
||||
@@ -452,19 +468,24 @@ void poll_pty()
|
||||
std::fwrite(avr->flash, 1, avr->flashend + 1, f);
|
||||
std::fclose(f);
|
||||
}
|
||||
avr_eeprom_desc_t ee = {.ee = nullptr, .offset = 0, .size = 0};
|
||||
avr_eeprom_desc_t ee = {
|
||||
.ee = nullptr,
|
||||
.offset = 0,
|
||||
.size = 0,
|
||||
};
|
||||
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &ee) == 0 && ee.ee && ee.size) {
|
||||
char path[512];
|
||||
std::snprintf(path, sizeof(path), "%s.eeprom", dump_path);
|
||||
f = std::fopen(path, "wb");
|
||||
std::array<char, 512> path;
|
||||
std::snprintf(path.data(), path.size(), "%s.eeprom", dump_path);
|
||||
f = std::fopen(path.data(), "wb");
|
||||
if (f) {
|
||||
std::fwrite(ee.ee, 1, ee.size, f);
|
||||
std::fclose(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!link_software)
|
||||
if (!link_software) {
|
||||
uart_pty_stop(&uart_pty);
|
||||
}
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
@@ -494,7 +515,7 @@ int main(int argc, char *argv[])
|
||||
" -w: print PB_WINDOW_TX <cycle> at the first transmit activity and\n"
|
||||
" free-run idle time (window measurement mode)\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",
|
||||
argv[0]);
|
||||
return 2;
|
||||
@@ -506,8 +527,9 @@ int main(int argc, char *argv[])
|
||||
auto baud = static_cast<unsigned>(std::atoi(argv[6]));
|
||||
dump_path = argv[7];
|
||||
const bool is_mega = mcu_name.starts_with("atmega");
|
||||
if (!link_given)
|
||||
if (!link_given) {
|
||||
link_software = !is_mega; // the chips' natural links: USART0, or PB0/PB1
|
||||
}
|
||||
|
||||
avr = avr_make_mcu_by_name(mcu_name.data());
|
||||
if (!avr) {
|
||||
@@ -534,17 +556,17 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
// An image past flash end would smash the simulator's heap and turn
|
||||
// into phantom peripheral behavior (lessons: believe the size gate
|
||||
// first) — refuse it loudly instead.
|
||||
// first) - refuse it loudly instead.
|
||||
if (base + fw.flashsize > avr->flashend + 1) {
|
||||
std::println(stderr, "device: {} B at {:#x} runs past flash end {:#x} — image does not fit its slot",
|
||||
std::println(stderr, "device: {} B at {:#x} runs past flash end {:#x} - image does not fit its slot",
|
||||
fw.flashsize, base, avr->flashend);
|
||||
return 1;
|
||||
}
|
||||
std::memcpy(avr->flash + base, fw.flash, fw.flashsize);
|
||||
}
|
||||
// The boot-sectioned megas enter the loader in hardware (BOOTRST, not
|
||||
// modeled — the argument picks the modeled fuse's target); the tinies
|
||||
// and the boot-section-less m48s reset to word 0 like silicon — erased
|
||||
// modeled - the argument picks the modeled fuse's target); the tinies
|
||||
// and the boot-section-less m48s reset to word 0 like silicon - erased
|
||||
// flash walks up into the loader, and after the host's surgery the
|
||||
// patched vector routes there.
|
||||
const bool boot_section = is_mega && !mcu_name.starts_with("atmega48");
|
||||
@@ -553,11 +575,15 @@ int main(int argc, char *argv[])
|
||||
avr->codeend = avr->flashend;
|
||||
|
||||
// Erased EEPROM, as hardware powers up (simavr zeroes it).
|
||||
std::uint8_t blank[1024];
|
||||
std::memset(blank, 0xff, sizeof(blank));
|
||||
avr_eeprom_desc_t seed = {.ee = blank, .offset = 0, .size = 0};
|
||||
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &seed) == 0 && seed.size <= sizeof(blank)) {
|
||||
seed.ee = blank;
|
||||
std::array<std::uint8_t, 1024> blank;
|
||||
std::memset(blank.data(), 0xff, blank.size());
|
||||
avr_eeprom_desc_t seed = {
|
||||
.ee = blank.data(),
|
||||
.offset = 0,
|
||||
.size = 0,
|
||||
};
|
||||
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &seed) == 0 && seed.size <= blank.size()) {
|
||||
seed.ee = blank.data();
|
||||
avr_ioctl(avr, AVR_IOCTL_EEPROM_SET, &seed);
|
||||
}
|
||||
|
||||
@@ -568,7 +594,7 @@ int main(int argc, char *argv[])
|
||||
fix_mega_flash_erase();
|
||||
} else {
|
||||
nvm.page = page;
|
||||
std::memset(nvm.buffer, 0xff, sizeof(nvm.buffer));
|
||||
std::memset(nvm.buffer.data(), 0xff, nvm.buffer.size());
|
||||
nvm.io.kind = "tiny_nvm";
|
||||
nvm.io.ioctl = nvm_ioctl;
|
||||
avr_register_io(avr, &nvm.io);
|
||||
@@ -582,35 +608,41 @@ int main(int argc, char *argv[])
|
||||
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
|
||||
// simavr leaves TXEN set out of reset where silicon clears the whole
|
||||
// UCSR#B (§20.11.3). Harmless to a loader that enables TXEN itself —
|
||||
// UCSR#B (section 20.11.3). Harmless to a loader that enables TXEN itself -
|
||||
// but a half-duplex build's receiver-only init then *drops* TXEN,
|
||||
// and this uart model clears UDRE on that edge and never re-raises
|
||||
// it on a later enable: the first transmitter after the hand-over
|
||||
// waits UDRE forever, a wedge silicon does not have. Start from the
|
||||
// datasheet's zero, as the software bridge's tx-owner model does.
|
||||
for (avr_io_t *io = avr->io_port; io; io = io->next)
|
||||
for (avr_io_t *io = avr->io_port; io; io = io->next) {
|
||||
if (io->kind && std::string_view{io->kind} == "uart" &&
|
||||
reinterpret_cast<avr_uart_t *>(io)->name == uart_digit)
|
||||
reinterpret_cast<avr_uart_t *>(io)->name == uart_digit) {
|
||||
hw_uart = reinterpret_cast<avr_uart_t *>(io);
|
||||
if (hw_uart)
|
||||
}
|
||||
}
|
||||
if (hw_uart) {
|
||||
avr_regbit_clear(avr, hw_uart->txen);
|
||||
}
|
||||
uart_pty_init(avr, &uart_pty);
|
||||
uart_pty_connect(&uart_pty, uart_digit);
|
||||
if (window_report)
|
||||
if (window_report) {
|
||||
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_UART_GETIRQ(uart_digit), UART_IRQ_OUTPUT),
|
||||
window_uart_hook, nullptr);
|
||||
}
|
||||
std::println("PB_PTY {}", uart_pty.pty.slavename);
|
||||
} else {
|
||||
bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly
|
||||
if (sw_tx_owner)
|
||||
if (sw_tx_owner) {
|
||||
find_tx_owner();
|
||||
}
|
||||
rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_rx_port), static_cast<unsigned>(sw_rx_bit));
|
||||
avr_irq_register_notify(
|
||||
avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_tx_port), static_cast<unsigned>(sw_tx_bit)), tx_hook,
|
||||
nullptr);
|
||||
if (link_one_wire)
|
||||
if (link_one_wire) {
|
||||
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_rx_port), IOPORT_IRQ_DIRECTION_ALL),
|
||||
on_ddr, nullptr);
|
||||
}
|
||||
bridge_drive(1); // idle line
|
||||
|
||||
int slave;
|
||||
@@ -632,8 +664,9 @@ int main(int argc, char *argv[])
|
||||
long since_poll = 0;
|
||||
for (;;) {
|
||||
int state = avr_run(avr);
|
||||
if (state == cpu_Done || state == cpu_Crashed)
|
||||
if (state == cpu_Done || state == cpu_Crashed) {
|
||||
break;
|
||||
}
|
||||
if (reset_requested) {
|
||||
reset_requested = 0;
|
||||
avr_reset(avr);
|
||||
@@ -643,8 +676,9 @@ int main(int argc, char *argv[])
|
||||
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);
|
||||
if (hw_uart) // and simavr's bogus reset TXEN (§20.11.3: zero)
|
||||
if (hw_uart) { // and simavr's bogus reset TXEN (section 20.11.3: zero)
|
||||
avr_regbit_clear(avr, hw_uart->txen);
|
||||
}
|
||||
} else {
|
||||
bridge_reset();
|
||||
reset_tx_owner();
|
||||
@@ -655,14 +689,15 @@ int main(int argc, char *argv[])
|
||||
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
|
||||
// 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 (!window_report && !rx_active && !tx_active && rx_head == rx_tail)
|
||||
if (!window_report && !rx_active && !tx_active && rx_head == rx_tail) {
|
||||
usleep(200);
|
||||
}
|
||||
}
|
||||
}
|
||||
finish(0);
|
||||
}
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
|
||||
`_handshake` drains the line after it sees a prompt, to absorb a real loader's
|
||||
trailing bytes before it asks for the identity. That drain must be bounded: a
|
||||
target that never falls quiet — a board stuck in a reset loop presents exactly
|
||||
target that never falls quiet - a board stuck in a reset loop presents exactly
|
||||
this, ~60 reboots/s of UART-reset garbage in which a stray 0x2b reads as a
|
||||
prompt — otherwise spins the tool forever. Regression for that hang, plus a
|
||||
prompt - otherwise spins the tool forever. Regression for that hang, plus a
|
||||
control that a well-behaved loader still connects.
|
||||
|
||||
The handshake must also survive its own leftovers: after `--stay` the loader's
|
||||
final prompt can still be in the USB pipeline when the next invocation opens
|
||||
the port, and on a board wired to reset on open, that opening starts a fresh
|
||||
activation window the stale prompt then betrays — the tool commits to an
|
||||
activation window the stale prompt then betrays - the tool commits to an
|
||||
identity read against a device that never heard its knock, and what it finally
|
||||
collects is the application's banner. StaleDTRPort is that moment as a port.
|
||||
|
||||
@@ -85,7 +85,7 @@ class StaleDTRPort:
|
||||
session's final prompt is still in transit and lands only after the
|
||||
opening flush has already run; the reset holds the device off the line
|
||||
at first, eating anything written before it completes; and the fresh
|
||||
window is finite — once it expires the application boots and prints a
|
||||
window is finite - once it expires the application boots and prints a
|
||||
banner whose bytes are what a pending identity read collects. A
|
||||
handshake that trusts the stale prompt spends the whole window waiting
|
||||
on a device that never heard its knock; one that drains the line first
|
||||
@@ -195,7 +195,7 @@ def main():
|
||||
check("well-behaved loader still connects (version 5)", info.version == 5)
|
||||
|
||||
# the stale prompt: a --stay leftover plus reset-on-open must not burn the
|
||||
# fresh window — the pre-knock drain absorbs it and the first real knock
|
||||
# fresh window - the pre-knock drain absorbs it and the first real knock
|
||||
# lands inside the window.
|
||||
try:
|
||||
stale_ok = pb.Loader(StaleDTRPort()).connect(2.5).version == 5
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Host-tool unit tests — the planning and policy logic, no simulator:
|
||||
"""Host-tool unit tests - the planning and policy logic, no simulator:
|
||||
programming orders and their recovery properties, the reset-vector surgery,
|
||||
the staging composition, the boot-fuse decode, and the update preflight over
|
||||
fuse combinations simavr cannot model.
|
||||
@@ -76,7 +76,7 @@ def main():
|
||||
"newer tool",
|
||||
)
|
||||
|
||||
# mega_boot: BOOTSZ words and the BOOTRST sense per chip — the fuse byte
|
||||
# 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
|
||||
@@ -114,15 +114,15 @@ def main():
|
||||
fail(f"mega_boot {signature[1]:02x}{signature[2]:02b} unprogrammed: {prog} {at:#07x}")
|
||||
|
||||
# Word-addressed info decode: the 1284P's base and page ride the wire
|
||||
# scaled — a 17-bit base halved into the block's two bytes, a 256-byte page
|
||||
# spelled 0 — and its slot is the same 512 bytes as everywhere else, so its
|
||||
# scaled - a 17-bit base halved into the block's two bytes, a 256-byte page
|
||||
# spelled 0 - and its slot is the same 512 bytes as everywhere else, so its
|
||||
# staging slot lands inside the 1 KiB minimum boot section.
|
||||
big = info_of(pb, 0x1FE00, 0, False, 0x20000, signature=(0x1E, 0x97, 0x05), word_flash=True)
|
||||
if big.page != 256 or big.base != 0x1FE00 or big.stage != 0x1FC00:
|
||||
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
|
||||
# entry — checked with an independent decoder.
|
||||
# entry - checked with an independent decoder.
|
||||
app = bytes((0xC0 | 0x00, 0xC0)) + bytes((0x12,)) * 300 # rjmp .+0x00C0... entry word 0xC0C0
|
||||
entry = rjmp_decode(app[0] | (app[1] << 8), 0, tiny.flash_size // 2)
|
||||
pages = pb.plan_flash(app, tiny)
|
||||
@@ -158,7 +158,7 @@ def main():
|
||||
|
||||
# Staging content: the identical image plus the through-word on a
|
||||
# patched-vector chip; hard size clamps either way.
|
||||
image = bytes(range(256)) * 2 # 512 B — too big for a tiny slot
|
||||
image = bytes(range(256)) * 2 # 512 B - too big for a tiny slot
|
||||
expect_error("tiny staging size", lambda: pb.staging_content(image, tiny), "510")
|
||||
staged = pb.staging_content(image[:508], tiny)
|
||||
through = staged[510] | (staged[511] << 8)
|
||||
@@ -170,7 +170,7 @@ def main():
|
||||
|
||||
# The image stamp: found in a synthetic binary, absent in noise. pureboot
|
||||
# 5 stamps the magic, its version and the signature, and the geometry is
|
||||
# looked up from there — so what comes back must equal what a live device
|
||||
# looked up from there - so what comes back must equal what a live device
|
||||
# of the same chip reports.
|
||||
stamp = bytes((0x50, 0x42, pb.NEWEST_LOADER)) + bytes(tiny.signature)
|
||||
binary = bytes((0xAA,)) * 10 + stamp + bytes((0xBB,)) * 10
|
||||
@@ -228,7 +228,7 @@ def main():
|
||||
|
||||
# The 1284s' smallest boot section (512 words) is exactly the resident
|
||||
# slot plus its staging slot, so self-update is possible at the minimum
|
||||
# BOOTSZ — no fuse step up, the 644's geometry. That holds only while a
|
||||
# BOOTSZ - no fuse step up, the 644's geometry. That holds only while a
|
||||
# slot is 512 B: at 1 KiB the staging slot would fall outside the section
|
||||
# and the preflight would refuse.
|
||||
notes = pb.update_preflight(bytes((0xAA,)) * 8 + big.raw, big, fuses(0xFE))
|
||||
@@ -291,7 +291,7 @@ def main():
|
||||
if device.writes != pb.RETRIES + 1:
|
||||
fail(f"unrepairable page took {device.writes} writes, expected {pb.RETRIES + 1}")
|
||||
|
||||
# The knock handshake against a device that is not listening yet — the
|
||||
# The knock handshake against a device that is not listening yet - the
|
||||
# state a port open leaves behind: it resets the chip into a fresh
|
||||
# activation window while the previous session's prompt is still in
|
||||
# flight, so the first knock is lost and a prompt arrives anyway.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""--scan's walk and report logic, no simulator: the probe order, the rate
|
||||
arithmetic, and the advice's direction. The rate physics itself is not
|
||||
sim-testable — a pty carries bytes at any termios rate — so what the wire
|
||||
sim-testable - a pty carries bytes at any termios rate - so what the wire
|
||||
would arbitrate is pinned here as logic instead.
|
||||
|
||||
Usage: test_scan.py <tool_py>
|
||||
@@ -47,7 +47,7 @@ def main():
|
||||
fail(f"the absolute clock must scale with the found ratio:\n{report}")
|
||||
|
||||
# The walk's rates mostly have no termios B-constant, so the POSIX port
|
||||
# must set them through termios2 — probed on a pty, which accepts the
|
||||
# must set them through termios2 - probed on a pty, which accepts the
|
||||
# ioctl without caring about the speed. Without this every off-nominal
|
||||
# probe would abort the walk on the platform --scan matters most on.
|
||||
if os.name == "posix":
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
|
||||
`--update-loader` installs the new image in the staging slot and then *enters
|
||||
it* to have it rewrite the resident. That copy is the new image, so it speaks the
|
||||
new image's baud and backend — but the host was talking to the *resident*. Where
|
||||
new image's baud and backend - but the host was talking to the *resident*. Where
|
||||
the two differ, the host kept knocking at the old rate in the old mode, the
|
||||
staging copy never answered, and the update stranded: staging installed, resident
|
||||
untouched, and on a 1 KiB tiny the application region (which *is* the staging
|
||||
slot there) already gone.
|
||||
|
||||
The wire cannot be probed for this — 512 bytes of position-independent code carry
|
||||
no header saying what rate they were built for — so the operator declares it, and
|
||||
The wire cannot be probed for this - 512 bytes of position-independent code carry
|
||||
no header saying what rate they were built for - so the operator declares it, and
|
||||
a mismatch with nothing declared has to say so instead of reporting a bare
|
||||
timeout.
|
||||
|
||||
@@ -32,7 +32,7 @@ P = F = 0
|
||||
def check(name, ok, detail=""):
|
||||
global P, F
|
||||
P, F = P + (1 if ok else 0), F + (0 if ok else 1)
|
||||
print(f" [{'PASS' if ok else 'FAIL'}] {name}" + (f" — {detail}" if detail else ""))
|
||||
print(f" [{'PASS' if ok else 'FAIL'}] {name}" + (f" - {detail}" if detail else ""))
|
||||
|
||||
|
||||
class TwoLinkPort:
|
||||
|
||||
@@ -144,7 +144,7 @@ class Host:
|
||||
self._expect(CONFIRM, "C end")
|
||||
return echo
|
||||
|
||||
# Activation when the config page carries a password: 3×'@' then the
|
||||
# Activation when the config page carries a password: 3x'@' then the
|
||||
# password bytes, then the info block + mainloop '!'.
|
||||
def activate_password(self, password):
|
||||
self.s.reset_input_buffer()
|
||||
@@ -181,7 +181,7 @@ PW_BYTES = bytes([0x50, 0x57])
|
||||
|
||||
def scenario_roundtrip(host):
|
||||
"""Activation + info block + flash/EEPROM/config read-write round-trips, on
|
||||
a device with a blank (erased) config page — the usual no-password case."""
|
||||
a device with a blank (erased) config page - the usual no-password case."""
|
||||
info = host.activate()
|
||||
check(info[0:3] == b"TSB", f"magic 'TSB' (got {info[0:3]!r})")
|
||||
check(info[6:9] == bytes([0x1E, 0x95, 0x0F]), f"signature 1E 95 0F (got {info[6:9].hex()})")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/bin/bash
|
||||
# The port's gate: every chip's generated workflow — build, size matrix, and
|
||||
# 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) and swaps the compact size matrix for the exhaustive
|
||||
# clock × baud × backend cross product. libavr resolves from the `libavr/`
|
||||
# clock x baud x backend cross product. libavr resolves from the `libavr/`
|
||||
# submodule; LIBAVR_ROOT overrides it for a working tree.
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
@@ -36,7 +36,7 @@ if ((full)); then
|
||||
done
|
||||
fi
|
||||
|
||||
# Every tree is freshly built now — the one moment the README's size table
|
||||
# Every tree is freshly built now - the one moment the README's size table
|
||||
# can be held to what the images measure (a per-preset ctest sees only its
|
||||
# own chip; the table needs all of them, and ungated it drifts: a
|
||||
# common-code shave moves every row at once with nothing over budget).
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regenerate CMakePresets.json — one uniform pipeline per chip.
|
||||
"""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
|
||||
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 — or with --check, which
|
||||
Run from the repo root: tools/make_presets.py - or with --check, which
|
||||
verifies the committed file matches this generator and edits nothing (the
|
||||
ctest entry `presets.generated` runs that, so drift reds the gate).
|
||||
"""
|
||||
@@ -90,7 +90,7 @@ def main():
|
||||
if "--check" in sys.argv[1:]:
|
||||
current = open(path).read() if os.path.exists(path) else ""
|
||||
if current != rendered:
|
||||
print("CMakePresets.json does not match its generator — run tools/make_presets.py")
|
||||
print("CMakePresets.json does not match its generator - run tools/make_presets.py")
|
||||
return 1
|
||||
return 0
|
||||
with open(path, "w") as f:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*board*: that the loader actually installed on it answers, that the memories
|
||||
round-trip over the real link, that the application it flashes runs afterwards,
|
||||
and that the refusals which keep a 512-byte slot alive still fire. Run it once
|
||||
when a board is brought up, and again whenever the deployment moves — a new
|
||||
when a board is brought up, and again whenever the deployment moves - a new
|
||||
clock, a new backend, new pins.
|
||||
|
||||
Every check derives its bounds from the info block the loader itself reports, so
|
||||
@@ -63,7 +63,7 @@ class Suite:
|
||||
info = loader.info
|
||||
self.check("identity read", True, info.describe())
|
||||
return info
|
||||
except Exception as error: # noqa: BLE001 — a dead link is a result
|
||||
except Exception as error: # noqa: BLE001 - a dead link is a result
|
||||
self.check("identity read", False, str(error)[:70])
|
||||
return None
|
||||
finally:
|
||||
@@ -75,7 +75,7 @@ class Suite:
|
||||
def scan(self) -> None:
|
||||
"""The --scan walk against real termios and a real oscillator: every
|
||||
probe rate must open a port (the off-nominal rates exist only through
|
||||
termios2), and one probe must answer — the nominal on a healthy board,
|
||||
termios2), and one probe must answer - the nominal on a healthy board,
|
||||
a neighbor on a drifted one. The rig injects the one reset per probe
|
||||
the operator supplies in the field; this is the rate physics the
|
||||
simulator cannot arbitrate (a pty carries bytes at any rate), pinned
|
||||
@@ -102,7 +102,7 @@ class Suite:
|
||||
continue
|
||||
finally:
|
||||
port.close()
|
||||
except Exception as error: # noqa: BLE001 — a rig hiccup is a result
|
||||
except Exception as error: # noqa: BLE001 - a rig hiccup is a result
|
||||
self.check("scan walks the probe ladder", False, str(error)[:70])
|
||||
return
|
||||
self.check("scan finds the board's rate", found is not None,
|
||||
@@ -139,7 +139,7 @@ class Suite:
|
||||
|
||||
if marker:
|
||||
# The tool hands over as it ends its session, so the application is
|
||||
# already running — but only on a board whose DTR is unwired, where
|
||||
# already running - but only on a board whose DTR is unwired, where
|
||||
# opening a port simply listens. Where DTR *is* wired to reset (an
|
||||
# Arduino, most USB-serial dev boards), this open resets the part
|
||||
# and the activation window comes first, so a marker emitted once at
|
||||
@@ -152,7 +152,7 @@ class Suite:
|
||||
sample = "".join(chr(b) if 32 <= b < 127 else "." for b in data[:40])
|
||||
self.check(f"application runs (emits {marker!r})", seen,
|
||||
f"|{sample}|" if seen or data else
|
||||
f"nothing in {marker_wait:g} s — if this board resets when its port "
|
||||
f"nothing in {marker_wait:g} s - if this board resets when its port "
|
||||
f"opens, that wait has to outlast the activation window")
|
||||
|
||||
back = self.work / "app-back.bin"
|
||||
@@ -166,7 +166,7 @@ class Suite:
|
||||
|
||||
Prefers ISP, because an independent reader is the only one that can
|
||||
testify about a loader just asked to erase around itself. Where no
|
||||
programmer is attached the link answers instead — which is weaker for
|
||||
programmer is attached the link answers instead - which is weaker for
|
||||
exactly the reason it is worth having, a destroyed loader being unable
|
||||
to report anything at all. The two are never printed under one word:
|
||||
an absent probe is a fact about the bench, a wrong byte is a verdict on
|
||||
@@ -189,8 +189,8 @@ class Suite:
|
||||
loader.connect(self.rig.d.wait)
|
||||
return (loader.read_flash(0, limit),
|
||||
loader.read_flash(info.base, slot_length),
|
||||
"the link, no probe attached — the loader's own account")
|
||||
except Exception as error: # noqa: BLE001 — a dead link is a result
|
||||
"the link, no probe attached - the loader's own account")
|
||||
except Exception as error: # noqa: BLE001 - a dead link is a result
|
||||
print(f" skip slot checks: no programmer, and the link did not "
|
||||
f"answer either ({str(error)[:60]})")
|
||||
return None, None, ""
|
||||
@@ -226,7 +226,7 @@ class Suite:
|
||||
|
||||
pureboot 9 has no running-slot guard: what stops a mangled command from
|
||||
erasing the loader is the seal and nothing else. So this aims the worst
|
||||
command the protocol has — an SPM erase at the loader's own first page —
|
||||
command the protocol has - an SPM erase at the loader's own first page -
|
||||
and damages one header byte at a time. Every one must come back NAK with
|
||||
the slot untouched and the session still in step.
|
||||
|
||||
@@ -271,10 +271,10 @@ class Suite:
|
||||
|
||||
# And the slot itself, read back over the link: the loader is the
|
||||
# thing that would have been erased, so its own account of its
|
||||
# first bytes is a real witness — an erased page reads all 0xff.
|
||||
# first bytes is a real witness - an erased page reads all 0xff.
|
||||
head = loader.read_flash(info.base, 16)
|
||||
self.check("loader slot intact", set(head) != {0xFF}, head[:8].hex())
|
||||
except Exception as error: # noqa: BLE001 — a dead link is a result
|
||||
except Exception as error: # noqa: BLE001 - a dead link is a result
|
||||
self.check("seal checks", False, str(error)[:70])
|
||||
finally:
|
||||
try:
|
||||
@@ -284,7 +284,7 @@ class Suite:
|
||||
|
||||
@staticmethod
|
||||
def _sealed(module, op, space, address, count, damage=None):
|
||||
"""A sealed header, damaged after sealing — the shape a link fault has."""
|
||||
"""A sealed header, damaged after sealing - the shape a link fault has."""
|
||||
head = bytearray((op, module.selector(space, address), address & 0xFF,
|
||||
(address >> 8) & 0xFF, count & 0xFF))
|
||||
seal = module.SEAL
|
||||
@@ -344,7 +344,7 @@ class Suite:
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="hardware acceptance suite for one pureboot deployment",
|
||||
epilog="overwrites the board's application flash and EEPROM — back them up first")
|
||||
epilog="overwrites the board's application flash and EEPROM - back them up first")
|
||||
pbrig.Deployment.add_arguments(parser)
|
||||
parser.add_argument("--app", type=pathlib.Path,
|
||||
help="application image to flash (test/pbapp.cpp built for this deployment)")
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
|
||||
The simulated suites (`test/pb*.py`) prove the protocol; this drives the same
|
||||
loader on real silicon, where the things a cycle-exact simulator cannot model
|
||||
live — an RC oscillator off its nominal, a reset edge that has to come from
|
||||
live - an RC oscillator off its nominal, a reset edge that has to come from
|
||||
somewhere, a serial bridge with its own idea of what a baud is.
|
||||
|
||||
Nothing here knows a port name, a part or a programmer. Every deployment fact
|
||||
arrives from the command line or the environment, so the same script serves any
|
||||
board: see `Deployment`. As a module it is the reset/flash/talk primitives that
|
||||
`pbhw.py` builds its acceptance suite from; as a command it is the handful of
|
||||
one-shot operations worth having on a rig — most importantly `backup`, which is
|
||||
one-shot operations worth having on a rig - most importantly `backup`, which is
|
||||
the only thing standing between a fuse experiment and an unrecoverable part.
|
||||
|
||||
Two rig facts are encoded here because they are not guessable and cost a
|
||||
@@ -18,7 +18,7 @@ session each to learn:
|
||||
|
||||
* **An ISP access resets the part**, and it runs again the moment the programmer
|
||||
releases it. That is the only reset edge available when the serial adapter's
|
||||
DTR is not wired to reset — so a loader session begins with an ISP touch and
|
||||
DTR is not wired to reset - so a loader session begins with an ISP touch and
|
||||
knocks immediately after, which is what `Rig.pureboot()` does.
|
||||
* **avrdude splits `-U memory:op:file:format` on colons**, so a Windows path's
|
||||
drive letter breaks the spec. Every file argument is therefore passed as a
|
||||
@@ -63,7 +63,7 @@ def bitclock_for(hz: int) -> str:
|
||||
"""A safe ISP bitclock for a part *currently running* at `hz`.
|
||||
|
||||
SCK must stay under a quarter of the target clock, so the bitclock follows
|
||||
the clock in force — not the one about to be fused in. Halving that ceiling
|
||||
the clock in force - not the one about to be fused in. Halving that ceiling
|
||||
again costs nothing on a link that moves a few hundred bytes and buys margin
|
||||
against an oscillator that is already known to be off its nominal.
|
||||
"""
|
||||
@@ -138,7 +138,7 @@ class Deployment:
|
||||
|
||||
|
||||
def load_pureboot(path: pathlib.Path = DEFAULT_PUREBOOT):
|
||||
"""The host tool as a module — its Port and Loader, not a subprocess.
|
||||
"""The host tool as a module - its Port and Loader, not a subprocess.
|
||||
|
||||
Used where a subprocess cannot express what is needed: a poke followed by a
|
||||
peek in the *same* session, or a raw read at an arbitrary baud.
|
||||
@@ -260,7 +260,7 @@ class Rig:
|
||||
def pureboot(self, *args: str, reset_first: bool = True, baud: int | None = None,
|
||||
autobaud: bool | None = None, timeout: int = 300,
|
||||
bitclock: str | None = None) -> tuple[int, str]:
|
||||
"""Reset, then knock immediately — see the module docstring.
|
||||
"""Reset, then knock immediately - see the module docstring.
|
||||
|
||||
Returns the host tool's exit status and its combined output, so a caller
|
||||
can assert on what it printed as well as on whether it succeeded.
|
||||
@@ -301,7 +301,7 @@ class Rig:
|
||||
"""Listen to whatever the board is saying, at an arbitrary rate.
|
||||
|
||||
Opening the port does not reset a board whose DTR is unwired, so this can
|
||||
sample a running application repeatedly without disturbing it — which is
|
||||
sample a running application repeatedly without disturbing it - which is
|
||||
what makes the rate sweep below possible.
|
||||
"""
|
||||
module = load_pureboot(self.d.pureboot)
|
||||
@@ -327,7 +327,7 @@ def measure_rate(rig: Rig, marker: bytes, built_baud: int, nominal_hz: int | Non
|
||||
"""Find a transmitting board's true bit rate, using only the serial port.
|
||||
|
||||
The board must be emitting something recognisable at a *fixed* cycles-per-bit
|
||||
— `test/pbapp.cpp` built with PUREBOOT_HEARTBEAT does. Since its bit timing is
|
||||
- `test/pbapp.cpp` built with PUREBOOT_HEARTBEAT does. Since its bit timing is
|
||||
a cycle count, its wire rate scales with its actual clock, so the host rates
|
||||
at which `marker` still decodes bracket that rate; the centre of the band is
|
||||
the answer, and with the clock the image was built for it gives the real one.
|
||||
@@ -430,7 +430,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
for baud, size, hit in result["samples"]:
|
||||
print(f" {baud:7d} Bd {size:5d} B {'MARKER' if hit else ''}")
|
||||
if not result["clean"]:
|
||||
print(f"no capture contained {args.marker!r} at any rate — is the board "
|
||||
print(f"no capture contained {args.marker!r} at any rate - is the board "
|
||||
f"transmitting, and on the pin this port is wired to?")
|
||||
return 1
|
||||
print(f"clean band {result['low']}..{result['high']} Bd")
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The size matrix asserts every image fits its slot; it says nothing about the
|
||||
numbers the README prints, and those drift. Every row of that table was eight
|
||||
bytes stale once `startup::caller_page()` landed — common code, so every build
|
||||
bytes stale once `startup::caller_page()` landed - common code, so every build
|
||||
moved at once and no test noticed, because none of them was over budget.
|
||||
|
||||
Two questions, both answered from built trees:
|
||||
@@ -12,14 +12,14 @@ Two questions, both answered from built trees:
|
||||
sizes.py check-readme the README's per-chip table against what is built
|
||||
|
||||
Nothing here knows a chip's geometry. The (image, budget) pairs come from each
|
||||
build's own `CTestTestfile.cmake` — the same values the gate checks — so the
|
||||
build's own `CTestTestfile.cmake` - the same values the gate checks - so the
|
||||
slot rules stay where they belong, in `pureboot/CMakeLists.txt`, and a chip
|
||||
added or a budget changed needs no edit here. Only trees a configure preset
|
||||
still owns are read: a stale directory keeps its last build, and a loader built
|
||||
before a slot changed will happily report a size that was true once
|
||||
(`tools/prune-build-trees.sh` in libavr removes them).
|
||||
|
||||
Sizes come from `avr-size`, and a target is only as current as its last build —
|
||||
Sizes come from `avr-size`, and a target is only as current as its last build -
|
||||
run the gate first if you want the table checked against today's source.
|
||||
"""
|
||||
|
||||
@@ -33,7 +33,7 @@ import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
# add_test(<name>.size ... -DELF=<path> ... -DLIMIT=<n> ...) — the gate's own
|
||||
# add_test(<name>.size ... -DELF=<path> ... -DLIMIT=<n> ...) - the gate's own
|
||||
# pairing of an image with the budget it must fit.
|
||||
# ctest writes the name as a bracket argument ([=[name.size]=]) and quotes the
|
||||
# rest, so the name starts after the bracket and the path ends at the quote.
|
||||
@@ -56,7 +56,7 @@ def preset_dirs() -> list[pathlib.Path]:
|
||||
listing = subprocess.run(["cmake", "--list-presets"], cwd=ROOT, capture_output=True, text=True)
|
||||
names = re.findall(r'^\s*"(.+)"$', listing.stdout, re.MULTILINE)
|
||||
if not names:
|
||||
sys.exit("cmake --list-presets returned nothing — run from a configured checkout")
|
||||
sys.exit("cmake --list-presets returned nothing - run from a configured checkout")
|
||||
return [d for d in (ROOT / "build" / n for n in names) if (d / "CTestTestfile.cmake").is_file()]
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ def collect() -> dict[str, list[tuple[str, int, int]]]:
|
||||
sizes = measure([elf for rows in found.values() for _, elf, _ in rows], tool)
|
||||
# A chip's generated and reflect trees must answer with the same bytes
|
||||
# (the identity invariant), so the same target measuring two sizes means
|
||||
# a stale tree — or an identity breach. Either is a finding; picking one
|
||||
# a stale tree - or an identity breach. Either is a finding; picking one
|
||||
# silently is how a gate reports another build's numbers as today's.
|
||||
for chip, rows in found.items():
|
||||
seen: dict[str, tuple[int, str]] = {}
|
||||
@@ -95,7 +95,7 @@ def collect() -> dict[str, list[tuple[str, int, int]]]:
|
||||
continue
|
||||
if name in seen and seen[name][0] != sizes[elf]:
|
||||
sys.exit(f"{chip} {name}: {seen[name][0]} B in {seen[name][1]} but "
|
||||
f"{sizes[elf]} B in {elf} — a stale tree (rebuild or remove it) "
|
||||
f"{sizes[elf]} B in {elf} - a stale tree (rebuild or remove it) "
|
||||
f"or a cross-mode identity breach")
|
||||
seen.setdefault(name, (sizes[elf], elf))
|
||||
measured = {
|
||||
@@ -111,7 +111,7 @@ def collect() -> dict[str, list[tuple[str, int, int]]]:
|
||||
def cmd_max(args) -> int:
|
||||
measured = collect()
|
||||
if not measured:
|
||||
sys.exit("nothing built — configure and build a preset first")
|
||||
sys.exit("nothing built - configure and build a preset first")
|
||||
over = []
|
||||
print(f"{'chip':<13} {'largest image':<34} {'.text':>6} {'budget':>7} headroom")
|
||||
for chip, rows in measured.items():
|
||||
@@ -129,7 +129,7 @@ def cmd_max(args) -> int:
|
||||
tightest = min(((chip, n, t, l) for chip, rows in measured.items() for n, t, l in rows),
|
||||
key=lambda row: row[3] - row[2])
|
||||
chip, name, text, limit = tightest
|
||||
print(f"tightest fit: {chip} {name} — {text} of {limit}, {limit - text} B spare")
|
||||
print(f"tightest fit: {chip} {name} - {text} of {limit}, {limit - text} B spare")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ def cmd_check_readme(args) -> int:
|
||||
"""The README's per-chip table, against the stock build and the worst
|
||||
autobaud configuration (OSCCAL baked, plus the USART-pin release where
|
||||
the chip has a USART; the one-wire fold of the same build is its twin
|
||||
and competes for the same cell) — the config the Autobaud column
|
||||
and competes for the same cell) - the config the Autobaud column
|
||||
documents."""
|
||||
readme = (ROOT / "pureboot" / "README.md").read_text()
|
||||
measured = collect()
|
||||
@@ -147,7 +147,8 @@ def cmd_check_readme(args) -> int:
|
||||
sys.exit("no size table found in pureboot/README.md")
|
||||
bad = skipped = 0
|
||||
for chips, stock_doc, auto_doc in rows:
|
||||
# "ATmega48, 48A, 48P, 48PA †" — the first name is the family's base.
|
||||
# "ATmega48, 48A, 48P, 48PA" plus any footnote mark - the first name
|
||||
# is the family's base, and the sub strips the rest.
|
||||
chip = re.sub(r"[^a-z0-9]", "", chips.split(",")[0].strip().lower())
|
||||
built = {name: text for name, text, _ in measured.get(chip, [])}
|
||||
# The on-USART pair defines the column where the chip has a USART;
|
||||
@@ -167,7 +168,7 @@ def cmd_check_readme(args) -> int:
|
||||
print(f" {chip:<12} {target:<18} README says {documented} B, built is {built[target]} B")
|
||||
bad += 1
|
||||
if bad:
|
||||
print(f"\n{bad} row(s) stale — update pureboot/README.md")
|
||||
print(f"\n{bad} row(s) stale - update pureboot/README.md")
|
||||
return 1
|
||||
print(f"README size table matches every built image ({len(rows)} rows"
|
||||
+ (f", {skipped} not built" if skipped else "") + ")")
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// TinySafeBoot on libavr — tier 3: full feature parity in the 512-byte boot
|
||||
// TinySafeBoot on libavr - tier 3: full feature parity in the 512-byte boot
|
||||
// section, in C++ except where the C ABI itself is the cost.
|
||||
//
|
||||
// The complete TinySafeBoot feature set — watchdog-reset bail, one-wire
|
||||
// The complete TinySafeBoot feature set - watchdog-reset bail, one-wire
|
||||
// half-duplex UART, a config-page activation timeout, the password gate,
|
||||
// emergency erase, and config/flash/EEPROM read-write — at 510 bytes in the
|
||||
// emergency erase, and config/flash/EEPROM read-write - at 510 bytes in the
|
||||
// 512-byte BOOTSZ=11 section the hand-written oracle occupies (500 B). This tier used to be one
|
||||
// monolithic inline-asm routine; it is now the tricks tier's C++ (same
|
||||
// register protocol, same structure — see tsb_tricks.cpp, including the
|
||||
// register protocol, same structure - see tsb_tricks.cpp, including the
|
||||
// global-register miscompile rules) with exactly two routines kept in
|
||||
// assembly, the two whose remaining cost *is* the calling convention:
|
||||
//
|
||||
@@ -16,12 +16,12 @@
|
||||
// countdown.
|
||||
// store the page-store loop: C++ cannot hold the receive byte pair and the
|
||||
// walked Z pointer across the rx calls without call-saved staging
|
||||
// (push/pop + a Y→Z copy per word); the asm calls rx knowing exactly
|
||||
// (push/pop + a Y->Z copy per word); the asm calls rx knowing exactly
|
||||
// which registers it touches and walks Z live across the whole page.
|
||||
//
|
||||
// Everything else — bring-up, activation, password gate, emergency erase,
|
||||
// Everything else - bring-up, activation, password gate, emergency erase,
|
||||
// dispatch, every SPM/EEPROM/flash primitive, every geometry/baud/info
|
||||
// constant — is C++ on libavr, and the two asm routines splice into the same
|
||||
// constant - is C++ on libavr, and the two asm routines splice into the same
|
||||
// global-register protocol the C++ uses (g_addr in Y, g_cnt in r16, g_window
|
||||
// in r7, g_receiving in r6), so calls cross the boundary with no marshalling.
|
||||
//
|
||||
@@ -41,10 +41,16 @@ namespace hw = avr::hw;
|
||||
namespace tsb {
|
||||
namespace {
|
||||
|
||||
// The loader is purely polled — it never enables interrupts — so every SPM and
|
||||
// The loader is purely polled - it never enables interrupts - so every SPM and
|
||||
// EEPROM lock folds to nothing under this posture.
|
||||
constexpr auto off = avr::irq::guard_policy::unused;
|
||||
|
||||
// Strict request/response: every SPM operation is waited out before the next
|
||||
// byte moves, so no flash operation is ever in flight at an EEPROM access -
|
||||
// the write procedure's step 2 has nothing to guard, the omission the
|
||||
// datasheet grants (DS40002061B section 8.6.3).
|
||||
constexpr auto no_spm = ee::spm_interlock::omitted;
|
||||
|
||||
constexpr std::uint8_t confirm = '!';
|
||||
constexpr std::uint8_t request = '?';
|
||||
constexpr std::uint8_t knock = '@';
|
||||
@@ -65,7 +71,7 @@ constexpr std::uint8_t comm_window = 200;
|
||||
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
||||
|
||||
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
||||
constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd);
|
||||
constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd, 8, avr::uart::parity::none);
|
||||
|
||||
// The 16-byte device-info block, streamed out on activation.
|
||||
// clang-format off
|
||||
@@ -94,7 +100,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
// Bounded byte receive (asm 1 of 2): release the one-wire line on a direction
|
||||
// change, poll RXC0 under the oracle's nested X-register countdown seeded from
|
||||
// g_window (floored against lockout), byte or 0-on-silence in r24. Z survives
|
||||
// — the property the store's word loop rides on.
|
||||
// - the property the store's word loop rides on.
|
||||
[[gnu::noinline, gnu::noclone]] std::uint8_t rx()
|
||||
{
|
||||
std::uint8_t byte;
|
||||
@@ -115,7 +121,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
" brne 3b \n\t"
|
||||
" sbiw r26, 1 \n\t"
|
||||
" brcc 2b \n\t"
|
||||
" clr %[b] \n\t" // silence → 0, which no compare accepts
|
||||
" clr %[b] \n\t" // silence -> 0, which no compare accepts
|
||||
" rjmp 5f \n\t"
|
||||
"4: lds %[b], %[udr0] \n\t"
|
||||
"5: \n\t"
|
||||
@@ -129,7 +135,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
// One-wire transmit: take the line (TXEN0 alone) on a direction change with a
|
||||
// turn-around guard, put the byte out, hold the line until the whole frame is
|
||||
// out (TXC0, not UDRE0), W1C TXC0 by storing the sampled status back (keeps
|
||||
// U2X0). Plain C++ — it compiles *smaller* than the oracle's routine.
|
||||
// U2X0). Plain C++ - it compiles *smaller* than the oracle's routine.
|
||||
[[gnu::noinline, gnu::noclone]] void tx(std::uint8_t byte)
|
||||
{
|
||||
if (g_receiving) {
|
||||
@@ -153,7 +159,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
return rx();
|
||||
}
|
||||
|
||||
// One flash byte ← [g_addr++] (the advance right before ret — the
|
||||
// One flash byte <- [g_addr++] (the advance right before ret - the
|
||||
// global-register rule, see tsb_tricks.cpp).
|
||||
[[gnu::noinline, gnu::noclone]] std::uint8_t sflash()
|
||||
{
|
||||
@@ -162,18 +168,18 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
return byte;
|
||||
}
|
||||
|
||||
// One EEPROM byte ← [g_addr++].
|
||||
// One EEPROM byte <- [g_addr++].
|
||||
[[gnu::noinline, gnu::noclone]] std::uint8_t eerd()
|
||||
{
|
||||
std::uint8_t byte = ee::read(g_addr);
|
||||
std::uint8_t byte = ee::read<no_spm>(g_addr);
|
||||
++g_addr;
|
||||
return byte;
|
||||
}
|
||||
|
||||
// One EEPROM byte → [g_addr++].
|
||||
// One EEPROM byte -> [g_addr++].
|
||||
[[gnu::noinline, gnu::noclone]] void eewr(std::uint8_t byte)
|
||||
{
|
||||
ee::write<off>(g_addr, byte);
|
||||
ee::write<off, no_spm>(g_addr, byte);
|
||||
++g_addr;
|
||||
}
|
||||
|
||||
@@ -185,7 +191,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
} while (--g_cnt);
|
||||
}
|
||||
|
||||
// Wait out a running SPM op, then re-open the RWW section — after every page
|
||||
// Wait out a running SPM op, then re-open the RWW section - after every page
|
||||
// op and before handing over, as the oracle does.
|
||||
[[gnu::noinline, gnu::noclone]] void settle()
|
||||
{
|
||||
@@ -201,12 +207,12 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
tsb_app();
|
||||
}
|
||||
|
||||
// Step g_addr one page down and erase that page (the decrement lives here —
|
||||
// Step g_addr one page down and erase that page (the decrement lives here -
|
||||
// the global-register rule).
|
||||
[[gnu::noinline, gnu::noclone]] void erase_below()
|
||||
{
|
||||
g_addr -= page;
|
||||
spm::erase_page<off>(g_addr);
|
||||
spm::command<off>(spm::op::erase, g_addr);
|
||||
settle();
|
||||
}
|
||||
|
||||
@@ -221,7 +227,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
}
|
||||
|
||||
// Stream one host page into the erased flash page at g_addr (asm 2 of 2): the
|
||||
// word pair stages in r0:r1 straight from rx (whose register set is known —
|
||||
// word pair stages in r0:r1 straight from rx (whose register set is known -
|
||||
// the cross-call liveness C++ cannot express), Z walks the page and PGWRT
|
||||
// programs it. g_addr is left at the next page base.
|
||||
[[gnu::noinline, gnu::noclone]] void store_flash()
|
||||
@@ -256,11 +262,12 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
{
|
||||
// A watchdog reset hands straight back to the application, as the
|
||||
// reference loader does, rather than re-entering the bootloader.
|
||||
if (hw::mcusr::wdrf.test())
|
||||
if (hw::mcusr::wdrf.test()) {
|
||||
appjump();
|
||||
}
|
||||
|
||||
// Lean bring-up from reset state: UCSR0C already reads 8N1, UBRR0H reads
|
||||
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use — only the divisor low
|
||||
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use - only the divisor low
|
||||
// byte and U2X0 need a store. The library still does the datasheet work.
|
||||
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
||||
hw::ubrr0::write(static_cast<std::uint8_t>(baud.ubrr));
|
||||
@@ -271,13 +278,15 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
// same reason.
|
||||
g_receiving = 0;
|
||||
|
||||
// Activation: 3×'@', each inside the config page's timeout window (rx
|
||||
// floors it so a corrupt page cannot lock the loader out); anything else —
|
||||
// including silence — hands over.
|
||||
// Activation: 3x'@', each inside the config page's timeout window (rx
|
||||
// floors it so a corrupt page cannot lock the loader out); anything else -
|
||||
// including silence - hands over.
|
||||
g_window = avr::flash_load(flash_ptr(app_end + 2));
|
||||
for (std::uint8_t k = 3; k; --k)
|
||||
if (rx() != knock)
|
||||
for (std::uint8_t k = 3; k; --k) {
|
||||
if (rx() != knock) {
|
||||
appjump();
|
||||
}
|
||||
}
|
||||
g_window = comm_window;
|
||||
|
||||
// Password gate (config page from app_end+3, 0xff-terminated; a blank
|
||||
@@ -298,10 +307,12 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
}
|
||||
std::uint8_t got = rx();
|
||||
if (got == 0) {
|
||||
if (mask == 0)
|
||||
if (mask == 0) {
|
||||
continue;
|
||||
if (rcnf() != confirm || rcnf() != confirm)
|
||||
}
|
||||
if (rcnf() != confirm || rcnf() != confirm) {
|
||||
appjump();
|
||||
}
|
||||
erase_application(); // leaves g_addr = 0 for the EEPROM walk
|
||||
do {
|
||||
eewr(0xff);
|
||||
@@ -310,9 +321,10 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
erase_below();
|
||||
break;
|
||||
}
|
||||
if (got != expected)
|
||||
if (got != expected) {
|
||||
mask = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
tx(confirm); // Mainloop ready
|
||||
@@ -320,23 +332,27 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
switch (rx()) {
|
||||
case 'f': // read application flash, one page per host '!'
|
||||
for (;;) {
|
||||
if (rx() != confirm)
|
||||
if (rx() != confirm) {
|
||||
break;
|
||||
}
|
||||
g_cnt = page;
|
||||
sendf();
|
||||
if (g_addr >= app_end)
|
||||
if (g_addr >= app_end) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'F': // erase the application, then take pages behind '?'
|
||||
erase_application(); // leaves g_addr = 0, the write start
|
||||
while (rcnf() == confirm)
|
||||
while (rcnf() == confirm) {
|
||||
store_flash();
|
||||
}
|
||||
break;
|
||||
case 'e': // read EEPROM, one page per host '!', until the host stops
|
||||
for (;;) {
|
||||
if (rx() != confirm)
|
||||
if (rx() != confirm) {
|
||||
break;
|
||||
}
|
||||
g_cnt = page;
|
||||
do {
|
||||
tx(eerd());
|
||||
@@ -358,8 +374,9 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
sendf();
|
||||
break;
|
||||
case 'C': // replace the config page, then echo it back to verify
|
||||
if (rcnf() != confirm)
|
||||
if (rcnf() != confirm) {
|
||||
break;
|
||||
}
|
||||
g_addr = app_end + page;
|
||||
erase_below(); // leaves g_addr = app_end, the store target
|
||||
store_flash();
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
// TinySafeBoot on libavr — the policy floor: pureboot's rules, measured.
|
||||
// TinySafeBoot on libavr - the policy floor: pureboot's rules, measured.
|
||||
//
|
||||
// The full TinySafeBoot feature set — watchdog bail, one-wire half-duplex,
|
||||
// The full TinySafeBoot feature set - watchdog bail, one-wire half-duplex,
|
||||
// config-page activation timeout, password gate, emergency erase, and
|
||||
// config/flash/EEPROM read-write — under philosophy #5 exactly as pureboot
|
||||
// config/flash/EEPROM read-write - under philosophy #5 exactly as pureboot
|
||||
// obeys it: no assembly, no register variables; code, attributes, and flags
|
||||
// only. Every lesson pureboot's development produced is applied — the
|
||||
// only. Every lesson pureboot's development produced is applied - the
|
||||
// library's half-duplex serial and startup entry, lean bring-up from reset
|
||||
// state, one merged send loop over both memories, oracle-shaped loop bounds,
|
||||
// locals threaded through noinline primitives, pureboot's codegen flags —
|
||||
// locals threaded through noinline primitives, pureboot's codegen flags -
|
||||
// and the result is 638 bytes: 198 below the idiomatic tier, and 126 above
|
||||
// the 512 B boot section the tricks/asm tiers reach with the banned
|
||||
// mechanisms (526/510). This tier exists to keep that number an artifact
|
||||
// rather than a claim: the gap to 512 is the rent of policy-clean C++ —
|
||||
// rather than a claim: the gap to 512 is the rent of policy-clean C++ -
|
||||
// helpers that hold a cursor across rx()/tx() pay push/pop and argument
|
||||
// threading where a global-register protocol pays nothing, and both
|
||||
// control-flow merges tried (a parametrized paged session, a merged store
|
||||
@@ -32,16 +32,25 @@ namespace ee = avr::eeprom;
|
||||
|
||||
using dev = avr::device<{.clock = 16_MHz}>;
|
||||
// One-wire: RX and TX share the line, exactly as the native-UART TSB expects.
|
||||
using serial_t = dev::uart0<{.baud = 115200_Bd, .max_baud_error = 3_pct, .half_duplex = true}>;
|
||||
// 115200 at 16 MHz lands +2.1 % off, past the receiver-tolerance table the
|
||||
// solver holds rates to - the oracle's own deployment has run there for a
|
||||
// decade, so the override states that it is meant.
|
||||
using serial_t = dev::uart0<{.baud = 115200_Bd, .allow_baud_error = true, .half_duplex = true}>;
|
||||
inline constexpr serial_t serial{};
|
||||
|
||||
namespace tsb {
|
||||
namespace {
|
||||
|
||||
// The loader is purely polled — it never enables interrupts — so every SPM and
|
||||
// The loader is purely polled - it never enables interrupts - so every SPM and
|
||||
// EEPROM lock folds to nothing under this posture.
|
||||
constexpr auto off = avr::irq::guard_policy::unused;
|
||||
|
||||
// Strict request/response: every SPM operation is waited out before the next
|
||||
// byte moves, so no flash operation is ever in flight at an EEPROM access -
|
||||
// the write procedure's step 2 has nothing to guard, the omission the
|
||||
// datasheet grants (DS40002061B section 8.6.3).
|
||||
constexpr auto no_spm = ee::spm_interlock::omitted;
|
||||
|
||||
// The handshake bytes, identical across every TSB host.
|
||||
constexpr std::uint8_t confirm = '!';
|
||||
constexpr std::uint8_t request = '?';
|
||||
@@ -87,8 +96,8 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
}
|
||||
|
||||
// Bounded byte receive: poll under nested countdowns, 0 on silence. The 0
|
||||
// then falls through every compare — not a knock, not a confirm, not a
|
||||
// command — so a silent host unwinds the loader to the application from
|
||||
// then falls through every compare - not a knock, not a confirm, not a
|
||||
// command - so a silent host unwinds the loader to the application from
|
||||
// anywhere, and a mid-session cable pull cannot wedge it. The line release on
|
||||
// a direction change is the serial backend's.
|
||||
[[gnu::noinline]] std::uint8_t rx()
|
||||
@@ -97,8 +106,9 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
do {
|
||||
std::uint8_t fine = 0;
|
||||
do {
|
||||
if (auto byte = serial.read())
|
||||
if (auto byte = serial.read()) {
|
||||
return *byte;
|
||||
}
|
||||
} while (--fine);
|
||||
} while (--outer);
|
||||
return 0;
|
||||
@@ -123,18 +133,18 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
[[gnu::noinline]] void send_block(bool eep, std::uint16_t at, std::uint8_t count)
|
||||
{
|
||||
do {
|
||||
tx(eep ? ee::read(at) : avr::flash_load(flash_ptr(at)));
|
||||
tx(eep ? ee::read<no_spm>(at) : avr::flash_load(flash_ptr(at)));
|
||||
++at;
|
||||
} while (--count);
|
||||
}
|
||||
|
||||
// One EEPROM byte in — shared by the emergency wipe and the 'E' stream.
|
||||
// One EEPROM byte in - shared by the emergency wipe and the 'E' stream.
|
||||
[[gnu::noinline]] void eeput(std::uint16_t at, std::uint8_t value)
|
||||
{
|
||||
ee::write<off>(at, value);
|
||||
ee::write<off, no_spm>(at, value);
|
||||
}
|
||||
|
||||
// Wait out a running SPM op, then re-open the RWW section — after every page
|
||||
// Wait out a running SPM op, then re-open the RWW section - after every page
|
||||
// op and before handing over, as the oracle does.
|
||||
[[gnu::noinline]] void settle()
|
||||
{
|
||||
@@ -142,19 +152,20 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
spm::rww_enable<off>();
|
||||
}
|
||||
|
||||
// One host page straight into the erased flash page at `at` — through the SPM
|
||||
// word buffer (low byte then high), no SRAM staging — then committed. `at`
|
||||
// One host page straight into the erased flash page at `at` - through the SPM
|
||||
// word buffer (low byte then high), no SRAM staging - then committed. `at`
|
||||
// names a page base, so the cursor's low byte reaching the boundary ends the
|
||||
// walk.
|
||||
[[gnu::noinline]] void store_flash_page(std::uint16_t at)
|
||||
{
|
||||
const auto open = spm::page::begin<spm::from::boot_section, off>(at);
|
||||
do {
|
||||
std::uint8_t low = rx();
|
||||
std::uint8_t high = rx();
|
||||
spm::fill<off>(at, std::bit_cast<std::uint16_t>(std::array{low, high}));
|
||||
spm::fill<off>(open, at, std::bit_cast<std::uint16_t>(std::array{low, high}));
|
||||
at += 2;
|
||||
} while (static_cast<std::uint8_t>(at) & (page - 1));
|
||||
spm::write_page<off>(at - page);
|
||||
spm::command<off>(spm::op::write, at - page);
|
||||
settle();
|
||||
}
|
||||
|
||||
@@ -166,12 +177,12 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
tsb_app();
|
||||
}
|
||||
|
||||
// Step one page down and erase it — the erase shared by the whole-app walk,
|
||||
// Step one page down and erase it - the erase shared by the whole-app walk,
|
||||
// the config rewrite and the emergency wipe; hands the stepped address back.
|
||||
[[gnu::noinline]] std::uint16_t erase_below(std::uint16_t at)
|
||||
{
|
||||
at -= page;
|
||||
spm::erase_page<off>(at);
|
||||
spm::command<off>(spm::op::erase, at);
|
||||
settle();
|
||||
return at;
|
||||
}
|
||||
@@ -192,27 +203,30 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
{
|
||||
// A watchdog reset hands straight back to the application, as the
|
||||
// reference loader does, rather than re-entering the bootloader.
|
||||
if (avr::hw::mcusr::wdrf.test())
|
||||
if (avr::hw::mcusr::wdrf.test()) {
|
||||
appjump();
|
||||
}
|
||||
|
||||
// Lean bring-up from reset state: UCSR0C already reads 8N1, UBRR0H reads
|
||||
// 0, and the half-duplex write()/read() raise TXEN0/RXEN0 on first use —
|
||||
// 0, and the half-duplex write()/read() raise TXEN0/RXEN0 on first use -
|
||||
// only the divisor low byte and U2X0 need a store. The solver still does
|
||||
// the datasheet work; the asserts pin the reset-state assumptions.
|
||||
{
|
||||
constexpr auto sol = avr::uart::solve_baud(dev::clock, 115200_Bd);
|
||||
constexpr auto sol = avr::uart::solve_baud(dev::clock, 115200_Bd, 8, avr::uart::parity::none);
|
||||
static_assert(sol.u2x && sol.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
||||
avr::hw::ubrr0::write(static_cast<std::uint8_t>(sol.ubrr));
|
||||
avr::hw::ucsr0a::write(avr::hw::ucsr0a::u2x0(1));
|
||||
}
|
||||
|
||||
// Activation: 3×'@', each inside the config page's timeout window
|
||||
// (floored so a corrupt page cannot lock the loader out); anything else —
|
||||
// including silence — hands over.
|
||||
// Activation: 3x'@', each inside the config page's timeout window
|
||||
// (floored so a corrupt page cannot lock the loader out); anything else -
|
||||
// including silence - hands over.
|
||||
window = avr::flash_load(flash_ptr(app_end + 2)) | act_min;
|
||||
for (std::uint8_t k = 3; k; --k)
|
||||
if (rx() != knock)
|
||||
for (std::uint8_t k = 3; k; --k) {
|
||||
if (rx() != knock) {
|
||||
appjump();
|
||||
}
|
||||
}
|
||||
window = comm_window;
|
||||
|
||||
// Password gate (config page from app_end+3, 0xff-terminated; a blank
|
||||
@@ -231,10 +245,12 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
}
|
||||
std::uint8_t got = rx();
|
||||
if (got == 0) {
|
||||
if (mask == 0)
|
||||
if (mask == 0) {
|
||||
continue;
|
||||
if (rcnf() != confirm || rcnf() != confirm)
|
||||
}
|
||||
if (rcnf() != confirm || rcnf() != confirm) {
|
||||
appjump();
|
||||
}
|
||||
std::uint16_t a = erase_application();
|
||||
do {
|
||||
eeput(a, 0xff);
|
||||
@@ -242,9 +258,10 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
erase_below(app_end + page);
|
||||
break;
|
||||
}
|
||||
if (got != expected)
|
||||
if (got != expected) {
|
||||
mask = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
tx(confirm); // Mainloop ready
|
||||
@@ -252,22 +269,25 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
switch (command) {
|
||||
case 'f': // read application flash, one page per host '!'
|
||||
for (std::uint16_t a = 0; a < app_end; a += page) {
|
||||
if (rx() != confirm)
|
||||
if (rx() != confirm) {
|
||||
break;
|
||||
}
|
||||
send_block(false, a, page);
|
||||
}
|
||||
break;
|
||||
case 'e': // read EEPROM, one page per host '!', until the host stops
|
||||
for (std::uint16_t a = 0;; a += page) {
|
||||
if (rx() != confirm)
|
||||
if (rx() != confirm) {
|
||||
break;
|
||||
}
|
||||
send_block(true, a, page);
|
||||
}
|
||||
break;
|
||||
case 'F': { // erase the application, then take pages behind '?'
|
||||
std::uint16_t a = erase_application();
|
||||
for (; rcnf() == confirm; a += page)
|
||||
for (; rcnf() == confirm; a += page) {
|
||||
store_flash_page(a);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'E': // take EEPROM pages behind '?', each write host-paced
|
||||
@@ -284,8 +304,9 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
send_block(false, app_end, page);
|
||||
break;
|
||||
case 'C': // replace the config page, then echo it back to verify
|
||||
if (rcnf() != confirm)
|
||||
if (rcnf() != confirm) {
|
||||
break;
|
||||
}
|
||||
store_flash_page(erase_below(app_end + page));
|
||||
goto read_config;
|
||||
default: // 'q' or any other byte runs the application
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// TinySafeBoot on libavr — tier 1: pure, idiomatic C++.
|
||||
// TinySafeBoot on libavr - tier 1: pure, idiomatic C++.
|
||||
//
|
||||
// A serial flash bootloader for the ATmega328P boot section, reimplementing the
|
||||
// TinySafeBoot native-UART fixed-baud protocol on libavr with the full feature
|
||||
// set of the hand-written oracle: a watchdog-reset bail, one-wire half-duplex,
|
||||
// a config-page activation timeout, the password gate, emergency erase, and
|
||||
// config/flash/EEPROM read-write. This variant is written for clarity —
|
||||
// config/flash/EEPROM read-write. This variant is written for clarity -
|
||||
// well-factored functions, no compiler-specific size hacks, no inline assembly.
|
||||
// The one-wire wiring, the flash-resident info block and every SPM/EEPROM lock
|
||||
// are libavr's to handle; the only attribute is the naked reset entry that
|
||||
@@ -20,16 +20,25 @@ namespace ee = avr::eeprom;
|
||||
|
||||
using dev = avr::device<{.clock = 16_MHz}>;
|
||||
// One-wire: RX and TX share the line, exactly as the native-UART TSB expects.
|
||||
using serial_t = dev::uart0<{.baud = 115200_Bd, .max_baud_error = 3_pct, .half_duplex = true}>;
|
||||
// 115200 at 16 MHz lands +2.1 % off, past the receiver-tolerance table the
|
||||
// solver holds rates to - the oracle's own deployment has run there for a
|
||||
// decade, so the override states that it is meant.
|
||||
using serial_t = dev::uart0<{.baud = 115200_Bd, .allow_baud_error = true, .half_duplex = true}>;
|
||||
inline constexpr serial_t serial{};
|
||||
|
||||
namespace tsb {
|
||||
namespace {
|
||||
|
||||
// The loader is purely polled — it never enables interrupts — so every SPM and
|
||||
// The loader is purely polled - it never enables interrupts - so every SPM and
|
||||
// EEPROM lock folds to nothing under this posture.
|
||||
constexpr auto off = avr::irq::guard_policy::unused;
|
||||
|
||||
// Strict request/response: every SPM operation is waited out before the next
|
||||
// byte moves, so no flash operation is ever in flight at an EEPROM access -
|
||||
// the write procedure's step 2 has nothing to guard, the omission the
|
||||
// datasheet grants (DS40002061B section 8.6.3).
|
||||
constexpr auto no_spm = ee::spm_interlock::omitted;
|
||||
|
||||
// The handshake bytes, identical across every TSB host.
|
||||
constexpr std::uint8_t confirm = '!';
|
||||
constexpr std::uint8_t request = '?';
|
||||
@@ -83,14 +92,16 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
// Stream `count` bytes to the host, from flash (LPM) or from EEPROM.
|
||||
void send_flash(std::uint16_t addr, std::uint8_t count)
|
||||
{
|
||||
while (count--)
|
||||
while (count--) {
|
||||
tx(avr::flash_load(flash_ptr(addr++)));
|
||||
}
|
||||
}
|
||||
|
||||
void send_eeprom(std::uint16_t addr, std::uint8_t count)
|
||||
{
|
||||
while (count--)
|
||||
tx(ee::read(addr++));
|
||||
while (count--) {
|
||||
tx(ee::read<no_spm>(addr++));
|
||||
}
|
||||
}
|
||||
|
||||
// Prompt the host with '?' and report whether it answered '!'.
|
||||
@@ -101,32 +112,33 @@ bool request_confirm()
|
||||
}
|
||||
|
||||
// Stream one page from the host straight into the already-erased flash page at
|
||||
// `addr`, filling the SPM word buffer low byte then high — no SRAM staging, so
|
||||
// `addr`, filling the SPM word buffer low byte then high - no SRAM staging, so
|
||||
// receiving and programming are the same loop.
|
||||
void store_flash_page(std::uint16_t addr)
|
||||
{
|
||||
const auto open = spm::page::begin<spm::from::boot_section, off>(addr);
|
||||
for (std::uint16_t i = 0; i < page; i += 2) {
|
||||
std::uint8_t lo = rx();
|
||||
std::uint8_t hi = rx();
|
||||
spm::fill<off>(addr + i, static_cast<std::uint16_t>(lo | (hi << 8)));
|
||||
spm::fill<off>(open, addr + i, static_cast<std::uint16_t>(lo | (hi << 8)));
|
||||
}
|
||||
spm::write_page<off>(addr);
|
||||
spm::wait();
|
||||
spm::write_page<spm::from::boot_section, off>(addr); // blocking: waits the write out
|
||||
}
|
||||
|
||||
// Stream one page from the host straight into EEPROM, byte by byte.
|
||||
void store_eeprom_page(std::uint16_t addr)
|
||||
{
|
||||
for (std::uint16_t i = 0; i < page; ++i)
|
||||
ee::write<off>(addr + i, rx());
|
||||
for (std::uint16_t i = 0; i < page; ++i) {
|
||||
ee::write<off, no_spm>(addr + i, rx());
|
||||
}
|
||||
}
|
||||
|
||||
// Erase one flash page and wait it out — the erase step shared by the whole-app
|
||||
// erase, the config-page rewrite and the emergency wipe.
|
||||
// Erase one flash page, waited out by the blocking spelling - the erase step
|
||||
// shared by the whole-app erase, the config-page rewrite and the emergency
|
||||
// wipe.
|
||||
void erase_page(std::uint16_t addr)
|
||||
{
|
||||
spm::erase_page<off>(addr);
|
||||
spm::wait();
|
||||
spm::erase_page<spm::from::boot_section, off>(addr);
|
||||
}
|
||||
|
||||
// Erase the whole application, one page at a time, top-down as the reference
|
||||
@@ -157,8 +169,9 @@ extern "C" [[noreturn]] void tsb_app();
|
||||
void read_flash()
|
||||
{
|
||||
for (std::uint16_t a = 0; a < app_end; a += page) {
|
||||
if (rx() != confirm)
|
||||
if (rx() != confirm) {
|
||||
return;
|
||||
}
|
||||
send_flash(a, page);
|
||||
}
|
||||
}
|
||||
@@ -167,8 +180,9 @@ void read_flash()
|
||||
void read_eeprom()
|
||||
{
|
||||
for (std::uint16_t a = 0;; a += page) {
|
||||
if (rx() != confirm)
|
||||
if (rx() != confirm) {
|
||||
return;
|
||||
}
|
||||
send_eeprom(a, page);
|
||||
}
|
||||
}
|
||||
@@ -178,22 +192,25 @@ void read_eeprom()
|
||||
void write_flash()
|
||||
{
|
||||
erase_application();
|
||||
for (std::uint16_t a = 0; request_confirm(); a += page)
|
||||
for (std::uint16_t a = 0; request_confirm(); a += page) {
|
||||
store_flash_page(a);
|
||||
}
|
||||
}
|
||||
|
||||
// 'E': take pages the host offers behind '?' into EEPROM.
|
||||
void write_eeprom()
|
||||
{
|
||||
for (std::uint16_t a = 0; request_confirm(); a += page)
|
||||
for (std::uint16_t a = 0; request_confirm(); a += page) {
|
||||
store_eeprom_page(a);
|
||||
}
|
||||
}
|
||||
|
||||
// 'C': replace the config page, then echo it back for the host to verify.
|
||||
void write_config()
|
||||
{
|
||||
if (!request_confirm())
|
||||
if (!request_confirm()) {
|
||||
return;
|
||||
}
|
||||
erase_page(app_end);
|
||||
store_flash_page(app_end);
|
||||
spm::rww_enable<off>();
|
||||
@@ -206,8 +223,9 @@ void write_config()
|
||||
void emergency_erase()
|
||||
{
|
||||
erase_application();
|
||||
for (std::uint16_t a = 0; a <= eeprom_end; ++a)
|
||||
ee::write<off>(a, 0xff);
|
||||
for (std::uint16_t a = 0; a <= eeprom_end; ++a) {
|
||||
ee::write<off, no_spm>(a, 0xff);
|
||||
}
|
||||
erase_page(app_end);
|
||||
spm::rww_enable<off>();
|
||||
}
|
||||
@@ -222,23 +240,28 @@ gate password_gate()
|
||||
{
|
||||
for (const std::uint8_t *pw = flash_ptr(app_end + 3);; ++pw) {
|
||||
std::uint8_t expected = avr::flash_load(pw);
|
||||
if (expected == 0xff)
|
||||
if (expected == 0xff) {
|
||||
return gate::pass;
|
||||
}
|
||||
std::uint8_t got = rx();
|
||||
if (got == 0)
|
||||
if (got == 0) {
|
||||
return gate::emergency;
|
||||
if (got != expected)
|
||||
for (;;)
|
||||
}
|
||||
if (got != expected) {
|
||||
for (;;) {
|
||||
rx();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[[noreturn]] void run()
|
||||
{
|
||||
// A watchdog reset hands straight back to the application, as the reference
|
||||
// loader does, rather than re-entering the bootloader.
|
||||
if (avr::hw::mcusr::wdrf.test())
|
||||
if (avr::hw::mcusr::wdrf.test()) {
|
||||
appjump();
|
||||
}
|
||||
|
||||
avr::init<serial_t>();
|
||||
|
||||
@@ -248,19 +271,21 @@ gate password_gate()
|
||||
__uint24 idle = static_cast<__uint24>(avr::flash_load(flash_ptr(app_end + 2)) | 16) << 16;
|
||||
std::uint8_t knocks = 0;
|
||||
while (knocks < 3) {
|
||||
if (auto byte = serial.read())
|
||||
if (auto byte = serial.read()) {
|
||||
knocks = *byte == knock ? knocks + 1 : 0;
|
||||
else if (--idle == 0)
|
||||
} else if (--idle == 0) {
|
||||
appjump();
|
||||
}
|
||||
}
|
||||
|
||||
switch (password_gate()) {
|
||||
case gate::pass:
|
||||
send_flash(reinterpret_cast<std::uint16_t>(info::storage.data()), info::size());
|
||||
break;
|
||||
case gate::emergency:
|
||||
if (!request_confirm() || !request_confirm())
|
||||
if (!request_confirm() || !request_confirm()) {
|
||||
appjump();
|
||||
}
|
||||
emergency_erase();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// TinySafeBoot on libavr — tier 2: C++ with compiler trickery, no assembly.
|
||||
// TinySafeBoot on libavr - tier 2: C++ with compiler trickery, no assembly.
|
||||
//
|
||||
// The full TinySafeBoot feature set — watchdog bail, one-wire half-duplex,
|
||||
// The full TinySafeBoot feature set - watchdog bail, one-wire half-duplex,
|
||||
// config-page activation timeout, password gate, emergency erase, and
|
||||
// config/flash/EEPROM read-write — in pure C++, 526 bytes: 14 over the 512-byte
|
||||
// config/flash/EEPROM read-write - in pure C++, 526 bytes: 14 over the 512-byte
|
||||
// boot section the hand-written oracle fits, from 168 over at this tier's first
|
||||
// floor. The structure mirrors the oracle's: a handful of tiny noinline
|
||||
// primitives sharing one whole-loader register allocation, expressed as global
|
||||
// register variables so no helper ever saves, spills, or reloads any of it.
|
||||
//
|
||||
// The register protocol (all call-saved, so calls preserve them by ABI):
|
||||
// Y (r28:r29) g_addr the walked flash/EEPROM address — adiw-able
|
||||
// r16 g_cnt byte countdown of the running block — ldi-able
|
||||
// Y (r28:r29) g_addr the walked flash/EEPROM address - adiw-able
|
||||
// r16 g_cnt byte countdown of the running block - ldi-able
|
||||
// r7 g_window rx timeout, roughly 30 ms units at 16 MHz
|
||||
// r6 g_receiving one-wire direction latch, cleared at bring-up
|
||||
// (power-on registers are undefined)
|
||||
@@ -18,10 +18,10 @@
|
||||
// GCC 16.1 miscompiles stores into global register variables: an update whose
|
||||
// remaining uses all hide inside callees is deleted whenever a CALL follows it
|
||||
// before any jump/ret (the backend's liveness walk lumps fixed registers with
|
||||
// call-clobbered ones — minimal repro in libavr's
|
||||
// call-clobbered ones - minimal repro in libavr's
|
||||
// local/scratch/probes/gcc-avr-globalreg-repro.cpp, lessons.md entry). Every
|
||||
// g_* update below therefore sits where a *local* read or a jump/ret follows
|
||||
// it — the helpers advance g_addr immediately before returning, and rx()
|
||||
// it - the helpers advance g_addr immediately before returning, and rx()
|
||||
// re-floors the window on every call instead of storing the floored value
|
||||
// once. The layout is load-bearing; do not "simplify" it.
|
||||
//
|
||||
@@ -41,10 +41,16 @@ namespace hw = avr::hw;
|
||||
namespace tsb {
|
||||
namespace {
|
||||
|
||||
// The loader is purely polled — it never enables interrupts — so every SPM and
|
||||
// The loader is purely polled - it never enables interrupts - so every SPM and
|
||||
// EEPROM lock folds to nothing under this posture.
|
||||
constexpr auto off = avr::irq::guard_policy::unused;
|
||||
|
||||
// Strict request/response: every SPM operation is waited out before the next
|
||||
// byte moves, so no flash operation is ever in flight at an EEPROM access -
|
||||
// the write procedure's step 2 has nothing to guard, the omission the
|
||||
// datasheet grants (DS40002061B section 8.6.3).
|
||||
constexpr auto no_spm = ee::spm_interlock::omitted;
|
||||
|
||||
constexpr std::uint8_t confirm = '!';
|
||||
constexpr std::uint8_t request = '?';
|
||||
constexpr std::uint8_t knock = '@';
|
||||
@@ -65,7 +71,7 @@ constexpr std::uint8_t comm_window = 200;
|
||||
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
||||
|
||||
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
||||
constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd);
|
||||
constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd, 8, avr::uart::parity::none);
|
||||
|
||||
// The 16-byte device-info block, streamed out on activation.
|
||||
// clang-format off
|
||||
@@ -93,8 +99,8 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
|
||||
// Bounded byte receive, the oracle's shape: release the one-wire line on a
|
||||
// direction change, poll RXC0 under nested countdowns, 0 on silence. The 0
|
||||
// then falls through every compare — not a knock, not a confirm, not a
|
||||
// command — so a silent host unwinds the loader to the application from
|
||||
// then falls through every compare - not a knock, not a confirm, not a
|
||||
// command - so a silent host unwinds the loader to the application from
|
||||
// anywhere, and a mid-session cable pull cannot wedge it.
|
||||
[[gnu::noinline, gnu::noclone]] std::uint8_t rx()
|
||||
{
|
||||
@@ -102,24 +108,25 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
g_receiving = 1;
|
||||
hw::ucsr0b::write(hw::ucsr0b::rxen0(1)); // RXEN0 alone: release and listen
|
||||
}
|
||||
// act_min ORs in here, per call, not once into g_window at setup — the
|
||||
// act_min ORs in here, per call, not once into g_window at setup - the
|
||||
// one placement the global-register-store miscompile cannot delete.
|
||||
std::uint16_t outer = static_cast<std::uint16_t>(g_window | act_min) << 8;
|
||||
do {
|
||||
std::uint8_t fine = 0;
|
||||
do {
|
||||
auto status = hw::ucsr0a::read();
|
||||
if (status & hw::ucsr0a::rxc0(1).value)
|
||||
if (status & hw::ucsr0a::rxc0(1).value) {
|
||||
return hw::udr0::read();
|
||||
}
|
||||
} while (--fine);
|
||||
} while (--outer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// One-wire transmit: take the line (TXEN0 alone — the receiver must be off
|
||||
// One-wire transmit: take the line (TXEN0 alone - the receiver must be off
|
||||
// while driving) on a direction change, with a turn-around guard so a shorted
|
||||
// peer can switch first; then hold the line until the whole frame is out
|
||||
// (TXC0, not UDRE0 — the stop bit must be on the wire before a caller may
|
||||
// (TXC0, not UDRE0 - the stop bit must be on the wire before a caller may
|
||||
// release the line), and W1C TXC0 by storing the sampled status back, which
|
||||
// keeps U2X0.
|
||||
[[gnu::noinline, gnu::noclone]] void tx(std::uint8_t byte)
|
||||
@@ -145,7 +152,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
return rx();
|
||||
}
|
||||
|
||||
// One flash byte ← [g_addr++] (the advance right before ret — see header).
|
||||
// One flash byte <- [g_addr++] (the advance right before ret - see header).
|
||||
[[gnu::noinline, gnu::noclone]] std::uint8_t sflash()
|
||||
{
|
||||
std::uint8_t byte = avr::flash_load(flash_ptr(g_addr));
|
||||
@@ -153,18 +160,18 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
return byte;
|
||||
}
|
||||
|
||||
// One EEPROM byte ← [g_addr++].
|
||||
// One EEPROM byte <- [g_addr++].
|
||||
[[gnu::noinline, gnu::noclone]] std::uint8_t eerd()
|
||||
{
|
||||
std::uint8_t byte = ee::read(g_addr);
|
||||
std::uint8_t byte = ee::read<no_spm>(g_addr);
|
||||
++g_addr;
|
||||
return byte;
|
||||
}
|
||||
|
||||
// One EEPROM byte → [g_addr++].
|
||||
// One EEPROM byte -> [g_addr++].
|
||||
[[gnu::noinline, gnu::noclone]] void eewr(std::uint8_t byte)
|
||||
{
|
||||
ee::write<off>(g_addr, byte);
|
||||
ee::write<off, no_spm>(g_addr, byte);
|
||||
++g_addr;
|
||||
}
|
||||
|
||||
@@ -176,7 +183,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
} while (--g_cnt);
|
||||
}
|
||||
|
||||
// Wait out a running SPM op, then re-open the RWW section — after every page
|
||||
// Wait out a running SPM op, then re-open the RWW section - after every page
|
||||
// op and before handing over, as the oracle does.
|
||||
[[gnu::noinline, gnu::noclone]] void settle()
|
||||
{
|
||||
@@ -198,12 +205,12 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
[[gnu::noinline, gnu::noclone]] void erase_below()
|
||||
{
|
||||
g_addr -= page;
|
||||
spm::erase_page<off>(g_addr);
|
||||
spm::command<off>(spm::op::erase, g_addr);
|
||||
settle();
|
||||
}
|
||||
|
||||
// Erase the whole application, top-down like the oracle: the loop bound is a
|
||||
// compare with zero, and g_addr = 0 — the value every caller wants next — is
|
||||
// compare with zero, and g_addr = 0 - the value every caller wants next - is
|
||||
// handed back for free.
|
||||
[[gnu::noinline, gnu::noclone]] void erase_application()
|
||||
{
|
||||
@@ -214,18 +221,19 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
}
|
||||
|
||||
// Stream one host page into the erased flash page at g_addr (SPM word buffer,
|
||||
// low byte then high) — no SRAM staging, receive and program are one loop.
|
||||
// low byte then high) - no SRAM staging, receive and program are one loop.
|
||||
// g_addr is left at the next page base.
|
||||
[[gnu::noinline, gnu::noclone]] void store_flash()
|
||||
{
|
||||
const auto open = spm::page::begin<spm::from::boot_section, off>(g_addr);
|
||||
g_cnt = page / 2;
|
||||
do {
|
||||
std::uint16_t word = rx();
|
||||
word |= static_cast<std::uint16_t>(rx()) << 8;
|
||||
spm::fill<off>(g_addr, word);
|
||||
spm::fill<off>(open, g_addr, word);
|
||||
g_addr += 2;
|
||||
} while (--g_cnt);
|
||||
spm::write_page<off>(g_addr - page);
|
||||
spm::command<off>(spm::op::write, g_addr - page);
|
||||
settle();
|
||||
}
|
||||
|
||||
@@ -233,11 +241,12 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
{
|
||||
// A watchdog reset hands straight back to the application, as the
|
||||
// reference loader does, rather than re-entering the bootloader.
|
||||
if (hw::mcusr::wdrf.test())
|
||||
if (hw::mcusr::wdrf.test()) {
|
||||
appjump();
|
||||
}
|
||||
|
||||
// Lean bring-up from reset state: UCSR0C already reads 8N1, UBRR0H reads
|
||||
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use — only the divisor low
|
||||
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use - only the divisor low
|
||||
// byte and U2X0 need a store. The library still does the datasheet work.
|
||||
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
||||
hw::ubrr0::write(static_cast<std::uint8_t>(baud.ubrr));
|
||||
@@ -248,13 +257,15 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
// same reason.
|
||||
g_receiving = 0;
|
||||
|
||||
// Activation: 3×'@', each inside the config page's timeout window (rx
|
||||
// floors it so a corrupt page cannot lock the loader out); anything else —
|
||||
// including silence — hands over.
|
||||
// Activation: 3x'@', each inside the config page's timeout window (rx
|
||||
// floors it so a corrupt page cannot lock the loader out); anything else -
|
||||
// including silence - hands over.
|
||||
g_window = avr::flash_load(flash_ptr(app_end + 2));
|
||||
for (std::uint8_t k = 3; k; --k)
|
||||
if (rx() != knock)
|
||||
for (std::uint8_t k = 3; k; --k) {
|
||||
if (rx() != knock) {
|
||||
appjump();
|
||||
}
|
||||
}
|
||||
g_window = comm_window;
|
||||
|
||||
// Password gate (config page from app_end+3, 0xff-terminated; a blank
|
||||
@@ -275,10 +286,12 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
}
|
||||
std::uint8_t got = rx();
|
||||
if (got == 0) {
|
||||
if (mask == 0)
|
||||
if (mask == 0) {
|
||||
continue;
|
||||
if (rcnf() != confirm || rcnf() != confirm)
|
||||
}
|
||||
if (rcnf() != confirm || rcnf() != confirm) {
|
||||
appjump();
|
||||
}
|
||||
erase_application(); // leaves g_addr = 0 for the EEPROM walk
|
||||
do {
|
||||
eewr(0xff);
|
||||
@@ -287,9 +300,10 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
erase_below();
|
||||
break;
|
||||
}
|
||||
if (got != expected)
|
||||
if (got != expected) {
|
||||
mask = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
tx(confirm); // Mainloop ready
|
||||
@@ -297,23 +311,27 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
switch (rx()) {
|
||||
case 'f': // read application flash, one page per host '!'
|
||||
for (;;) {
|
||||
if (rx() != confirm)
|
||||
if (rx() != confirm) {
|
||||
break;
|
||||
}
|
||||
g_cnt = page;
|
||||
sendf();
|
||||
if (g_addr >= app_end)
|
||||
if (g_addr >= app_end) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'F': // erase the application, then take pages behind '?'
|
||||
erase_application(); // leaves g_addr = 0, the write start
|
||||
while (rcnf() == confirm)
|
||||
while (rcnf() == confirm) {
|
||||
store_flash();
|
||||
}
|
||||
break;
|
||||
case 'e': // read EEPROM, one page per host '!', until the host stops
|
||||
for (;;) {
|
||||
if (rx() != confirm)
|
||||
if (rx() != confirm) {
|
||||
break;
|
||||
}
|
||||
g_cnt = page;
|
||||
do {
|
||||
tx(eerd());
|
||||
@@ -335,8 +353,9 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
sendf();
|
||||
break;
|
||||
case 'C': // replace the config page, then echo it back to verify
|
||||
if (rcnf() != confirm)
|
||||
if (rcnf() != confirm) {
|
||||
break;
|
||||
}
|
||||
g_addr = app_end + page;
|
||||
erase_below(); // leaves g_addr = app_end, the store target
|
||||
store_flash();
|
||||
|
||||
Reference in New Issue
Block a user