# 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 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: ```sh git submodule update --init libavr cmake --preset atmega328p-generated cmake --build --preset atmega328p-generated ``` ## 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`.