build: pureboot rides as a pinned submodule, and the libavr pin advances

pureboot moved to its own repo, so the loader this board hands over to stopped
being reachable through a sibling checkout of the bootloader repo. It is a
submodule here now: this board has no reset line and no programming header, so
the resident loader is the only way in, and the commit naming the firmware
should name the loader it has to reach.

The reachability check stops carrying its own copy of where that loader is.
0x7e00 was a literal beside pureboot's own geometry, which the submodule
exports as PUREBOOT_BASE_HEX - one source for the fact now, and the check reads
whichever slot the pinned loader actually has. The boot-section bound stays a
literal, being a fuse fact rather than a loader one.

Built and tested at both pins on the bench: 5/5, cross-mode identity included,
and the image deployed to the board verifies byte-for-byte through its loader.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 22:38:19 +02:00
parent e56e8b977f
commit 491ff76447
6 changed files with 25 additions and 7 deletions

3
.gitmodules vendored
View File

@@ -1,3 +1,6 @@
[submodule "libavr"] [submodule "libavr"]
path = libavr path = libavr
url = ../libavr.git url = ../libavr.git
[submodule "pureboot"]
path = pureboot
url = ../pureboot.git

View File

@@ -14,7 +14,16 @@ endif()
if(NOT EXISTS ${LIBAVR_ROOT}/CMakeLists.txt) if(NOT EXISTS ${LIBAVR_ROOT}/CMakeLists.txt)
message(FATAL_ERROR "libavr not found at ${LIBAVR_ROOT} - run: git submodule update --init libavr") message(FATAL_ERROR "libavr not found at ${LIBAVR_ROOT} - run: git submodule update --init libavr")
endif() endif()
# pureboot rides as a pinned submodule too: this board's only way in is its
# resident loader, so the commit that names this firmware names the loader it
# has to hand over to. Consumed for the geometry it exports - the loader links
# the libavr target above, so pureboot's own libavr submodule stays
# uninitialised.
if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/CMakeLists.txt)
message(FATAL_ERROR "pureboot not found - run: git submodule update --init pureboot")
endif()
add_subdirectory(${LIBAVR_ROOT} libavr-build) add_subdirectory(${LIBAVR_ROOT} libavr-build)
add_subdirectory(pureboot pureboot-build)
include(${LIBAVR_ROOT}/cmake/checks.cmake) include(${LIBAVR_ROOT}/cmake/checks.cmake)
add_executable(fantemp src/main.cpp) add_executable(fantemp src/main.cpp)

2
libavr

Submodule libavr updated: e69792013e...3678ed7e5b

1
pureboot Submodule

Submodule pureboot added at 416ee188e1

View File

@@ -25,7 +25,8 @@ if(Python3_FOUND)
add_test(NAME fantemp.reachability add_test(NAME fantemp.reachability
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/check_reachability.py COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/check_reachability.py
--objdump ${CMAKE_OBJDUMP} --elf $<TARGET_FILE:fantemp> --objdump ${CMAKE_OBJDUMP} --elf $<TARGET_FILE:fantemp>
--image $<TARGET_FILE_DIR:fantemp>/fantemp.bin) --image $<TARGET_FILE_DIR:fantemp>/fantemp.bin
--loader-base ${PUREBOOT_BASE_HEX})
else() else()
message(STATUS "Python not found - the reachability check is skipped") message(STATUS "Python not found - the reachability check is skipped")
endif() endif()

View File

@@ -35,8 +35,7 @@ import re
import subprocess import subprocess
import sys import sys
BOOT_SECTION = 0x7C00 # hfuse d4: BOOTSZ 512 words BOOT_SECTION = 0x7C00 # hfuse d4: BOOTSZ 512 words - a fuse fact, not a loader one
LOADER_BASE = 0x7E00 # pureboot's 512-byte slot, at the top
WDTCSR = 0x60 WDTCSR = 0x60
@@ -45,7 +44,12 @@ def main() -> int:
parser.add_argument("--objdump", required=True) parser.add_argument("--objdump", required=True)
parser.add_argument("--elf", type=pathlib.Path, required=True) parser.add_argument("--elf", type=pathlib.Path, required=True)
parser.add_argument("--image", type=pathlib.Path, required=True) parser.add_argument("--image", type=pathlib.Path, required=True)
# pureboot's own geometry, from the pinned submodule that exports it, so the
# slot's base is stated once for the loader this firmware is deployed with.
parser.add_argument("--loader-base", required=True,
type=lambda v: int(v, 0), metavar="ADDR")
args = parser.parse_args() args = parser.parse_args()
loader_base = args.loader_base
failures = [] failures = []
@@ -81,17 +85,17 @@ def main() -> int:
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"])
want = LOADER_BASE // 2 want = loader_base // 2
if not sites: if not sites:
failures.append("no call to bootloader::call with a loaded target - the " failures.append("no call to bootloader::call with a loaded target - the "
"hand-over could not be read out of the image") "hand-over could not be read out of the image")
elif wrong := [a for a in sites if a != want]: elif wrong := [a for a in sites if a != want]:
failures.append(f"the hand-over targets word {[hex(a) for a in wrong]} " failures.append(f"the hand-over targets word {[hex(a) for a in wrong]} "
f"(byte {[hex(a * 2) for a in wrong]}), not the loader at " f"(byte {[hex(a * 2) for a in wrong]}), not the loader at "
f"0x{LOADER_BASE:04x}") f"0x{loader_base:04x}")
else: else:
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})")
# 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):