Compare commits

...

8 Commits

Author SHA1 Message Date
949dc125cd build: the libavr pin advances to current main
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 07:24:10 +02:00
ba2cae8f8f build: the libavr pin advances to current main
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 06:48:36 +02:00
5afe29359c build: libavr rides as the pinned submodule
The submodule replaces FetchContent and the unpinned forge fallback;
LIBAVR_ROOT stays as the tandem-development override, the presets take the
toolchain file from the submodule, and the Studio project's include path
anchors there — correct by construction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 00:29:38 +02:00
2cdf56f8d6 ide: the whole project in Solution Explorer, and a path Studio can resolve
Three things the solution got wrong, all found by opening it in the GUI rather
than building it headlessly.

The project listed only main.cpp, so none of the headers could be opened from
Solution Explorer. Every source and header is listed now, with <Link> mirroring
the on-disk src/ layout, which is what master's project did.

Neither configuration compiled in the GUI: libavr was found through
$(LIBAVR_ROOT), and a variable exported in a shell is not visible to Studio
launched from the Start menu. Release only looked healthy because its objects
were already up to date from a headless build. The path is now anchored to the
project directory, which also side-steps a second trap: a plain relative include
is resolved against the generated makefile's own directory, the configuration's
output directory, not the project's. Pinning libavr as a submodule would remove
the assumption that the two sit side by side, and is on libavr's task list.

Both configurations verified with LIBAVR_ROOT deliberately unset, Release still
byte-identical to the CMake build and the flag gate still green. Debug's own
translation unit carries DWARF-4 as intended.

Studio's per-user state under ide/.vs/ and the build logs are ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 19:19:20 +02:00
00849d25d5 ide: the Atmel Studio solution master has, on the libavr port
master opens in Studio, so this branch should too. One project, the port's own
chip, flags mirrored by hand against the CMake build — and the acceptance is
not that it builds but that it builds the same firmware: .text (7094 B) and
.data (336 B) come out byte-identical to the CMake output from the same
sources.

libavr is found through $(LIBAVR_ROOT), the variable the CMake build already
uses, so no machine path is committed; Studio expands it from the environment.
The toolchain is named by flavour only, since nothing Studio ships can compile
-std=c++26 and the path to one that can is per-machine state.

The componentinfo file the project cannot load without is generated by
libavr's tools/atmelstudio/componentinfo.py and ignored here, as are Studio's
output directories. Release is what the flag gate compares, the presets
defining no debug build; Debug carries the -Og -gdwarf-4 pair libavr's own
debug preset uses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 18:37:56 +02:00
b7a2e50506 Keep the curve and thermistor comments timeless
Both headers described the port transformation ("legacy cubic becomes",
"no hand-rolled series needed") rather than what the code is. State the
cubic and the __builtin_log rationale directly.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-18 10:10:47 +02:00
f17ebd17e5 Use libavr flash_table and __builtin_log
The thermistor Beta curve and the fan cubic move onto avr::flash_table
instead of hand-rolled [[gnu::progmem]] arrays with raw pgm_read, and
the compile-time logarithm uses __builtin_log (which constant-folds on
the AVR backend) instead of a hand-rolled series. Same 11284 B, still
byte-identical across libavr modes.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-18 08:03:42 +02:00
ab78d94872 Rewrite on libavr
Same controller: thermistor on ADC0 averaged over 1000 free-running
conversions, 50 kHz fan PWM on OC0B, 115200 Bd console with the full
command set, EEPROM temperature histogram, watchdog-reset path into the
boot section. The Steinhart-Hart math and the libm log are gone — the
Beta equation and the cubic fan curve are consteval-evaluated into
flash tables; the firmware never does floating point. Byte-identical
.text in both libavr modes. Legacy stays on master.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-18 04:37:26 +02:00
37 changed files with 898 additions and 1220 deletions

20
.gitignore vendored
View File

@@ -1,11 +1,9 @@
.vs
Release
Debug
*.componentinfo.xml
*.elf
*.o
*.hex
*.srec
*.eeprom
*.lss
*.map
build/
.cache/
# Atmel Studio: generated per machine, and its build outputs
ide/*.componentinfo.xml
ide/Debug/
ide/Release/
ide/.vs/
ide/*.log

21
.gitmodules vendored
View File

@@ -1,18 +1,3 @@
[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
[submodule "libavr"]
path = libavr
url = ../libavr.git

21
CMakeLists.txt Normal file
View File

@@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 3.28)
project(fantemp LANGUAGES CXX)
# libavr rides as the pinned submodule; LIBAVR_ROOT (cache or environment)
# overrides it for tandem development against a working tree. The toolchain
# file comes from the submodule via CMakePresets.json either way.
if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT})
set(LIBAVR_ROOT $ENV{LIBAVR_ROOT})
endif()
if(NOT LIBAVR_ROOT)
set(LIBAVR_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/libavr)
endif()
if(NOT EXISTS ${LIBAVR_ROOT}/CMakeLists.txt)
message(FATAL_ERROR "libavr not found at ${LIBAVR_ROOT} — run: git submodule update --init libavr")
endif()
add_subdirectory(${LIBAVR_ROOT} libavr-build)
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
View File

@@ -0,0 +1,43 @@
{
"version": 8,
"configurePresets": [
{
"name": "base",
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"toolchainFile": "${sourceDir}/libavr/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"
}
]
}

58
README.md Normal file
View File

@@ -0,0 +1,58 @@
# 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 SteinhartHart 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.
libavr rides as the `libavr/` submodule, pinned to the commit this firmware
builds against; `LIBAVR_ROOT` (cache or environment) overrides it for
development against a working tree:
```sh
git submodule update --init libavr
cmake --preset atmega328p-generated
cmake --build --preset atmega328p-generated
```
## Atmel Studio
`master` carries a Studio solution, so this branch does too: `ide/fantemp.atsln`
builds the same firmware — byte-identical `.text` and `.data` to the CMake
build — from the same sources, with the flags mirrored by hand.
Studio finds libavr in the **submodule**, at
`$(MSBuildProjectDirectory)\..\libavr\include` — correct by construction, and
anchored to the project rather than written relative to the generated makefile,
which runs from the configuration's output directory and would need a different
number of `..`. Unlike the CMake build there is no `LIBAVR_ROOT` to point
elsewhere: an environment variable set in a shell is not visible to Studio
launched from the Start menu — which is what the submodule answers.
It also needs a GCC 16.1 toolchain registered as flavour `avr-g++-16.1.0`;
nothing older can compile `-std=c++26`.
One generated file is required before the project will load, and one command
checks the flags have not drifted (both from libavr's `tools/atmelstudio/`):
```sh
python ../libavr/tools/atmelstudio/componentinfo.py \
ide/fantemp.componentinfo.xml --device ATmega328P
python ../libavr/tools/atmelstudio/check-flags.py --solution ide/fantemp.atsln \
--compile-commands build/atmega328p-generated/compile_commands.json \
--log build/atmelstudio.log
```
CMake remains the build system; the solution is there so the project opens in
Studio as its predecessor did. Only the Release configuration is gated against
CMake — the presets define no debug build — and Debug carries the `-Og
-gdwarf-4` pair libavr's own debug preset uses.
Legacy (yazoalfa submodules) stays on `master`.

Submodule fantemp/adc deleted from 5e9dac872a

View File

@@ -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();
}

View File

@@ -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();
};

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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*(?&lt;address&gt;[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

View File

@@ -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;
}

View File

@@ -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

View File

@@ -1,10 +0,0 @@
#pragma once
#include <stdint.h>
namespace pwm {
void init();
void setDuty(uint8_t percent);
} // namespace pwm

View File

@@ -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);
}

View File

@@ -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);
};

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -3,7 +3,7 @@ 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}"
Project("{E66E83B9-2572-4076-B26E-6BE79FF3018A}") = "fantemp", "fantemp.cppproj", "{4F0C1D92-6A5B-4E33-9A71-2C8F5B0D47AE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -11,10 +11,10 @@ Global
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
{4F0C1D92-6A5B-4E33-9A71-2C8F5B0D47AE}.Debug|AVR.ActiveCfg = Debug|AVR
{4F0C1D92-6A5B-4E33-9A71-2C8F5B0D47AE}.Debug|AVR.Build.0 = Debug|AVR
{4F0C1D92-6A5B-4E33-9A71-2C8F5B0D47AE}.Release|AVR.ActiveCfg = Release|AVR
{4F0C1D92-6A5B-4E33-9A71-2C8F5B0D47AE}.Release|AVR.Build.0 = Release|AVR
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

140
ide/fantemp.cppproj Normal file
View File

@@ -0,0 +1,140 @@
<?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>4f0c1d92-6a5b-4e33-9a71-2c8f5b0d47ae</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++-16.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 />
<AsfFrameworkConfig>
<framework-data xmlns="">
<options />
<configurations />
<files />
<documentation help="" />
<offline-documentation help="" />
<dependencies>
<content-extension eid="atmel.asf" uuidref="Atmel.ASF" version="3.52.0" />
</dependencies>
</framework-data>
</AsfFrameworkConfig>
</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>
<avrgcccpp.compiler.symbols.DefSymbols>
<ListValues>
<Value>NDEBUG</Value>
</ListValues>
</avrgcccpp.compiler.symbols.DefSymbols>
<avrgcccpp.compiler.directories.IncludePaths>
<ListValues>
<Value>$(MSBuildProjectDirectory)\..\libavr\include</Value>
</ListValues>
</avrgcccpp.compiler.directories.IncludePaths>
<avrgcccpp.compiler.optimization.level>Optimize for size (-Os)</avrgcccpp.compiler.optimization.level>
<avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>
<avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
<avrgcccpp.compiler.miscellaneous.OtherFlags>-std=c++26 -Wextra -mrelax -fno-exceptions -fno-rtti -fno-threadsafe-statics</avrgcccpp.compiler.miscellaneous.OtherFlags>
<avrgcccpp.linker.optimization.GarbageCollectUnusedSections>True</avrgcccpp.linker.optimization.GarbageCollectUnusedSections>
<avrgcccpp.linker.miscellaneous.LinkerFlags>-mrelax</avrgcccpp.linker.miscellaneous.LinkerFlags>
</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>
<avrgcccpp.compiler.symbols.DefSymbols>
<ListValues>
<Value>DEBUG</Value>
</ListValues>
</avrgcccpp.compiler.symbols.DefSymbols>
<avrgcccpp.compiler.directories.IncludePaths>
<ListValues>
<Value>$(MSBuildProjectDirectory)\..\libavr\include</Value>
</ListValues>
</avrgcccpp.compiler.directories.IncludePaths>
<avrgcccpp.compiler.optimization.level>Optimize debugging experience (-Og)</avrgcccpp.compiler.optimization.level>
<avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>
<avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
<avrgcccpp.compiler.miscellaneous.OtherFlags>-std=c++26 -Wextra -mrelax -fno-exceptions -fno-rtti -fno-threadsafe-statics -gdwarf-4</avrgcccpp.compiler.miscellaneous.OtherFlags>
<avrgcccpp.linker.optimization.GarbageCollectUnusedSections>True</avrgcccpp.linker.optimization.GarbageCollectUnusedSections>
<avrgcccpp.linker.miscellaneous.LinkerFlags>-mrelax</avrgcccpp.linker.miscellaneous.LinkerFlags>
</AvrGccCpp>
</ToolchainSettings>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\src\board.hpp">
<SubType>compile</SubType>
<Link>src\board.hpp</Link>
</Compile>
<Compile Include="..\src\bootloader.hpp">
<SubType>compile</SubType>
<Link>src\bootloader.hpp</Link>
</Compile>
<Compile Include="..\src\controller.hpp">
<SubType>compile</SubType>
<Link>src\controller.hpp</Link>
</Compile>
<Compile Include="..\src\curve.hpp">
<SubType>compile</SubType>
<Link>src\curve.hpp</Link>
</Compile>
<Compile Include="..\src\main.cpp">
<SubType>compile</SubType>
<Link>src\main.cpp</Link>
</Compile>
<Compile Include="..\src\statistics.hpp">
<SubType>compile</SubType>
<Link>src\statistics.hpp</Link>
</Compile>
<Compile Include="..\src\terminal.hpp">
<SubType>compile</SubType>
<Link>src\terminal.hpp</Link>
</Compile>
<Compile Include="..\src\thermistor.hpp">
<SubType>compile</SubType>
<Link>src\thermistor.hpp</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<Folder Include="src" />
</ItemGroup>
<Import Project="$(AVRSTUDIO_EXE_PATH)\Vs\Compiler.targets" />
</Project>

1
libavr Submodule

Submodule libavr added at f5d71225a2

68
src/board.hpp Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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