From ab78d948720d36962d14fd09b592100d8c3f8687 Mon Sep 17 00:00:00 2001 From: BlackMark Date: Sat, 18 Jul 2026 04:37:26 +0200 Subject: [PATCH] Rewrite on libavr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitignore | 13 +- .gitmodules | 18 -- CMakeLists.txt | 20 +++ CMakePresets.json | 43 +++++ README.md | 22 +++ fantemp.atsln | 22 --- fantemp/adc | 1 - fantemp/bootloader.cpp | 46 ----- fantemp/bootloader.hpp | 24 --- fantemp/clock.cpp | 37 ---- fantemp/clock.hpp | 13 -- fantemp/controller.cpp | 69 -------- fantemp/controller.hpp | 46 ----- fantemp/eeprom | 1 - fantemp/fantemp.cppproj | 309 -------------------------------- fantemp/flash | 1 - fantemp/io | 1 - fantemp/main.cpp | 36 ---- fantemp/pwm.cpp | 25 --- fantemp/pwm.hpp | 10 -- fantemp/statistics.cpp | 122 ------------- fantemp/statistics.hpp | 31 ---- fantemp/terminal.hpp | 377 ---------------------------------------- fantemp/thermistor.cpp | 19 -- fantemp/thermistor.hpp | 16 -- fantemp/type | 1 - fantemp/uart | 1 - src/board.hpp | 68 ++++++++ src/bootloader.hpp | 45 +++++ src/controller.hpp | 77 ++++++++ src/curve.hpp | 49 ++++++ src/main.cpp | 29 ++++ src/statistics.hpp | 105 +++++++++++ src/terminal.hpp | 173 ++++++++++++++++++ src/thermistor.hpp | 101 +++++++++++ 35 files changed, 734 insertions(+), 1237 deletions(-) delete mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 CMakePresets.json create mode 100644 README.md delete mode 100644 fantemp.atsln delete mode 160000 fantemp/adc delete mode 100644 fantemp/bootloader.cpp delete mode 100644 fantemp/bootloader.hpp delete mode 100644 fantemp/clock.cpp delete mode 100644 fantemp/clock.hpp delete mode 100644 fantemp/controller.cpp delete mode 100644 fantemp/controller.hpp delete mode 160000 fantemp/eeprom delete mode 100644 fantemp/fantemp.cppproj delete mode 160000 fantemp/flash delete mode 160000 fantemp/io delete mode 100644 fantemp/main.cpp delete mode 100644 fantemp/pwm.cpp delete mode 100644 fantemp/pwm.hpp delete mode 100644 fantemp/statistics.cpp delete mode 100644 fantemp/statistics.hpp delete mode 100644 fantemp/terminal.hpp delete mode 100644 fantemp/thermistor.cpp delete mode 100644 fantemp/thermistor.hpp delete mode 160000 fantemp/type delete mode 160000 fantemp/uart create mode 100644 src/board.hpp create mode 100644 src/bootloader.hpp create mode 100644 src/controller.hpp create mode 100644 src/curve.hpp create mode 100644 src/main.cpp create mode 100644 src/statistics.hpp create mode 100644 src/terminal.hpp create mode 100644 src/thermistor.hpp diff --git a/.gitignore b/.gitignore index 9f584cc..a4fb4fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,2 @@ -.vs -Release -Debug -*.componentinfo.xml -*.elf -*.o -*.hex -*.srec -*.eeprom -*.lss -*.map +build/ +.cache/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 65d439e..0000000 --- a/.gitmodules +++ /dev/null @@ -1,18 +0,0 @@ -[submodule "fantemp/uart"] - path = fantemp/uart - url = git@git.blackmark.me:avr/uart.git -[submodule "fantemp/flash"] - path = fantemp/flash - url = git@git.blackmark.me:avr/flash.git -[submodule "fantemp/io"] - path = fantemp/io - url = git@git.blackmark.me:avr/io.git -[submodule "fantemp/adc"] - path = fantemp/adc - url = git@git.blackmark.me:avr/adc.git -[submodule "fantemp/type"] - path = fantemp/type - url = git@git.blackmark.me:avr/type.git -[submodule "fantemp/eeprom"] - path = fantemp/eeprom - url = git@git.blackmark.me:avr/eeprom.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..4fe2d3a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.28) + +project(fantemp LANGUAGES CXX) + +# libavr from a local checkout (LIBAVR_ROOT) or the forge; the toolchain +# file comes from the same checkout via CMakePresets.json. +include(FetchContent) +if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT}) + set(LIBAVR_ROOT $ENV{LIBAVR_ROOT}) +endif() +if(LIBAVR_ROOT) + FetchContent_Declare(libavr SOURCE_DIR ${LIBAVR_ROOT}) +else() + FetchContent_Declare(libavr GIT_REPOSITORY git@git.blackmark.me:avr/libavr.git GIT_TAG main) +endif() +FetchContent_MakeAvailable(libavr) + +add_executable(fantemp src/main.cpp) +target_link_libraries(fantemp PRIVATE libavr) +add_custom_command(TARGET fantemp POST_BUILD COMMAND ${CMAKE_SIZE} $) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..cd08300 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,43 @@ +{ + "version": 8, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "toolchainFile": "$env{LIBAVR_ROOT}/cmake/avr-toolchain.cmake", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "CMAKE_COLOR_DIAGNOSTICS": "ON" + } + }, + { + "name": "atmega328p-generated", + "inherits": "base", + "cacheVariables": { + "LIBAVR_MCU": "atmega328p", + "LIBAVR_REFLECT": "OFF" + } + }, + { + "name": "atmega328p-reflect", + "inherits": "base", + "cacheVariables": { + "LIBAVR_MCU": "atmega328p", + "LIBAVR_REFLECT": "ON" + } + } + ], + "buildPresets": [ + { + "name": "atmega328p-generated", + "configurePreset": "atmega328p-generated" + }, + { + "name": "atmega328p-reflect", + "configurePreset": "atmega328p-reflect" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7b0fbab --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# fantemp + +Temperature-controlled fan firmware (ATmega328P, 16 MHz), rewritten on +[libavr](https://git.blackmark.me/avr/libavr): thermistor on ADC0 sampled +free-running and averaged over 1000 conversions, fan on OC0B at 50 kHz, +115200 Bd serial console (`help` lists the commands), temperature +histogram persisted to EEPROM, watchdog-reset path into a boot-section +bootloader. + +The Steinhart–Hart math of the legacy firmware (runtime doubles + libm +log) is gone: the Beta equation and the cubic fan curve are evaluated +consteval into flash tables — the firmware itself never touches floating +point. + +Build against a libavr checkout: + +```sh +LIBAVR_ROOT=/path/to/libavr cmake --preset atmega328p-generated +cmake --build --preset atmega328p-generated +``` + +Legacy (yazoalfa submodules) stays on `master`. diff --git a/fantemp.atsln b/fantemp.atsln deleted file mode 100644 index 2f7bec5..0000000 --- a/fantemp.atsln +++ /dev/null @@ -1,22 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Atmel Studio Solution File, Format Version 11.00 -VisualStudioVersion = 14.0.23107.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{E66E83B9-2572-4076-B26E-6BE79FF3018A}") = "fantemp", "fantemp\fantemp.cppproj", "{DCE6C7E3-EE26-4D79-826B-08594B9AD897}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|AVR = Debug|AVR - Release|AVR = Release|AVR - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {DCE6C7E3-EE26-4D79-826B-08594B9AD897}.Debug|AVR.ActiveCfg = Debug|AVR - {DCE6C7E3-EE26-4D79-826B-08594B9AD897}.Debug|AVR.Build.0 = Debug|AVR - {DCE6C7E3-EE26-4D79-826B-08594B9AD897}.Release|AVR.ActiveCfg = Release|AVR - {DCE6C7E3-EE26-4D79-826B-08594B9AD897}.Release|AVR.Build.0 = Release|AVR - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/fantemp/adc b/fantemp/adc deleted file mode 160000 index 5e9dac8..0000000 --- a/fantemp/adc +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5e9dac872aac65a7dbcc300430efe1d95e7b15fc diff --git a/fantemp/bootloader.cpp b/fantemp/bootloader.cpp deleted file mode 100644 index f34ad74..0000000 --- a/fantemp/bootloader.cpp +++ /dev/null @@ -1,46 +0,0 @@ -#include "bootloader.hpp" - -#include -#include -#include - -namespace { - -typedef void (*jmp_fn)() __attribute__((noreturn)); - -jmp_fn boot = reinterpret_cast(0x0000); -jmp_fn bootloader = reinterpret_cast(0x7800 / 2); - -} // namespace - -bool Bootloader::handleReset() -{ - wdt_reset(); - uint8_t mcuStatus = MCUSR; - MCUSR &= ~(1 << WDRF); - wdt_disable(); - return (mcuStatus & (1 << WDRF)); -} - -void Bootloader::reset() -{ - wdt_enable(WDTO_15MS); - while (true) - ; -} - -bool Bootloader::check() -{ - if (pgm_read_byte(reinterpret_cast(bootloader) * 2) != 0xFF) - return true; - - return false; -} - -void Bootloader::call() -{ - if (check()) - bootloader(); - else - boot(); -} diff --git a/fantemp/bootloader.hpp b/fantemp/bootloader.hpp deleted file mode 100644 index 8f7574d..0000000 --- a/fantemp/bootloader.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -class Bootloader { - public: - template - static inline void init(Fn callback) - { - if (handleReset()) { - callback(); - call(); - } - } - - static inline void enter() - { - reset(); - } - - private: - static bool handleReset(); - static void reset(); - static bool check(); - static void call(); -}; diff --git a/fantemp/clock.cpp b/fantemp/clock.cpp deleted file mode 100644 index 390a0d0..0000000 --- a/fantemp/clock.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include "clock.hpp" - -#include -#include - -namespace clk { - -namespace detail { - -volatile uint64_t sm_millisCounter = 0; - -ISR(TIMER2_COMPA_vect) -{ - ++sm_millisCounter; -} - -} // namespace detail - -void init() -{ - TCCR2A |= (1 << WGM21); - TCCR2B |= (1 << CS22) | (1 << CS20); - OCR2A = 124; - TIMSK2 |= (1 << OCIE2A); -} - -uint64_t millis() -{ - const auto oldSreg = SREG; - cli(); - const auto millisCounter = detail::sm_millisCounter; - SREG = oldSreg; - - return millisCounter; -} - -} // namespace clk diff --git a/fantemp/clock.hpp b/fantemp/clock.hpp deleted file mode 100644 index 1667865..0000000 --- a/fantemp/clock.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#define F_CPU 16'000'000 -#include - -#include - -namespace clk { - -void init(); -uint64_t millis(); - -} // namespace clk diff --git a/fantemp/controller.cpp b/fantemp/controller.cpp deleted file mode 100644 index bfa8bda..0000000 --- a/fantemp/controller.cpp +++ /dev/null @@ -1,69 +0,0 @@ -#include "controller.hpp" - -#define ADC_INT_VECTOR -#include "adc/adc.hpp" - -double Controller::m_adcSample; -double Controller::m_resistance; -double Controller::m_temperature; -uint8_t Controller::m_fanSpeed; -bool Controller::m_dataAvailable = false; -bool Controller::m_autoMode = true; - -volatile uint32_t Controller::m_adcSampleSum; -volatile bool Controller::m_adcSampleReady = false; - -void Controller::init() -{ - m_adcPin.init(sampleCallback); - pwm::init(); - pwm::setDuty(100); -} - -void Controller::callback() -{ - if (m_adcSampleReady) { - m_adcSample = static_cast(m_adcSampleSum) / NUM_ADC_SAMPLES; - m_dataAvailable = true; - m_adcSampleReady = false; - - m_resistance = m_thermistor.getResistance(m_adcSample); - m_temperature = m_thermistor.getTemperature(m_resistance); - - if (m_autoMode) - m_fanSpeed = mapTemperature(m_temperature); - - pwm::setDuty(m_fanSpeed); - } -} - -uint8_t Controller::mapTemperature(double temperature) -{ - [[maybe_unused]] constexpr auto linearCurve = [](double x) { return (10 * x - 200) / 3; }; - constexpr auto cubicCurve = [](double x) { - if (x < 20) - return 0.0; - return 0.002246 * x * x * x - 0.09 * x * x + 0.91 * x; - }; - - double fanSpeed = cubicCurve(temperature); - return clamp(fanSpeed, 0, 100); -} - -void Controller::sampleCallback(const uint16_t &adcSample) -{ - static uint32_t s_sampleSum = 0; - static auto s_sampleCounter = NUM_ADC_SAMPLES; - - s_sampleSum += adcSample; - - if (--s_sampleCounter <= 0) { - if (!m_adcSampleReady) { - m_adcSampleSum = s_sampleSum; - m_adcSampleReady = true; - } - // else lose this sample, which happens during long running commands like "curve", but has no impact - s_sampleSum = 0; - s_sampleCounter = NUM_ADC_SAMPLES; - } -} diff --git a/fantemp/controller.hpp b/fantemp/controller.hpp deleted file mode 100644 index f4785c1..0000000 --- a/fantemp/controller.hpp +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once - -#include "adc/adc.hpp" -#include "io/io.hpp" - -#include "pwm.hpp" -#include "thermistor.hpp" - -class Controller { - public: - static double m_adcSample; - static double m_resistance; - static double m_temperature; - static uint8_t m_fanSpeed; - static bool m_dataAvailable; - static bool m_autoMode; - - static void init(); - - static void callback(); - - static uint8_t mapTemperature(double temperature); - - private: - using adc_conf = adc::Config; - static adc::Adc m_adcPin; - - static constexpr auto NUM_ADC_SAMPLES = 1000; - - static volatile uint32_t m_adcSampleSum; - static volatile bool m_adcSampleReady; - - static Thermistor m_thermistor; - - static void sampleCallback(const uint16_t &adcSample); - - template - static T clamp(double value, T lower, T upper) - { - if (value < lower) - return lower; - if (value > upper) - return upper; - return static_cast(value); - } -}; diff --git a/fantemp/eeprom b/fantemp/eeprom deleted file mode 160000 index 3bcba0a..0000000 --- a/fantemp/eeprom +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3bcba0a1919906d7cb358d3d8185a064bc1aa705 diff --git a/fantemp/fantemp.cppproj b/fantemp/fantemp.cppproj deleted file mode 100644 index 86d5b66..0000000 --- a/fantemp/fantemp.cppproj +++ /dev/null @@ -1,309 +0,0 @@ - - - - 2.0 - 7.0 - com.Atmel.AVRGCC8.CPP - dce6c7e3-ee26-4d79-826b-08594b9ad897 - ATmega328P - none - Executable - CPP - $(MSBuildProjectName) - .elf - $(MSBuildProjectDirectory)\$(Configuration) - fantemp - fantemp - fantemp - avr-g++-9.1.0 - true - false - true - true - 0x20000000 - - true - exception_table - 2 - 0 - 0 - - com.atmel.avrdbg.tool.atmelice - J41800099437 - 0x1E950F - - - - 125000 - - ISP - - com.atmel.avrdbg.tool.stk500 - - - STK500 - - ISP - 125000 - - - - - - - - - - - - - - - - 125000 - - ISP - - com.atmel.avrdbg.tool.atmelice - J41800099437 - Atmel-ICE - - - - - 125000 - - - - - custom - - - Custom Programming Tool - - - - - \Debug\fantemp.lss - - - .lss - ^\s*(?<address>[a-f0-9]*):\s*.*$ - true - address - $pc - - - - - - - - - -mmcu=atmega328p - True - True - True - True - False - True - True - - - NDEBUG - - - - - %24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include - - - Optimize for size (-Os) - True - True - True - True - -fno-threadsafe-statics -std=c11 - True - True - - - NDEBUG - - - - - %24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include - - - Optimize for size (-Os) - True - True - True - -fno-threadsafe-statics -Wextra -std=c++17 - - - libm - - - - - %24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include - - - - - - - - - -mmcu=atmega328p - True - True - True - True - False - True - True - - - DEBUG - - - - - %24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include - - - Optimize (-O1) - True - Maximum (-g3) - True - True - True - -fno-threadsafe-statics -std=c11 - True - True - - - DEBUG - - - - - %24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include - - - Optimize (-O1) - True - Maximum (-g3) - True - True - -fno-threadsafe-statics -Wextra -std=c++17 - - - libm - - - - - %24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include - - - Default (-Wa,-g) - - - - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - compile - - - - - - - - - - - - \ No newline at end of file diff --git a/fantemp/flash b/fantemp/flash deleted file mode 160000 index 6edb2e5..0000000 --- a/fantemp/flash +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6edb2e5a21e0ce58ff2df936caee8b84e240a46b diff --git a/fantemp/io b/fantemp/io deleted file mode 160000 index 80de36e..0000000 --- a/fantemp/io +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 80de36ee7ee3e6b0842d5eaee81d54062cb496b2 diff --git a/fantemp/main.cpp b/fantemp/main.cpp deleted file mode 100644 index 11a04bf..0000000 --- a/fantemp/main.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include "clock.hpp" - -#include "uart/uart.hpp" - -#define UART0_INT_VECTORS -#include "uart/hardware0.hpp" - -#include "bootloader.hpp" -#include "controller.hpp" -#include "statistics.hpp" -#include "terminal.hpp" - -int main() -{ - Bootloader::init([]() {}); - - clk::init(); - - using serial = uart::Uart0>; - Terminal terminal; - terminal.init(); - - Controller controller; - controller.init(); - - Statistics statistics; - statistics.init(); - - while (true) { - controller.callback(); - terminal.callback(); - statistics.callback(); - } - - return 0; -} diff --git a/fantemp/pwm.cpp b/fantemp/pwm.cpp deleted file mode 100644 index 57d1233..0000000 --- a/fantemp/pwm.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include "pwm.hpp" - -#include "io/io.hpp" - -namespace pwm { - -static constexpr uint8_t PWM_TOP = 40; - -void init() -{ - io::Pin pwmPin; - pwmPin.dir(io::Dir::OUT); - pwmPin = false; - - TCCR0A = (1 << COM0B1) | (1 << WGM00); - TCCR0B = (1 << WGM02) | (1 << CS01); - OCR0A = PWM_TOP; -} - -void setDuty(uint8_t percent) -{ - OCR0B = (percent * PWM_TOP) / 100; -} - -} // namespace pwm diff --git a/fantemp/pwm.hpp b/fantemp/pwm.hpp deleted file mode 100644 index 964ebb1..0000000 --- a/fantemp/pwm.hpp +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include - -namespace pwm { - -void init(); -void setDuty(uint8_t percent); - -} // namespace pwm diff --git a/fantemp/statistics.cpp b/fantemp/statistics.cpp deleted file mode 100644 index 4a4c2c0..0000000 --- a/fantemp/statistics.cpp +++ /dev/null @@ -1,122 +0,0 @@ -#include "statistics.hpp" - -#include - -#include "eeprom/eeprom.hpp" -#include "type/type.hpp" - -#include "clock.hpp" -#include "controller.hpp" - -uint64_t Statistics::m_lastTemperatureWriteback = 0; -uint64_t Statistics::m_lastTemperatureSample = 0; -uint32_t Statistics::m_temperatureHistogram[TEMPERATURE_RANGE]; - -namespace { - -// Must be in translation unit and cannot be forward declared -EEARRAY(uint32_t, e_temperatureHistogram, Statistics::TEMPERATURE_RANGE); - -// Could be declared in every function that uses it, but that would be code duplication -EepromArray g_eepTemperatureHistogram; - -} // namespace - -void Statistics::init() -{ - for (uint8_t i = 0; i < g_eepTemperatureHistogram.size(); ++i) { - m_temperatureHistogram[i] = g_eepTemperatureHistogram[i]; - if (m_temperatureHistogram[i] == type::numeric_limits::max()) { - m_temperatureHistogram[i] = 0; - } - } -} - -void Statistics::callback() -{ - if (Controller::m_dataAvailable) { - if (clk::millis() >= m_lastTemperatureSample + TEMPERATURE_SAMPLE_DELAY) { - const auto temperature = clampTemperature(static_cast(round(Controller::m_temperature))); - ++m_temperatureHistogram[temperature]; - m_lastTemperatureSample = clk::millis(); - } - - if (clk::millis() >= m_lastTemperatureWriteback + TEMPERATURE_WRITEBACK_DELAY) { - saveTemperatureHistogram(); - m_lastTemperatureWriteback = clk::millis(); - } - } -} - -void Statistics::reset() -{ - for (uint8_t i = 0; i < TEMPERATURE_RANGE; ++i) { - m_temperatureHistogram[i] = 0; - g_eepTemperatureHistogram[i] = 0; - } -} - -uint8_t Statistics::getMinTemperature() -{ - for (uint8_t i = 0; i < TEMPERATURE_RANGE; ++i) { - if (m_temperatureHistogram[i] > 0) - return i; - } - - return TEMPERATURE_RANGE; -} - -uint8_t Statistics::getMaxTemperature() -{ - for (int8_t i = TEMPERATURE_RANGE - 1; i >= 0; --i) { - if (m_temperatureHistogram[i] > 0) - return i; - } - - return TEMPERATURE_RANGE; -} - -uint64_t Statistics::getTotalHistogramSamples() -{ - uint64_t totalSamples = 0; - - for (uint8_t i = 0; i < TEMPERATURE_RANGE; ++i) { - totalSamples += m_temperatureHistogram[i]; - } - - return totalSamples; -} - -uint32_t Statistics::getHighestHistogramSamples() -{ - uint32_t max = 0; - - for (uint8_t i = 0; i < TEMPERATURE_RANGE; ++i) { - if (m_temperatureHistogram[i] > max) { - max = m_temperatureHistogram[i]; - } - } - - return max; -} - -uint32_t Statistics::getHistogram(const int8_t &temperature) -{ - return m_temperatureHistogram[clampTemperature(temperature)]; -} - -void Statistics::saveTemperatureHistogram() -{ - for (uint8_t i = 0; i < g_eepTemperatureHistogram.size(); ++i) { - g_eepTemperatureHistogram[i] = m_temperatureHistogram[i]; - } -} - -uint8_t Statistics::clampTemperature(const int8_t &temperature) -{ - if (temperature < 0) - return 0; - if (temperature >= TEMPERATURE_RANGE) - return TEMPERATURE_RANGE - 1; - return static_cast(temperature); -} diff --git a/fantemp/statistics.hpp b/fantemp/statistics.hpp deleted file mode 100644 index 448b652..0000000 --- a/fantemp/statistics.hpp +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include - -class Statistics { - public: - static constexpr auto TEMPERATURE_RANGE = 100; - - static void init(); - static void callback(); - static void reset(); - - static uint8_t getMinTemperature(); - static uint8_t getMaxTemperature(); - - static uint64_t getTotalHistogramSamples(); - static uint32_t getHighestHistogramSamples(); - static uint32_t getHistogram(const int8_t &temperature); - - static void saveTemperatureHistogram(); - - private: - static constexpr auto TEMPERATURE_WRITEBACK_DELAY = 1'800'000; - static constexpr auto TEMPERATURE_SAMPLE_DELAY = 1'000; - - static uint64_t m_lastTemperatureWriteback; - static uint64_t m_lastTemperatureSample; - static uint32_t m_temperatureHistogram[TEMPERATURE_RANGE]; - - static uint8_t clampTemperature(const int8_t &temperature); -}; diff --git a/fantemp/terminal.hpp b/fantemp/terminal.hpp deleted file mode 100644 index 7190d84..0000000 --- a/fantemp/terminal.hpp +++ /dev/null @@ -1,377 +0,0 @@ -#pragma once - -#include -#include -#include - -#include - -#include "flash/flash.hpp" - -#include "clock.hpp" -#include "controller.hpp" -#include "statistics.hpp" - -namespace detail { - -GF(ENDL, "\r\n"); -GF(HELP_CMD, "help"); -GF(SHOW_CMD, "show"); -GF(CURVE_CMD, "curve"); -GF(MONITOR_CMD, "monitor"); -GF(BOOTLOADER_CMD, "bootloader"); -GF(UPTIME_CMD, "uptime"); -GF(STATISTICS_CMD, "statistics"); -GF(HISTOGRAM_CMD, "histogram"); -GF(RESET_CMD, "reset"); -GF(SET_CMD, "set"); -GF(AUTO_CMD, "auto"); - -GF(VERSION_CMD, "version"); -GF(VERSION, "1.8"); - -static inline bool substringEquals(const char *str, const ::detail::FlashString *flashStr, const size_t &size) -{ - return (strncmp_P(str, reinterpret_cast(flashStr), size) == 0); -} - -static inline bool stringEquals(const char *str, const ::detail::FlashString *flashStr, const size_t &size) -{ - if (size == strlen_P(reinterpret_cast(flashStr))) { - return substringEquals(str, flashStr, size); - } - - return false; -} - -} // namespace detail - -template -class Terminal { - public: - static void init() - { - m_serial.init(); - - m_serial << detail::ENDL; - printVersion(); - m_serial << detail::ENDL << F("$ "); - } - - static void callback() - { - if (receiveInput()) { - parseInput(); - } - - if (m_state == State::MONITOR && clk::millis() >= m_monitorDelayLastUpdate + MONITOR_DELAY) { - showState(); - m_monitorDelayLastUpdate = clk::millis(); - } - } - - private: - static constexpr auto INPUT_BUFFER_SIZE = 128; - static constexpr auto BACKSPACE = uint8_t{0x7f}; - static constexpr auto CTRL_C = uint8_t{0x03}; - static constexpr auto MONITOR_DELAY = 500; - - enum class State { - NONE, - MONITOR, - }; - - static Uart m_serial; - static char m_inputBuffer[INPUT_BUFFER_SIZE]; - static uint16_t m_inputSize; - static State m_state; - static uint64_t m_monitorDelayLastUpdate; - - static bool receiveInput() - { - uint8_t inputByte; - - while (m_serial.rxByte(inputByte)) { - if (isprint(inputByte) || inputByte == CTRL_C) { - m_inputBuffer[m_inputSize++] = inputByte; - - // Handle Ctrl + C - if (inputByte == CTRL_C) { - m_serial << F("^C") << detail::ENDL; - return true; - } - // Echo - else { - m_serial << static_cast(inputByte); - } - } - - // Handle backspace - if (inputByte == BACKSPACE && m_inputSize > 0) { - m_serial << F("\b \b"); - --m_inputSize; - } - // Handle line terminator - else if (inputByte == '\r' || inputByte == '\n') { - // Consume possible second line terminator - if (m_serial.peek(inputByte) && (inputByte == '\r' || inputByte == '\n')) { - m_serial.rxByte(inputByte); - } - m_serial << detail::ENDL; - return true; - } - - if (m_inputSize >= INPUT_BUFFER_SIZE) { - m_serial << detail::ENDL << F("WARNING: Terminal input buffer overflow!") << detail::ENDL; - return true; - } - } - - return false; - } - - static uint8_t parseFanSpeed() - { - const auto setCmdLen = strlen_P(reinterpret_cast(detail::SET_CMD)); - - if (m_inputSize > setCmdLen && substringEquals(m_inputBuffer, detail::SET_CMD, setCmdLen) && - m_inputSize < INPUT_BUFFER_SIZE) { - m_inputBuffer[m_inputSize] = '\0'; // Null terminate to be parsable by stdlib - const auto *fanSpeedStr = m_inputBuffer + setCmdLen; - auto *fanSpeedStrEnd = m_inputBuffer + setCmdLen; - const auto fanSpeed = strtol(fanSpeedStr, &fanSpeedStrEnd, 10); - - if (fanSpeedStrEnd != fanSpeedStr && *fanSpeedStr != '\0' && *fanSpeedStrEnd == '\0') { - if (fanSpeed >= 0 && fanSpeed <= 100) - return static_cast(fanSpeed); - } - } - - return 0xFF; - } - - static void parseInput() - { - if (m_inputSize) { - if (m_inputBuffer[m_inputSize - 1] == CTRL_C) { - handleCtrlC(); - } else if (m_state == State::NONE) { - if (substringEquals(m_inputBuffer, detail::HELP_CMD, m_inputSize)) { - printHelp(); - } else if (substringEquals(m_inputBuffer, detail::SHOW_CMD, m_inputSize)) { - showState(); - } else if (substringEquals(m_inputBuffer, detail::CURVE_CMD, m_inputSize)) { - printCurve(); - } else if (substringEquals(m_inputBuffer, detail::MONITOR_CMD, m_inputSize)) { - m_state = State::MONITOR; - } else if (substringEquals(m_inputBuffer, detail::BOOTLOADER_CMD, m_inputSize)) { - handleBootloader(); - } else if (substringEquals(m_inputBuffer, detail::UPTIME_CMD, m_inputSize)) { - printUptime(); - } else if (substringEquals(m_inputBuffer, detail::STATISTICS_CMD, m_inputSize)) { - printStatistics(); - } else if (substringEquals(m_inputBuffer, detail::HISTOGRAM_CMD, m_inputSize)) { - printHistogram(); - } else if (stringEquals(m_inputBuffer, detail::RESET_CMD, m_inputSize)) { - handleReset(); - } else if (uint8_t targetFanSpeed = parseFanSpeed(); targetFanSpeed <= 100) { - handleSet(targetFanSpeed); - } else if (substringEquals(m_inputBuffer, detail::AUTO_CMD, m_inputSize)) { - handleAuto(); - } else if (substringEquals(m_inputBuffer, detail::VERSION_CMD, m_inputSize)) { - printVersion(); - } else { - printUnknown(); - } - } - } - - m_inputSize = 0; - if (m_state == State::NONE) - m_serial << F("$ "); - } - - static void handleCtrlC() - { - m_serial << F("Abort!") << detail::ENDL; - m_state = State::NONE; - } - - static void printHelp() - { - m_serial << F("FanTemp command overview: ") << detail::ENDL; - m_serial << detail::HELP_CMD << F(" .......: prints this help message") << detail::ENDL; - m_serial << detail::SHOW_CMD << F(" .......: shows current temperature and fan speed") << detail::ENDL; - m_serial << detail::CURVE_CMD << F(" ......: shows mapping from temperature to fan speed") << detail::ENDL; - m_serial << detail::MONITOR_CMD << F(" ....: loops the show command until Ctrl + C is pressed") << detail::ENDL; - m_serial << detail::BOOTLOADER_CMD << F(" .: enters the bootloader after 3 seconds") << detail::ENDL; - m_serial << detail::UPTIME_CMD << F(" .....: shows system uptime") << detail::ENDL; - m_serial << detail::STATISTICS_CMD << F(" .: prints overall statistics like min and max temp") << detail::ENDL; - m_serial << detail::HISTOGRAM_CMD << F(" ..: prints a histogram of the temperature") << detail::ENDL; - m_serial << detail::RESET_CMD << F(" ......: resets statistics to 0 in EEPROM and RAM") << detail::ENDL; - m_serial << detail::SET_CMD << F(" ........: sets the fan speed to the provided value") << detail::ENDL; - m_serial << detail::AUTO_CMD << F(" .......: turns on automatic fan control") << detail::ENDL; - m_serial << detail::VERSION_CMD << F(" ....: displays firmware version") << detail::ENDL; - } - - static void showState() - { - if (Controller::m_dataAvailable) { - char floatBuffer[16]; - - dtostrf(Controller::m_adcSample, 0, 2, floatBuffer); - m_serial << F("ADC value ...: ") << floatBuffer << F(" / 1023") << detail::ENDL; - dtostrf(Controller::m_resistance, 0, 2, floatBuffer); - m_serial << F("Resistance ..: ") << floatBuffer << F(" Ohm") << detail::ENDL; - dtostrf(Controller::m_temperature, 0, 2, floatBuffer); - m_serial << F("Temperature .: ") << floatBuffer << F(" C") << detail::ENDL; - m_serial << F("Fan speed ...: ") << Controller::m_fanSpeed << F("%") - << (Controller::m_autoMode ? F(" auto") : F(" manual")) << detail::ENDL; - } else { - m_serial << F("No data available yet!") << detail::ENDL; - } - } - - static void printCurve() - { - for (uint8_t i = 10; i <= 60; ++i) { - m_serial << i << F(" C = "); - m_serial.template txNumber(Controller::mapTemperature(i)); - m_serial << F("%\t"); - for (uint8_t s = 0; s < Controller::mapTemperature(i); ++s) { - m_serial << '#'; - } - m_serial << detail::ENDL; - } - } - - static void handleBootloader() - { - m_serial << F("Saving statistics to EEPROM") << detail::ENDL; - Statistics::saveTemperatureHistogram(); - - m_serial << F("Entering bootloader...") << detail::ENDL; - m_serial.flushTx(); - - _delay_ms(3000); - - Bootloader::enter(); - } - - static void printUptime() - { - constexpr auto delimiter = ':'; - - const auto uptime = clk::millis(); - - const auto hours = static_cast(uptime / 1000 / 60 / 60); - const auto minutes = static_cast((uptime / 1000 / 60) % 60); - const auto seconds = static_cast((uptime / 1000) % 60); - - m_serial << F("System uptime: "); - m_serial.template txNumber(hours); - m_serial << delimiter; - m_serial.template txNumber(minutes); - m_serial << delimiter; - m_serial.template txNumber(seconds); - m_serial << detail::ENDL; - } - - static void printStatistics() - { - const auto minTemp = Statistics::getMinTemperature(); - const auto maxTemp = Statistics::getMaxTemperature(); - - m_serial << F("Minimum temperature .: "); - if (minTemp != Statistics::TEMPERATURE_RANGE) { - m_serial << minTemp << F(" C"); - } else { - m_serial << F("Not available"); - } - - m_serial << detail::ENDL << F("Maximum temperature .: "); - if (maxTemp != Statistics::TEMPERATURE_RANGE) { - m_serial << maxTemp << F(" C"); - } else { - m_serial << F("Not available"); - } - m_serial << detail::ENDL; - } - - static void printHistogram() - { - const auto totalSamples = Statistics::getTotalHistogramSamples(); - - if (totalSamples > 0) { - const auto maximumSamples = Statistics::getHighestHistogramSamples(); - auto normalizationFactor = (maximumSamples / 100 > 1) ? (maximumSamples / 100) : 1; - - while (maximumSamples / normalizationFactor > 100) - ++normalizationFactor; - - for (uint8_t t = Statistics::getMinTemperature(); t <= Statistics::getMaxTemperature(); ++t) { - const auto histogramSamples = Statistics::getHistogram(t); - m_serial.template txNumber(t); - m_serial << F(" C = "); - const auto percent = - static_cast((2 * 100 * histogramSamples + totalSamples) / (2 * totalSamples)); - m_serial.template txNumber(percent); - m_serial << F("%\t"); - const auto normalizedSamples = static_cast(histogramSamples / normalizationFactor); - for (uint8_t i = 0; i < normalizedSamples; ++i) { - m_serial << '#'; - } - m_serial << detail::ENDL; - } - } else { - m_serial << F("There is no data yet!") << detail::ENDL; - } - } - - static void handleReset() - { - m_serial << F("Resetting statistics in EEPROM and RAM") << detail::ENDL; - Statistics::reset(); - m_serial << F("Reset statistics") << detail::ENDL; - } - - static void handleSet(uint8_t targetFanSpeed) - { - m_serial << F("Setting fan speed to "); - m_serial.txNumber(targetFanSpeed); - m_serial << detail::ENDL; - Controller::m_autoMode = false; - Controller::m_fanSpeed = targetFanSpeed; - } - - static void handleAuto() - { - m_serial << F("Turning on automatic fan control") << detail::ENDL; - Controller::m_autoMode = true; - } - - static void printVersion() - { - m_serial << F("FanTemp v") << detail::VERSION << detail::ENDL; - } - - static void printUnknown() - { - m_serial << F("Unknown command \""); - for (uint16_t i = 0; i < m_inputSize; ++i) - m_serial << static_cast(m_inputBuffer[i]); - m_serial << F("\"") << detail::ENDL; - } -}; - -template -char Terminal::m_inputBuffer[INPUT_BUFFER_SIZE]; - -template -uint16_t Terminal::m_inputSize = 0; - -template -typename Terminal::State Terminal::m_state = State::NONE; - -template -uint64_t Terminal::m_monitorDelayLastUpdate = 0; diff --git a/fantemp/thermistor.cpp b/fantemp/thermistor.cpp deleted file mode 100644 index 43d130f..0000000 --- a/fantemp/thermistor.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "thermistor.hpp" - -#include - -double Thermistor::getResistance(double adcSample) -{ - return SERIES_RESISTOR * adcSample / (1023 - adcSample); -} - -double Thermistor::getTemperature(double resistance) -{ - double steinhart = resistance / THERMISTOR_NOMINAL; - steinhart = log(steinhart); - steinhart /= BETA_COEFFICIENT; - steinhart += 1.0 / (NOMINAL_TEMPERATURE + 273.15); - steinhart = 1.0 / steinhart; - steinhart -= 273.15; - return steinhart; -} diff --git a/fantemp/thermistor.hpp b/fantemp/thermistor.hpp deleted file mode 100644 index a542f7f..0000000 --- a/fantemp/thermistor.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include - -class Thermistor { - public: - static double getResistance(double adcSample); - - static double getTemperature(double resistance); - - private: - static constexpr auto SERIES_RESISTOR = 9951; - static constexpr auto THERMISTOR_NOMINAL = 9270; - static constexpr auto BETA_COEFFICIENT = 3212; - static constexpr auto NOMINAL_TEMPERATURE = 25; -}; diff --git a/fantemp/type b/fantemp/type deleted file mode 160000 index ce31ef0..0000000 --- a/fantemp/type +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ce31ef017f738baaca6f0cbf99dac9d59b5e6811 diff --git a/fantemp/uart b/fantemp/uart deleted file mode 160000 index 04b6782..0000000 --- a/fantemp/uart +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 04b6782ec457fb22759a097892b863a3ec6eaab4 diff --git a/src/board.hpp b/src/board.hpp new file mode 100644 index 0000000..b7913f4 --- /dev/null +++ b/src/board.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include + +// 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(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; + +using serial_t = dev::uart0<{.baud = 115200_Bd, .rx_buffer = 32, .max_baud_error = 2.5_pct}>; +inline constexpr serial_t serial{}; + +} // namespace app diff --git a/src/bootloader.hpp b/src/bootloader.hpp new file mode 100644 index 0000000..7cbb4c5 --- /dev/null +++ b/src/bootloader.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include + +#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(0x7800 / 2)(); + } + + [[noreturn]] static void enter() + { + guard::init(); + while (true) { + } + } +}; + +} // namespace app diff --git a/src/controller.hpp b/src/controller.hpp new file mode 100644 index 0000000..1a0795f --- /dev/null +++ b/src/controller.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include + +#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((temp_quarters + 2) / 4)); + fan::duty(avr::percent_t{static_cast(percent * 100)}); + } + + static void set_manual(std::uint8_t p) + { + auto_mode = false; + percent = p; + fan::duty(avr::percent_t{static_cast(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 diff --git a/src/curve.hpp b/src/curve.hpp new file mode 100644 index 0000000..09050ef --- /dev/null +++ b/src/curve.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include + +#include + +// 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(duty + 0.5); +} + +struct table { + [[gnu::progmem]] static constexpr std::array data = [] { + std::array out{}; + for (int t = 0; t < 100; ++t) + out[static_cast(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(celsius)]); +} + +} // namespace app::curve diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..bbb54a6 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,29 @@ +#include + +#include "board.hpp" +#include "bootloader.hpp" +#include "controller.hpp" +#include "statistics.hpp" +#include "terminal.hpp" + +using namespace app; + +template struct avr::isr::emit; + +int main() +{ + bootloader::handle_reset(); + + avr::init(); + avr::irq::enable(); + sampler::input::start(); + controller::init(); + terminal::init(); + + while (true) { + controller::poll(); + if (controller::data_available()) + statistics::record(static_cast((controller::temperature_quarters() + 2) / 4)); + terminal::poll(); + } +} diff --git a/src/statistics.hpp b/src/statistics.hpp new file mode 100644 index 0000000..1ac35b1 --- /dev/null +++ b/src/statistics.hpp @@ -0,0 +1,105 @@ +#pragma once + +#include +#include + +#include + +#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, 0>; + + static inline std::array 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(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(celsius))]; + } +}; + +} // namespace app diff --git a/src/terminal.hpp b/src/terminal.hpp new file mode 100644 index 0000000..203600a --- /dev/null +++ b/src/terminal.hpp @@ -0,0 +1,173 @@ +#pragma once + +#include +#include + +#include + +#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(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(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((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(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(*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 diff --git a/src/thermistor.hpp b/src/thermistor.hpp new file mode 100644 index 0000000..67bb9cd --- /dev/null +++ b/src/thermistor.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include +#include + +#include + +// 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 ): +// 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(t < 0 ? t - 0.5 : t + 0.5); +} + +struct table { + [[gnu::progmem]] static constexpr std::array data = [] { + std::array out{}; + for (int i = 0; i < 257; ++i) + out[static_cast(i)] = quarters_entry(i < 256 ? i : 255); + return out; + }(); +}; + +inline std::int16_t read_entry(std::uint16_t index) +{ + return static_cast(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(index + 1)); + return static_cast(a + ((b - a) * frac) / 4); +} + +inline std::int8_t celsius(std::uint16_t adc) +{ + return static_cast((quarters(adc) + 2) / 4); +} + +} // namespace app::thermistor