fix: the four tiers stop describing features they do not have, and three gates start failing
The reading pass over this repo found the tiers disagreeing with themselves,
and every fix here was measured.
**The turn-around guard is real code.** `tsb_asm` and `tsb_tricks` wrote
`for (std::uint8_t guard = 46; guard; --guard) ;` between taking the one-wire
line and the first UDR0 store, under a comment naming it a turn-around guard.
It has no side effect, so GCC deleted it - `sts UCSR0B` went straight to
`sts UDR0` - while the hand-written oracle spends six bytes on that wait and
libavr's own half-duplex spends them through `delay::cycles`. Two of four
tiers described a feature they did not have, which made the size gradient a
comparison between different loaders. `avr::delay::cycles<one bit time>()`
bottoms out in asm and cannot be deleted.
**The entry belongs to the library, and hand-rolling it was expensive.** Three
tiers wrote their own naked `.vectors` stub with `asm volatile("clr
__zero_reg__")` - which design.md fences to libavr and never a port, and which
`tsb_tricks` denied having in its own title line. `avr::startup::entry` also
keeps the body `noinline` for a stated reason: avr-ld must not shrink a
`.vectors` section, so a loader inlined into one forfeits call relaxation
everywhere. `tsb_pure` came out **836 -> 734** bytes for that alone.
`stack::hardware` - the reset value this part guarantees, with the write kept
where a part does not - saved another four, which is what let `tsb_asm` afford
the guard it had been four bytes short of. It fills its 512-byte section
exactly now, with the whole feature set.
**`tsb_pure` had no receive timeout.** Its `rx()` was `read_blocking()`, so a
silent host wedged the password gate and the command loop forever - the one
fix the oracle's own header lists by name, and one the other three tiers
implement. It is bounded now, and 0-on-silence falls through every compare as
theirs does.
Three gates could pass without proving anything. `sizes.py check-readme`
reported a match when every row's lookup missed; `check_size.cmake` used
`CMAKE_MATCH_1` without checking the match succeeded, which is the guard its
sibling `check_unit.cmake` has and it is the size gate; `check_pi.py` raised
IndexError instead of reporting a position-independence break that changed the
image's length. And `check.sh` spelled the 37-chip list a second time beside
make_presets.py, where a chip added to one and missed in the other is a
silently unbuilt chip - it reads the presets now, and produces the same 37 and
12.
tsbtest.py gains the scenario nothing covered: a wrong password byte must
neither activate the loader nor reach the emergency erase behind it. Red-green
on a tier with the refusal removed.
Smaller, all measured or checked: the signature is `hw::db.signature` in every
tier as the page size and EEPROM end beside it already were; `act_min` derives
from the clock; pureboot.py's `rjmp` helpers refuse a part past rjmp's
4096-word reach rather than silently folding an offset (unreachable today, the
ATtiny85 sits exactly on it); the host tool calls space 2 `data` as the wire
and the loader do; `.clangd` strips the fifth GCC-only flag the build passes;
pbrig's bitclock guard reads its own ladder; pbreloc's unexplained retry is
gone, the write being reliable on five runs without it; and the four tier
sizes live in oracle/README.md's table instead of four file headers and a
CMake comment.
`--poke` before `--peek` turned out to be right - pbtest.py round-trips a poke
through the peek behind it - so the parser order and README say so now.
Every chip green, the README size table matching every image.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1
.clangd
1
.clangd
@@ -25,6 +25,7 @@ CompileFlags:
|
||||
- -fno-split-wide-types
|
||||
- -fno-tree-ter
|
||||
- -fno-ivopts
|
||||
- -fno-move-loop-invariants
|
||||
# The build promotes warnings for the compiler that has to be right about
|
||||
# them; in the editor the flag paints a second frontend's opinions in the
|
||||
# colour reserved for things that do not compile.
|
||||
|
||||
@@ -84,26 +84,24 @@ endfunction()
|
||||
# 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".
|
||||
# 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_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. Everything else, bring-up to
|
||||
# dispatch, is C++ on libavr.
|
||||
# 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
|
||||
# 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
|
||||
# (internal linkage), streaming (no SRAM page buffer): 836 B in
|
||||
# the 1 KB section.
|
||||
# SPM/EEPROM.
|
||||
# tsb_pure - pure idiomatic libavr, one function per command, TU-local
|
||||
# (internal linkage), streaming (no SRAM page buffer).
|
||||
# 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
|
||||
# property of the mechanisms philosophy #5 bans.
|
||||
# variables) with every pureboot lesson applied, and the
|
||||
# measured evidence that the 512 B fit is a property of the
|
||||
# mechanisms philosophy #5 bans.
|
||||
#
|
||||
# What each measures is oracle/README.md's table, which is the one place the
|
||||
# four numbers and the hand-written loader's own are compared.
|
||||
#
|
||||
# add_tsb_variant(<name> <boot-section-bytes>)
|
||||
function(add_tsb_variant name bytes)
|
||||
|
||||
@@ -38,11 +38,23 @@ avra -I /usr/share/avra tsb-fixedbaud.asm # after uncommenting .include "m328P
|
||||
```
|
||||
|
||||
**500 bytes with every feature** — the proof that ≤512 B and full feature parity
|
||||
are simultaneously reachable. The port's `tsb_asm` tier meets the same bar at
|
||||
510 B in the same 512 B section, written in C++ on libavr except the two
|
||||
routines whose remaining cost is the calling convention itself (the bounded rx
|
||||
and the page-store loop); `tsb_tricks` needs no assembly at all at 526 B, and
|
||||
`tsb_pure` stays fully idiomatic at 836 B, both in the 1 KB section.
|
||||
are simultaneously reachable. The port's four tiers reach it from the other
|
||||
side, and the gradient between them is the cost of the mechanisms each is
|
||||
allowed:
|
||||
|
||||
| tier | bytes | section | what it is allowed |
|
||||
|---|---|---|---|
|
||||
| oracle | 500 | 512 B | hand-written assembly, the reference |
|
||||
| `tsb_asm` | 512 | 512 B | C++ on libavr, two routines in asm |
|
||||
| `tsb_tricks` | 528 | 1 KB | no asm; global register variables |
|
||||
| `tsb_policy` | 630 | 1 KB | pureboot's rules: no asm, no register variables |
|
||||
| `tsb_pure` | 776 | 1 KB | idiomatic libavr throughout |
|
||||
|
||||
The two routines `tsb_asm` keeps are the ones whose remaining cost is the
|
||||
calling convention itself: the bounded rx and the page-store loop. It fills
|
||||
its section exactly, with the same one-bit-time turn-around guard the oracle
|
||||
spends six bytes on - every tier implements the whole feature set, which is
|
||||
what makes the column a gradient rather than four different loaders.
|
||||
|
||||
The oracle targets 20 MHz / 33333 baud; the port targets 16 MHz / 115200 baud
|
||||
(what the simavr protocol test drives). Baud and geometry differ, code size and
|
||||
|
||||
@@ -604,7 +604,8 @@ to reset gets its reset pulse and opens the activation window by itself.
|
||||
|
||||
Operations run in a fixed order within one session: info, fuses, loader
|
||||
update, flash (erase / program / read / verify), EEPROM (the same), then
|
||||
`--peek`/`--poke` — then the loader hands over to the application. `--stay` keeps the session alive
|
||||
`--poke` and `--peek` in that order, so one invocation writes and reads the
|
||||
write back — then the loader hands over to the application. `--stay` keeps the session alive
|
||||
instead, and a later invocation reconnects into it. `--flash` and `--eeprom`
|
||||
verify by read-back unless `--no-verify`, and a flash page that reads back
|
||||
wrong is rewritten up to three times before the run stops (see the fill above).
|
||||
@@ -633,7 +634,7 @@ found rate as the session workaround, the offset, the OSCCAL correction's
|
||||
direction at ~1 % per step, and the autobaud way out. Standalone — no other
|
||||
operation combines with it.
|
||||
|
||||
`--peek ADDR[:N]` and `--poke ADDR:HEX` reach the data space (pureboot 5) —
|
||||
`--poke ADDR:HEX` and `--peek ADDR[:N]` reach the data space (pureboot 5) —
|
||||
SRAM, and through the same address space the register file and every I/O
|
||||
register. Reading an I/O register can have side effects (reading UDR clears its
|
||||
flags), which is the caller's business to know.
|
||||
|
||||
@@ -55,7 +55,7 @@ RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
||||
# 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
|
||||
SP_FLASH, SP_EEPROM, SP_DATA, 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,
|
||||
@@ -898,13 +898,13 @@ class Loader:
|
||||
return
|
||||
self._write_space(SP_SPM, address, bytes((operation,)))
|
||||
|
||||
def read_ram(self, address, count):
|
||||
def read_data(self, address, count):
|
||||
"""Data space: SRAM, and with it the register file and every I/O
|
||||
register, which share the address space on AVR. New in pureboot 5."""
|
||||
return self._read_space(SP_RAM, address, count)
|
||||
return self._read_space(SP_DATA, address, count)
|
||||
|
||||
def write_ram(self, address, data):
|
||||
self._write_space(SP_RAM, address, data)
|
||||
def write_data(self, address, data):
|
||||
self._write_space(SP_DATA, address, data)
|
||||
|
||||
def read_flash(self, address, count):
|
||||
if self.unified:
|
||||
@@ -1077,11 +1077,30 @@ def load_image(path):
|
||||
# --------------------------------------------------------------- surgery ---
|
||||
|
||||
|
||||
# rjmp's displacement is 12 bits, so a part whose flash is wider than 4096
|
||||
# words cannot be walked this way: an offset the hardware wraps modulo the
|
||||
# flash size and one it wraps modulo 4096 are then different addresses, and
|
||||
# nothing in the opcode says which was meant. Every chip that needs the
|
||||
# reset-vector surgery is at or under that today - the ATtiny85 sits exactly on
|
||||
# it - and this is what fails loudly if one is ever added that is not.
|
||||
RJMP_REACH_WORDS = 1 << 12
|
||||
|
||||
|
||||
def rjmp_wraps_cleanly(flash_words):
|
||||
return flash_words <= RJMP_REACH_WORDS
|
||||
|
||||
|
||||
def rjmp_target(word_address, opcode, flash_words):
|
||||
if not rjmp_wraps_cleanly(flash_words):
|
||||
raise Error(f"{flash_words} words of flash is past rjmp's {RJMP_REACH_WORDS}-word reach - "
|
||||
f"a relocated reset vector cannot be read back from the opcode alone")
|
||||
return (word_address + 1 + (opcode & 0x0FFF)) % flash_words
|
||||
|
||||
|
||||
def rjmp_to(word_address, destination, flash_words):
|
||||
if not rjmp_wraps_cleanly(flash_words):
|
||||
raise Error(f"{flash_words} words of flash is past rjmp's {RJMP_REACH_WORDS}-word reach - "
|
||||
f"a relocated reset vector cannot be spelled as one rjmp")
|
||||
return 0xC000 | ((destination - word_address - 1) % flash_words % 0x1000)
|
||||
|
||||
|
||||
@@ -1612,7 +1631,7 @@ def _peek_spec(spec):
|
||||
def op_peek(loader, spec):
|
||||
_require_unified(loader, "--peek")
|
||||
address, count = _peek_spec(spec)
|
||||
data = loader.read_ram(address, count)
|
||||
data = loader.read_data(address, count)
|
||||
for offset in range(0, len(data), 16):
|
||||
row = data[offset : offset + 16]
|
||||
text = "".join(chr(b) if 0x20 <= b < 0x7F else "." for b in row)
|
||||
@@ -1625,7 +1644,7 @@ def op_poke(loader, spec):
|
||||
if not payload:
|
||||
raise Error("--poke needs ADDR:HEX, for example 0x200:deadbeef")
|
||||
data = bytes.fromhex(payload.replace(" ", ""))
|
||||
loader.write_ram(int(address, 0), data)
|
||||
loader.write_data(int(address, 0), data)
|
||||
print(f"poke: {len(data)} B at {int(address, 0):#06x}")
|
||||
|
||||
|
||||
@@ -1757,10 +1776,10 @@ def main():
|
||||
parser.add_argument("--eeprom", metavar="FILE", help="program the EEPROM (bin or ihex)")
|
||||
parser.add_argument("--read-eeprom", metavar="FILE", help="dump the EEPROM")
|
||||
parser.add_argument("--verify-eeprom", metavar="FILE", help="compare EEPROM against an image")
|
||||
parser.add_argument("--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 - "
|
||||
"pureboot 5 and later")
|
||||
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("--force", action="store_true", help="override refusable safety checks")
|
||||
parser.add_argument("--stay", action="store_true", help="leave the loader in its session")
|
||||
parser.add_argument("-v", "--verbose", action="store_true",
|
||||
@@ -1804,7 +1823,7 @@ def main():
|
||||
# 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.
|
||||
unit = int.from_bytes(loader.read_ram(info.unit_home, 2), "little")
|
||||
unit = int.from_bytes(loader.read_data(info.unit_home, 2), "little")
|
||||
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 ""
|
||||
|
||||
@@ -64,6 +64,9 @@ def main():
|
||||
check=True, capture_output=True)
|
||||
subprocess.run([objcopy, "-O", "binary", relinked, binary], check=True)
|
||||
images.append(open(binary, "rb").read())
|
||||
if len(images[0]) != len(images[1]):
|
||||
fail(f"the image is {len(images[0])} B linked at {text_start:#x} and "
|
||||
f"{len(images[1])} B at {elsewhere:#x} - relaxation followed the address")
|
||||
if images[0] != images[1]:
|
||||
differing = [i for i, (a, b) in enumerate(zip(*images)) if a != b]
|
||||
fail(f"the image changes when linked at {elsewhere:#x} instead of {text_start:#x}: "
|
||||
|
||||
@@ -4,6 +4,9 @@ if(NOT _res EQUAL 0)
|
||||
endif()
|
||||
# avr-size line 2 is "<text> <data> <bss> <dec> <hex> <file>".
|
||||
string(REGEX MATCH "\n[ \t]*([0-9]+)" _m "${_out}")
|
||||
if(NOT _m)
|
||||
message(FATAL_ERROR "could not read a .text size out of ${SIZE_TOOL}'s output for ${ELF}:\n${_out}")
|
||||
endif()
|
||||
set(_text ${CMAKE_MATCH_1})
|
||||
if(_text GREATER LIMIT)
|
||||
message(FATAL_ERROR ".text is ${_text} bytes, over the ${LIMIT}-byte boot section")
|
||||
|
||||
@@ -136,13 +136,13 @@ def main():
|
||||
# the stack at the top. Reading it back over the same
|
||||
# locked link proves both directions of the new space.
|
||||
probe = bytes(range(0x30, 0x40))
|
||||
loader.write_ram(0x0200, probe)
|
||||
if loader.read_ram(0x0200, len(probe)) != probe:
|
||||
loader.write_data(0x0200, probe)
|
||||
if loader.read_data(0x0200, len(probe)) != probe:
|
||||
fail(f"{label}: RAM round-trip mismatch")
|
||||
# The register file and the I/O space share the data
|
||||
# address space on AVR, so the same command reaches a
|
||||
# peripheral register. SPMCSR reads back as idle here.
|
||||
verbose_ram = loader.read_ram(0x0200, 4)
|
||||
verbose_ram = loader.read_data(0x0200, 4)
|
||||
print(f" {label}: RAM read/write ok ({verbose_ram.hex()})")
|
||||
loader.run_application()
|
||||
banner = port.read_exact(3, 5.0)
|
||||
|
||||
@@ -72,9 +72,7 @@ def main():
|
||||
marker = bytes((i * 3) & 0xFF for i in range(page))
|
||||
loader.write_page(base, marker)
|
||||
if loader.read_flash(base, page) != marker:
|
||||
loader.write_page(base, marker)
|
||||
if loader.read_flash(base, page) != marker:
|
||||
fail("the staged copy could not write the resident slot, even on retry")
|
||||
fail("the staged copy could not write the resident slot")
|
||||
|
||||
# Restore the resident image through the staged copy, then 'J' back
|
||||
# into it and prove it lives.
|
||||
|
||||
@@ -167,6 +167,20 @@ class Host:
|
||||
self._expect(CONFIRM, "emergency mainloop ready")
|
||||
|
||||
|
||||
# A wrong password byte hangs the loader, still draining the line. Two
|
||||
# things must not happen: it must not activate, and it must not fall
|
||||
# through to the emergency erase - a byte the gate has already refused
|
||||
# reaching the erase would let a guess wipe the part.
|
||||
def refuse_password(self, byte):
|
||||
self.s.reset_input_buffer()
|
||||
self.s.write(bytes([KNOCK, KNOCK, KNOCK, byte]))
|
||||
return self.s.read(1)
|
||||
|
||||
def say(self, byte):
|
||||
self.s.write(bytes([byte]))
|
||||
return self.s.read(1)
|
||||
|
||||
|
||||
def check(cond, msg):
|
||||
if not cond:
|
||||
raise AssertionError(msg)
|
||||
@@ -219,6 +233,15 @@ def scenario_emergency(host):
|
||||
check(host.read_eeprom(1) == b"\xff" * PAGE, "EEPROM wiped")
|
||||
|
||||
|
||||
def scenario_wrong_password(host):
|
||||
"""A wrong password byte neither activates the loader nor opens the
|
||||
emergency erase behind it - the oracle carries a dedicated fix for the
|
||||
second, and nothing here exercised either half."""
|
||||
check(host.refuse_password(PW_BYTES[0] ^ 1) == b"", "a wrong password byte draws no reply")
|
||||
check(host.say(0x00) == b"", "a 0 byte after it does not request the erase")
|
||||
check(host.say(CONFIRM) == b"", "and neither does a confirm")
|
||||
|
||||
|
||||
def main():
|
||||
binary, elf, boot_base = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
failures = []
|
||||
@@ -229,6 +252,7 @@ def main():
|
||||
("round-trip", None, scenario_roundtrip),
|
||||
("password activation", PW_CONFIG, scenario_password),
|
||||
("emergency erase", PW_CONFIG, scenario_emergency),
|
||||
("wrong password", PW_CONFIG, scenario_wrong_password),
|
||||
]
|
||||
for name, config, fn in groups:
|
||||
print(f"--- {name} ---")
|
||||
|
||||
@@ -11,18 +11,27 @@ cd "$(dirname "$0")/.."
|
||||
full=0
|
||||
[[ "$1" == "--full" ]] && { full=1; shift; export PUREBOOT_FULL_MATRIX=1; }
|
||||
|
||||
CHIPS=(attiny13 attiny13a attiny25 attiny45 attiny85
|
||||
atmega8 atmega8a atmega16 atmega16a atmega32 atmega32a
|
||||
atmega48 atmega48a atmega48p atmega48pa
|
||||
atmega88 atmega88a atmega88p atmega88pa
|
||||
atmega168 atmega168a atmega168p atmega168pa
|
||||
atmega328 atmega328p
|
||||
atmega164a atmega164p atmega164pa
|
||||
atmega324a atmega324p atmega324pa
|
||||
atmega644 atmega644a atmega644p atmega644pa
|
||||
atmega1284 atmega1284p)
|
||||
REFLECT_SPOT=(attiny13a attiny85 atmega8 atmega16a atmega32a atmega48pa
|
||||
atmega88 atmega168pa atmega328p atmega164a atmega644p atmega1284)
|
||||
# The chip lists come from the presets rather than being spelled a second time
|
||||
# here: a chip added to make_presets.py and missed in a copy of its list would
|
||||
# be a gate that silently never builds it, which is the one failure mode a gate
|
||||
# cannot report. tools/make_presets.py is the single source, CMakePresets.json
|
||||
# is its output, and this reads that.
|
||||
readarray -t WORKFLOWS < <(python3 -c '
|
||||
import json, sys
|
||||
presets = json.load(open("CMakePresets.json"))["workflowPresets"]
|
||||
print("\n".join(p["name"] for p in presets))')
|
||||
if ((${#WORKFLOWS[@]} == 0)); then
|
||||
echo "no workflow presets in CMakePresets.json - run tools/make_presets.py" >&2
|
||||
exit 1
|
||||
fi
|
||||
CHIPS=()
|
||||
REFLECT_SPOT=()
|
||||
for workflow in "${WORKFLOWS[@]}"; do
|
||||
case $workflow in
|
||||
*-generated) CHIPS+=("${workflow%-generated}") ;;
|
||||
*-reflect) REFLECT_SPOT+=("${workflow%-reflect}") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Every preset runs even after one goes red, and the gate fails at the end
|
||||
# naming all of them: stopping at the first failure turns a red - a stale size
|
||||
|
||||
@@ -68,15 +68,10 @@ def bitclock_for(hz: int) -> str:
|
||||
against an oscillator that is already known to be off its nominal.
|
||||
"""
|
||||
ceiling = hz // 8
|
||||
for candidate in (1000, 4000, 8000, 32000, 125000, 400000):
|
||||
if candidate <= ceiling:
|
||||
best = candidate
|
||||
else:
|
||||
break
|
||||
else:
|
||||
best = 400000
|
||||
if ceiling < 1000:
|
||||
rungs = (1000, 4000, 8000, 32000, 125000, 400000)
|
||||
if ceiling < rungs[0]:
|
||||
raise Error(f"a part at {hz} Hz is too slow to reach over ISP safely")
|
||||
best = max(rung for rung in rungs if rung <= ceiling)
|
||||
return f"{best // 1000}kHz"
|
||||
|
||||
|
||||
|
||||
@@ -170,6 +170,12 @@ def cmd_check_readme(args) -> int:
|
||||
if bad:
|
||||
print(f"\n{bad} row(s) stale - update pureboot/README.md")
|
||||
return 1
|
||||
# A row whose target was not built is only skipped, so a chip-name change
|
||||
# or a build tree that holds nothing would otherwise skip every row and
|
||||
# report a match over an empty comparison.
|
||||
if skipped == 2 * len(rows):
|
||||
sys.exit(f"none of the {len(rows)} README rows matched a built image - "
|
||||
f"refusing to report a match over nothing")
|
||||
print(f"README size table matches every built image ({len(rows)} rows"
|
||||
+ (f", {skipped} not built" if skipped else "") + ")")
|
||||
return 0
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
//
|
||||
// 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
|
||||
// 512-byte BOOTSZ=11 section the hand-written oracle occupies (500 B). The
|
||||
// emergency erase, and config/flash/EEPROM read-write - inside the 512-byte
|
||||
// BOOTSZ=11 section the hand-written oracle occupies (oracle/README.md holds
|
||||
// what each tier measures, in one table rather than four). The
|
||||
// body is the tricks tier's C++ (same 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
|
||||
@@ -63,8 +64,9 @@ constexpr std::uint16_t boot_bytes = 512;
|
||||
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page;
|
||||
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
|
||||
|
||||
// Lockout-proof floor for the activation window (the oracle's F_CPU/1MHz).
|
||||
constexpr std::uint8_t act_min = 16;
|
||||
// Lockout-proof floor for the activation window: the oracle's F_CPU/1MHz, so
|
||||
// it follows the clock rather than restating it (rule 41).
|
||||
constexpr auto act_min = static_cast<std::uint8_t>((16_MHz).hz / 1'000'000);
|
||||
// Post-activation window: the host gets seconds, not milliseconds, mid-session.
|
||||
constexpr std::uint8_t comm_window = 200;
|
||||
|
||||
@@ -73,13 +75,18 @@ 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, 8, avr::uart::parity::none);
|
||||
|
||||
// One bit time on the wire: the turn-around a shared-line peer needs to stop
|
||||
// driving before this one starts. Derived from the solved rate, so it follows
|
||||
// the link rather than a count measured against one.
|
||||
constexpr auto guard_cycles = static_cast<std::uint32_t>((16_MHz).hz / baud.actual);
|
||||
|
||||
// The 16-byte device-info block, streamed out on activation.
|
||||
// clang-format off
|
||||
[[gnu::progmem]] constexpr auto info = std::to_array<std::uint8_t>({
|
||||
'T', 'S', 'B',
|
||||
build_date & 0xFF, build_date >> 8,
|
||||
0xF3, // status: native-UART fixed-baud lineage
|
||||
0x1E, 0x95, 0x0F, // ATmega328P signature
|
||||
avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2],
|
||||
page / 2, // page size in words
|
||||
(app_end / 2) & 0xFF, (app_end / 2) >> 8,
|
||||
eeprom_end & 0xFF, eeprom_end >> 8,
|
||||
@@ -141,8 +148,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
if (g_receiving) {
|
||||
g_receiving = 0;
|
||||
hw::ucsr0b::write(hw::ucsr0b::txen0(1));
|
||||
for (std::uint8_t guard = 46; guard; --guard) {
|
||||
}
|
||||
avr::delay::cycles<guard_cycles>();
|
||||
}
|
||||
hw::udr0::write(byte);
|
||||
std::uint8_t status;
|
||||
@@ -390,14 +396,6 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
} // namespace
|
||||
} // namespace tsb
|
||||
|
||||
// Reset lands here: BOOTRST vectors to the boot section base and .vectors is
|
||||
// laid first, so this is the first instruction executed. No crt ran, so set
|
||||
// the stack pointer before anything is called.
|
||||
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
|
||||
{
|
||||
SP = RAMEND;
|
||||
// The one line of crt this loader needs: compiled code assumes
|
||||
// __zero_reg__ (r1) is 0, and power-on registers are undefined.
|
||||
asm volatile("clr __zero_reg__");
|
||||
tsb::run();
|
||||
}
|
||||
// Reset lands at the boot section base (BOOTRST): the entry stub in .vectors
|
||||
// is laid first and does the one line of crt a crt-less image needs.
|
||||
template struct avr::startup::entry<tsb::run, avr::startup::stack::hardware>;
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
// 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 -
|
||||
// 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
|
||||
// and the result sits below the idiomatic tier and above the 512 B boot
|
||||
// section the tricks/asm tiers reach with the banned mechanisms
|
||||
// (oracle/README.md holds all four). This tier exists to keep that gap an
|
||||
// artifact
|
||||
// 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
|
||||
@@ -68,8 +69,9 @@ constexpr std::uint16_t boot_bytes = 1024;
|
||||
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page;
|
||||
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
|
||||
|
||||
// Lockout-proof floor for the activation window (the oracle's F_CPU/1MHz).
|
||||
constexpr std::uint8_t act_min = 16;
|
||||
// Lockout-proof floor for the activation window: the oracle's F_CPU/1MHz, so
|
||||
// it follows the clock rather than restating it (rule 41).
|
||||
constexpr auto act_min = static_cast<std::uint8_t>(dev::clock.hz / 1'000'000);
|
||||
// Post-activation window: the host gets seconds, not milliseconds, mid-session.
|
||||
constexpr std::uint8_t comm_window = 200;
|
||||
|
||||
@@ -324,4 +326,4 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
|
||||
// Reset lands at the boot section base (BOOTRST): the entry stub in .vectors
|
||||
// is laid first and does the one line of crt a crt-less image needs.
|
||||
template struct avr::startup::entry<tsb::run>;
|
||||
template struct avr::startup::entry<tsb::run, avr::startup::stack::hardware>;
|
||||
|
||||
@@ -63,26 +63,48 @@ constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
||||
// The 16-byte device-info block the host reads on activation. A flash_table
|
||||
// keeps it in progmem with no .data image (there is no crt to copy one).
|
||||
// clang-format off
|
||||
inline constexpr std::array<std::uint8_t, 16> info_data = {
|
||||
inline constexpr auto info_data = std::to_array<std::uint8_t>({
|
||||
'T', 'S', 'B',
|
||||
build_date & 0xFF, build_date >> 8,
|
||||
0xF3, // status byte (native-UART fixed-baud lineage)
|
||||
0x1E, 0x95, 0x0F, // ATmega328P signature
|
||||
avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2],
|
||||
page / 2, // page size in words
|
||||
(app_end / 2) & 0xFF, (app_end / 2) >> 8, // app-flash boundary, words
|
||||
eeprom_end & 0xFF, eeprom_end >> 8,
|
||||
0xAA, 0xAA, // ATmega processor-type marker (bytes 14 == 15)
|
||||
};
|
||||
});
|
||||
// clang-format on
|
||||
using info = avr::flash_table<info_data>;
|
||||
|
||||
// Blocking byte read/write over the one-wire line: read() releases the line to
|
||||
// the receiver, write() takes it and holds it until the frame is out.
|
||||
// The lockout-proof floor for the receive window: the oracle's F_CPU/1MHz, so
|
||||
// it follows the clock rather than restating it.
|
||||
constexpr auto act_min = static_cast<std::uint8_t>(dev::clock.hz / 1'000'000);
|
||||
|
||||
// The receive window, pre-floored where it is set. In .noinit: there is no crt
|
||||
// to clear a .bss image, and run() stores it before the first receive.
|
||||
[[gnu::section(".noinit")]] std::uint8_t window;
|
||||
|
||||
// Bounded byte read over the one-wire line - read() releases the line to the
|
||||
// receiver - answering 0 on silence. That 0 falls through every compare below:
|
||||
// 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 oracle lists that timeout among its own fixes, and a blocking
|
||||
// read is how a tier loses it.
|
||||
std::uint8_t rx()
|
||||
{
|
||||
return serial.read_blocking();
|
||||
std::uint16_t outer = static_cast<std::uint16_t>(window) << 8;
|
||||
do {
|
||||
std::uint8_t fine = 0;
|
||||
do {
|
||||
if (auto byte = serial.read()) {
|
||||
return *byte;
|
||||
}
|
||||
} while (--fine);
|
||||
} while (--outer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// write() takes the line and holds it until the frame is out.
|
||||
void tx(std::uint8_t byte)
|
||||
{
|
||||
serial.write(byte);
|
||||
@@ -270,9 +292,11 @@ gate password_gate()
|
||||
avr::init<serial_t>();
|
||||
|
||||
// Activation: the host knocks three '@' inside a window whose length is the
|
||||
// config page's timeout byte (floored so a corrupt page can never lock the
|
||||
// loader out). An idle port times out and boots the application.
|
||||
__uint24 idle = static_cast<__uint24>(avr::flash_load(flash_ptr(app_end + 2)) | 16) << 16;
|
||||
// config page's timeout byte, floored so a corrupt page can never lock the
|
||||
// loader out. An idle port times out and boots the application; the same
|
||||
// window then bounds every receive of the session.
|
||||
window = avr::flash_load(flash_ptr(app_end + 2)) | act_min;
|
||||
__uint24 idle = static_cast<__uint24>(window) << 16;
|
||||
std::uint8_t knocks = 0;
|
||||
while (knocks < 3) {
|
||||
if (auto byte = serial.read()) {
|
||||
@@ -324,14 +348,6 @@ gate password_gate()
|
||||
} // namespace
|
||||
} // namespace tsb
|
||||
|
||||
// Reset lands here: BOOTRST vectors to the boot section base and .vectors is
|
||||
// laid first, so this is the first instruction executed. No crt ran, so set the
|
||||
// stack pointer before anything is called.
|
||||
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
|
||||
{
|
||||
SP = RAMEND;
|
||||
// The one line of crt this loader needs: compiled code assumes
|
||||
// __zero_reg__ (r1) is 0, and power-on registers are undefined.
|
||||
asm volatile("clr __zero_reg__");
|
||||
tsb::run();
|
||||
}
|
||||
// Reset lands at the boot section base (BOOTRST): the entry stub in .vectors
|
||||
// is laid first and does the one line of crt a crt-less image needs.
|
||||
template struct avr::startup::entry<tsb::run, avr::startup::stack::hardware>;
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
//
|
||||
// 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
|
||||
// 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
|
||||
// config/flash/EEPROM read-write - in pure C++, a little over the 512-byte boot
|
||||
// section the hand-written oracle fits (oracle/README.md holds what each tier
|
||||
// measures). 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.
|
||||
//
|
||||
@@ -63,8 +63,9 @@ constexpr std::uint16_t boot_bytes = 1024;
|
||||
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page;
|
||||
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
|
||||
|
||||
// Lockout-proof floor for the activation window (the oracle's F_CPU/1MHz).
|
||||
constexpr std::uint8_t act_min = 16;
|
||||
// Lockout-proof floor for the activation window: the oracle's F_CPU/1MHz, so
|
||||
// it follows the clock rather than restating it (rule 41).
|
||||
constexpr auto act_min = static_cast<std::uint8_t>((16_MHz).hz / 1'000'000);
|
||||
// Post-activation window: the host gets seconds, not milliseconds, mid-session.
|
||||
constexpr std::uint8_t comm_window = 200;
|
||||
|
||||
@@ -73,13 +74,18 @@ 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, 8, avr::uart::parity::none);
|
||||
|
||||
// One bit time on the wire: the turn-around a shared-line peer needs to stop
|
||||
// driving before this one starts. Derived from the solved rate, so it follows
|
||||
// the link rather than a count measured against one.
|
||||
constexpr auto guard_cycles = static_cast<std::uint32_t>((16_MHz).hz / baud.actual);
|
||||
|
||||
// The 16-byte device-info block, streamed out on activation.
|
||||
// clang-format off
|
||||
[[gnu::progmem]] constexpr auto info = std::to_array<std::uint8_t>({
|
||||
'T', 'S', 'B',
|
||||
build_date & 0xFF, build_date >> 8,
|
||||
0xF3, // status: native-UART fixed-baud lineage
|
||||
0x1E, 0x95, 0x0F, // ATmega328P signature
|
||||
avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2],
|
||||
page / 2, // page size in words
|
||||
(app_end / 2) & 0xFF, (app_end / 2) >> 8,
|
||||
eeprom_end & 0xFF, eeprom_end >> 8,
|
||||
@@ -134,8 +140,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
if (g_receiving) {
|
||||
g_receiving = 0;
|
||||
hw::ucsr0b::write(hw::ucsr0b::txen0(1));
|
||||
for (std::uint8_t guard = 46; guard; --guard) {
|
||||
}
|
||||
avr::delay::cycles<guard_cycles>();
|
||||
}
|
||||
hw::udr0::write(byte);
|
||||
std::uint8_t status;
|
||||
@@ -369,14 +374,6 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
||||
} // namespace
|
||||
} // namespace tsb
|
||||
|
||||
// Reset lands here: BOOTRST vectors to the boot section base and .vectors is
|
||||
// laid first, so this is the first instruction executed. No crt ran, so set
|
||||
// the stack pointer before anything is called.
|
||||
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
|
||||
{
|
||||
SP = RAMEND;
|
||||
// The one line of crt this loader needs: compiled code assumes
|
||||
// __zero_reg__ (r1) is 0, and power-on registers are undefined.
|
||||
asm volatile("clr __zero_reg__");
|
||||
tsb::run();
|
||||
}
|
||||
// Reset lands at the boot section base (BOOTRST): the entry stub in .vectors
|
||||
// is laid first and does the one line of crt a crt-less image needs.
|
||||
template struct avr::startup::entry<tsb::run, avr::startup::stack::hardware>;
|
||||
|
||||
Reference in New Issue
Block a user