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:
1 parent
78c9071f0c
commit
e44b45dd1e
2 files changed
+45
-28
No files matched your search
+38
-23
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user