Files
fantemp/README.md
BlackMark 0f5e40510c perf: the console's constants move to flash, -378 B and -214 B of RAM
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>
2026-08-23 05:29:01 +02:00

106 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# fantemp
**v2.2.** Temperature-controlled fan firmware (ATmega328P, 16 MHz), rewritten on
[libavr](https://git.blackmark.me/avr/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 used `0x7800`,
a 2 KB boot section's base, which on a board with a 512-byte boot section reads
erased — so its `bootloader` command silently never arrived anywhere.
- `UCSR0B` is cleared first. While `TXEN0` is 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 SteinhartHart 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:
```sh
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/`):
```sh
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`.