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>
fantemp
v2.2. Temperature-controlled fan firmware (ATmega328P, 16 MHz), rewritten on
libavr: thermistor on ADC0 sampled
free-running and averaged over 1000 conversions, fan on OC0B at 50 kHz,
115200 Bd serial console (help lists the commands), temperature
histogram persisted to EEPROM, and a direct jump into a boot-section
bootloader at 0x7e00.
The EEPROM format is the legacy firmware's, unchanged: 100 little-endian
uint32 buckets at address 0, one per °C. A board carrying years of history
from FanTemp 1.8b keeps every count — verified on hardware, all 67 non-empty
buckets byte-identical across the conversion.
The console
Commands may be abbreviated to any unambiguous-by-order prefix, as the legacy
firmware allowed: up is uptime, st is statistics, sa is save. The
table order resolves ties, so s is show — and reset is deliberately the one
command that cannot be abbreviated, because r should not be able to wipe the
histogram. save (new) forces a writeback, which otherwise happens every 30
minutes and on the way into the bootloader.
show, statistics and the histogram print one value per line behind a dotted
label, the way the original did — a run-on line is fine for one reading and
unreadable when monitor emits one a second. curve walks every whole degree
from 10 to 60 with a bar, because the curve is a cubic and five-degree samples
without a graph show none of its shape.
Ctrl+C abandons a half-typed line and gives a fresh prompt, echoing ^C, and
it is what stops monitor. Stopping on any byte, which is what the port did
first, reads well right up until a host sends a line ending: monitor\r\n then
stopped itself on the \n it arrived with, one reading in.
Reaching the bootloader
bootloader jumps; it does not reset. That is not a style choice:
- pureboot hands straight back on WDRF, by design — an unattended board that watchdog-resets in a loop must not sit in a loader. So the legacy watchdog-reset hand-over arrives and opens no window at all, and on a board with no reset line that is a board that cannot be reflashed.
- The address is
0x7e00, the top 512 bytes. The legacy firmware used0x7800, a 2 KB boot section's base, which on a board with a 512-byte boot section reads erased — so itsbootloadercommand silently never arrived anywhere. UCSR0Bis cleared first. WhileTXEN0is set the USART owns PD1, so a loader 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.
The Steinhart–Hart math of the legacy firmware (runtime doubles + libm log) is gone: the Beta equation and the cubic fan curve are evaluated consteval into flash tables — the firmware itself never touches floating point.
libavr rides as the libavr/ submodule, pinned to the commit this firmware
builds against; LIBAVR_ROOT (cache or environment) overrides it for
development against a working tree:
git submodule update --init libavr
cmake --preset atmega328p-generated
cmake --build --preset atmega328p-generated
The firmware is 8004 B of flash, byte-identical between the generated and
reflect modes, and ctest holds it to that number.
Atmel Studio
master carries a Studio solution, so this branch does too: ide/fantemp.atsln
builds the same firmware — byte-identical .text and .data to the CMake
build — from the same sources, with the flags mirrored by hand.
Studio finds libavr in the submodule, at
$(MSBuildProjectDirectory)\..\libavr\include — correct by construction, and
anchored to the project rather than written relative to the generated makefile,
which runs from the configuration's output directory and would need a different
number of ... Unlike the CMake build there is no LIBAVR_ROOT to point
elsewhere: an environment variable set in a shell is not visible to Studio
launched from the Start menu — which is what the submodule answers.
It also needs a GCC 16.1 toolchain registered as flavour avr-g++-16.1.0;
nothing older can compile -std=c++26.
One generated file is required before the project will load, and one command
checks the flags have not drifted (both from libavr's tools/atmelstudio/):
python ../libavr/tools/atmelstudio/componentinfo.py \
ide/fantemp.componentinfo.xml --device ATmega328P
python ../libavr/tools/atmelstudio/check-flags.py --solution ide/fantemp.atsln \
--compile-commands build/atmega328p-generated/compile_commands.json \
--log build/atmelstudio.log
CMake remains the build system; the solution is there so the project opens in
Studio as its predecessor did. Only the Release configuration is gated against
CMake — the presets define no debug build — and Debug carries the -Og -gdwarf-4 pair libavr's own debug preset uses.
Legacy (yazoalfa submodules) stays on master.