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:
1 parent
c58ff23e90
commit
999d087eb4
2 files changed
+25
-18
No files matched your search
@@ -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
|
||||
|
||||
+21
-15
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user