test: the reachability check reads the USART release back too

The README says ctest reads all three hand-over properties out of the image,
and the UCSR0B clear was the one it never read. Every hand-over site must now
store zero to UCSR0B before the call; red with the write removed, green with
it. The docstring states the properties rather than the legacy bug's story.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
BlackMarkandClaude Opus 5.5 committed 2026-10-23 21:50:13 +02:00
1 parent c58ff23e90
commit 999d087eb4
2 files changed
+25 -18

No files matched your search

+21 -15
View File
@@ -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")