`(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>
The pin crosses libavr's phase-6 close and the guideline sweep behind it;
the image is byte-identical in both modes at 8206 bytes.
The port's own sweep, against the same rules. Every mutable `static inline`
takes `m_` - uptime's counter, the sampler's window, the controller's five,
the statistics histogram and the terminal's line state (rule 46; a private
`static constexpr` is a constant rather than state and keeps its bare name).
The command table is `std::to_array` and the serial config breaks one member
per line (rules 36, 40). And three numbers get the name they already had
somewhere: duty goes through `percent_t::of()` rather than a hand-built
basis-point count, the ADC's top count is `thermistor::adc_full_scale`
instead of 1023 in four places, and the two `0xffffffff` are `open_circuit`
- which was already declared five lines away - and `never_written`, which
replaces a comment explaining the literal (rules 5, 6, 41).
Measured, not assumed: rendering `adc_full_scale` into the `show` line
instead of leaving it in the message string cost 6 bytes, so the display
text stays text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three files libavr's consumers carry, in the leaf-application shape:
nothing vendors this repo, so .clangd names the one build tree an editor
should read (build/atmega328p-generated) beside the stand-ins clang needs
for GCC's AVR dialect; .vscode/settings.json passes --query-driver and names
the toolchain prefix for the window that opens this folder. The libavr pin
advances to the editor-audit fixes. One residue stays red and is the
documented frontend divergence, not a defect: thermistor.hpp's consteval
Beta-equation table folds __builtin_log under GCC, which clang refuses to
fold in a constant expression -- the same class as libavr's breathe example
and its __builtin_cos.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The renames land (interrupt_guard, consume_reset_cause, set_duty), the
sampler binds its input in the new converter shape (the input pack plus
in<>::start() as free-running's one kick), and the console states
.allow_baud_error = true for the 115200-at-16-MHz this board has always
spoken - the receiver-tolerance table libavr now enforces is stricter
than the rate's own +2.1 %. The loader probe reads through
avr::flash_load instead of raw pgmspace, the terminal's line buffer is
std::array with backspace and delete named, the tree is reformatted
under InsertBraces, and the sources are ASCII.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
timer::engine gains stop()/start() and a runtime TOP, adc gains
disable()/enable(), and libavr_programming_targets() stops leaving .fuse bytes
in the flash HEX. Every one of them is additive, and this port adopts none of
them yet: its 3 built images come out byte-identical across the pin change,
which is what the advance is here to keep true.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Built and tested against it in a clean checkout of this port, through its own
submodule rather than a working-tree override, so the pin is what was proved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
The console's decimals render arithmetically now: the to_chars digit
table leaves SRAM (-202 B of data) and the streaming frames shrink with
it (-664 B of text, 7094 to 6430). Cross-mode .text stays
byte-identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The de-string-2 pass upstream: every peripheral block behind generated
instance traits, the string layer gone. The firmware rebuilds
byte-identically across modes at its recorded size.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The submodule replaces FetchContent and the unpinned forge fallback;
LIBAVR_ROOT stays as the tandem-development override, the presets take the
toolchain file from the submodule, and the Studio project's include path
anchors there — correct by construction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three things the solution got wrong, all found by opening it in the GUI rather
than building it headlessly.
The project listed only main.cpp, so none of the headers could be opened from
Solution Explorer. Every source and header is listed now, with <Link> mirroring
the on-disk src/ layout, which is what master's project did.
Neither configuration compiled in the GUI: libavr was found through
$(LIBAVR_ROOT), and a variable exported in a shell is not visible to Studio
launched from the Start menu. Release only looked healthy because its objects
were already up to date from a headless build. The path is now anchored to the
project directory, which also side-steps a second trap: a plain relative include
is resolved against the generated makefile's own directory, the configuration's
output directory, not the project's. Pinning libavr as a submodule would remove
the assumption that the two sit side by side, and is on libavr's task list.
Both configurations verified with LIBAVR_ROOT deliberately unset, Release still
byte-identical to the CMake build and the flag gate still green. Debug's own
translation unit carries DWARF-4 as intended.
Studio's per-user state under ide/.vs/ and the build logs are ignored.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
master opens in Studio, so this branch should too. One project, the port's own
chip, flags mirrored by hand against the CMake build — and the acceptance is
not that it builds but that it builds the same firmware: .text (7094 B) and
.data (336 B) come out byte-identical to the CMake output from the same
sources.
libavr is found through $(LIBAVR_ROOT), the variable the CMake build already
uses, so no machine path is committed; Studio expands it from the environment.
The toolchain is named by flavour only, since nothing Studio ships can compile
-std=c++26 and the path to one that can is per-machine state.
The componentinfo file the project cannot load without is generated by
libavr's tools/atmelstudio/componentinfo.py and ignored here, as are Studio's
output directories. Release is what the flag gate compares, the presets
defining no debug build; Debug carries the -Og -gdwarf-4 pair libavr's own
debug preset uses.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both headers described the port transformation ("legacy cubic becomes",
"no hand-rolled series needed") rather than what the code is. State the
cubic and the __builtin_log rationale directly.
Co-Authored-By: Claude <noreply@anthropic.com>
The thermistor Beta curve and the fan cubic move onto avr::flash_table
instead of hand-rolled [[gnu::progmem]] arrays with raw pgm_read, and
the compile-time logarithm uses __builtin_log (which constant-folds on
the AVR backend) instead of a hand-rolled series. Same 11284 B, still
byte-identical across libavr modes.
Co-Authored-By: Claude <noreply@anthropic.com>
Same controller: thermistor on ADC0 averaged over 1000 free-running
conversions, 50 kHz fan PWM on OC0B, 115200 Bd console with the full
command set, EEPROM temperature histogram, watchdog-reset path into the
boot section. The Steinhart-Hart math and the libm log are gone — the
Beta equation and the cubic fan curve are consteval-evaluated into
flash tables; the firmware never does floating point. Byte-identical
.text in both libavr modes. Legacy stays on master.
Co-Authored-By: Claude <noreply@anthropic.com>