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: // NTC thermistor on a series divider, solved entirely at compile time:
// the Beta equation (logarithm and all) runs consteval into a libavr // the Beta equation (logarithm and all) runs consteval into a libavr
// flash_table - the firmware never does floating point. Raw 10-bit ADC // 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 { namespace app::thermistor {
// The converter's top count: this board reads the divider at the ADC's // 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 beta = 3212;
inline constexpr double nominal_temperature = 25; 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 { 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) consteval double temperature_of(double adc)
{ {
double resistance = series_resistor * adc / (adc_full_scale - adc); double resistance = series_resistor * adc / (adc_full_scale - adc);
// __builtin_log constant-folds on the AVR backend, so the table is // __builtin_log constant-folds on the AVR backend, so the table is
// built at compile time with no runtime libm. // built at compile time with no runtime libm.
double steinhart = __builtin_log(resistance / nominal_resistance) / beta + 1.0 / (nominal_temperature + 273.15); double steinhart =
return 1.0 / steinhart - 273.15; __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, // One entry per step, in quarter-degrees, with the reading kept a step clear of
// clamped to a sane sensor window; entry 256 mirrors 255 so interpolation // both rails, where the divider has no solution.
// at full scale has a right neighbour.
consteval std::int16_t quarters_entry(std::int32_t index) consteval std::int16_t quarters_entry(std::int32_t index)
{ {
double adc = index * 4.0; double adc = index * double{counts_per_step};
if (adc < 4) { if (adc < counts_per_step) {
adc = 4; adc = counts_per_step;
} }
if (adc > 1019) { if (adc > adc_full_scale - counts_per_step) {
adc = 1019; adc = adc_full_scale - counts_per_step;
} }
double t = temperature_of(adc) * 4.0; double t = temperature_of(adc) * quarters_per_degree;
if (t < -40 * 4) { if (t < coldest * quarters_per_degree) {
t = -40 * 4; t = coldest * quarters_per_degree;
} }
if (t > 125 * 4) { if (t > hottest * quarters_per_degree) {
t = 125 * 4; t = hottest * quarters_per_degree;
} }
return static_cast<std::int16_t>(t < 0 ? t - 0.5 : t + 0.5); 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<[] { 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) { 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; return out;
}()> }()>
@@ -64,14 +79,14 @@ inline constexpr avr::flash_table<[] {
} // namespace detail } // 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) 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::uint16_t index = adc / detail::counts_per_step;
std::uint8_t frac = adc & 3; std::uint8_t frac = adc % detail::counts_per_step;
auto a = detail::table[index]; auto a = detail::table[index];
auto b = detail::table[static_cast<std::uint16_t>(index + 1)]; 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 // 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. // -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) 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 } // 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. // Monotone across the whole table: a warmer reading never asks for less air.
consteval bool curve_rises() 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)) { if (duty_entry(t) < duty_entry(t - 1)) {
return false; return false;
} }
@@ -57,23 +57,25 @@ static_assert(curve_rises());
// thermistor's nominal resistance at // thermistor's nominal resistance at
// adc = full_scale * nominal / (series + nominal), and the equation's own // adc = full_scale * nominal / (series + nominal), and the equation's own
// definition puts that count at the nominal temperature. // 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::quarters_entry;
using app::thermistor::detail::steps;
inline constexpr std::int32_t nominal_count = inline constexpr std::int32_t nominal_count =
static_cast<std::int32_t>(app::thermistor::adc_full_scale * app::thermistor::nominal_resistance / static_cast<std::int32_t>(app::thermistor::adc_full_scale * app::thermistor::nominal_resistance /
(app::thermistor::series_resistor + 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 / counts_per_step) >= 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 + 1) < 100); // and the step below it
// Both clamps, at the ends the divider cannot leave. // Both clamps, at the ends the divider cannot leave.
static_assert(quarters_entry(0) == 125 * 4); 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 // An NTC on this divider falls with the count: more counts is more resistance
// is a colder sensor, over every step of the table. // is a colder sensor, over every step of the table.
consteval bool thermistor_falls() 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)) { if (quarters_entry(i) > quarters_entry(i - 1)) {
return false; return false;
} }