diff --git a/src/thermistor.hpp b/src/thermistor.hpp index 74c95e1..66618d8 100644 --- a/src/thermistor.hpp +++ b/src/thermistor.hpp @@ -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(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 out{}; + std::array out{}; for (std::int32_t i = 0; i < static_cast(out.size()); ++i) { - out[static_cast(i)] = quarters_entry(i < 256 ? i : 255); + out[static_cast(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(index + 1)]; - return static_cast(a + ((b - a) * frac) / 4); + return static_cast(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((quarters + 2) >> 2); + return static_cast((quarters + quarters_per_degree / 2) >> quarter_bits); } } // namespace app::thermistor diff --git a/test/consteval.cpp b/test/consteval.cpp index 05a731d..f0504a0 100644 --- a/test/consteval.cpp +++ b/test/consteval.cpp @@ -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(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(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; }