BlackMark 3e95160b00 fix: the quarter-degree rounding was wrong below zero, in three copies
`(quarters + 2) / 4` is round-half-up only for positive values: C truncates a
negative quotient toward zero, so -3.00 C read as -2, -1.00 C as 0 and -0.75 C
as 0 - nine of fifteen negative quarter-values off by a whole degree, always
toward zero. `(quarters + 2) >> 2` is an arithmetic shift, which floors, and
is right across the whole range. It is also smaller: the shift skips the bias
correction signed division needs.

The formula was written three times - thermistor::celsius(), which nothing
called, and inline at both live call sites - so the defect had three homes and
so would its fix. One `thermistor::whole_degrees()` now, called from both, and
the dead entry point is gone (rules 6, 27).

test/consteval.cpp is new and is what should have caught it: whole_degrees
across zero including both ties and both table limits, the curve's start and
saturation points and its monotonicity, and the thermistor table anchored
where the Beta equation fixes it - the count at which the divider reads the
thermistor's nominal resistance must read the nominal temperature - plus both
clamps and the fall across every step. Red-green: four assertions fire against
the old division.

Beside it: the cubic's three coefficients are named rather than inlined and
restated in prose (rule 5), the consteval table builders take explicit 32-bit
types (rule 25), the curve's clamp reads the table's own size (rule 36), and
the serial override says what expects the rate rather than what the board has
always done (rules 12, 13).

And bootloader::handle_reset()'s watchdog diversion is gone. Its own comment
called it a leftover "kept only because it is free and cannot hurt", and it
did not: pureboot peeks WDRF without clearing it and hands back on purpose, so
a watchdog reset arrives here with the flag still set and the diversion jumped
into the loader with MCUSR already cleared - opening the activation window
that policy exists to close. Clearing MCUSR is the whole job and stays.

8206 -> 8168 bytes, byte-identical between generated and reflect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:23:05 +02:00
2020-03-29 01:29:59 +01:00
2020-03-29 01:29:59 +01:00

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 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:

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/):

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.

Description
Firmware for temperature controller board
Readme 317 KiB
Languages
C++ 100%