Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7a2e50506 | |||
| f17ebd17e5 | |||
| ab78d94872 |
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,11 +1,2 @@
|
|||||||
.vs
|
build/
|
||||||
Release
|
.cache/
|
||||||
Debug
|
|
||||||
*.componentinfo.xml
|
|
||||||
*.elf
|
|
||||||
*.o
|
|
||||||
*.hex
|
|
||||||
*.srec
|
|
||||||
*.eeprom
|
|
||||||
*.lss
|
|
||||||
*.map
|
|
||||||
|
|||||||
18
.gitmodules
vendored
18
.gitmodules
vendored
@@ -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
|
|
||||||
20
CMakeLists.txt
Normal file
20
CMakeLists.txt
Normal file
@@ -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} $<TARGET_FILE:fantemp>)
|
||||||
43
CMakePresets.json
Normal file
43
CMakePresets.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
22
README.md
Normal file
22
README.md
Normal file
@@ -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`.
|
||||||
@@ -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
|
|
||||||
Submodule fantemp/adc deleted from 5e9dac872a
@@ -1,46 +0,0 @@
|
|||||||
#include "bootloader.hpp"
|
|
||||||
|
|
||||||
#include <avr/io.h>
|
|
||||||
#include <avr/pgmspace.h>
|
|
||||||
#include <avr/wdt.h>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
typedef void (*jmp_fn)() __attribute__((noreturn));
|
|
||||||
|
|
||||||
jmp_fn boot = reinterpret_cast<jmp_fn>(0x0000);
|
|
||||||
jmp_fn bootloader = reinterpret_cast<jmp_fn>(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<uint16_t>(bootloader) * 2) != 0xFF)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Bootloader::call()
|
|
||||||
{
|
|
||||||
if (check())
|
|
||||||
bootloader();
|
|
||||||
else
|
|
||||||
boot();
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class Bootloader {
|
|
||||||
public:
|
|
||||||
template <typename Fn>
|
|
||||||
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();
|
|
||||||
};
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
#include "clock.hpp"
|
|
||||||
|
|
||||||
#include <avr/interrupt.h>
|
|
||||||
#include <avr/io.h>
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#define F_CPU 16'000'000
|
|
||||||
#include <util/delay.h>
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
namespace clk {
|
|
||||||
|
|
||||||
void init();
|
|
||||||
uint64_t millis();
|
|
||||||
|
|
||||||
} // namespace clk
|
|
||||||
@@ -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<double>(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<uint8_t>(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<adc::FreeRunningMode>;
|
|
||||||
static adc::Adc<adc_conf, io::P, io::P::C0> 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 <typename T>
|
|
||||||
static T clamp(double value, T lower, T upper)
|
|
||||||
{
|
|
||||||
if (value < lower)
|
|
||||||
return lower;
|
|
||||||
if (value > upper)
|
|
||||||
return upper;
|
|
||||||
return static_cast<T>(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Submodule fantemp/eeprom deleted from 3bcba0a191
@@ -1,309 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="14.0">
|
|
||||||
<PropertyGroup>
|
|
||||||
<SchemaVersion>2.0</SchemaVersion>
|
|
||||||
<ProjectVersion>7.0</ProjectVersion>
|
|
||||||
<ToolchainName>com.Atmel.AVRGCC8.CPP</ToolchainName>
|
|
||||||
<ProjectGuid>dce6c7e3-ee26-4d79-826b-08594b9ad897</ProjectGuid>
|
|
||||||
<avrdevice>ATmega328P</avrdevice>
|
|
||||||
<avrdeviceseries>none</avrdeviceseries>
|
|
||||||
<OutputType>Executable</OutputType>
|
|
||||||
<Language>CPP</Language>
|
|
||||||
<OutputFileName>$(MSBuildProjectName)</OutputFileName>
|
|
||||||
<OutputFileExtension>.elf</OutputFileExtension>
|
|
||||||
<OutputDirectory>$(MSBuildProjectDirectory)\$(Configuration)</OutputDirectory>
|
|
||||||
<AssemblyName>fantemp</AssemblyName>
|
|
||||||
<Name>fantemp</Name>
|
|
||||||
<RootNamespace>fantemp</RootNamespace>
|
|
||||||
<ToolchainFlavour>avr-g++-9.1.0</ToolchainFlavour>
|
|
||||||
<KeepTimersRunning>true</KeepTimersRunning>
|
|
||||||
<OverrideVtor>false</OverrideVtor>
|
|
||||||
<CacheFlash>true</CacheFlash>
|
|
||||||
<ProgFlashFromRam>true</ProgFlashFromRam>
|
|
||||||
<RamSnippetAddress>0x20000000</RamSnippetAddress>
|
|
||||||
<UncachedRange />
|
|
||||||
<preserveEEPROM>true</preserveEEPROM>
|
|
||||||
<OverrideVtorValue>exception_table</OverrideVtorValue>
|
|
||||||
<BootSegment>2</BootSegment>
|
|
||||||
<ResetRule>0</ResetRule>
|
|
||||||
<eraseonlaunchrule>0</eraseonlaunchrule>
|
|
||||||
<EraseKey />
|
|
||||||
<avrtool>com.atmel.avrdbg.tool.atmelice</avrtool>
|
|
||||||
<avrtoolserialnumber>J41800099437</avrtoolserialnumber>
|
|
||||||
<avrdeviceexpectedsignature>0x1E950F</avrdeviceexpectedsignature>
|
|
||||||
<com_atmel_avrdbg_tool_stk500>
|
|
||||||
<ToolOptions>
|
|
||||||
<InterfaceProperties>
|
|
||||||
<IspClock>125000</IspClock>
|
|
||||||
</InterfaceProperties>
|
|
||||||
<InterfaceName>ISP</InterfaceName>
|
|
||||||
</ToolOptions>
|
|
||||||
<ToolType>com.atmel.avrdbg.tool.stk500</ToolType>
|
|
||||||
<ToolNumber>
|
|
||||||
</ToolNumber>
|
|
||||||
<ToolName>STK500</ToolName>
|
|
||||||
</com_atmel_avrdbg_tool_stk500>
|
|
||||||
<avrtoolinterface>ISP</avrtoolinterface>
|
|
||||||
<avrtoolinterfaceclock>125000</avrtoolinterfaceclock>
|
|
||||||
<AsfFrameworkConfig>
|
|
||||||
<framework-data xmlns="">
|
|
||||||
<options />
|
|
||||||
<configurations />
|
|
||||||
<files />
|
|
||||||
<documentation help="" />
|
|
||||||
<offline-documentation help="" />
|
|
||||||
<dependencies>
|
|
||||||
<content-extension eid="atmel.asf" uuidref="Atmel.ASF" version="3.47.0" />
|
|
||||||
</dependencies>
|
|
||||||
</framework-data>
|
|
||||||
</AsfFrameworkConfig>
|
|
||||||
<com_atmel_avrdbg_tool_atmelice>
|
|
||||||
<ToolOptions>
|
|
||||||
<InterfaceProperties>
|
|
||||||
<IspClock>125000</IspClock>
|
|
||||||
</InterfaceProperties>
|
|
||||||
<InterfaceName>ISP</InterfaceName>
|
|
||||||
</ToolOptions>
|
|
||||||
<ToolType>com.atmel.avrdbg.tool.atmelice</ToolType>
|
|
||||||
<ToolNumber>J41800099437</ToolNumber>
|
|
||||||
<ToolName>Atmel-ICE</ToolName>
|
|
||||||
</com_atmel_avrdbg_tool_atmelice>
|
|
||||||
<custom>
|
|
||||||
<ToolOptions>
|
|
||||||
<InterfaceProperties>
|
|
||||||
<IspClock>125000</IspClock>
|
|
||||||
</InterfaceProperties>
|
|
||||||
<InterfaceName>
|
|
||||||
</InterfaceName>
|
|
||||||
</ToolOptions>
|
|
||||||
<ToolType>custom</ToolType>
|
|
||||||
<ToolNumber>
|
|
||||||
</ToolNumber>
|
|
||||||
<ToolName>Custom Programming Tool</ToolName>
|
|
||||||
</custom>
|
|
||||||
<AAFDebugger>
|
|
||||||
<AAFDebugFiles>
|
|
||||||
<DebugFile>
|
|
||||||
<path>\Debug\fantemp.lss</path>
|
|
||||||
<AAFSetting>
|
|
||||||
<Label>Lss Files</Label>
|
|
||||||
<Extention>.lss</Extention>
|
|
||||||
<Regex>^\s*(?<address>[a-f0-9]*):\s*.*$</Regex>
|
|
||||||
<DebugEnabled>true</DebugEnabled>
|
|
||||||
<RegexGroups>address</RegexGroups>
|
|
||||||
<DebuggerExpression>$pc</DebuggerExpression>
|
|
||||||
</AAFSetting>
|
|
||||||
</DebugFile>
|
|
||||||
</AAFDebugFiles>
|
|
||||||
</AAFDebugger>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
|
||||||
<ToolchainSettings>
|
|
||||||
<AvrGccCpp>
|
|
||||||
<avrgcc.common.Device>-mmcu=atmega328p</avrgcc.common.Device>
|
|
||||||
<avrgcc.common.outputfiles.hex>True</avrgcc.common.outputfiles.hex>
|
|
||||||
<avrgcc.common.outputfiles.lss>True</avrgcc.common.outputfiles.lss>
|
|
||||||
<avrgcc.common.outputfiles.eep>True</avrgcc.common.outputfiles.eep>
|
|
||||||
<avrgcc.common.outputfiles.srec>True</avrgcc.common.outputfiles.srec>
|
|
||||||
<avrgcc.common.outputfiles.usersignatures>False</avrgcc.common.outputfiles.usersignatures>
|
|
||||||
<avrgcc.compiler.general.ChangeDefaultCharTypeUnsigned>True</avrgcc.compiler.general.ChangeDefaultCharTypeUnsigned>
|
|
||||||
<avrgcc.compiler.general.ChangeDefaultBitFieldUnsigned>True</avrgcc.compiler.general.ChangeDefaultBitFieldUnsigned>
|
|
||||||
<avrgcc.compiler.symbols.DefSymbols>
|
|
||||||
<ListValues>
|
|
||||||
<Value>NDEBUG</Value>
|
|
||||||
</ListValues>
|
|
||||||
</avrgcc.compiler.symbols.DefSymbols>
|
|
||||||
<avrgcc.compiler.directories.IncludePaths>
|
|
||||||
<ListValues>
|
|
||||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
|
||||||
</ListValues>
|
|
||||||
</avrgcc.compiler.directories.IncludePaths>
|
|
||||||
<avrgcc.compiler.optimization.level>Optimize for size (-Os)</avrgcc.compiler.optimization.level>
|
|
||||||
<avrgcc.compiler.optimization.AllocateBytesNeededForEnum>True</avrgcc.compiler.optimization.AllocateBytesNeededForEnum>
|
|
||||||
<avrgcc.compiler.warnings.AllWarnings>True</avrgcc.compiler.warnings.AllWarnings>
|
|
||||||
<avrgcc.compiler.warnings.ExtraWarnings>True</avrgcc.compiler.warnings.ExtraWarnings>
|
|
||||||
<avrgcc.compiler.warnings.Pedantic>True</avrgcc.compiler.warnings.Pedantic>
|
|
||||||
<avrgcc.compiler.miscellaneous.OtherFlags>-fno-threadsafe-statics -std=c11</avrgcc.compiler.miscellaneous.OtherFlags>
|
|
||||||
<avrgcccpp.compiler.general.ChangeDefaultCharTypeUnsigned>True</avrgcccpp.compiler.general.ChangeDefaultCharTypeUnsigned>
|
|
||||||
<avrgcccpp.compiler.general.ChangeDefaultBitFieldUnsigned>True</avrgcccpp.compiler.general.ChangeDefaultBitFieldUnsigned>
|
|
||||||
<avrgcccpp.compiler.symbols.DefSymbols>
|
|
||||||
<ListValues>
|
|
||||||
<Value>NDEBUG</Value>
|
|
||||||
</ListValues>
|
|
||||||
</avrgcccpp.compiler.symbols.DefSymbols>
|
|
||||||
<avrgcccpp.compiler.directories.IncludePaths>
|
|
||||||
<ListValues>
|
|
||||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
|
||||||
</ListValues>
|
|
||||||
</avrgcccpp.compiler.directories.IncludePaths>
|
|
||||||
<avrgcccpp.compiler.optimization.level>Optimize for size (-Os)</avrgcccpp.compiler.optimization.level>
|
|
||||||
<avrgcccpp.compiler.optimization.AllocateBytesNeededForEnum>True</avrgcccpp.compiler.optimization.AllocateBytesNeededForEnum>
|
|
||||||
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
|
|
||||||
<avrgcccpp.compiler.warnings.Pedantic>True</avrgcccpp.compiler.warnings.Pedantic>
|
|
||||||
<avrgcccpp.compiler.miscellaneous.OtherFlags>-fno-threadsafe-statics -Wextra -std=c++17</avrgcccpp.compiler.miscellaneous.OtherFlags>
|
|
||||||
<avrgcccpp.linker.libraries.Libraries>
|
|
||||||
<ListValues>
|
|
||||||
<Value>libm</Value>
|
|
||||||
</ListValues>
|
|
||||||
</avrgcccpp.linker.libraries.Libraries>
|
|
||||||
<avrgcccpp.assembler.general.IncludePaths>
|
|
||||||
<ListValues>
|
|
||||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
|
||||||
</ListValues>
|
|
||||||
</avrgcccpp.assembler.general.IncludePaths>
|
|
||||||
</AvrGccCpp>
|
|
||||||
</ToolchainSettings>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
|
||||||
<ToolchainSettings>
|
|
||||||
<AvrGccCpp>
|
|
||||||
<avrgcc.common.Device>-mmcu=atmega328p</avrgcc.common.Device>
|
|
||||||
<avrgcc.common.outputfiles.hex>True</avrgcc.common.outputfiles.hex>
|
|
||||||
<avrgcc.common.outputfiles.lss>True</avrgcc.common.outputfiles.lss>
|
|
||||||
<avrgcc.common.outputfiles.eep>True</avrgcc.common.outputfiles.eep>
|
|
||||||
<avrgcc.common.outputfiles.srec>True</avrgcc.common.outputfiles.srec>
|
|
||||||
<avrgcc.common.outputfiles.usersignatures>False</avrgcc.common.outputfiles.usersignatures>
|
|
||||||
<avrgcc.compiler.general.ChangeDefaultCharTypeUnsigned>True</avrgcc.compiler.general.ChangeDefaultCharTypeUnsigned>
|
|
||||||
<avrgcc.compiler.general.ChangeDefaultBitFieldUnsigned>True</avrgcc.compiler.general.ChangeDefaultBitFieldUnsigned>
|
|
||||||
<avrgcc.compiler.symbols.DefSymbols>
|
|
||||||
<ListValues>
|
|
||||||
<Value>DEBUG</Value>
|
|
||||||
</ListValues>
|
|
||||||
</avrgcc.compiler.symbols.DefSymbols>
|
|
||||||
<avrgcc.compiler.directories.IncludePaths>
|
|
||||||
<ListValues>
|
|
||||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
|
||||||
</ListValues>
|
|
||||||
</avrgcc.compiler.directories.IncludePaths>
|
|
||||||
<avrgcc.compiler.optimization.level>Optimize (-O1)</avrgcc.compiler.optimization.level>
|
|
||||||
<avrgcc.compiler.optimization.AllocateBytesNeededForEnum>True</avrgcc.compiler.optimization.AllocateBytesNeededForEnum>
|
|
||||||
<avrgcc.compiler.optimization.DebugLevel>Maximum (-g3)</avrgcc.compiler.optimization.DebugLevel>
|
|
||||||
<avrgcc.compiler.warnings.AllWarnings>True</avrgcc.compiler.warnings.AllWarnings>
|
|
||||||
<avrgcc.compiler.warnings.ExtraWarnings>True</avrgcc.compiler.warnings.ExtraWarnings>
|
|
||||||
<avrgcc.compiler.warnings.Pedantic>True</avrgcc.compiler.warnings.Pedantic>
|
|
||||||
<avrgcc.compiler.miscellaneous.OtherFlags>-fno-threadsafe-statics -std=c11</avrgcc.compiler.miscellaneous.OtherFlags>
|
|
||||||
<avrgcccpp.compiler.general.ChangeDefaultCharTypeUnsigned>True</avrgcccpp.compiler.general.ChangeDefaultCharTypeUnsigned>
|
|
||||||
<avrgcccpp.compiler.general.ChangeDefaultBitFieldUnsigned>True</avrgcccpp.compiler.general.ChangeDefaultBitFieldUnsigned>
|
|
||||||
<avrgcccpp.compiler.symbols.DefSymbols>
|
|
||||||
<ListValues>
|
|
||||||
<Value>DEBUG</Value>
|
|
||||||
</ListValues>
|
|
||||||
</avrgcccpp.compiler.symbols.DefSymbols>
|
|
||||||
<avrgcccpp.compiler.directories.IncludePaths>
|
|
||||||
<ListValues>
|
|
||||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
|
||||||
</ListValues>
|
|
||||||
</avrgcccpp.compiler.directories.IncludePaths>
|
|
||||||
<avrgcccpp.compiler.optimization.level>Optimize (-O1)</avrgcccpp.compiler.optimization.level>
|
|
||||||
<avrgcccpp.compiler.optimization.AllocateBytesNeededForEnum>True</avrgcccpp.compiler.optimization.AllocateBytesNeededForEnum>
|
|
||||||
<avrgcccpp.compiler.optimization.DebugLevel>Maximum (-g3)</avrgcccpp.compiler.optimization.DebugLevel>
|
|
||||||
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
|
|
||||||
<avrgcccpp.compiler.warnings.Pedantic>True</avrgcccpp.compiler.warnings.Pedantic>
|
|
||||||
<avrgcccpp.compiler.miscellaneous.OtherFlags>-fno-threadsafe-statics -Wextra -std=c++17</avrgcccpp.compiler.miscellaneous.OtherFlags>
|
|
||||||
<avrgcccpp.linker.libraries.Libraries>
|
|
||||||
<ListValues>
|
|
||||||
<Value>libm</Value>
|
|
||||||
</ListValues>
|
|
||||||
</avrgcccpp.linker.libraries.Libraries>
|
|
||||||
<avrgcccpp.assembler.general.IncludePaths>
|
|
||||||
<ListValues>
|
|
||||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
|
||||||
</ListValues>
|
|
||||||
</avrgcccpp.assembler.general.IncludePaths>
|
|
||||||
<avrgcccpp.assembler.debugging.DebugLevel>Default (-Wa,-g)</avrgcccpp.assembler.debugging.DebugLevel>
|
|
||||||
</AvrGccCpp>
|
|
||||||
</ToolchainSettings>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Include="adc\adc.hpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="adc\config.hpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="adc\hardware.hpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="bootloader.cpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="bootloader.hpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="clock.cpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="clock.hpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="controller.cpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="controller.hpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="eeprom\eeprom.hpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="flash\flash.hpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="io\io.hpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="main.cpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="pwm.cpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="pwm.hpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="statistics.cpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="statistics.hpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="terminal.hpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="thermistor.cpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="thermistor.hpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="type\type.hpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="uart\config.hpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="uart\hardware.hpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="uart\hardware0.hpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="uart\hardware1.hpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="uart\software.hpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="uart\uart.hpp">
|
|
||||||
<SubType>compile</SubType>
|
|
||||||
</Compile>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="flash" />
|
|
||||||
<Folder Include="io" />
|
|
||||||
<Folder Include="adc" />
|
|
||||||
<Folder Include="eeprom" />
|
|
||||||
<Folder Include="type" />
|
|
||||||
<Folder Include="uart" />
|
|
||||||
</ItemGroup>
|
|
||||||
<Import Project="$(AVRSTUDIO_EXE_PATH)\\Vs\\Compiler.targets" />
|
|
||||||
</Project>
|
|
||||||
Submodule fantemp/flash deleted from 6edb2e5a21
Submodule fantemp/io deleted from 80de36ee7e
@@ -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<uart::Config<115200>>;
|
|
||||||
Terminal<serial> terminal;
|
|
||||||
terminal.init();
|
|
||||||
|
|
||||||
Controller controller;
|
|
||||||
controller.init();
|
|
||||||
|
|
||||||
Statistics statistics;
|
|
||||||
statistics.init();
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
controller.callback();
|
|
||||||
terminal.callback();
|
|
||||||
statistics.callback();
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#include "pwm.hpp"
|
|
||||||
|
|
||||||
#include "io/io.hpp"
|
|
||||||
|
|
||||||
namespace pwm {
|
|
||||||
|
|
||||||
static constexpr uint8_t PWM_TOP = 40;
|
|
||||||
|
|
||||||
void init()
|
|
||||||
{
|
|
||||||
io::Pin<io::P::D5> 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
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
namespace pwm {
|
|
||||||
|
|
||||||
void init();
|
|
||||||
void setDuty(uint8_t percent);
|
|
||||||
|
|
||||||
} // namespace pwm
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
#include "statistics.hpp"
|
|
||||||
|
|
||||||
#include <math.h>
|
|
||||||
|
|
||||||
#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<e_temperatureHistogram, EEARRAY_SIZE(e_temperatureHistogram), true> 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<uint32_t>::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<int8_t>(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<uint8_t>(temperature);
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
@@ -1,377 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <ctype.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
#include <avr/pgmspace.h>
|
|
||||||
|
|
||||||
#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<const char *>(flashStr), size) == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline bool stringEquals(const char *str, const ::detail::FlashString *flashStr, const size_t &size)
|
|
||||||
{
|
|
||||||
if (size == strlen_P(reinterpret_cast<const char *>(flashStr))) {
|
|
||||||
return substringEquals(str, flashStr, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace detail
|
|
||||||
|
|
||||||
template <class Uart>
|
|
||||||
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<char>(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<const char *>(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<uint8_t>(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<uint8_t, 10, 3, ' '>(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<uint16_t>(uptime / 1000 / 60 / 60);
|
|
||||||
const auto minutes = static_cast<uint8_t>((uptime / 1000 / 60) % 60);
|
|
||||||
const auto seconds = static_cast<uint8_t>((uptime / 1000) % 60);
|
|
||||||
|
|
||||||
m_serial << F("System uptime: ");
|
|
||||||
m_serial.template txNumber<uint16_t, 10, 2>(hours);
|
|
||||||
m_serial << delimiter;
|
|
||||||
m_serial.template txNumber<uint8_t, 10, 2>(minutes);
|
|
||||||
m_serial << delimiter;
|
|
||||||
m_serial.template txNumber<uint8_t, 10, 2>(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<uint8_t, 10, 2>(t);
|
|
||||||
m_serial << F(" C = ");
|
|
||||||
const auto percent =
|
|
||||||
static_cast<uint8_t>((2 * 100 * histogramSamples + totalSamples) / (2 * totalSamples));
|
|
||||||
m_serial.template txNumber<uint8_t, 10, 3, ' '>(percent);
|
|
||||||
m_serial << F("%\t");
|
|
||||||
const auto normalizedSamples = static_cast<uint8_t>(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<char>(m_inputBuffer[i]);
|
|
||||||
m_serial << F("\"") << detail::ENDL;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
template <class Uart>
|
|
||||||
char Terminal<Uart>::m_inputBuffer[INPUT_BUFFER_SIZE];
|
|
||||||
|
|
||||||
template <class Uart>
|
|
||||||
uint16_t Terminal<Uart>::m_inputSize = 0;
|
|
||||||
|
|
||||||
template <class Uart>
|
|
||||||
typename Terminal<Uart>::State Terminal<Uart>::m_state = State::NONE;
|
|
||||||
|
|
||||||
template <class Uart>
|
|
||||||
uint64_t Terminal<Uart>::m_monitorDelayLastUpdate = 0;
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
#include "thermistor.hpp"
|
|
||||||
|
|
||||||
#include <math.h>
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
Submodule fantemp/type deleted from ce31ef017f
Submodule fantemp/uart deleted from 04b6782ec4
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
|
||||||
48
src/curve.hpp
Normal file
48
src/curve.hpp
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include <libavr/flash.hpp>
|
||||||
|
|
||||||
|
// The auto-mode fan curve, tabulated at compile time: a cubic in °C
|
||||||
|
// (0.002246·x³ − 0.09·x² + 0.91·x, zero below 20 °C) as a flash_table 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline constexpr avr::flash_table<[] {
|
||||||
|
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;
|
||||||
|
}()>
|
||||||
|
table;
|
||||||
|
|
||||||
|
} // 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 detail::table[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
|
||||||
73
src/thermistor.hpp
Normal file
73
src/thermistor.hpp
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include <libavr/flash.hpp>
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
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 {
|
||||||
|
|
||||||
|
consteval double temperature_of(double adc)
|
||||||
|
{
|
||||||
|
double resistance = series_resistor * adc / (1023.0 - 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline constexpr avr::flash_table<[] {
|
||||||
|
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;
|
||||||
|
}()>
|
||||||
|
table;
|
||||||
|
|
||||||
|
} // 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::table[index];
|
||||||
|
auto b = detail::table[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