8382 -> 8004 B of flash, 725 -> 511 B of RAM on a part that has 2048. The command names were a std::to_array of string_view, and on a Harvard machine that is the worst of both: the characters land in .data and so does the table's own pointer-and-length pair for each of them, so the firmware carried 216 B of RAM for thirteen words that never change - and paid for them in flash too, since .data is copied out of an initialiser image at startup. They are one NUL-separated blob in flash now, walked with lpm. Separators rather than an offset table, because an offset table is the RAM this exists to give back; the names sit in it in match order, so the walk that finds a name is the same walk that compares it and measures it. Flash falls further than RAM does: the initialiser image and the two tables were 216 B of it, and the blob is 87. The header said the names "cannot" be in flash because they are matched at run time. Being matched at run time is not a reason to be in RAM on a machine with two address spaces - only being *written* is, and nothing writes these. Two smaller things came with it. `reset`'s exact-match rule was a bool on every entry to protect one; it is an index found by searching the list, so reordering the commands cannot move the protection onto a different one. And `version` was the last string_view left, holding its own characters and a pointer to them. The matching is now pinned rather than assumed: lookup() is constexpr and the battery asserts the load-bearing order the README documents - `s` is show and not statistics, `st` is statistics, no abbreviation of `reset` resolves, and `helpful` is not `help`. Red-checked by claiming `s` is statistics. Both modes byte-identical, ten tests green. 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.