Steps, counts per step, the rail clearance, the sensor window, the Kelvin offset and the quarter-degree scale were literals the reader had to re-derive (4, 256, 1019, 273.15, the shift by 2). Each is now a constant the table, the interpolation and the battery share. The table is byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
123 lines
5.1 KiB
C++
123 lines
5.1 KiB
C++
// Compile-only battery, built with the cross compiler so the target's 16-bit
|
|
// int is exercised. Two of this firmware's three numeric surfaces are decided
|
|
// entirely at compile time - the fan curve and the thermistor table - and the
|
|
// third, quarter-degrees to whole ones, is the arithmetic between them.
|
|
|
|
#include <cstdint>
|
|
|
|
#include "curve.hpp"
|
|
#include "terminal.hpp"
|
|
#include "thermistor.hpp"
|
|
|
|
namespace {
|
|
|
|
using app::thermistor::whole_degrees;
|
|
|
|
// Rounding to nearest, across zero. A negative quotient truncates toward zero
|
|
// in C, so a `(q + 2) / 4` reads -3.00 C as -2 and every case below the tie
|
|
// with it; the arithmetic shift floors, which is what these hold it to.
|
|
static_assert(whole_degrees(0) == 0);
|
|
static_assert(whole_degrees(1) == 0); // +0.25
|
|
static_assert(whole_degrees(2) == 1); // +0.50, the tie
|
|
static_assert(whole_degrees(3) == 1); // +0.75
|
|
static_assert(whole_degrees(4) == 1); // +1.00
|
|
static_assert(whole_degrees(-1) == 0); // -0.25
|
|
static_assert(whole_degrees(-2) == 0); // -0.50, the tie, toward zero
|
|
static_assert(whole_degrees(-3) == -1);
|
|
static_assert(whole_degrees(-4) == -1);
|
|
static_assert(whole_degrees(-12) == -3);
|
|
static_assert(whole_degrees(-160) == -40); // the table's own floor
|
|
static_assert(whole_degrees(500) == 125); // and its ceiling
|
|
|
|
// The curve is held at zero below the temperature the fan starts at, and
|
|
// saturates inside the table's window rather than at its edge.
|
|
using app::curve::detail::duty_entry;
|
|
static_assert(duty_entry(app::curve::start_celsius - 1) == 0);
|
|
static_assert(duty_entry(app::curve::start_celsius) == 0);
|
|
static_assert(duty_entry(30) == 7);
|
|
static_assert(duty_entry(40) == 36);
|
|
static_assert(duty_entry(49) == 93);
|
|
static_assert(duty_entry(50) == 100);
|
|
static_assert(duty_entry(99) == 100);
|
|
|
|
// Monotone across the whole table: a warmer reading never asks for less air.
|
|
consteval bool curve_rises()
|
|
{
|
|
for (std::int32_t t = 1; t < static_cast<std::int32_t>(app::curve::detail::table.size()); ++t) {
|
|
if (duty_entry(t) < duty_entry(t - 1)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
static_assert(curve_rises());
|
|
|
|
// The thermistor table, anchored where the Beta equation fixes it rather than
|
|
// against numbers this file computed the same way: the divider reads the
|
|
// thermistor's nominal resistance at
|
|
// adc = full_scale * nominal / (series + nominal), and the equation's own
|
|
// definition puts that count at the nominal temperature.
|
|
using app::thermistor::detail::counts_per_step;
|
|
using app::thermistor::detail::quarters_entry;
|
|
using app::thermistor::detail::steps;
|
|
|
|
inline constexpr std::int32_t nominal_count =
|
|
static_cast<std::int32_t>(app::thermistor::adc_full_scale * app::thermistor::nominal_resistance /
|
|
(app::thermistor::series_resistor + app::thermistor::nominal_resistance));
|
|
static_assert(quarters_entry(nominal_count / counts_per_step) >= 100); // 25.00 C, in quarters
|
|
static_assert(quarters_entry(nominal_count / counts_per_step + 1) < 100); // and the step below it
|
|
|
|
// Both clamps, at the ends the divider cannot leave.
|
|
static_assert(quarters_entry(0) == 125 * 4);
|
|
static_assert(quarters_entry(steps - 1) == -40 * 4);
|
|
|
|
// An NTC on this divider falls with the count: more counts is more resistance
|
|
// is a colder sensor, over every step of the table.
|
|
consteval bool thermistor_falls()
|
|
{
|
|
for (std::int32_t i = 1; i < steps; ++i) {
|
|
if (quarters_entry(i) > quarters_entry(i - 1)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
static_assert(thermistor_falls());
|
|
|
|
// The console's one parsing rule, read out of the flash blob the names live
|
|
// in. Every case here is a property of the *order* the names sit in, so this
|
|
// is what an edit to that list has to answer to.
|
|
using app::terminal;
|
|
|
|
// Full names, and the ends of the list - a walk that miscounts a separator
|
|
// lands on a neighbour rather than failing, so both ends are named.
|
|
static_assert(terminal::lookup("help") == 0);
|
|
static_assert(terminal::lookup("save") == 12);
|
|
static_assert(terminal::lookup("bootloader") == 4);
|
|
|
|
// Abbreviations resolve to the first entry they prefix. `s` prefixes show,
|
|
// statistics, set and save, and show is first - which is the original's
|
|
// behaviour and the reason the list is ordered rather than sorted.
|
|
static_assert(terminal::lookup("s") == 1);
|
|
static_assert(terminal::lookup("st") == 6);
|
|
static_assert(terminal::lookup("se") == 9);
|
|
static_assert(terminal::lookup("sa") == 12);
|
|
static_assert(terminal::lookup("up") == 5);
|
|
static_assert(terminal::lookup("b") == 4);
|
|
|
|
// `reset` is the exception: it must be typed in full, so no abbreviation of it
|
|
// resolves - and none of its prefixes names anything else either.
|
|
static_assert(terminal::lookup("reset") == 8);
|
|
static_assert(terminal::lookup("rese") == terminal::no_command);
|
|
static_assert(terminal::lookup("res") == terminal::no_command);
|
|
static_assert(terminal::lookup("r") == terminal::no_command);
|
|
|
|
// Nothing, and nothing that matches.
|
|
static_assert(terminal::lookup("") == terminal::no_command);
|
|
static_assert(terminal::lookup("xyzzy") == terminal::no_command);
|
|
|
|
// Longer than the name it prefixes is not a match: `helpful` is not `help`.
|
|
static_assert(terminal::lookup("helpful") == terminal::no_command);
|
|
|
|
} // namespace
|