console: the thermistor table's geometry is named rather than spelled

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>
This commit is contained in:
BlackMarkandClaude Opus 5.5 committed 2026-10-23 21:49:50 +02:00
1 parent 78c9071f0c
commit e44b45dd1e
2 files changed
+45 -28

No files matched your search

+38 -23
View File
@@ -8,7 +8,7 @@
// NTC thermistor on a series divider, solved entirely at compile time:
// the Beta equation (logarithm and all) runs consteval into a libavr
// flash_table - the firmware never does floating point. Raw 10-bit ADC
// counts map to quarter- C with linear interpolation between table steps.
// counts map to quarter-degrees C with linear interpolation between table steps.
namespace app::thermistor {
// The converter's top count: this board reads the divider at the ADC's
@@ -20,43 +20,58 @@ inline constexpr double nominal_resistance = 9270;
inline constexpr double beta = 3212;
inline constexpr double nominal_temperature = 25;
inline constexpr std::uint8_t quarter_bits = 2;
inline constexpr std::uint8_t quarters_per_degree = 1 << quarter_bits;
namespace detail {
inline constexpr double kelvin_offset = 273.15;
// Signed-promoting, so the interpolation below divides a negative slope as one.
inline constexpr std::uint8_t counts_per_step = 4;
inline constexpr std::int32_t steps = (adc_full_scale + 1) / counts_per_step;
// A sane sensor window, in whole degrees.
inline constexpr std::int32_t coldest = -40;
inline constexpr std::int32_t hottest = 125;
consteval double temperature_of(double adc)
{
double resistance = series_resistor * adc / (adc_full_scale - adc);
// __builtin_log constant-folds on the AVR backend, so the table is
// built at compile time with no runtime libm.
double steinhart = __builtin_log(resistance / nominal_resistance) / beta + 1.0 / (nominal_temperature + 273.15);
return 1.0 / steinhart - 273.15;
double steinhart =
__builtin_log(resistance / nominal_resistance) / beta + 1.0 / (nominal_temperature + kelvin_offset);
return 1.0 / steinhart - kelvin_offset;
}
// 256 entries over the 10-bit range (steps of 4 counts), quarter- C,
// clamped to a sane sensor window; entry 256 mirrors 255 so interpolation
// at full scale has a right neighbour.
// One entry per step, in quarter-degrees, with the reading kept a step clear of
// both rails, where the divider has no solution.
consteval std::int16_t quarters_entry(std::int32_t index)
{
double adc = index * 4.0;
if (adc < 4) {
adc = 4;
double adc = index * double{counts_per_step};
if (adc < counts_per_step) {
adc = counts_per_step;
}
if (adc > 1019) {
adc = 1019;
if (adc > adc_full_scale - counts_per_step) {
adc = adc_full_scale - counts_per_step;
}
double t = temperature_of(adc) * 4.0;
if (t < -40 * 4) {
t = -40 * 4;
double t = temperature_of(adc) * quarters_per_degree;
if (t < coldest * quarters_per_degree) {
t = coldest * quarters_per_degree;
}
if (t > 125 * 4) {
t = 125 * 4;
if (t > hottest * quarters_per_degree) {
t = hottest * quarters_per_degree;
}
return static_cast<std::int16_t>(t < 0 ? t - 0.5 : t + 0.5);
}
// One entry past the last step, mirroring it, so interpolation at full scale
// has a right neighbour.
inline constexpr avr::flash_table<[] {
std::array<std::int16_t, 257> out{};
std::array<std::int16_t, steps + 1> out{};
for (std::int32_t i = 0; i < static_cast<std::int32_t>(out.size()); ++i) {
out[static_cast<std::size_t>(i)] = quarters_entry(i < 256 ? i : 255);
out[static_cast<std::size_t>(i)] = quarters_entry(i < steps ? i : steps - 1);
}
return out;
}()>
@@ -64,14 +79,14 @@ inline constexpr avr::flash_table<[] {
} // namespace detail
// Temperature in quarter- C from a raw (or averaged) 10-bit sample.
// Temperature in quarter-degrees C from a raw (or averaged) 10-bit sample.
inline std::int16_t quarters(std::uint16_t adc)
{
std::uint16_t index = adc >> 2; // the 257th entry backs index+1 at full scale
std::uint8_t frac = adc & 3;
std::uint16_t index = adc / detail::counts_per_step;
std::uint8_t frac = adc % detail::counts_per_step;
auto a = detail::table[index];
auto b = detail::table[static_cast<std::uint16_t>(index + 1)];
return static_cast<std::int16_t>(a + ((b - a) * frac) / 4);
return static_cast<std::int16_t>(a + ((b - a) * frac) / detail::counts_per_step);
}
// Whole degrees from quarter-degrees, rounded to nearest. The shift is the
@@ -79,7 +94,7 @@ inline std::int16_t quarters(std::uint16_t adc)
// -3.00 C as -2, where an arithmetic shift floors and reads it as -3.
inline constexpr std::int8_t whole_degrees(std::int16_t quarters)
{
return static_cast<std::int8_t>((quarters + 2) >> 2);
return static_cast<std::int8_t>((quarters + quarters_per_degree / 2) >> quarter_bits);
}
} // namespace app::thermistor
+7 -5
View File
@@ -43,7 +43,7 @@ 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 < 100; ++t) {
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;
}
@@ -57,23 +57,25 @@ static_assert(curve_rises());
// 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 / 4) >= 100); // 25.00 C, in quarters
static_assert(quarters_entry(nominal_count / 4 + 1) < 100); // and the step below it
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(255) == -40 * 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 < 256; ++i) {
for (std::int32_t i = 1; i < steps; ++i) {
if (quarters_entry(i) > quarters_entry(i - 1)) {
return false;
}