Three more things the original did better, and two bugs found doing them. `curve` walks every whole degree from 10 to 60 with a bar, which is the original's. The port sampled it every five degrees and printed a bare percentage — ten numbers for a cubic, showing none of its shape. The bar is the duty itself, so it needs no scale. `show` and `statistics` print one value per line behind a dotted label instead of a run-on line. That reads the same either way for a single reading and is the whole difference when `monitor` emits one a second forever. The label renderer is now shared with the help, since it is the same thing three times; the flash overload takes its width from the string's type, so the padding needs no hand-counted constant and the labels stay out of SRAM. `statistics` gains the sample total, and says "not available" rather than a zero it never measured. Ctrl+C echoes `^C` and gives a fresh prompt, abandoning whatever was half-typed, and it is what stops `monitor` now. Stopping on *any* byte was the port's own invention and it reads fine until a host sends a line ending: `monitor\r\n` stopped itself on the `\n` it arrived with, one reading in, which is why monitoring looked broken from a script and fine by hand. The other bug is arithmetic. A temperature's fraction came from `(quarters % 4) * 25`, and C++ gives a negative remainder for a negative dividend — so -40.25 C printed as "-40.-25". The sign comes off first now, and the fraction is two digits, so the column lines up: -40.00, -40.25. Verified against v1.8b on the board, which was flashed back to compare against directly: same 51 curve rows over the same span with the same 100-column bars, agreeing within the one percentage point the consteval table costs against the legacy runtime doubles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
103 lines
4.9 KiB
Markdown
103 lines
4.9 KiB
Markdown
# 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`.
|