console: the terminal is the original's again, and the way out is a jump

Six things the port had dropped or got wrong, and the one that matters is
the last.

The help is a table again — name, dots, description, one command per line
— instead of a single line of bare words that said nothing about what any
of them did. The layout is the original's, colons at column 12, which
`bootloader` at ten characters is what sets.

Abbreviations are back, and they were a feature: any prefix resolves to
the first command it matches, so `up` is uptime and `st` is statistics.
Order does the disambiguating, which is why the table is in the
original's dispatch order and new entries go on the end — appending
cannot take an abbreviation that already meant something. `reset` keeps
the original's exception and must be typed in full: `r` should not be
able to clear the histogram.

The histogram gets its resolution back. The bar was capped at 40 columns
where the original scaled to 100, and on a distribution this narrow that
threw away most of the difference between neighbouring buckets. Same
normalisation as before: divide by whatever makes the tallest bucket fit.
The sample count moves to a fixed ten-column field before the bar, so the
numbers read as a table instead of trailing off the ragged right end.

`version` exists again, and this is 2.1 — 2.0 being the port as it stood.

Added while here: `save`, to force the writeback that otherwise waits up to
thirty minutes; the resistance in `show`, which is the one number that
says *why* a temperature is wrong and which the original printed; a
report when a line overflows the buffer rather than silently acting on
its head; "no data yet" where there is none; and a blank line after each
command's output.

And the way out. `bootloader` now jumps rather than resetting, because
pureboot hands straight back on WDRF by design — so the legacy
watchdog-reset hand-over reaches it and opens no window, which on a board
with no reset line is a board that cannot be reflashed. Two more bugs in
the same three lines: the target was 0x7800, a 2 KB boot section's base,
which on this board's 512-byte section reads erased and made the check
false and the command a no-op; and UCSR0B was left set, which mutes a
loader that bit-bangs the pin the USART still owns. All three are now
read back out of the emitted image by ctest, the address and the watchdog
red-proven against exactly the legacy behaviour they exist to catch.

libavr advances to 71cfb2f. Verified on the board: FanTemp v2.1, min 0 C
/ max 74 C matching what 1.8b reported off the same EEPROM, the fan curve
within one percentage point of the legacy double-precision one at every
5 C from 15 to 60, and `bootloader` -> pureboot 7 -> back to a running
application.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 01:58:09 +02:00
parent e5a9a38bba
commit c01e583597
8 changed files with 547 additions and 107 deletions

View File

@@ -1,11 +1,44 @@
# fantemp
Temperature-controlled fan firmware (ATmega328P, 16 MHz), rewritten on
**v2.1.** 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, watchdog-reset path into a boot-section
bootloader.
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.
## 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