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.
|
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`),
|
`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
|
with the image's clearance below the boot section, because none of them is
|
||||||
unreflashable board. Both the address and the watchdog checks are red-proven
|
visible from the source alone and the failure mode is an unreflashable board.
|
||||||
against the legacy behaviour they exist to catch.
|
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
|
The Steinhart–Hart math of the legacy firmware (runtime doubles + libm
|
||||||
log) is gone: the Beta equation and the cubic fan curve are evaluated
|
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
|
gets that route wrong is a board that cannot be reflashed - and the failure is
|
||||||
silent, because everything else still works.
|
silent, because everything else still works.
|
||||||
|
|
||||||
It has been wrong before. The firmware this one replaces probed and jumped to
|
Four properties, all read out of the disassembly rather than the source:
|
||||||
`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:
|
|
||||||
|
|
||||||
1. The image ends below the boot section. `hfuse d4` puts that at 0x7c00, so an
|
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,
|
application reaching into it would be overwritten by the loader - or worse,
|
||||||
executed at reset, since BOOTRST points there.
|
executed at reset, since BOOTRST points there.
|
||||||
2. The hand-over targets the loader base. A word address of 0x3f00 is byte
|
2. The hand-over targets the loader base. A word address of 0x3f00 is byte
|
||||||
0x7e00; anything else is the 0x7800 bug again.
|
0x7e00; 0x7800, a 2 KB boot section's base, reads erased on this board.
|
||||||
3. The hand-over does not arm the watchdog. pureboot hands straight back on
|
3. The hand-over releases the USART first. While TXEN0 is set the peripheral
|
||||||
WDRF by design, so a reset-based route reaches it and opens no window. The
|
owns PD1, so a loader that bit-bangs the pin answers into nothing.
|
||||||
legacy firmware's route was exactly that, and it is the one change that
|
4. The hand-over does not arm the watchdog. pureboot hands straight back on
|
||||||
cannot be walked back from the host.
|
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
|
from __future__ import annotations
|
||||||
@@ -36,6 +31,7 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
BOOT_SECTION = 0x7C00 # hfuse d4: BOOTSZ 512 words - a fuse fact, not a loader one
|
BOOT_SECTION = 0x7C00 # hfuse d4: BOOTSZ 512 words - a fuse fact, not a loader one
|
||||||
|
UCSR0B = 0xC1
|
||||||
WDTCSR = 0x60
|
WDTCSR = 0x60
|
||||||
|
|
||||||
|
|
||||||
@@ -73,17 +69,21 @@ def main() -> int:
|
|||||||
# loads the *word* address into a register pair immediately before it.
|
# loads the *word* address into a register pair immediately before it.
|
||||||
lines = text.splitlines()
|
lines = text.splitlines()
|
||||||
helper = re.compile(r"\b(?:r?call)\b.*<_ZN3app10bootloader4call")
|
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):
|
for index, line in enumerate(lines):
|
||||||
if not helper.search(line):
|
if not helper.search(line):
|
||||||
continue
|
continue
|
||||||
held: dict[str, int] = {}
|
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):
|
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)
|
held[m.group(1)] = int(m.group(2), 16)
|
||||||
# The AVR ABI passes the pointer in r25:r24.
|
# The AVR ABI passes the pointer in r25:r24.
|
||||||
if "r24" in held and "r25" in held:
|
if "r24" in held and "r25" in held:
|
||||||
sites.append(held["r25"] << 8 | held["r24"])
|
sites.append(held["r25"] << 8 | held["r24"])
|
||||||
|
if not any(release.search(back) for back in window):
|
||||||
|
unreleased += 1
|
||||||
|
|
||||||
want = loader_base // 2
|
want = loader_base // 2
|
||||||
if not sites:
|
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} "
|
print(f" ok all {len(sites)} hand-over site(s) target word 0x{want:04x} "
|
||||||
f"(byte 0x{loader_base: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.
|
# An icall/ijmp has to exist for that address to be jumped to indirectly.
|
||||||
if not re.search(r"\b(icall|ijmp)\b", text):
|
if not re.search(r"\b(icall|ijmp)\b", text):
|
||||||
failures.append("no icall/ijmp - the hand-over cannot reach across flash")
|
failures.append("no icall/ijmp - the hand-over cannot reach across flash")
|
||||||
|
|||||||
Reference in new issue
Block a user