diff --git a/README.md b/README.md index bfca542..732628e 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,10 @@ stopped itself on the `\n` it arrived with, one reading in. that bit-bangs the same pin receives perfectly and answers into nothing. `ctest` reads all three back out of the emitted image (`test/check_reachability.py`), -because none of them is visible from the source alone and the failure mode is an -unreflashable board. Both the address and the watchdog checks are red-proven -against the legacy behaviour they exist to catch. +with the image's clearance below the boot section, because none of them is +visible from the source alone and the failure mode is an unreflashable board. +Each is red-proven: the address and the watchdog checks against the legacy +behaviour they exist to catch, the USART check against the write removed. The Steinhart–Hart math of the legacy firmware (runtime doubles + libm log) is gone: the Beta equation and the cubic fan curve are evaluated diff --git a/test/check_reachability.py b/test/check_reachability.py index 7ec8b31..81d7b59 100644 --- a/test/check_reachability.py +++ b/test/check_reachability.py @@ -6,25 +6,20 @@ bootloader is the running firmware's `bootloader` command, so a firmware that gets that route wrong is a board that cannot be reflashed - and the failure is silent, because everything else still works. -It has been wrong before. The firmware this one replaces probed and jumped to -`0x7800`, the base of a 2 KB boot section, while the board's loader sits at -`0x7e00`; `check()` therefore read an erased byte, was false, and the command -never arrived anywhere. Nothing about that is visible short of trying it on the -hardware, which is what this replaces. - -Three properties, all read out of the disassembly rather than the source: +Four properties, all read out of the disassembly rather than the source: 1. The image ends below the boot section. `hfuse d4` puts that at 0x7c00, so an application reaching into it would be overwritten by the loader - or worse, executed at reset, since BOOTRST points there. 2. The hand-over targets the loader base. A word address of 0x3f00 is byte - 0x7e00; anything else is the 0x7800 bug again. -3. The hand-over does not arm the watchdog. pureboot hands straight back on - WDRF by design, so a reset-based route reaches it and opens no window. The - legacy firmware's route was exactly that, and it is the one change that - cannot be walked back from the host. + 0x7e00; 0x7800, a 2 KB boot section's base, reads erased on this board. +3. The hand-over releases the USART first. While TXEN0 is set the peripheral + owns PD1, so a loader that bit-bangs the pin answers into nothing. +4. The hand-over does not arm the watchdog. pureboot hands straight back on + WDRF by design, so a reset-based route reaches it and opens no window. - check_reachability.py --objdump avr-objdump --elf fantemp --image fantemp.bin + check_reachability.py --objdump avr-objdump --elf fantemp --image fantemp.bin \ + --loader-base 0x7e00 """ from __future__ import annotations @@ -36,6 +31,7 @@ import subprocess import sys BOOT_SECTION = 0x7C00 # hfuse d4: BOOTSZ 512 words - a fuse fact, not a loader one +UCSR0B = 0xC1 WDTCSR = 0x60 @@ -73,17 +69,21 @@ def main() -> int: # loads the *word* address into a register pair immediately before it. lines = text.splitlines() helper = re.compile(r"\b(?:r?call)\b.*<_ZN3app10bootloader4call") - sites = [] + release = re.compile(rf"\bsts\s+0x00{UCSR0B:02X},\s*r1\b", re.I) + sites, unreleased = [], 0 for index, line in enumerate(lines): if not helper.search(line): continue held: dict[str, int] = {} - for back in lines[max(0, index - 8):index]: + window = lines[max(0, index - 8):index] + for back in window: if m := re.search(r"\bldi\s+(r\d+),\s*0x([0-9A-Fa-f]{2})", back): held[m.group(1)] = int(m.group(2), 16) # The AVR ABI passes the pointer in r25:r24. if "r24" in held and "r25" in held: sites.append(held["r25"] << 8 | held["r24"]) + if not any(release.search(back) for back in window): + unreleased += 1 want = loader_base // 2 if not sites: @@ -97,6 +97,12 @@ def main() -> int: print(f" ok all {len(sites)} hand-over site(s) target word 0x{want:04x} " f"(byte 0x{loader_base:04x})") + if unreleased: + failures.append(f"{unreleased} hand-over site(s) do not clear UCSR0B first - " + f"the USART keeps PD1 and the loader answers into nothing") + elif sites: + print(" ok every hand-over site clears UCSR0B before the jump") + # An icall/ijmp has to exist for that address to be jumped to indirectly. if not re.search(r"\b(icall|ijmp)\b", text): failures.append("no icall/ijmp - the hand-over cannot reach across flash")