Rewrite on libavr
Same controller: thermistor on ADC0 averaged over 1000 free-running conversions, 50 kHz fan PWM on OC0B, 115200 Bd console with the full command set, EEPROM temperature histogram, watchdog-reset path into the boot section. The Steinhart-Hart math and the libm log are gone — the Beta equation and the cubic fan curve are consteval-evaluated into flash tables; the firmware never does floating point. Byte-identical .text in both libavr modes. Legacy stays on master. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
68
src/board.hpp
Normal file
68
src/board.hpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include <libavr/libavr.hpp>
|
||||
|
||||
// The board composition: every peripheral of the fan controller in one
|
||||
// place. ATmega328P at 16 MHz — thermistor divider on ADC0 (PC0), fan on
|
||||
// OC0B (PD5) at 50 kHz, console on the hardware UART.
|
||||
namespace app {
|
||||
|
||||
using namespace avr::literals;
|
||||
|
||||
using dev = avr::device<{.clock = 16_MHz}>;
|
||||
|
||||
// Millisecond uptime from timer2 CTC (the fan owns timer0).
|
||||
class uptime {
|
||||
static inline volatile std::uint64_t ms = 0;
|
||||
|
||||
public:
|
||||
using ticker = dev::timer2<{.frequency = 1_kHz, .on_compare = [] { ms = ms + 1; }}>;
|
||||
|
||||
static std::uint64_t millis()
|
||||
{
|
||||
avr::irq::atomic_guard lock;
|
||||
return ms;
|
||||
}
|
||||
};
|
||||
|
||||
// 1000-sample averaging window fed by the conversion interrupt.
|
||||
class sampler {
|
||||
static inline volatile std::uint32_t sum = 0;
|
||||
static inline volatile std::uint16_t count = 0;
|
||||
static inline volatile std::uint16_t window = 0;
|
||||
static inline volatile bool ready = false;
|
||||
|
||||
static constexpr std::uint16_t samples = 1000;
|
||||
|
||||
public:
|
||||
using input = dev::adc<{.input = avr::adc::input_pin(0),
|
||||
.trigger = avr::adc::trigger::free_running,
|
||||
.on_conversion = [](std::uint16_t value) {
|
||||
sum = sum + value;
|
||||
count = count + 1;
|
||||
if (count >= samples) {
|
||||
window = static_cast<std::uint16_t>(sum / samples);
|
||||
sum = 0;
|
||||
count = 0;
|
||||
ready = true;
|
||||
}
|
||||
}}>;
|
||||
|
||||
// The finished average (raw 10-bit), once per window.
|
||||
static bool take(std::uint16_t &value)
|
||||
{
|
||||
avr::irq::atomic_guard lock;
|
||||
if (!ready)
|
||||
return false;
|
||||
value = window;
|
||||
ready = false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
using fan = dev::pwm<avr::pd5, {.frequency = 50_kHz}>;
|
||||
|
||||
using serial_t = dev::uart0<{.baud = 115200_Bd, .rx_buffer = 32, .max_baud_error = 2.5_pct}>;
|
||||
inline constexpr serial_t serial{};
|
||||
|
||||
} // namespace app
|
||||
45
src/bootloader.hpp
Normal file
45
src/bootloader.hpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <avr/pgmspace.h>
|
||||
|
||||
#include <libavr/libavr.hpp>
|
||||
|
||||
#include "board.hpp"
|
||||
|
||||
// Reset-into-bootloader: `bootloader` on the console arms the watchdog
|
||||
// and hangs; the next boot sees WDRF and jumps to the boot section at
|
||||
// 0x7800 (byte address) — if one is flashed there — before anything else
|
||||
// runs.
|
||||
namespace app {
|
||||
|
||||
class bootloader {
|
||||
using jump_fn = void (*)();
|
||||
|
||||
using guard = dev::watchdog<{.timeout = 16_ms}>;
|
||||
|
||||
static bool present()
|
||||
{
|
||||
return pgm_read_byte(0x7800) != 0xff;
|
||||
}
|
||||
|
||||
public:
|
||||
// Call first thing in main: reset_cause() clears MCUSR (a lingering
|
||||
// WDRF would re-arm the watchdog), then a watchdog reset diverts into
|
||||
// the bootloader when one is flashed.
|
||||
static void handle_reset()
|
||||
{
|
||||
auto cause = avr::power::reset_cause();
|
||||
guard::disable();
|
||||
if (cause.watchdog && present())
|
||||
reinterpret_cast<jump_fn>(0x7800 / 2)();
|
||||
}
|
||||
|
||||
[[noreturn]] static void enter()
|
||||
{
|
||||
guard::init();
|
||||
while (true) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
77
src/controller.hpp
Normal file
77
src/controller.hpp
Normal file
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "board.hpp"
|
||||
#include "curve.hpp"
|
||||
#include "thermistor.hpp"
|
||||
|
||||
// Control loop: averaged thermistor samples → temperature → fan duty
|
||||
// through the curve table (auto) or a console-set value (manual).
|
||||
namespace app {
|
||||
|
||||
class controller {
|
||||
static inline std::uint16_t adc_average = 0;
|
||||
static inline std::int16_t temp_quarters = 0;
|
||||
static inline std::uint8_t percent = 100;
|
||||
static inline bool auto_mode = true;
|
||||
static inline bool have_data = false;
|
||||
|
||||
public:
|
||||
static void init()
|
||||
{
|
||||
fan::duty(avr::percent_t{10000}); // full blast until the first reading
|
||||
}
|
||||
|
||||
static void poll()
|
||||
{
|
||||
std::uint16_t sample;
|
||||
if (!sampler::take(sample))
|
||||
return;
|
||||
adc_average = sample;
|
||||
temp_quarters = thermistor::quarters(sample);
|
||||
have_data = true;
|
||||
if (auto_mode)
|
||||
percent = curve::duty(static_cast<std::int8_t>((temp_quarters + 2) / 4));
|
||||
fan::duty(avr::percent_t{static_cast<std::uint16_t>(percent * 100)});
|
||||
}
|
||||
|
||||
static void set_manual(std::uint8_t p)
|
||||
{
|
||||
auto_mode = false;
|
||||
percent = p;
|
||||
fan::duty(avr::percent_t{static_cast<std::uint16_t>(p * 100)});
|
||||
}
|
||||
|
||||
static void set_automatic()
|
||||
{
|
||||
auto_mode = true;
|
||||
}
|
||||
|
||||
static bool automatic()
|
||||
{
|
||||
return auto_mode;
|
||||
}
|
||||
|
||||
static bool data_available()
|
||||
{
|
||||
return have_data;
|
||||
}
|
||||
|
||||
static std::int16_t temperature_quarters()
|
||||
{
|
||||
return temp_quarters;
|
||||
}
|
||||
|
||||
static std::uint16_t last_adc()
|
||||
{
|
||||
return adc_average;
|
||||
}
|
||||
|
||||
static std::uint8_t fan_percent()
|
||||
{
|
||||
return percent;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
49
src/curve.hpp
Normal file
49
src/curve.hpp
Normal file
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
#include <avr/pgmspace.h>
|
||||
|
||||
// The auto-mode fan curve, tabulated at compile time: the legacy cubic
|
||||
// (0.002246·x³ − 0.09·x² + 0.91·x, zero below 20 °C) becomes a flash
|
||||
// lookup of duty percent per °C.
|
||||
namespace app::curve {
|
||||
|
||||
namespace detail {
|
||||
|
||||
consteval std::uint8_t duty_entry(int celsius)
|
||||
{
|
||||
double x = celsius;
|
||||
if (x < 20)
|
||||
return 0;
|
||||
double duty = 0.002246 * x * x * x - 0.09 * x * x + 0.91 * x;
|
||||
if (duty < 0)
|
||||
duty = 0;
|
||||
if (duty > 100)
|
||||
duty = 100;
|
||||
return static_cast<std::uint8_t>(duty + 0.5);
|
||||
}
|
||||
|
||||
struct table {
|
||||
[[gnu::progmem]] static constexpr std::array<std::uint8_t, 100> data = [] {
|
||||
std::array<std::uint8_t, 100> out{};
|
||||
for (int t = 0; t < 100; ++t)
|
||||
out[static_cast<std::size_t>(t)] = duty_entry(t);
|
||||
return out;
|
||||
}();
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// Duty percent for a temperature (clamped to the 0..99 °C table window).
|
||||
inline std::uint8_t duty(std::int8_t celsius)
|
||||
{
|
||||
if (celsius < 0)
|
||||
celsius = 0;
|
||||
if (celsius > 99)
|
||||
celsius = 99;
|
||||
return pgm_read_byte(&detail::table::data[static_cast<std::uint8_t>(celsius)]);
|
||||
}
|
||||
|
||||
} // namespace app::curve
|
||||
29
src/main.cpp
Normal file
29
src/main.cpp
Normal file
@@ -0,0 +1,29 @@
|
||||
#include <libavr/libavr.hpp>
|
||||
|
||||
#include "board.hpp"
|
||||
#include "bootloader.hpp"
|
||||
#include "controller.hpp"
|
||||
#include "statistics.hpp"
|
||||
#include "terminal.hpp"
|
||||
|
||||
using namespace app;
|
||||
|
||||
template struct avr::isr::emit<uptime::ticker, sampler::input, serial_t>;
|
||||
|
||||
int main()
|
||||
{
|
||||
bootloader::handle_reset();
|
||||
|
||||
avr::init<uptime::ticker, sampler::input, fan, serial_t, statistics>();
|
||||
avr::irq::enable();
|
||||
sampler::input::start();
|
||||
controller::init();
|
||||
terminal::init();
|
||||
|
||||
while (true) {
|
||||
controller::poll();
|
||||
if (controller::data_available())
|
||||
statistics::record(static_cast<std::int8_t>((controller::temperature_quarters() + 2) / 4));
|
||||
terminal::poll();
|
||||
}
|
||||
}
|
||||
105
src/statistics.hpp
Normal file
105
src/statistics.hpp
Normal file
@@ -0,0 +1,105 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
#include <libavr/libavr.hpp>
|
||||
|
||||
#include "board.hpp"
|
||||
|
||||
// Temperature histogram: one uint32 bucket per °C 0..99, sampled once a
|
||||
// second, written back to EEPROM every 30 minutes (update() only touches
|
||||
// changed bytes). Erased EEPROM reads back as 0xffffffff — treated as 0.
|
||||
namespace app {
|
||||
|
||||
class statistics {
|
||||
static constexpr std::uint8_t range = 100;
|
||||
static constexpr std::uint32_t sample_delay_ms = 1'000;
|
||||
static constexpr std::uint32_t writeback_delay_ms = 1'800'000;
|
||||
|
||||
using stored = avr::eeprom::var<std::array<std::uint32_t, range>, 0>;
|
||||
|
||||
static inline std::array<std::uint32_t, range> histogram{};
|
||||
static inline std::uint64_t last_sample = 0;
|
||||
static inline std::uint64_t last_writeback = 0;
|
||||
|
||||
static constexpr std::uint8_t clamp(std::int8_t t)
|
||||
{
|
||||
return t < 0 ? 0 : (t >= range ? range - 1 : static_cast<std::uint8_t>(t));
|
||||
}
|
||||
|
||||
public:
|
||||
static constexpr auto claims = stored::claims;
|
||||
|
||||
static void init()
|
||||
{
|
||||
histogram = stored::read();
|
||||
for (auto &bucket : histogram)
|
||||
if (bucket == 0xffffffff)
|
||||
bucket = 0;
|
||||
}
|
||||
|
||||
static void record(std::int8_t celsius)
|
||||
{
|
||||
auto now = uptime::millis();
|
||||
if (now >= last_sample + sample_delay_ms) {
|
||||
++histogram[clamp(celsius)];
|
||||
last_sample = now;
|
||||
}
|
||||
if (now >= last_writeback + writeback_delay_ms) {
|
||||
save();
|
||||
last_writeback = now;
|
||||
}
|
||||
}
|
||||
|
||||
static void save()
|
||||
{
|
||||
stored::update(histogram);
|
||||
}
|
||||
|
||||
static void reset()
|
||||
{
|
||||
histogram = {};
|
||||
stored::update(histogram);
|
||||
}
|
||||
|
||||
static std::uint8_t min_temperature()
|
||||
{
|
||||
for (std::uint8_t i = 0; i < range; ++i)
|
||||
if (histogram[i])
|
||||
return i;
|
||||
return range;
|
||||
}
|
||||
|
||||
static std::uint8_t max_temperature()
|
||||
{
|
||||
for (std::uint8_t i = range; i > 0; --i)
|
||||
if (histogram[i - 1])
|
||||
return i - 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static std::uint64_t total_samples()
|
||||
{
|
||||
std::uint64_t total = 0;
|
||||
for (auto bucket : histogram)
|
||||
total += bucket;
|
||||
return total;
|
||||
}
|
||||
|
||||
static std::uint32_t highest_bucket()
|
||||
{
|
||||
std::uint32_t highest = 0;
|
||||
for (auto bucket : histogram)
|
||||
if (bucket > highest)
|
||||
highest = bucket;
|
||||
return highest;
|
||||
}
|
||||
|
||||
static std::uint32_t bucket(std::uint8_t celsius)
|
||||
{
|
||||
return histogram[clamp(static_cast<std::int8_t>(celsius))];
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
173
src/terminal.hpp
Normal file
173
src/terminal.hpp
Normal file
@@ -0,0 +1,173 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
#include <libavr/libavr.hpp>
|
||||
|
||||
#include "board.hpp"
|
||||
#include "bootloader.hpp"
|
||||
#include "controller.hpp"
|
||||
#include "curve.hpp"
|
||||
#include "statistics.hpp"
|
||||
|
||||
// The serial console: line-buffered commands over the hardware UART.
|
||||
// `help` lists everything; `monitor` streams until any key.
|
||||
namespace app {
|
||||
|
||||
class terminal {
|
||||
static constexpr std::uint8_t line_max = 24;
|
||||
static inline char line[line_max]{};
|
||||
static inline std::uint8_t at = 0;
|
||||
static inline bool monitoring = false;
|
||||
static inline std::uint64_t last_monitor = 0;
|
||||
|
||||
static void prompt()
|
||||
{
|
||||
serial << "> "_P;
|
||||
}
|
||||
|
||||
static void show()
|
||||
{
|
||||
serial << "temperature "_P << controller::temperature_quarters() / 4 << '.'
|
||||
<< (controller::temperature_quarters() % 4) * 25 << " C, adc "_P << controller::last_adc() << ", fan "_P
|
||||
<< controller::fan_percent() << " %, "_P;
|
||||
if (controller::automatic())
|
||||
serial << "auto"_P;
|
||||
else
|
||||
serial << "manual"_P;
|
||||
serial << "\r\n"_P;
|
||||
}
|
||||
|
||||
static void print_curve()
|
||||
{
|
||||
for (std::int8_t t = 15; t <= 60; t += 5)
|
||||
serial << t << " C -> "_P << curve::duty(t) << " %\r\n"_P;
|
||||
}
|
||||
|
||||
static void print_uptime()
|
||||
{
|
||||
auto seconds = static_cast<std::uint32_t>(uptime::millis() / 1000);
|
||||
serial << seconds / 86400 << "d "_P << (seconds / 3600) % 24 << "h "_P << (seconds / 60) % 60 << "m "_P
|
||||
<< seconds % 60 << "s\r\n"_P;
|
||||
}
|
||||
|
||||
static void print_statistics()
|
||||
{
|
||||
serial << "min "_P << statistics::min_temperature() << " C, max "_P << statistics::max_temperature()
|
||||
<< " C, samples "_P << static_cast<std::uint32_t>(statistics::total_samples()) << "\r\n"_P;
|
||||
}
|
||||
|
||||
static void print_histogram()
|
||||
{
|
||||
auto highest = statistics::highest_bucket();
|
||||
if (highest == 0) {
|
||||
serial << "empty\r\n"_P;
|
||||
return;
|
||||
}
|
||||
for (std::uint8_t t = statistics::min_temperature(); t <= statistics::max_temperature(); ++t) {
|
||||
serial << t << " C |"_P;
|
||||
auto width = static_cast<std::uint8_t>((statistics::bucket(t) * 40) / highest);
|
||||
for (std::uint8_t i = 0; i < width; ++i)
|
||||
serial << '#';
|
||||
serial << ' ' << statistics::bucket(t) << "\r\n"_P;
|
||||
}
|
||||
}
|
||||
|
||||
static void help()
|
||||
{
|
||||
serial << "help show curve monitor uptime statistics histogram reset set <0-100> auto version bootloader\r\n"_P;
|
||||
}
|
||||
|
||||
static void dispatch(std::string_view cmd)
|
||||
{
|
||||
if (cmd.empty()) {
|
||||
} else if (cmd == "help") {
|
||||
help();
|
||||
} else if (cmd == "show") {
|
||||
show();
|
||||
} else if (cmd == "curve") {
|
||||
print_curve();
|
||||
} else if (cmd == "monitor") {
|
||||
monitoring = true;
|
||||
} else if (cmd == "uptime") {
|
||||
print_uptime();
|
||||
} else if (cmd == "statistics") {
|
||||
print_statistics();
|
||||
} else if (cmd == "histogram") {
|
||||
print_histogram();
|
||||
} else if (cmd == "reset") {
|
||||
statistics::reset();
|
||||
serial << "statistics cleared\r\n"_P;
|
||||
} else if (cmd == "auto") {
|
||||
controller::set_automatic();
|
||||
serial << "auto\r\n"_P;
|
||||
} else if (cmd == "version") {
|
||||
serial << "fantemp on libavr\r\n"_P;
|
||||
} else if (cmd == "bootloader") {
|
||||
serial << "entering bootloader\r\n"_P;
|
||||
statistics::save();
|
||||
bootloader::enter();
|
||||
} else if (cmd.starts_with("set ")) {
|
||||
std::uint8_t percent = 0;
|
||||
bool valid = cmd.size() > 4;
|
||||
for (std::size_t i = 4; i < cmd.size(); ++i) {
|
||||
if (cmd[i] < '0' || cmd[i] > '9') {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
percent = static_cast<std::uint8_t>(percent * 10 + (cmd[i] - '0'));
|
||||
}
|
||||
if (valid && percent <= 100) {
|
||||
controller::set_manual(percent);
|
||||
serial << "fan "_P << percent << " %\r\n"_P;
|
||||
} else {
|
||||
serial << "set 0..100\r\n"_P;
|
||||
}
|
||||
} else {
|
||||
serial << "? (help)\r\n"_P;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
static void init()
|
||||
{
|
||||
serial << "\r\nfantemp on libavr — help for commands\r\n"_P;
|
||||
prompt();
|
||||
}
|
||||
|
||||
static void poll()
|
||||
{
|
||||
if (monitoring) {
|
||||
if (uptime::millis() >= last_monitor + 1000) {
|
||||
show();
|
||||
last_monitor = uptime::millis();
|
||||
}
|
||||
if (serial_t::read()) { // any key stops
|
||||
monitoring = false;
|
||||
prompt();
|
||||
}
|
||||
return;
|
||||
}
|
||||
while (auto in = serial_t::read()) {
|
||||
char c = static_cast<char>(*in);
|
||||
if (c == '\r' || c == '\n') {
|
||||
serial << "\r\n"_P;
|
||||
dispatch(std::string_view{line, at});
|
||||
at = 0;
|
||||
if (!monitoring)
|
||||
prompt();
|
||||
} else if (c == 0x7f || c == 0x08) {
|
||||
if (at) {
|
||||
--at;
|
||||
serial << "\b \b"_P;
|
||||
}
|
||||
} else if (at < line_max && c >= ' ') {
|
||||
line[at++] = c;
|
||||
serial << c; // echo
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
101
src/thermistor.hpp
Normal file
101
src/thermistor.hpp
Normal file
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
#include <avr/pgmspace.h>
|
||||
|
||||
// NTC thermistor on a series divider, solved entirely at compile time:
|
||||
// the Beta equation (with its logarithm) runs consteval into a flash
|
||||
// table — the firmware never does floating point. Raw 10-bit ADC counts
|
||||
// map to quarter-°C with linear interpolation between table steps.
|
||||
namespace app::thermistor {
|
||||
|
||||
inline constexpr double series_resistor = 9951;
|
||||
inline constexpr double nominal_resistance = 9270;
|
||||
inline constexpr double beta = 3212;
|
||||
inline constexpr double nominal_temperature = 25;
|
||||
|
||||
namespace detail {
|
||||
|
||||
// Natural log for the consteval evaluator (no freestanding <cmath>):
|
||||
// range-reduce by powers of two, then the atanh series around 1.
|
||||
consteval double ln(double x)
|
||||
{
|
||||
constexpr double ln2 = 0.6931471805599453;
|
||||
int k = 0;
|
||||
while (x > 1.5) {
|
||||
x /= 2;
|
||||
++k;
|
||||
}
|
||||
while (x < 0.75) {
|
||||
x *= 2;
|
||||
--k;
|
||||
}
|
||||
double z = (x - 1) / (x + 1);
|
||||
double z2 = z * z;
|
||||
double term = z;
|
||||
double sum = 0;
|
||||
for (int n = 1; n < 30; n += 2) {
|
||||
sum += term / n;
|
||||
term *= z2;
|
||||
}
|
||||
return 2 * sum + k * ln2;
|
||||
}
|
||||
|
||||
consteval double temperature_of(double adc)
|
||||
{
|
||||
double resistance = series_resistor * adc / (1023.0 - adc);
|
||||
double steinhart = ln(resistance / nominal_resistance) / beta + 1.0 / (nominal_temperature + 273.15);
|
||||
return 1.0 / steinhart - 273.15;
|
||||
}
|
||||
|
||||
// 256 entries over the 10-bit range (steps of 4 counts), quarter-°C,
|
||||
// clamped to a sane sensor window.
|
||||
consteval std::int16_t quarters_entry(int index)
|
||||
{
|
||||
double adc = index * 4.0;
|
||||
if (adc < 4)
|
||||
adc = 4;
|
||||
if (adc > 1019)
|
||||
adc = 1019;
|
||||
double t = temperature_of(adc) * 4.0;
|
||||
if (t < -40 * 4)
|
||||
t = -40 * 4;
|
||||
if (t > 125 * 4)
|
||||
t = 125 * 4;
|
||||
return static_cast<std::int16_t>(t < 0 ? t - 0.5 : t + 0.5);
|
||||
}
|
||||
|
||||
struct table {
|
||||
[[gnu::progmem]] static constexpr std::array<std::int16_t, 257> data = [] {
|
||||
std::array<std::int16_t, 257> out{};
|
||||
for (int i = 0; i < 257; ++i)
|
||||
out[static_cast<std::size_t>(i)] = quarters_entry(i < 256 ? i : 255);
|
||||
return out;
|
||||
}();
|
||||
};
|
||||
|
||||
inline std::int16_t read_entry(std::uint16_t index)
|
||||
{
|
||||
return static_cast<std::int16_t>(pgm_read_word(&table::data[index]));
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// Temperature in quarter-°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;
|
||||
auto a = detail::read_entry(index);
|
||||
auto b = detail::read_entry(static_cast<std::uint16_t>(index + 1));
|
||||
return static_cast<std::int16_t>(a + ((b - a) * frac) / 4);
|
||||
}
|
||||
|
||||
inline std::int8_t celsius(std::uint16_t adc)
|
||||
{
|
||||
return static_cast<std::int8_t>((quarters(adc) + 2) / 4);
|
||||
}
|
||||
|
||||
} // namespace app::thermistor
|
||||
Reference in New Issue
Block a user