Compare commits
42
Commits
master
...
b493426298
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b493426298 | ||
|
|
f8e136e8ad | ||
|
|
51535953b9 | ||
|
|
999d087eb4 | ||
|
|
c58ff23e90 | ||
|
|
e44b45dd1e | ||
|
|
78c9071f0c | ||
|
|
d6673db131 | ||
|
|
fb82f0f7a9 | ||
|
|
88df1c1ada | ||
|
|
fc65457de3 | ||
|
|
11da387ffa | ||
|
|
491ff76447 | ||
|
|
e56e8b977f | ||
|
|
8f43584c5c | ||
|
|
996453c2a1 | ||
|
|
0f5e40510c | ||
|
|
bf18f99635 | ||
|
|
5244953654 | ||
|
|
cde7e60170 | ||
|
|
4c351c6c9d | ||
|
|
5974a5aa90 | ||
|
|
ba37ecea04 | ||
|
|
0f99b7787c | ||
|
|
3e95160b00 | ||
|
|
837b832bc7 | ||
|
|
1489f4c3a0 | ||
|
|
b1caf49522 | ||
|
|
0e9060db69 | ||
|
|
3fbbcdade9 | ||
|
|
6abf0b5563 | ||
|
|
c01e583597 | ||
|
|
e5a9a38bba | ||
|
|
d77fe8ea9e | ||
|
|
949dc125cd | ||
|
|
ba2cae8f8f | ||
|
|
5afe29359c | ||
|
|
2cdf56f8d6 | ||
|
|
00849d25d5 | ||
|
|
b7a2e50506 | ||
|
|
f17ebd17e5 | ||
|
|
ab78d94872 |
No files matched your search
+3
-1
@@ -1,13 +1,15 @@
|
|||||||
---
|
---
|
||||||
BasedOnStyle: LLVM
|
BasedOnStyle: LLVM
|
||||||
|
Standard: Latest
|
||||||
ColumnLimit: 120
|
ColumnLimit: 120
|
||||||
IndentWidth: 4
|
IndentWidth: 4
|
||||||
TabWidth: 4
|
TabWidth: 4
|
||||||
UseTab: ForIndentation
|
UseTab: ForIndentation
|
||||||
AlignEscapedNewlines: DontAlign
|
AlignEscapedNewlines: DontAlign
|
||||||
AllowShortFunctionsOnASingleLine: Empty
|
AllowShortFunctionsOnASingleLine: Empty
|
||||||
AlwaysBreakTemplateDeclarations: true
|
BreakTemplateDeclarations: Yes
|
||||||
BreakBeforeBraces: Custom
|
BreakBeforeBraces: Custom
|
||||||
BraceWrapping:
|
BraceWrapping:
|
||||||
AfterFunction: true
|
AfterFunction: true
|
||||||
|
InsertBraces: true
|
||||||
...
|
...
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
CompileFlags:
|
||||||
|
# Named here rather than in the editor's settings because nothing vendors
|
||||||
|
# this repo: the one build tree is the one an editor should read.
|
||||||
|
CompilationDatabase: build/atmega328p-generated
|
||||||
|
Add:
|
||||||
|
# clang has no 24-bit integer and GCC's are keywords, not macros, so the
|
||||||
|
# editor needs a stand-in for avr::uint24_t. The next width up is the only
|
||||||
|
# one available - clang rejects _BitInt(24) on this target.
|
||||||
|
- -D__uint24=unsigned long
|
||||||
|
- -D__int24=long
|
||||||
|
# clangd forwards the driver's system includes but not its own header
|
||||||
|
# directory, so <stdint.h> resolves to avr-libc's, which still gates the
|
||||||
|
# limit and constant macros on the C++98 opt-in.
|
||||||
|
- -D__STDC_LIMIT_MACROS
|
||||||
|
- -D__STDC_CONSTANT_MACROS
|
||||||
|
# isr::emit spells a vector number into [[gnu::signal(N)]], which clang
|
||||||
|
# rejects rather than ignores - enough of them in one TU to reach the
|
||||||
|
# default limit of 19 inside the headers and truncate the parse.
|
||||||
|
- -ferror-limit=0
|
||||||
|
Remove:
|
||||||
|
# The build promotes warnings for the compiler that has to be right about
|
||||||
|
# them; in the editor the flag paints a second frontend's opinions in the
|
||||||
|
# colour reserved for things that do not compile.
|
||||||
|
- -Werror
|
||||||
|
Diagnostics:
|
||||||
|
Suppress:
|
||||||
|
# clang's AVR `signal` attribute takes no arguments and it knows none of
|
||||||
|
# progmem, naked or OS_main. A misspelling is what the build is for.
|
||||||
|
- attribute_wrong_number_arguments
|
||||||
|
- unknown-attributes
|
||||||
+5
-7
@@ -1,9 +1,7 @@
|
|||||||
*.h eol=lf
|
# Line endings are the repository's, not the editing tool's: a file rewritten
|
||||||
*.hpp eol=lf
|
# by a Windows tool must not come back with every line changed.
|
||||||
*.c eol=lf
|
* text=auto eol=lf
|
||||||
*.cpp eol=lf
|
|
||||||
.git* eol=lf
|
# Atmel Studio writes these and expects them back.
|
||||||
*.vcxproj* eol=crlf
|
|
||||||
*.cppproj eol=crlf
|
*.cppproj eol=crlf
|
||||||
*.sln eol=crlf
|
|
||||||
*.atsln eol=crlf
|
*.atsln eol=crlf
|
||||||
+10
-11
@@ -1,11 +1,10 @@
|
|||||||
.vs
|
build/
|
||||||
Release
|
local/
|
||||||
Debug
|
.cache/
|
||||||
*.componentinfo.xml
|
|
||||||
*.elf
|
# Atmel Studio: generated per machine, and its build outputs
|
||||||
*.o
|
ide/*.componentinfo.xml
|
||||||
*.hex
|
ide/Debug/
|
||||||
*.srec
|
ide/Release/
|
||||||
*.eeprom
|
ide/.vs/
|
||||||
*.lss
|
ide/*.log
|
||||||
*.map
|
|
||||||
+6
-18
@@ -1,18 +1,6 @@
|
|||||||
[submodule "fantemp/uart"]
|
[submodule "libavr"]
|
||||||
path = fantemp/uart
|
path = libavr
|
||||||
url = git@git.blackmark.me:avr/uart.git
|
url = ../libavr.git
|
||||||
[submodule "fantemp/flash"]
|
[submodule "pureboot"]
|
||||||
path = fantemp/flash
|
path = pureboot
|
||||||
url = git@git.blackmark.me:avr/flash.git
|
url = ../pureboot.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
|
|
||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"llvm-vs-code-extensions.vscode-clangd",
|
||||||
|
"ms-vscode.cmake-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+33
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
// clangd is the language server; the cpptools engine would parse every file
|
||||||
|
// a second time and disagree, since nothing tells it about a cross
|
||||||
|
// compiler. Which database to read is in .clangd.
|
||||||
|
"C_Cpp.intelliSenseEngine": "disabled",
|
||||||
|
|
||||||
|
// --query-driver lets clangd ask the cross compiler for its own system
|
||||||
|
// includes and target; without it every standard header is missing.
|
||||||
|
"clangd.arguments": [
|
||||||
|
"--query-driver=**avr-g++*",
|
||||||
|
"--header-insertion=never"
|
||||||
|
],
|
||||||
|
|
||||||
|
// The presets are the build interface, and the toolchain file inside the
|
||||||
|
// libavr submodule is the one place the compiler is chosen. **No prefix is
|
||||||
|
// named here**: a committed file may not name a path that is true of one
|
||||||
|
// machine (libavr guidance rule 50), so the gitignored local/machine.cmake
|
||||||
|
// at this repository's root is where a checkout says where its toolchain
|
||||||
|
// is - one file, and it answers for both hosts.
|
||||||
|
"cmake.useCMakePresets": "always",
|
||||||
|
"cmake.configureOnOpen": true,
|
||||||
|
"cmake.options.statusBarVisibility": "compact",
|
||||||
|
|
||||||
|
"files.watcherExclude": {
|
||||||
|
"**/build/**": true,
|
||||||
|
"**/libavr/**": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"files.associations": {
|
||||||
|
".clangd": "yaml",
|
||||||
|
".clang-format": "yaml"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
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()
|
||||||
|
# pureboot rides as a pinned submodule too: this board's only way in is its
|
||||||
|
# resident loader, so the commit that names this firmware names the loader it
|
||||||
|
# has to hand over to. Consumed for the geometry it exports - the loader links
|
||||||
|
# the libavr target above, so pureboot's own libavr submodule stays
|
||||||
|
# uninitialised.
|
||||||
|
if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/CMakeLists.txt)
|
||||||
|
message(FATAL_ERROR "pureboot not found - run: git submodule update --init pureboot")
|
||||||
|
endif()
|
||||||
|
add_subdirectory(${LIBAVR_ROOT} libavr-build)
|
||||||
|
add_subdirectory(pureboot pureboot-build)
|
||||||
|
include(${LIBAVR_ROOT}/cmake/checks.cmake)
|
||||||
|
|
||||||
|
add_executable(fantemp src/main.cpp)
|
||||||
|
target_link_libraries(fantemp PRIVATE libavr)
|
||||||
|
# The raw image is what the loader takes, and what the reachability check
|
||||||
|
# measures the boot-section clearance against.
|
||||||
|
add_custom_command(TARGET fantemp POST_BUILD
|
||||||
|
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:fantemp>
|
||||||
|
COMMAND ${CMAKE_OBJCOPY} -O binary -R .eeprom
|
||||||
|
$<TARGET_FILE:fantemp> $<TARGET_FILE_DIR:fantemp>/fantemp.bin
|
||||||
|
COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom
|
||||||
|
$<TARGET_FILE:fantemp> $<TARGET_FILE_DIR:fantemp>/fantemp.hex)
|
||||||
|
|
||||||
|
# The board's loader, built from the pin, so the one image this deployment
|
||||||
|
# cannot be recovered without is rebuilt by the commit that names it. Every
|
||||||
|
# parameter is pureboot's own default for this chip - USART0 at 115200 on the
|
||||||
|
# 16 MHz crystal `src/board.hpp` declares. It goes on over the wire
|
||||||
|
# (`--update-loader`), which is why there is no flash target beside it: the
|
||||||
|
# board has no programming header.
|
||||||
|
pureboot_add_loader(pureboot)
|
||||||
|
|
||||||
|
enable_testing()
|
||||||
|
add_subdirectory(test)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"testPresets": [
|
||||||
|
{
|
||||||
|
"name": "atmega328p-generated",
|
||||||
|
"configurePreset": "atmega328p-generated",
|
||||||
|
"output": {
|
||||||
|
"outputOnFailure": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "atmega328p-reflect",
|
||||||
|
"configurePreset": "atmega328p-reflect",
|
||||||
|
"output": {
|
||||||
|
"outputOnFailure": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# fantemp
|
||||||
|
|
||||||
|
**v2.2.** 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, and a direct jump into a boot-section
|
||||||
|
bootloader at `0x7e00`.
|
||||||
|
|
||||||
|
The EEPROM format is the legacy firmware's, unchanged: 100 little-endian
|
||||||
|
`uint32` buckets at address 0, one per °C. A board carrying years of history
|
||||||
|
from FanTemp 1.8b keeps every count — verified on hardware, all 67 non-empty
|
||||||
|
buckets byte-identical across the conversion.
|
||||||
|
|
||||||
|
## The console
|
||||||
|
|
||||||
|
Commands may be abbreviated to any unambiguous-by-order prefix, as the legacy
|
||||||
|
firmware allowed: `up` is `uptime`, `st` is `statistics`, `sa` is `save`. The
|
||||||
|
table order resolves ties, so `s` is `show` — and `reset` is deliberately the one
|
||||||
|
command that cannot be abbreviated, because `r` should not be able to wipe the
|
||||||
|
histogram. `save` forces a writeback, which otherwise happens every 30
|
||||||
|
minutes and on the way into the bootloader.
|
||||||
|
|
||||||
|
`show`, `statistics` and the histogram print one value per line behind a dotted
|
||||||
|
label, the way the original did — a run-on line is fine for one reading and
|
||||||
|
unreadable when `monitor` emits one a second. `curve` walks every whole degree
|
||||||
|
from 10 to 60 with a bar, because the curve is a cubic and five-degree samples
|
||||||
|
without a graph show none of its shape.
|
||||||
|
|
||||||
|
**Ctrl+C** abandons a half-typed line and gives a fresh prompt, echoing `^C`, and
|
||||||
|
it is what stops `monitor`. Stopping on *any* byte would not do: a host's line
|
||||||
|
ending would stop `monitor\r\n` on the `\n` it arrived with, one reading in.
|
||||||
|
|
||||||
|
## Reaching the bootloader
|
||||||
|
|
||||||
|
`bootloader` **jumps**; it does not reset. That is not a style choice:
|
||||||
|
|
||||||
|
- **pureboot hands straight back on WDRF**, by design — an unattended board that
|
||||||
|
watchdog-resets in a loop must not sit in a loader. So the legacy
|
||||||
|
watchdog-reset hand-over arrives and opens no window at all, and on a board
|
||||||
|
with no reset line that is a board that cannot be reflashed.
|
||||||
|
- The address is `0x7e00`, the top 512 bytes. The legacy firmware used `0x7800`,
|
||||||
|
a 2 KB boot section's base, which on a board with a 512-byte boot section reads
|
||||||
|
erased — so its `bootloader` command silently never arrived anywhere.
|
||||||
|
- `UCSR0B` is cleared first. While `TXEN0` is set the USART owns PD1, so a loader
|
||||||
|
that bit-bangs the same pin receives perfectly and answers into nothing.
|
||||||
|
|
||||||
|
`ctest` reads all three back out of the emitted image (`test/check_reachability.py`),
|
||||||
|
with the image's clearance below the boot section, because none of them is
|
||||||
|
visible from the source alone and the failure mode is an unreflashable board.
|
||||||
|
Each is red-proven: the address and the watchdog checks against the legacy
|
||||||
|
behaviour they exist to catch, the USART check against the write removed.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
The firmware is **7836 B** of flash, byte-identical between the generated and
|
||||||
|
reflect modes, and `ctest` holds it to that number.
|
||||||
|
|
||||||
|
## 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`.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Tasks
|
||||||
|
|
||||||
|
This file is current work. The repo is the sole task tracker (libavr guidance
|
||||||
|
rule 20).
|
||||||
|
|
||||||
|
## Open
|
||||||
|
|
||||||
|
Nothing.
|
||||||
|
|
||||||
|
## Decided against
|
||||||
|
|
||||||
|
Both of these are measured, and both are recorded here rather than deleted so
|
||||||
|
that the next person to measure them does not read a number as an opportunity
|
||||||
|
and re-propose work the owner has already refused.
|
||||||
|
|
||||||
|
### The millisecond clock stays 64 bits
|
||||||
|
|
||||||
|
Narrowing `uptime::millis()` and the three timestamps that hold its value from
|
||||||
|
`uint64_t` to `uint32_t` is **602 B of flash, 7.7 % of the image** (7838 B down
|
||||||
|
to 7236), plus 16 B of RAM.
|
||||||
|
|
||||||
|
**Refused, owner-stated: this board runs continuously and an uptime that
|
||||||
|
restarts every 49.7 days is not acceptable.** That is what a 32-bit
|
||||||
|
millisecond counter wraps at, and the display is not the only cost - the
|
||||||
|
interval tests would have to become subtractions, since `now - last >=
|
||||||
|
interval` survives a wrap where `now >= last + interval` does not, and
|
||||||
|
`terminal.hpp`'s monitor tick is written the second way. The 64-bit counter is
|
||||||
|
what puts the wrap out of reach, which is the property being bought.
|
||||||
|
|
||||||
|
### The 1 kHz tick keeps its 64-bit increment
|
||||||
|
|
||||||
|
The compare handler increments 64 bits, so it calls libgcc's `__adddi3_s8`, and
|
||||||
|
a call inside a signal handler decides the prologue - twelve push/pop pairs for
|
||||||
|
what the helper might clobber. Splitting the counter into two 32-bit halves
|
||||||
|
removes the call and keeps the full range, taking the handler from ~47
|
||||||
|
instructions to ~26 with the carry running once every 49.7 days.
|
||||||
|
|
||||||
|
**Refused: it costs 66 B of flash to buy about 0.4 % of the CPU**, and nothing
|
||||||
|
here is timing-critical. It was only ever worth considering alongside the
|
||||||
|
narrowing above, which is refused outright.
|
||||||
-1
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.
-1
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;
|
|
||||||
};
|
|
||||||
-1
Submodule fantemp/type deleted from ce31ef017f.
-1
Submodule fantemp/uart deleted from 04b6782ec4.
@@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
|||||||
# Atmel Studio Solution File, Format Version 11.00
|
# Atmel Studio Solution File, Format Version 11.00
|
||||||
VisualStudioVersion = 14.0.23107.0
|
VisualStudioVersion = 14.0.23107.0
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
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
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -11,10 +11,10 @@ Global
|
|||||||
Release|AVR = Release|AVR
|
Release|AVR = Release|AVR
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{DCE6C7E3-EE26-4D79-826B-08594B9AD897}.Debug|AVR.ActiveCfg = Debug|AVR
|
{4F0C1D92-6A5B-4E33-9A71-2C8F5B0D47AE}.Debug|AVR.ActiveCfg = Debug|AVR
|
||||||
{DCE6C7E3-EE26-4D79-826B-08594B9AD897}.Debug|AVR.Build.0 = Debug|AVR
|
{4F0C1D92-6A5B-4E33-9A71-2C8F5B0D47AE}.Debug|AVR.Build.0 = Debug|AVR
|
||||||
{DCE6C7E3-EE26-4D79-826B-08594B9AD897}.Release|AVR.ActiveCfg = Release|AVR
|
{4F0C1D92-6A5B-4E33-9A71-2C8F5B0D47AE}.Release|AVR.ActiveCfg = Release|AVR
|
||||||
{DCE6C7E3-EE26-4D79-826B-08594B9AD897}.Release|AVR.Build.0 = Release|AVR
|
{4F0C1D92-6A5B-4E33-9A71-2C8F5B0D47AE}.Release|AVR.Build.0 = Release|AVR
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -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>
|
||||||
Submodule
+1
Submodule libavr added at 73fde9c243.
Submodule
+1
Submodule pureboot added at 9d6b81c024.
@@ -0,0 +1,79 @@
|
|||||||
|
#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 m_ms = 0;
|
||||||
|
|
||||||
|
public:
|
||||||
|
using ticker = dev::timer2<{.frequency = 1_kHz, .on_compare = [] { m_ms = m_ms + 1; }}>;
|
||||||
|
|
||||||
|
static std::uint64_t millis()
|
||||||
|
{
|
||||||
|
avr::irq::interrupt_guard lock;
|
||||||
|
return m_ms;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1000-sample averaging window fed by the conversion interrupt.
|
||||||
|
class sampler {
|
||||||
|
static inline volatile std::uint32_t m_sum = 0;
|
||||||
|
static inline volatile std::uint16_t m_count = 0;
|
||||||
|
static inline volatile std::uint16_t m_window = 0;
|
||||||
|
static inline volatile bool m_ready = false;
|
||||||
|
|
||||||
|
static constexpr std::uint16_t samples = 1000;
|
||||||
|
|
||||||
|
public:
|
||||||
|
using input = dev::adc<{.trigger = avr::adc::trigger::free_running,
|
||||||
|
.on_conversion =
|
||||||
|
[](std::uint16_t value) {
|
||||||
|
m_sum = m_sum + value;
|
||||||
|
m_count = m_count + 1;
|
||||||
|
if (m_count >= samples) {
|
||||||
|
m_window = static_cast<std::uint16_t>(m_sum / samples);
|
||||||
|
m_sum = 0;
|
||||||
|
m_count = 0;
|
||||||
|
m_ready = true;
|
||||||
|
}
|
||||||
|
}},
|
||||||
|
avr::adc::input<avr::adc::input_pin(0)>>;
|
||||||
|
// The bound input, whose start() is free-running's one kick.
|
||||||
|
using thermistor = input::in<avr::adc::input_pin(0)>;
|
||||||
|
|
||||||
|
// The finished average (raw 10-bit), once per window.
|
||||||
|
static bool take(std::uint16_t &value)
|
||||||
|
{
|
||||||
|
avr::irq::interrupt_guard lock;
|
||||||
|
if (!m_ready) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
value = m_window;
|
||||||
|
m_ready = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using fan = dev::pwm<avr::pd5, {.frequency = 50_kHz}>;
|
||||||
|
|
||||||
|
// 115200 at 16 MHz lands +2.1 % off, past the receiver-tolerance table the
|
||||||
|
// solver holds rates to. It is the rate the console on the other end of the
|
||||||
|
// cable expects, so the override states that the miss is meant.
|
||||||
|
using serial_t = dev::uart0<{
|
||||||
|
.baud = 115200_Bd,
|
||||||
|
.rx_buffer = 32,
|
||||||
|
.allow_baud_error = true,
|
||||||
|
}>;
|
||||||
|
inline constexpr serial_t serial{};
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
|
#include "board.hpp"
|
||||||
|
|
||||||
|
// Reaching the resident bootloader from the console. pureboot hands straight
|
||||||
|
// back to the application on WDRF - an unattended board that watchdog-resets in
|
||||||
|
// a loop must not sit in a loader instead of running - so a reset-based route
|
||||||
|
// opens no window at all, and on a board whose only way in is the firmware that
|
||||||
|
// is a lockout. This route jumps instead, with the reset flags already clear,
|
||||||
|
// so the loader starts as if from a clean power-on and opens its window.
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
class bootloader {
|
||||||
|
using jump_fn = void (*)();
|
||||||
|
|
||||||
|
using guard = dev::watchdog<{.timeout = 16_ms}>;
|
||||||
|
|
||||||
|
// The top 512 bytes: `hfuse d4` puts the boot section at 0x7c00, with
|
||||||
|
// pureboot's staging slot below its own slot.
|
||||||
|
static constexpr std::uint16_t base = 0x7e00;
|
||||||
|
|
||||||
|
// A function pointer holds a word address on AVR, so the byte address
|
||||||
|
// halves. [[gnu::noipa]] keeps the call indirect: a constant target folds
|
||||||
|
// into a relative call that cannot reach across flash.
|
||||||
|
[[gnu::noipa, noreturn]] static void call(jump_fn target)
|
||||||
|
{
|
||||||
|
target();
|
||||||
|
__builtin_unreachable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Call first thing in main. Clearing MCUSR is the whole job: a lingering
|
||||||
|
// WDRF forces the watchdog back on at its shortest timeout, which is a
|
||||||
|
// reset loop rather than a boot. The cause is not routed on - pureboot
|
||||||
|
// peeks WDRF without clearing it and hands back on purpose, so a watchdog
|
||||||
|
// reset arrives here with the flag still set, and diverting into the
|
||||||
|
// loader on it would reopen the very window that policy closes.
|
||||||
|
static void handle_reset()
|
||||||
|
{
|
||||||
|
avr::power::consume_reset_cause();
|
||||||
|
guard::disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hand over for real: no reset, so no WDRF for the loader to refuse.
|
||||||
|
[[noreturn]] static void enter()
|
||||||
|
{
|
||||||
|
// Interrupts first - the receive vector and the timer live in this
|
||||||
|
// application's vector table, and once the loader is running there is no
|
||||||
|
// application to vector into.
|
||||||
|
avr::irq::disable();
|
||||||
|
guard::disable();
|
||||||
|
|
||||||
|
// Release the USART. While TXEN0 is set the peripheral owns PD1, not the
|
||||||
|
// port register, so a loader that bit-bangs the same pin receives
|
||||||
|
// perfectly and answers into nothing. pureboot releases it itself; this
|
||||||
|
// keeps the hand-over correct for a loader that does not.
|
||||||
|
avr::hw::ucsr0b::write(0);
|
||||||
|
|
||||||
|
call(reinterpret_cast<jump_fn>(base / 2));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#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 m_adc_average = 0;
|
||||||
|
static inline std::int16_t m_temp_quarters = 0;
|
||||||
|
static inline std::uint8_t m_percent = 100;
|
||||||
|
static inline bool m_auto_mode = true;
|
||||||
|
static inline bool m_have_data = false;
|
||||||
|
|
||||||
|
public:
|
||||||
|
static void init()
|
||||||
|
{
|
||||||
|
fan::set_duty(100_pct); // full blast until the first reading
|
||||||
|
}
|
||||||
|
|
||||||
|
static void poll()
|
||||||
|
{
|
||||||
|
std::uint16_t sample;
|
||||||
|
if (!sampler::take(sample)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_adc_average = sample;
|
||||||
|
m_temp_quarters = thermistor::quarters(sample);
|
||||||
|
m_have_data = true;
|
||||||
|
if (m_auto_mode) {
|
||||||
|
m_percent = curve::duty(thermistor::whole_degrees(m_temp_quarters));
|
||||||
|
}
|
||||||
|
fan::set_duty(avr::percent_t::of(m_percent));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void set_manual(std::uint8_t percent)
|
||||||
|
{
|
||||||
|
m_auto_mode = false;
|
||||||
|
m_percent = percent;
|
||||||
|
fan::set_duty(avr::percent_t::of(percent));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void set_automatic()
|
||||||
|
{
|
||||||
|
m_auto_mode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool automatic()
|
||||||
|
{
|
||||||
|
return m_auto_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool data_available()
|
||||||
|
{
|
||||||
|
return m_have_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::int16_t temperature_quarters()
|
||||||
|
{
|
||||||
|
return m_temp_quarters;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::uint16_t last_adc()
|
||||||
|
{
|
||||||
|
return m_adc_average;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::uint8_t fan_percent()
|
||||||
|
{
|
||||||
|
return m_percent;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include <libavr/flash.hpp>
|
||||||
|
|
||||||
|
// The auto-mode fan curve, tabulated at compile time as a flash_table of duty
|
||||||
|
// percent per degree C: a cubic that is held at zero below the temperature the fan
|
||||||
|
// starts at.
|
||||||
|
namespace app::curve {
|
||||||
|
|
||||||
|
inline constexpr double cubic_term = 0.002246;
|
||||||
|
inline constexpr double square_term = -0.09;
|
||||||
|
inline constexpr double linear_term = 0.91;
|
||||||
|
inline constexpr std::int8_t start_celsius = 20;
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
|
||||||
|
consteval std::uint8_t duty_entry(std::int32_t celsius)
|
||||||
|
{
|
||||||
|
double x = celsius;
|
||||||
|
if (x < start_celsius) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
double duty = cubic_term * x * x * x + square_term * x * x + linear_term * 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 (std::int32_t t = 0; t < static_cast<std::int32_t>(out.size()); ++t) {
|
||||||
|
out[static_cast<std::size_t>(t)] = duty_entry(t);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}()>
|
||||||
|
table;
|
||||||
|
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
// Duty percent for a temperature, clamped to the table's own window.
|
||||||
|
inline std::uint8_t duty(std::int8_t celsius)
|
||||||
|
{
|
||||||
|
constexpr auto highest = static_cast<std::int8_t>(detail::table.size() - 1);
|
||||||
|
if (celsius < 0) {
|
||||||
|
celsius = 0;
|
||||||
|
}
|
||||||
|
if (celsius > highest) {
|
||||||
|
celsius = highest;
|
||||||
|
}
|
||||||
|
return detail::table[static_cast<std::uint8_t>(celsius)];
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace app::curve
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
|
#include "board.hpp"
|
||||||
|
#include "bootloader.hpp"
|
||||||
|
#include "controller.hpp"
|
||||||
|
#include "statistics.hpp"
|
||||||
|
#include "terminal.hpp"
|
||||||
|
#include "thermistor.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::thermistor::start();
|
||||||
|
controller::init();
|
||||||
|
terminal::init();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
controller::poll();
|
||||||
|
if (controller::data_available()) {
|
||||||
|
statistics::record(thermistor::whole_degrees(controller::temperature_quarters()));
|
||||||
|
}
|
||||||
|
terminal::poll();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
|
#include "board.hpp"
|
||||||
|
|
||||||
|
// Temperature histogram: one uint32 bucket per degree C 0..99, sampled once a
|
||||||
|
// second, written back to EEPROM every 30 minutes (update() only touches
|
||||||
|
// changed bytes).
|
||||||
|
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;
|
||||||
|
|
||||||
|
// What an erased cell reads back as, so a bucket nobody has written yet
|
||||||
|
// counts as no samples rather than four billion.
|
||||||
|
static constexpr std::uint32_t never_written = ~std::uint32_t{0};
|
||||||
|
|
||||||
|
using stored = avr::eeprom::var<std::array<std::uint32_t, range>, 0>;
|
||||||
|
|
||||||
|
static inline std::array<std::uint32_t, range> m_histogram{};
|
||||||
|
static inline std::uint64_t m_last_sample = 0;
|
||||||
|
static inline std::uint64_t m_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()
|
||||||
|
{
|
||||||
|
m_histogram = stored::read();
|
||||||
|
for (auto &bucket : m_histogram) {
|
||||||
|
if (bucket == never_written) {
|
||||||
|
bucket = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void record(std::int8_t celsius)
|
||||||
|
{
|
||||||
|
auto now = uptime::millis();
|
||||||
|
if (now >= m_last_sample + sample_delay_ms) {
|
||||||
|
++m_histogram[clamp(celsius)];
|
||||||
|
m_last_sample = now;
|
||||||
|
}
|
||||||
|
if (now >= m_last_writeback + writeback_delay_ms) {
|
||||||
|
save();
|
||||||
|
m_last_writeback = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void save()
|
||||||
|
{
|
||||||
|
stored::update(m_histogram);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void reset()
|
||||||
|
{
|
||||||
|
m_histogram = {};
|
||||||
|
stored::update(m_histogram);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::uint8_t min_temperature()
|
||||||
|
{
|
||||||
|
for (std::uint8_t i = 0; i < range; ++i) {
|
||||||
|
if (m_histogram[i]) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::uint8_t max_temperature()
|
||||||
|
{
|
||||||
|
for (std::uint8_t i = range; i > 0; --i) {
|
||||||
|
if (m_histogram[i - 1]) {
|
||||||
|
return i - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::uint64_t total_samples()
|
||||||
|
{
|
||||||
|
std::uint64_t total = 0;
|
||||||
|
for (auto bucket : m_histogram) {
|
||||||
|
total += bucket;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::uint32_t highest_bucket()
|
||||||
|
{
|
||||||
|
std::uint32_t highest = 0;
|
||||||
|
for (auto bucket : m_histogram) {
|
||||||
|
if (bucket > highest) {
|
||||||
|
highest = bucket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return highest;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::uint32_t bucket(std::uint8_t celsius)
|
||||||
|
{
|
||||||
|
return m_histogram[clamp(static_cast<std::int8_t>(celsius))];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
@@ -0,0 +1,521 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
|
#include "board.hpp"
|
||||||
|
#include "bootloader.hpp"
|
||||||
|
#include "controller.hpp"
|
||||||
|
#include "curve.hpp"
|
||||||
|
#include "statistics.hpp"
|
||||||
|
#include "thermistor.hpp"
|
||||||
|
|
||||||
|
// The serial console: line-buffered commands over the hardware UART.
|
||||||
|
// `help` lists everything, command names may be abbreviated, `monitor` streams
|
||||||
|
// until Ctrl+C, and Ctrl+C abandons a half-typed line anywhere else.
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
struct command {
|
||||||
|
std::string_view name;
|
||||||
|
std::string_view summary;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Commands, in the order they are matched - which is the order the original
|
||||||
|
// firmware matched them in, and that order is load-bearing. An abbreviation
|
||||||
|
// resolves to the *first* entry it prefixes, so `s` is show (not statistics,
|
||||||
|
// not set) exactly as it always was, and anything appended to this list cannot
|
||||||
|
// steal an abbreviation that already meant something else.
|
||||||
|
//
|
||||||
|
// A function rather than a variable, so the list exists only while the flash
|
||||||
|
// blobs below are being built and never as storage of its own.
|
||||||
|
consteval auto commands()
|
||||||
|
{
|
||||||
|
return std::to_array<command>({
|
||||||
|
{"help", "prints this help message"},
|
||||||
|
{"show", "shows current temperature and fan speed"},
|
||||||
|
{"curve", "shows mapping from temperature to fan speed"},
|
||||||
|
{"monitor", "loops the show command until Ctrl+C is pressed"},
|
||||||
|
{"bootloader", "enters the bootloader"},
|
||||||
|
{"uptime", "shows system uptime"},
|
||||||
|
{"statistics", "prints overall statistics like min and max temp"},
|
||||||
|
{"histogram", "prints a histogram of the temperature"},
|
||||||
|
{"reset", "resets statistics to 0 in EEPROM and RAM (no abbreviation)"},
|
||||||
|
{"set", "sets the fan speed to the provided value, 0-100"},
|
||||||
|
{"auto", "turns on automatic fan control"},
|
||||||
|
{"version", "displays firmware version"},
|
||||||
|
{"save", "writes the statistics to EEPROM now"},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Found by name rather than written as an index, so reordering the list cannot
|
||||||
|
// point a command at another one's code. A name not in the list does not
|
||||||
|
// compile.
|
||||||
|
consteval std::uint8_t command_index(std::string_view name)
|
||||||
|
{
|
||||||
|
const auto list = commands();
|
||||||
|
for (std::uint8_t i = 0; i < list.size(); ++i) {
|
||||||
|
if (list[i].name == name) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::unreachable();
|
||||||
|
}
|
||||||
|
|
||||||
|
class terminal {
|
||||||
|
static constexpr char ctrl_c = 0x03;
|
||||||
|
static constexpr char backspace = 0x08;
|
||||||
|
static constexpr char del = 0x7f;
|
||||||
|
static constexpr std::uint16_t monitor_interval_ms = 1'000;
|
||||||
|
static inline std::array<char, 24> m_line{};
|
||||||
|
static inline std::uint8_t m_at = 0;
|
||||||
|
static inline bool m_overflowed = false;
|
||||||
|
static inline bool m_monitoring = false;
|
||||||
|
static inline std::uint64_t m_last_monitor = 0;
|
||||||
|
|
||||||
|
static constexpr std::uint8_t command_count = commands().size();
|
||||||
|
|
||||||
|
// One column of the command table as a NUL-separated blob in flash. Matched
|
||||||
|
// and printed at run time, which is not a reason to sit in RAM on a machine
|
||||||
|
// with two address spaces: a table of `string_view` puts the characters
|
||||||
|
// *and* its own pointers there. Separators rather than an offset table,
|
||||||
|
// because an offset table is the RAM this exists to give back.
|
||||||
|
template <std::string_view command::*Column>
|
||||||
|
static constexpr auto joined = [] {
|
||||||
|
constexpr auto bytes = [] {
|
||||||
|
std::size_t total = 0;
|
||||||
|
for (const auto &one : commands()) {
|
||||||
|
total += (one.*Column).size() + 1;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}();
|
||||||
|
std::array<char, bytes> chars{};
|
||||||
|
std::size_t at = 0;
|
||||||
|
for (const auto &one : commands()) {
|
||||||
|
for (char c : one.*Column) {
|
||||||
|
chars[at++] = c;
|
||||||
|
}
|
||||||
|
chars[at++] = '\0';
|
||||||
|
}
|
||||||
|
return chars;
|
||||||
|
}();
|
||||||
|
|
||||||
|
static constexpr avr::flash_table<joined<&command::name>> names{};
|
||||||
|
static constexpr avr::flash_table<joined<&command::summary>> summaries{};
|
||||||
|
|
||||||
|
// `reset` must be typed in full - an abbreviation must not be able to wipe
|
||||||
|
// data.
|
||||||
|
static constexpr std::uint8_t exact_command = command_index("reset");
|
||||||
|
|
||||||
|
// One space and at least one dot after the longest name, the original's
|
||||||
|
// layout exactly.
|
||||||
|
static constexpr std::uint8_t help_column = [] {
|
||||||
|
std::size_t longest = 0;
|
||||||
|
for (const auto &one : commands()) {
|
||||||
|
longest = one.name.size() > longest ? one.name.size() : longest;
|
||||||
|
}
|
||||||
|
return static_cast<std::uint8_t>(longest + 2);
|
||||||
|
}();
|
||||||
|
|
||||||
|
static void prompt()
|
||||||
|
{
|
||||||
|
serial << "> "_P;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The tail of `name ....: ` - the dotted label the original used everywhere
|
||||||
|
// it printed a list of things, which is what makes a column of values
|
||||||
|
// readable without counting spaces.
|
||||||
|
static void pad(std::uint8_t width, std::uint8_t column)
|
||||||
|
{
|
||||||
|
serial << ' ';
|
||||||
|
for (std::uint8_t i = width + 1; i < column; ++i) {
|
||||||
|
serial << '.';
|
||||||
|
}
|
||||||
|
serial << ": "_P;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Flash>
|
||||||
|
static void label(Flash text, std::uint8_t column)
|
||||||
|
{
|
||||||
|
serial << text;
|
||||||
|
pad(Flash::size, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prints one entry of a blob, from `at` to its separator, and answers its
|
||||||
|
// width - counted as it is printed rather than measured first, which would
|
||||||
|
// read the blob twice.
|
||||||
|
template <typename Blob>
|
||||||
|
static std::uint8_t print_entry(Blob blob, std::uint16_t at)
|
||||||
|
{
|
||||||
|
std::uint8_t width = 0;
|
||||||
|
for (char c; (c = blob.at(at + width)) != '\0'; ++width) {
|
||||||
|
serial << c;
|
||||||
|
}
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quarter-degrees as a signed decimal with a two-digit fraction. The sign is
|
||||||
|
// taken off first: C++ gives a negative remainder for a negative dividend, so
|
||||||
|
// the fraction of -40.25 C would print as "-40.-25".
|
||||||
|
static void temperature(std::int16_t quarters)
|
||||||
|
{
|
||||||
|
constexpr auto per_degree = thermistor::quarters_per_degree;
|
||||||
|
if (quarters < 0) {
|
||||||
|
serial << '-';
|
||||||
|
}
|
||||||
|
auto magnitude = static_cast<std::uint16_t>(quarters < 0 ? -quarters : quarters);
|
||||||
|
serial << magnitude / per_degree << '.'
|
||||||
|
<< avr::dec<{.width = 2, .fill = '0'}>((magnitude % per_degree) * (100 / per_degree));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void help()
|
||||||
|
{
|
||||||
|
serial << "\r\nFanTemp "_P << version << " command overview\r\n"_P;
|
||||||
|
std::uint16_t name = 0;
|
||||||
|
std::uint16_t summary = 0;
|
||||||
|
for (std::uint8_t i = 0; i < command_count; ++i) {
|
||||||
|
const auto width = print_entry(names, name);
|
||||||
|
name += width + 1;
|
||||||
|
pad(width, help_column);
|
||||||
|
summary += print_entry(summaries, summary) + 1;
|
||||||
|
serial << "\r\n"_P;
|
||||||
|
}
|
||||||
|
serial << "commands may be abbreviated: 'up' is uptime\r\n"_P;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The thermistor's resistance from the divider, in whole ohms. The original
|
||||||
|
// printed this beside the reading and it is the one number that says *why* a
|
||||||
|
// temperature is wrong: an open sensor rails the ADC and the resistance goes
|
||||||
|
// to the tens of megohms, a shorted one to zero.
|
||||||
|
static std::uint32_t resistance()
|
||||||
|
{
|
||||||
|
auto adc = controller::last_adc();
|
||||||
|
if (adc >= thermistor::adc_full_scale) {
|
||||||
|
return open_circuit;
|
||||||
|
}
|
||||||
|
return static_cast<std::uint32_t>(thermistor::series_resistor) * adc / (thermistor::adc_full_scale - adc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One value per line behind a dotted label, as the original had it. A single
|
||||||
|
// run-on line is fine for one reading and unreadable when `monitor` prints
|
||||||
|
// one a second forever.
|
||||||
|
static void show()
|
||||||
|
{
|
||||||
|
if (!controller::data_available()) {
|
||||||
|
serial << "no data yet\r\n"_P;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
label("ADC value"_P, reading_column);
|
||||||
|
serial << controller::last_adc() << " / "_P << thermistor::adc_full_scale << "\r\n"_P;
|
||||||
|
|
||||||
|
label("Resistance"_P, reading_column);
|
||||||
|
if (auto ohms = resistance(); ohms == open_circuit) {
|
||||||
|
serial << "open circuit\r\n"_P;
|
||||||
|
} else {
|
||||||
|
serial << ohms << " Ohm\r\n"_P;
|
||||||
|
}
|
||||||
|
|
||||||
|
label("Temperature"_P, reading_column);
|
||||||
|
temperature(controller::temperature_quarters());
|
||||||
|
serial << " C\r\n"_P;
|
||||||
|
|
||||||
|
label("Fan speed"_P, reading_column);
|
||||||
|
serial << controller::fan_percent() << "% "_P;
|
||||||
|
if (controller::automatic()) {
|
||||||
|
serial << "auto\r\n"_P;
|
||||||
|
} else {
|
||||||
|
serial << "manual\r\n"_P;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every whole degree from 10 to 60 with a bar, which is the original's and is
|
||||||
|
// the point of the command: the curve is a cubic, and five-degree samples
|
||||||
|
// without a graph show none of its shape. The bar is the duty itself, so it
|
||||||
|
// reads as a percentage without needing a scale.
|
||||||
|
static void print_curve()
|
||||||
|
{
|
||||||
|
for (std::uint8_t t = curve_low; t <= curve_high; ++t) {
|
||||||
|
auto duty = curve::duty(static_cast<std::int8_t>(t));
|
||||||
|
serial << avr::dec<{.width = 2, .fill = '0'}>(t) << " C = "_P << avr::dec<{.width = 3, .fill = ' '}>(duty)
|
||||||
|
<< "% |"_P;
|
||||||
|
for (std::uint8_t i = 0; i < duty; ++i) {
|
||||||
|
serial << '#';
|
||||||
|
}
|
||||||
|
serial << "\r\n"_P;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print_uptime()
|
||||||
|
{
|
||||||
|
constexpr std::uint32_t minute = 60;
|
||||||
|
constexpr std::uint32_t hour = 60 * minute;
|
||||||
|
constexpr std::uint32_t day = 24 * hour;
|
||||||
|
auto seconds = static_cast<std::uint32_t>(uptime::millis() / 1000);
|
||||||
|
serial << seconds / day << "d "_P << seconds % day / hour << "h "_P << seconds % hour / minute << "m "_P
|
||||||
|
<< seconds % minute << "s\r\n"_P;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print_statistics()
|
||||||
|
{
|
||||||
|
auto empty = statistics::total_samples() == 0;
|
||||||
|
label("Minimum temperature"_P, stat_column);
|
||||||
|
if (empty) {
|
||||||
|
serial << "not available\r\n"_P;
|
||||||
|
} else {
|
||||||
|
serial << statistics::min_temperature() << " C\r\n"_P;
|
||||||
|
}
|
||||||
|
|
||||||
|
label("Maximum temperature"_P, stat_column);
|
||||||
|
if (empty) {
|
||||||
|
serial << "not available\r\n"_P;
|
||||||
|
} else {
|
||||||
|
serial << statistics::max_temperature() << " C\r\n"_P;
|
||||||
|
}
|
||||||
|
|
||||||
|
label("Total samples"_P, stat_column);
|
||||||
|
serial << static_cast<std::uint32_t>(statistics::total_samples()) << "\r\n"_P;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print_histogram()
|
||||||
|
{
|
||||||
|
auto highest = statistics::highest_bucket();
|
||||||
|
if (highest == 0) {
|
||||||
|
serial << "no data yet\r\n"_P;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The original's normalisation, and its resolution: divide by whatever
|
||||||
|
// makes the tallest bucket fit in a hundred columns, not forty. A bar
|
||||||
|
// that tops out at 40 throws away most of the difference between
|
||||||
|
// neighbouring buckets, which on a distribution this narrow is the whole
|
||||||
|
// picture.
|
||||||
|
std::uint32_t factor = highest / bar_max > 1 ? highest / bar_max : 1;
|
||||||
|
while (highest / factor > bar_max) {
|
||||||
|
++factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::uint8_t t = statistics::min_temperature(); t <= statistics::max_temperature(); ++t) {
|
||||||
|
auto count = statistics::bucket(t);
|
||||||
|
// Count first, in a fixed column, so the numbers read as a table
|
||||||
|
// instead of trailing off the ragged right-hand end of the bars.
|
||||||
|
serial << avr::dec<{.width = 2, .fill = '0'}>(t) << " C : "_P << avr::dec<{.width = 10, .fill = ' '}>(count)
|
||||||
|
<< " |"_P;
|
||||||
|
for (std::uint32_t i = 0; i < count / factor; ++i) {
|
||||||
|
serial << '#';
|
||||||
|
}
|
||||||
|
serial << "\r\n"_P;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Which command a word names, or `no_command` for none. The input matches
|
||||||
|
// when it is a non-empty prefix of a name; `reset` is the exception and must
|
||||||
|
// be typed in full.
|
||||||
|
//
|
||||||
|
// One pass over the blob answers both, because the names sit in it in match
|
||||||
|
// order: each name is compared as it is walked, and the walk to its
|
||||||
|
// separator is also what measures it.
|
||||||
|
//
|
||||||
|
// Public and `constexpr` so the battery can pin it: the match order is
|
||||||
|
// load-bearing, an abbreviation must never reach `reset`, and reading the
|
||||||
|
// names from flash must not have changed either.
|
||||||
|
static constexpr std::uint8_t lookup(std::string_view input)
|
||||||
|
{
|
||||||
|
if (input.empty()) {
|
||||||
|
return command_count;
|
||||||
|
}
|
||||||
|
std::uint8_t at = 0;
|
||||||
|
for (std::uint8_t i = 0; i < command_count; ++i) {
|
||||||
|
std::uint8_t length = 0;
|
||||||
|
bool prefix = true;
|
||||||
|
for (char c; (c = names.at(at + length)) != '\0'; ++length) {
|
||||||
|
if (length < input.size() && input[length] != c) {
|
||||||
|
prefix = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const bool whole = input.size() == length;
|
||||||
|
if (prefix && input.size() <= length && (i != exact_command || whole)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
at += length + 1;
|
||||||
|
}
|
||||||
|
return command_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr std::uint8_t no_command = command_count;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void dispatch(std::string_view input)
|
||||||
|
{
|
||||||
|
// A line that overflowed the buffer is not a command - it is the tail of
|
||||||
|
// one. Acting on it is how a truncated `reset` becomes a surprise.
|
||||||
|
if (m_overflowed) {
|
||||||
|
serial << "input too long, ignored\r\n"_P;
|
||||||
|
m_overflowed = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split on the first space with the (pointer, length) constructor:
|
||||||
|
// substr checks its bounds by throwing, which this build cannot.
|
||||||
|
const auto space = input.find(' ');
|
||||||
|
const auto word = space == std::string_view::npos ? input : std::string_view{input.data(), space};
|
||||||
|
const auto rest = space == std::string_view::npos
|
||||||
|
? std::string_view{}
|
||||||
|
: std::string_view{input.data() + space + 1, input.size() - space - 1};
|
||||||
|
if (word.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (lookup(word)) {
|
||||||
|
case command_index("help"):
|
||||||
|
help();
|
||||||
|
return;
|
||||||
|
case command_index("show"):
|
||||||
|
show();
|
||||||
|
return;
|
||||||
|
case command_index("curve"):
|
||||||
|
print_curve();
|
||||||
|
return;
|
||||||
|
case command_index("monitor"):
|
||||||
|
m_monitoring = true;
|
||||||
|
return;
|
||||||
|
case command_index("bootloader"):
|
||||||
|
serial << "entering bootloader\r\n"_P;
|
||||||
|
statistics::save();
|
||||||
|
serial.drain();
|
||||||
|
bootloader::enter();
|
||||||
|
case command_index("uptime"):
|
||||||
|
print_uptime();
|
||||||
|
return;
|
||||||
|
case command_index("statistics"):
|
||||||
|
print_statistics();
|
||||||
|
return;
|
||||||
|
case command_index("histogram"):
|
||||||
|
print_histogram();
|
||||||
|
return;
|
||||||
|
case command_index("reset"):
|
||||||
|
statistics::reset();
|
||||||
|
serial << "statistics cleared in EEPROM and RAM\r\n"_P;
|
||||||
|
return;
|
||||||
|
case command_index("set"): {
|
||||||
|
std::uint16_t percent = 0;
|
||||||
|
bool valid = !rest.empty();
|
||||||
|
for (char c : rest) {
|
||||||
|
if (c < '0' || c > '9') {
|
||||||
|
valid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
percent = static_cast<std::uint16_t>(percent * 10 + (c - '0'));
|
||||||
|
if (percent > 100) {
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (valid) {
|
||||||
|
controller::set_manual(static_cast<std::uint8_t>(percent));
|
||||||
|
serial << "fan "_P << percent << " %, manual\r\n"_P;
|
||||||
|
} else {
|
||||||
|
serial << "set 0..100\r\n"_P;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case command_index("auto"):
|
||||||
|
controller::set_automatic();
|
||||||
|
serial << "automatic fan control\r\n"_P;
|
||||||
|
return;
|
||||||
|
case command_index("version"):
|
||||||
|
serial << "FanTemp "_P << version << " on libavr\r\n"_P;
|
||||||
|
return;
|
||||||
|
case command_index("save"):
|
||||||
|
statistics::save();
|
||||||
|
serial << "statistics written to EEPROM\r\n"_P;
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
serial << '\'' << word << "' is not a command; 'help' for the list\r\n"_P;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
// A flash string, like every other literal the console prints: as a
|
||||||
|
// `string_view` it was the last constant left in RAM, holding its own
|
||||||
|
// characters and a pointer to them.
|
||||||
|
static constexpr auto version = "v2.2"_P;
|
||||||
|
static constexpr std::uint8_t bar_max = 100;
|
||||||
|
|
||||||
|
// Columns the dotted labels' colons land in, and the curve's span. All four
|
||||||
|
// are the original's.
|
||||||
|
static constexpr std::uint8_t reading_column = 13;
|
||||||
|
static constexpr std::uint8_t stat_column = 21;
|
||||||
|
static constexpr std::uint8_t curve_low = 10;
|
||||||
|
static constexpr std::uint8_t curve_high = 60;
|
||||||
|
|
||||||
|
// A railed ADC leaves the divider with no solution rather than a huge one.
|
||||||
|
static constexpr std::uint32_t open_circuit = 0xffffffff;
|
||||||
|
|
||||||
|
static void init()
|
||||||
|
{
|
||||||
|
serial << "\r\nFanTemp "_P << version << " on libavr -- 'help' for commands\r\n"_P;
|
||||||
|
prompt();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void poll()
|
||||||
|
{
|
||||||
|
if (m_monitoring) {
|
||||||
|
if (uptime::millis() >= m_last_monitor + monitor_interval_ms) {
|
||||||
|
show();
|
||||||
|
m_last_monitor = uptime::millis();
|
||||||
|
}
|
||||||
|
// Ctrl+C only, as the original had it. Stopping on *any* byte reads
|
||||||
|
// well until a host sends a line ending: `monitor\r\n` then stops
|
||||||
|
// itself on the `\n` it arrived with, one reading in.
|
||||||
|
if (auto in = serial_t::read(); in && *in == ctrl_c) {
|
||||||
|
serial << "^C\r\n"_P;
|
||||||
|
m_monitoring = false;
|
||||||
|
prompt();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (auto in = serial_t::read()) {
|
||||||
|
char c = static_cast<char>(*in);
|
||||||
|
if (c == ctrl_c) {
|
||||||
|
// Abandon whatever was typed and start a fresh line, which is
|
||||||
|
// what Ctrl+C means at every other prompt in the world.
|
||||||
|
serial << "^C\r\n"_P;
|
||||||
|
m_at = 0;
|
||||||
|
m_overflowed = false;
|
||||||
|
prompt();
|
||||||
|
} else if (c == '\r' || c == '\n') {
|
||||||
|
serial << "\r\n"_P;
|
||||||
|
if (m_at == 0 && !m_overflowed) {
|
||||||
|
prompt(); // a bare Enter just reprompts, no gap needed
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
dispatch(std::string_view{m_line.data(), m_at});
|
||||||
|
m_at = 0;
|
||||||
|
if (!m_monitoring) {
|
||||||
|
// A blank line between a command's output and the next
|
||||||
|
// prompt: without it the answer and the thing you type
|
||||||
|
// next run together and a screen of them is unreadable.
|
||||||
|
serial << "\r\n"_P;
|
||||||
|
prompt();
|
||||||
|
}
|
||||||
|
} else if (c == del || c == backspace) {
|
||||||
|
if (m_at) {
|
||||||
|
--m_at;
|
||||||
|
serial << "\b \b"_P;
|
||||||
|
}
|
||||||
|
} else if (c >= ' ') {
|
||||||
|
if (m_at < m_line.size()) {
|
||||||
|
m_line[m_at++] = c;
|
||||||
|
serial << c; // echo
|
||||||
|
} else {
|
||||||
|
m_overflowed = true; // reported when the line is submitted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#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-degrees C with linear interpolation between table steps.
|
||||||
|
namespace app::thermistor {
|
||||||
|
|
||||||
|
// The converter's top count: this board reads the divider at the ADC's
|
||||||
|
// full 10 bits, so a reading and the resistance it implies both scale by it.
|
||||||
|
inline constexpr std::uint16_t adc_full_scale = (1u << 10) - 1;
|
||||||
|
|
||||||
|
inline constexpr double series_resistor = 9951;
|
||||||
|
inline constexpr double nominal_resistance = 9270;
|
||||||
|
inline constexpr double beta = 3212;
|
||||||
|
inline constexpr double nominal_temperature = 25;
|
||||||
|
|
||||||
|
inline constexpr std::uint8_t quarter_bits = 2;
|
||||||
|
inline constexpr std::uint8_t quarters_per_degree = 1 << quarter_bits;
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
|
||||||
|
inline constexpr double kelvin_offset = 273.15;
|
||||||
|
|
||||||
|
// Signed-promoting, so the interpolation below divides a negative slope as one.
|
||||||
|
inline constexpr std::uint8_t counts_per_step = 4;
|
||||||
|
inline constexpr std::int32_t steps = (adc_full_scale + 1) / counts_per_step;
|
||||||
|
|
||||||
|
// A sane sensor window, in whole degrees.
|
||||||
|
inline constexpr std::int32_t coldest = -40;
|
||||||
|
inline constexpr std::int32_t hottest = 125;
|
||||||
|
|
||||||
|
consteval double temperature_of(double adc)
|
||||||
|
{
|
||||||
|
double resistance = series_resistor * adc / (adc_full_scale - adc);
|
||||||
|
// __builtin_log constant-folds on the AVR backend, so the table is
|
||||||
|
// built at compile time with no runtime libm.
|
||||||
|
double steinhart =
|
||||||
|
__builtin_log(resistance / nominal_resistance) / beta + 1.0 / (nominal_temperature + kelvin_offset);
|
||||||
|
return 1.0 / steinhart - kelvin_offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One entry per step, in quarter-degrees, with the reading kept a step clear of
|
||||||
|
// both rails, where the divider has no solution.
|
||||||
|
consteval std::int16_t quarters_entry(std::int32_t index)
|
||||||
|
{
|
||||||
|
double adc = index * double{counts_per_step};
|
||||||
|
if (adc < counts_per_step) {
|
||||||
|
adc = counts_per_step;
|
||||||
|
}
|
||||||
|
if (adc > adc_full_scale - counts_per_step) {
|
||||||
|
adc = adc_full_scale - counts_per_step;
|
||||||
|
}
|
||||||
|
double t = temperature_of(adc) * quarters_per_degree;
|
||||||
|
if (t < coldest * quarters_per_degree) {
|
||||||
|
t = coldest * quarters_per_degree;
|
||||||
|
}
|
||||||
|
if (t > hottest * quarters_per_degree) {
|
||||||
|
t = hottest * quarters_per_degree;
|
||||||
|
}
|
||||||
|
return static_cast<std::int16_t>(t < 0 ? t - 0.5 : t + 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One entry past the last step, mirroring it, so interpolation at full scale
|
||||||
|
// has a right neighbour.
|
||||||
|
inline constexpr avr::flash_table<[] {
|
||||||
|
std::array<std::int16_t, steps + 1> out{};
|
||||||
|
for (std::int32_t i = 0; i < static_cast<std::int32_t>(out.size()); ++i) {
|
||||||
|
out[static_cast<std::size_t>(i)] = quarters_entry(i < steps ? i : steps - 1);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}()>
|
||||||
|
table;
|
||||||
|
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
// Temperature in quarter-degrees C from a raw (or averaged) 10-bit sample.
|
||||||
|
inline std::int16_t quarters(std::uint16_t adc)
|
||||||
|
{
|
||||||
|
std::uint16_t index = adc / detail::counts_per_step;
|
||||||
|
std::uint8_t frac = adc % detail::counts_per_step;
|
||||||
|
auto a = detail::table[index];
|
||||||
|
auto b = detail::table[static_cast<std::uint16_t>(index + 1)];
|
||||||
|
return static_cast<std::int16_t>(a + ((b - a) * frac) / detail::counts_per_step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whole degrees from quarter-degrees, rounded to nearest. The shift is the
|
||||||
|
// point: C truncates a negative quotient toward zero, so `(q + 2) / 4` reads
|
||||||
|
// -3.00 C as -2, where an arithmetic shift floors and reads it as -3.
|
||||||
|
inline constexpr std::int8_t whole_degrees(std::int16_t quarters)
|
||||||
|
{
|
||||||
|
return static_cast<std::int8_t>((quarters + quarters_per_degree / 2) >> quarter_bits);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace app::thermistor
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
libavr_format_test()
|
||||||
|
|
||||||
|
# The image must stay below the boot section at 0x7e00, and the README states
|
||||||
|
# what it measures; a library advance that moves it says nothing on its own.
|
||||||
|
libavr_size_claim_test(fantemp 7836)
|
||||||
|
|
||||||
|
# The README says the two modes emit the same image, and only a tree with
|
||||||
|
# both built can say whether they do.
|
||||||
|
libavr_mode_identity_test(fantemp)
|
||||||
|
|
||||||
|
# The battery is a compile: a static_assert that fails is the failure. It is a
|
||||||
|
# target rather than only a ctest so a plain build catches a regression too.
|
||||||
|
add_library(consteval_tests OBJECT consteval.cpp)
|
||||||
|
target_include_directories(consteval_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../src)
|
||||||
|
target_link_libraries(consteval_tests PRIVATE libavr)
|
||||||
|
|
||||||
|
add_test(NAME fantemp.consteval
|
||||||
|
COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target consteval_tests)
|
||||||
|
|
||||||
|
# The board has no reset line and no programming header, so the loader-entry
|
||||||
|
# route in the emitted image is the only thing standing between a firmware change
|
||||||
|
# and an unreflashable board.
|
||||||
|
# A check whose interpreter this host lacks is registered and *skips*, rather
|
||||||
|
# than leaving the suite a different size on a different machine - which is a
|
||||||
|
# suite nothing can be compared against. The launcher and the marker are
|
||||||
|
# libavr's (`cmake/checks.cmake`).
|
||||||
|
libavr_launcher(_python "${LIBAVR_NO_PYTHON}" ${Python3_EXECUTABLE})
|
||||||
|
add_test(NAME fantemp.reachability
|
||||||
|
COMMAND ${_python} ${CMAKE_CURRENT_SOURCE_DIR}/check_reachability.py
|
||||||
|
--objdump ${CMAKE_OBJDUMP} --elf $<TARGET_FILE:fantemp>
|
||||||
|
--image $<TARGET_FILE_DIR:fantemp>/fantemp.bin
|
||||||
|
--loader-base ${PUREBOOT_BASE_HEX})
|
||||||
|
|
||||||
|
libavr_skip_unverified()
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""The board's only way in, checked in the emitted image.
|
||||||
|
|
||||||
|
This board has no reset line and no programming header. The single route to the
|
||||||
|
bootloader is the running firmware's `bootloader` command, so a firmware that
|
||||||
|
gets that route wrong is a board that cannot be reflashed - and the failure is
|
||||||
|
silent, because everything else still works.
|
||||||
|
|
||||||
|
Four properties, all read out of the disassembly rather than the source:
|
||||||
|
|
||||||
|
1. The image ends below the boot section. `hfuse d4` puts that at 0x7c00, so an
|
||||||
|
application reaching into it would be overwritten by the loader - or worse,
|
||||||
|
executed at reset, since BOOTRST points there.
|
||||||
|
2. The hand-over targets the loader base. A word address of 0x3f00 is byte
|
||||||
|
0x7e00; 0x7800, a 2 KB boot section's base, reads erased on this board.
|
||||||
|
3. The hand-over releases the USART first. While TXEN0 is set the peripheral
|
||||||
|
owns PD1, so a loader that bit-bangs the pin answers into nothing.
|
||||||
|
4. The hand-over does not arm the watchdog. pureboot hands straight back on
|
||||||
|
WDRF by design, so a reset-based route reaches it and opens no window.
|
||||||
|
|
||||||
|
check_reachability.py --objdump avr-objdump --elf fantemp --image fantemp.bin \
|
||||||
|
--loader-base 0x7e00
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
BOOT_SECTION = 0x7C00 # hfuse d4: BOOTSZ 512 words - a fuse fact, not a loader one
|
||||||
|
UCSR0B = 0xC1
|
||||||
|
WDTCSR = 0x60
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--objdump", required=True)
|
||||||
|
parser.add_argument("--elf", type=pathlib.Path, required=True)
|
||||||
|
parser.add_argument("--image", type=pathlib.Path, required=True)
|
||||||
|
# pureboot's own geometry, from the pinned submodule that exports it, so the
|
||||||
|
# slot's base is stated once for the loader this firmware is deployed with.
|
||||||
|
parser.add_argument("--loader-base", required=True,
|
||||||
|
type=lambda v: int(v, 0), metavar="ADDR")
|
||||||
|
args = parser.parse_args()
|
||||||
|
loader_base = args.loader_base
|
||||||
|
|
||||||
|
failures = []
|
||||||
|
|
||||||
|
size = args.image.stat().st_size
|
||||||
|
if size >= BOOT_SECTION:
|
||||||
|
failures.append(f"the image is {size} B and reaches 0x{size - 1:04x}, "
|
||||||
|
f"into the boot section at 0x{BOOT_SECTION:04x}")
|
||||||
|
else:
|
||||||
|
print(f" ok image {size} B, ends 0x{size - 1:04x}, "
|
||||||
|
f"{BOOT_SECTION - size} B clear of the boot section")
|
||||||
|
|
||||||
|
text = subprocess.run([args.objdump, "-d", str(args.elf)],
|
||||||
|
capture_output=True, text=True, check=True).stdout
|
||||||
|
|
||||||
|
# The address the hand-over actually targets, read at its call sites - not
|
||||||
|
# "does the image contain this byte somewhere", which proves nothing: 0x3f is
|
||||||
|
# an ordinary constant that appears in the curve tables, so a check like that
|
||||||
|
# passes just as happily on the 0x7800 bug it is supposed to catch.
|
||||||
|
#
|
||||||
|
# bootloader::call() takes the target as a function pointer, so each call site
|
||||||
|
# loads the *word* address into a register pair immediately before it.
|
||||||
|
lines = text.splitlines()
|
||||||
|
helper = re.compile(r"\b(?:r?call)\b.*<_ZN3app10bootloader4call")
|
||||||
|
release = re.compile(rf"\bsts\s+0x00{UCSR0B:02X},\s*r1\b", re.I)
|
||||||
|
sites, unreleased = [], 0
|
||||||
|
for index, line in enumerate(lines):
|
||||||
|
if not helper.search(line):
|
||||||
|
continue
|
||||||
|
held: dict[str, int] = {}
|
||||||
|
window = lines[max(0, index - 8):index]
|
||||||
|
for back in window:
|
||||||
|
if m := re.search(r"\bldi\s+(r\d+),\s*0x([0-9A-Fa-f]{2})", back):
|
||||||
|
held[m.group(1)] = int(m.group(2), 16)
|
||||||
|
# The AVR ABI passes the pointer in r25:r24.
|
||||||
|
if "r24" in held and "r25" in held:
|
||||||
|
sites.append(held["r25"] << 8 | held["r24"])
|
||||||
|
if not any(release.search(back) for back in window):
|
||||||
|
unreleased += 1
|
||||||
|
|
||||||
|
want = loader_base // 2
|
||||||
|
if not sites:
|
||||||
|
failures.append("no call to bootloader::call with a loaded target - the "
|
||||||
|
"hand-over could not be read out of the image")
|
||||||
|
elif wrong := [a for a in sites if a != want]:
|
||||||
|
failures.append(f"the hand-over targets word {[hex(a) for a in wrong]} "
|
||||||
|
f"(byte {[hex(a * 2) for a in wrong]}), not the loader at "
|
||||||
|
f"0x{loader_base:04x}")
|
||||||
|
else:
|
||||||
|
print(f" ok all {len(sites)} hand-over site(s) target word 0x{want:04x} "
|
||||||
|
f"(byte 0x{loader_base:04x})")
|
||||||
|
|
||||||
|
if unreleased:
|
||||||
|
failures.append(f"{unreleased} hand-over site(s) do not clear UCSR0B first - "
|
||||||
|
f"the USART keeps PD1 and the loader answers into nothing")
|
||||||
|
elif sites:
|
||||||
|
print(" ok every hand-over site clears UCSR0B before the jump")
|
||||||
|
|
||||||
|
# An icall/ijmp has to exist for that address to be jumped to indirectly.
|
||||||
|
if not re.search(r"\b(icall|ijmp)\b", text):
|
||||||
|
failures.append("no icall/ijmp - the hand-over cannot reach across flash")
|
||||||
|
else:
|
||||||
|
print(" ok an indirect call exists (a relative one cannot reach)")
|
||||||
|
|
||||||
|
# What actually reaches WDTCSR, not what the image happens to load somewhere.
|
||||||
|
# A timed disable writes WDCE|WDE (0x18) and then zero. Arming writes WDE
|
||||||
|
# *without* WDCE - including 0x08, a 16 ms timeout with every prescaler bit
|
||||||
|
# clear, which is precisely what the legacy route used and is why this cannot
|
||||||
|
# be a check for "a prescaler is present".
|
||||||
|
WDCE, WDE = 0x10, 0x08
|
||||||
|
values, held = [], {}
|
||||||
|
for line in text.splitlines():
|
||||||
|
if m := re.search(r"\bldi\s+(r\d+),\s*0x([0-9A-Fa-f]{2})", line):
|
||||||
|
held[m.group(1)] = int(m.group(2), 16)
|
||||||
|
elif m := re.search(rf"\bsts\s+0x00{WDTCSR:02X},\s*(r\d+)", line, re.I):
|
||||||
|
reg = m.group(1)
|
||||||
|
values.append(0 if reg == "r1" else held.get(reg))
|
||||||
|
armed = [v for v in values if v is not None and (v & WDE) and not (v & WDCE)]
|
||||||
|
if armed:
|
||||||
|
failures.append(f"WDTCSR is written {[hex(v) for v in armed]} - WDE without "
|
||||||
|
f"WDCE is arming the watchdog, and a reset-based hand-over "
|
||||||
|
f"opens no pureboot window")
|
||||||
|
elif not values:
|
||||||
|
print(" ok the watchdog is never written")
|
||||||
|
else:
|
||||||
|
print(f" ok WDTCSR writes are {[hex(v) if v is not None else '?' for v in values]}"
|
||||||
|
f" - unlock and clear, never an arm")
|
||||||
|
|
||||||
|
for line in failures:
|
||||||
|
print(f" FAIL {line}")
|
||||||
|
return 1 if failures else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
// Compile-only battery, built with the cross compiler so the target's 16-bit
|
||||||
|
// int is exercised. Two of this firmware's three numeric surfaces are decided
|
||||||
|
// entirely at compile time - the fan curve and the thermistor table - and the
|
||||||
|
// third, quarter-degrees to whole ones, is the arithmetic between them.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "curve.hpp"
|
||||||
|
#include "terminal.hpp"
|
||||||
|
#include "thermistor.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using app::thermistor::whole_degrees;
|
||||||
|
|
||||||
|
// Rounding to nearest, across zero. A negative quotient truncates toward zero
|
||||||
|
// in C, so a `(q + 2) / 4` reads -3.00 C as -2 and every case below the tie
|
||||||
|
// with it; the arithmetic shift floors, which is what these hold it to.
|
||||||
|
static_assert(whole_degrees(0) == 0);
|
||||||
|
static_assert(whole_degrees(1) == 0); // +0.25
|
||||||
|
static_assert(whole_degrees(2) == 1); // +0.50, the tie
|
||||||
|
static_assert(whole_degrees(3) == 1); // +0.75
|
||||||
|
static_assert(whole_degrees(4) == 1); // +1.00
|
||||||
|
static_assert(whole_degrees(-1) == 0); // -0.25
|
||||||
|
static_assert(whole_degrees(-2) == 0); // -0.50, the tie, toward zero
|
||||||
|
static_assert(whole_degrees(-3) == -1);
|
||||||
|
static_assert(whole_degrees(-4) == -1);
|
||||||
|
static_assert(whole_degrees(-12) == -3);
|
||||||
|
static_assert(whole_degrees(-160) == -40); // the table's own floor
|
||||||
|
static_assert(whole_degrees(500) == 125); // and its ceiling
|
||||||
|
|
||||||
|
// The curve is held at zero below the temperature the fan starts at, and
|
||||||
|
// saturates inside the table's window rather than at its edge.
|
||||||
|
using app::curve::detail::duty_entry;
|
||||||
|
static_assert(duty_entry(app::curve::start_celsius - 1) == 0);
|
||||||
|
static_assert(duty_entry(app::curve::start_celsius) == 0);
|
||||||
|
static_assert(duty_entry(30) == 7);
|
||||||
|
static_assert(duty_entry(40) == 36);
|
||||||
|
static_assert(duty_entry(49) == 93);
|
||||||
|
static_assert(duty_entry(50) == 100);
|
||||||
|
static_assert(duty_entry(99) == 100);
|
||||||
|
|
||||||
|
// Monotone across the whole table: a warmer reading never asks for less air.
|
||||||
|
consteval bool curve_rises()
|
||||||
|
{
|
||||||
|
for (std::int32_t t = 1; t < static_cast<std::int32_t>(app::curve::detail::table.size()); ++t) {
|
||||||
|
if (duty_entry(t) < duty_entry(t - 1)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static_assert(curve_rises());
|
||||||
|
|
||||||
|
// The thermistor table, anchored where the Beta equation fixes it rather than
|
||||||
|
// against numbers this file computed the same way: the divider reads the
|
||||||
|
// thermistor's nominal resistance at
|
||||||
|
// adc = full_scale * nominal / (series + nominal), and the equation's own
|
||||||
|
// definition puts that count at the nominal temperature.
|
||||||
|
using app::thermistor::detail::counts_per_step;
|
||||||
|
using app::thermistor::detail::quarters_entry;
|
||||||
|
using app::thermistor::detail::steps;
|
||||||
|
|
||||||
|
inline constexpr std::int32_t nominal_count =
|
||||||
|
static_cast<std::int32_t>(app::thermistor::adc_full_scale * app::thermistor::nominal_resistance /
|
||||||
|
(app::thermistor::series_resistor + app::thermistor::nominal_resistance));
|
||||||
|
static_assert(quarters_entry(nominal_count / counts_per_step) >= 100); // 25.00 C, in quarters
|
||||||
|
static_assert(quarters_entry(nominal_count / counts_per_step + 1) < 100); // and the step below it
|
||||||
|
|
||||||
|
// Both clamps, at the ends the divider cannot leave.
|
||||||
|
static_assert(quarters_entry(0) == 125 * 4);
|
||||||
|
static_assert(quarters_entry(steps - 1) == -40 * 4);
|
||||||
|
|
||||||
|
// An NTC on this divider falls with the count: more counts is more resistance
|
||||||
|
// is a colder sensor, over every step of the table.
|
||||||
|
consteval bool thermistor_falls()
|
||||||
|
{
|
||||||
|
for (std::int32_t i = 1; i < steps; ++i) {
|
||||||
|
if (quarters_entry(i) > quarters_entry(i - 1)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static_assert(thermistor_falls());
|
||||||
|
|
||||||
|
// The console's one parsing rule, read out of the flash blob the names live
|
||||||
|
// in. Every case here is a property of the *order* the names sit in, so this
|
||||||
|
// is what an edit to that list has to answer to.
|
||||||
|
using app::terminal;
|
||||||
|
|
||||||
|
// Full names, and the ends of the list - a walk that miscounts a separator
|
||||||
|
// lands on a neighbour rather than failing, so both ends are named.
|
||||||
|
static_assert(terminal::lookup("help") == 0);
|
||||||
|
static_assert(terminal::lookup("save") == 12);
|
||||||
|
static_assert(terminal::lookup("bootloader") == 4);
|
||||||
|
|
||||||
|
// Abbreviations resolve to the first entry they prefix. `s` prefixes show,
|
||||||
|
// statistics, set and save, and show is first - which is the original's
|
||||||
|
// behaviour and the reason the list is ordered rather than sorted.
|
||||||
|
static_assert(terminal::lookup("s") == 1);
|
||||||
|
static_assert(terminal::lookup("st") == 6);
|
||||||
|
static_assert(terminal::lookup("se") == 9);
|
||||||
|
static_assert(terminal::lookup("sa") == 12);
|
||||||
|
static_assert(terminal::lookup("up") == 5);
|
||||||
|
static_assert(terminal::lookup("b") == 4);
|
||||||
|
|
||||||
|
// `reset` is the exception: it must be typed in full, so no abbreviation of it
|
||||||
|
// resolves - and none of its prefixes names anything else either.
|
||||||
|
static_assert(terminal::lookup("reset") == 8);
|
||||||
|
static_assert(terminal::lookup("rese") == terminal::no_command);
|
||||||
|
static_assert(terminal::lookup("res") == terminal::no_command);
|
||||||
|
static_assert(terminal::lookup("r") == terminal::no_command);
|
||||||
|
|
||||||
|
// Nothing, and nothing that matches.
|
||||||
|
static_assert(terminal::lookup("") == terminal::no_command);
|
||||||
|
static_assert(terminal::lookup("xyzzy") == terminal::no_command);
|
||||||
|
|
||||||
|
// Longer than the name it prefixes is not a match: `helpful` is not `help`.
|
||||||
|
static_assert(terminal::lookup("helpful") == terminal::no_command);
|
||||||
|
|
||||||
|
} // namespace
|
||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# The task gate: every configure preset built, and only then every one tested.
|
||||||
|
#
|
||||||
|
# The loop is libavr's (tools/check-presets.sh): the mode-identity checks read
|
||||||
|
# the sibling mode's tree, so a build-then-test-per-preset run would compare a
|
||||||
|
# fresh image against a stale sibling. Anything this repository needs beyond
|
||||||
|
# the presets goes after the call, where every tree is built and tested.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||||
|
"${LIBAVR_ROOT:-libavr}/tools/check-presets.sh" "$@"
|
||||||
Reference in new issue
Block a user