Files
fantemp/src/board.hpp
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

80 lines
2.4 KiB
C++

#pragma once
#include <libavr/libavr.hpp>
// The board composition: every peripheral of the fan controller in one
// place. ATmega328P at 16 MHz - thermistor divider on ADC0 (PC0), fan on
// OC0B (PD5) at 50 kHz, console on the hardware UART.
namespace app {
using namespace avr::literals;
using dev = avr::device<{.clock = 16_MHz}>;
// Millisecond uptime from timer2 CTC (the fan owns timer0).
class uptime {
static inline volatile std::uint64_t m_ms = 0;
public:
using ticker = dev::timer2<{.frequency = 1_kHz, .on_compare = [] { m_ms = m_ms + 1; }}>;
static std::uint64_t millis()
{
avr::irq::interrupt_guard lock;
return m_ms;
}
};
// 1000-sample averaging window fed by the conversion interrupt.
class sampler {
static inline volatile std::uint32_t m_sum = 0;
static inline volatile std::uint16_t m_count = 0;
static inline volatile std::uint16_t m_window = 0;
static inline volatile bool m_ready = false;
static constexpr std::uint16_t samples = 1000;
public:
using input = dev::adc<{.trigger = avr::adc::trigger::free_running,
.on_conversion =
[](std::uint16_t value) {
m_sum = m_sum + value;
m_count = m_count + 1;
if (m_count >= samples) {
m_window = static_cast<std::uint16_t>(m_sum / samples);
m_sum = 0;
m_count = 0;
m_ready = true;
}
}},
avr::adc::input<avr::adc::input_pin(0)>>;
// The bound input, whose start() is free-running's one kick.
using thermistor = input::in<avr::adc::input_pin(0)>;
// The finished average (raw 10-bit), once per window.
static bool take(std::uint16_t &value)
{
avr::irq::interrupt_guard lock;
if (!m_ready) {
return false;
}
value = m_window;
m_ready = false;
return true;
}
};
using fan = dev::pwm<avr::pd5, {.frequency = 50_kHz}>;
// 115200 at 16 MHz lands +2.1 % off, past the receiver-tolerance table the
// solver holds rates to. It is the rate the console on the other end of the
// cable expects, so the override states that the miss is meant.
using serial_t = dev::uart0<{
.baud = 115200_Bd,
.rx_buffer = 32,
.allow_baud_error = true,
}>;
inline constexpr serial_t serial{};
} // namespace app