Compare commits
24 Commits
6abf0b5563
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 78c9071f0c | |||
| d6673db131 | |||
| fb82f0f7a9 | |||
| 88df1c1ada | |||
| fc65457de3 | |||
| 11da387ffa | |||
| 491ff76447 | |||
| e56e8b977f | |||
| 8f43584c5c | |||
| 996453c2a1 | |||
| 0f5e40510c | |||
| bf18f99635 | |||
| 5244953654 | |||
| cde7e60170 | |||
| 4c351c6c9d | |||
| 5974a5aa90 | |||
| ba37ecea04 | |||
| 0f99b7787c | |||
| 3e95160b00 | |||
| 837b832bc7 | |||
| 1489f4c3a0 | |||
| b1caf49522 | |||
| 0e9060db69 | |||
| 3fbbcdade9 |
@@ -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
|
||||||
...
|
...
|
||||||
|
|||||||
30
.clangd
Normal file
30
.clangd
Normal file
@@ -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
|
||||||
13
.gitattributes
vendored
13
.gitattributes
vendored
@@ -1,8 +1,11 @@
|
|||||||
*.h eol=lf
|
# Line endings are the repository's, not the editing machine's: this checkout
|
||||||
*.hpp eol=lf
|
# is reached from two hosts, and a file rewritten by a Windows tool comes back
|
||||||
*.c eol=lf
|
# with every line changed unless something says otherwise. Naming the source
|
||||||
*.cpp eol=lf
|
# extensions left Markdown, Python, shell and CMake to whatever the writing
|
||||||
.git* eol=lf
|
# tool defaulted to, which is CRLF on one of the two.
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# Atmel Studio writes these and expects them back.
|
||||||
*.vcxproj* eol=crlf
|
*.vcxproj* eol=crlf
|
||||||
*.cppproj eol=crlf
|
*.cppproj eol=crlf
|
||||||
*.sln eol=crlf
|
*.sln eol=crlf
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
build/
|
build/
|
||||||
|
local/
|
||||||
.cache/
|
.cache/
|
||||||
|
|
||||||
# Atmel Studio: generated per machine, and its build outputs
|
# Atmel Studio: generated per machine, and its build outputs
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +1,6 @@
|
|||||||
[submodule "libavr"]
|
[submodule "libavr"]
|
||||||
path = libavr
|
path = libavr
|
||||||
url = ../libavr.git
|
url = ../libavr.git
|
||||||
|
[submodule "pureboot"]
|
||||||
|
path = pureboot
|
||||||
|
url = ../pureboot.git
|
||||||
|
|||||||
6
.vscode/extensions.json
vendored
Normal file
6
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"llvm-vs-code-extensions.vscode-clangd",
|
||||||
|
"ms-vscode.cmake-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
33
.vscode/settings.json
vendored
Normal file
33
.vscode/settings.json
vendored
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,9 +12,19 @@ if(NOT LIBAVR_ROOT)
|
|||||||
set(LIBAVR_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/libavr)
|
set(LIBAVR_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/libavr)
|
||||||
endif()
|
endif()
|
||||||
if(NOT EXISTS ${LIBAVR_ROOT}/CMakeLists.txt)
|
if(NOT EXISTS ${LIBAVR_ROOT}/CMakeLists.txt)
|
||||||
message(FATAL_ERROR "libavr not found at ${LIBAVR_ROOT} — run: git submodule update --init libavr")
|
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()
|
endif()
|
||||||
add_subdirectory(${LIBAVR_ROOT} libavr-build)
|
add_subdirectory(${LIBAVR_ROOT} libavr-build)
|
||||||
|
add_subdirectory(pureboot pureboot-build)
|
||||||
|
include(${LIBAVR_ROOT}/cmake/checks.cmake)
|
||||||
|
|
||||||
add_executable(fantemp src/main.cpp)
|
add_executable(fantemp src/main.cpp)
|
||||||
target_link_libraries(fantemp PRIVATE libavr)
|
target_link_libraries(fantemp PRIVATE libavr)
|
||||||
@@ -27,5 +37,15 @@ add_custom_command(TARGET fantemp POST_BUILD
|
|||||||
COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom
|
COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom
|
||||||
$<TARGET_FILE:fantemp> $<TARGET_FILE_DIR:fantemp>/fantemp.hex)
|
$<TARGET_FILE:fantemp> $<TARGET_FILE_DIR:fantemp>/fantemp.hex)
|
||||||
|
|
||||||
|
# The board's loader, built here rather than named from memory: consuming
|
||||||
|
# pureboot for its geometry alone left the one image this deployment cannot be
|
||||||
|
# recovered without outside the repository that pins it. Every parameter is
|
||||||
|
# pureboot's own default for this chip - USART0 at 115200 on the 16 MHz crystal
|
||||||
|
# `src/board.hpp` declares - and that is measured rather than assumed, the
|
||||||
|
# image being byte-for-byte the 386 B the board's slot reads back. It goes on
|
||||||
|
# over the wire (`--update-loader`), which is why there is no flash target
|
||||||
|
# beside it: no programmer has ever been in this board's path.
|
||||||
|
pureboot_add_loader(pureboot)
|
||||||
|
|
||||||
enable_testing()
|
enable_testing()
|
||||||
add_subdirectory(test)
|
add_subdirectory(test)
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ cmake --preset atmega328p-generated
|
|||||||
cmake --build --preset atmega328p-generated
|
cmake --build --preset atmega328p-generated
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The firmware is **8004 B** of flash, byte-identical between the generated and
|
||||||
|
reflect modes, and `ctest` holds it to that number.
|
||||||
|
|
||||||
## Atmel Studio
|
## Atmel Studio
|
||||||
|
|
||||||
`master` carries a Studio solution, so this branch does too: `ide/fantemp.atsln`
|
`master` carries a Studio solution, so this branch does too: `ide/fantemp.atsln`
|
||||||
|
|||||||
40
dev/tasks.md
Normal file
40
dev/tasks.md
Normal file
@@ -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 **608 B of flash, 7.6 % of the image** (8004 B down
|
||||||
|
to 7396), 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.
|
||||||
2
libavr
2
libavr
Submodule libavr updated: 71cfb2f1ae...93d8b0e491
1
pureboot
Submodule
1
pureboot
Submodule
Submodule pureboot added at 26a86213ca
@@ -3,7 +3,7 @@
|
|||||||
#include <libavr/libavr.hpp>
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
// The board composition: every peripheral of the fan controller in one
|
// The board composition: every peripheral of the fan controller in one
|
||||||
// place. ATmega328P at 16 MHz — thermistor divider on ADC0 (PC0), fan on
|
// place. ATmega328P at 16 MHz - thermistor divider on ADC0 (PC0), fan on
|
||||||
// OC0B (PD5) at 50 kHz, console on the hardware UART.
|
// OC0B (PD5) at 50 kHz, console on the hardware UART.
|
||||||
namespace app {
|
namespace app {
|
||||||
|
|
||||||
@@ -13,56 +13,67 @@ using dev = avr::device<{.clock = 16_MHz}>;
|
|||||||
|
|
||||||
// Millisecond uptime from timer2 CTC (the fan owns timer0).
|
// Millisecond uptime from timer2 CTC (the fan owns timer0).
|
||||||
class uptime {
|
class uptime {
|
||||||
static inline volatile std::uint64_t ms = 0;
|
static inline volatile std::uint64_t m_ms = 0;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
using ticker = dev::timer2<{.frequency = 1_kHz, .on_compare = [] { ms = ms + 1; }}>;
|
using ticker = dev::timer2<{.frequency = 1_kHz, .on_compare = [] { m_ms = m_ms + 1; }}>;
|
||||||
|
|
||||||
static std::uint64_t millis()
|
static std::uint64_t millis()
|
||||||
{
|
{
|
||||||
avr::irq::atomic_guard lock;
|
avr::irq::interrupt_guard lock;
|
||||||
return ms;
|
return m_ms;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1000-sample averaging window fed by the conversion interrupt.
|
// 1000-sample averaging window fed by the conversion interrupt.
|
||||||
class sampler {
|
class sampler {
|
||||||
static inline volatile std::uint32_t sum = 0;
|
static inline volatile std::uint32_t m_sum = 0;
|
||||||
static inline volatile std::uint16_t count = 0;
|
static inline volatile std::uint16_t m_count = 0;
|
||||||
static inline volatile std::uint16_t window = 0;
|
static inline volatile std::uint16_t m_window = 0;
|
||||||
static inline volatile bool ready = false;
|
static inline volatile bool m_ready = false;
|
||||||
|
|
||||||
static constexpr std::uint16_t samples = 1000;
|
static constexpr std::uint16_t samples = 1000;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
using input = dev::adc<{.input = avr::adc::input_pin(0),
|
using input = dev::adc<{.trigger = avr::adc::trigger::free_running,
|
||||||
.trigger = avr::adc::trigger::free_running,
|
.on_conversion =
|
||||||
.on_conversion = [](std::uint16_t value) {
|
[](std::uint16_t value) {
|
||||||
sum = sum + value;
|
m_sum = m_sum + value;
|
||||||
count = count + 1;
|
m_count = m_count + 1;
|
||||||
if (count >= samples) {
|
if (m_count >= samples) {
|
||||||
window = static_cast<std::uint16_t>(sum / samples);
|
m_window = static_cast<std::uint16_t>(m_sum / samples);
|
||||||
sum = 0;
|
m_sum = 0;
|
||||||
count = 0;
|
m_count = 0;
|
||||||
ready = true;
|
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.
|
// The finished average (raw 10-bit), once per window.
|
||||||
static bool take(std::uint16_t &value)
|
static bool take(std::uint16_t &value)
|
||||||
{
|
{
|
||||||
avr::irq::atomic_guard lock;
|
avr::irq::interrupt_guard lock;
|
||||||
if (!ready)
|
if (!m_ready) {
|
||||||
return false;
|
return false;
|
||||||
value = window;
|
}
|
||||||
ready = false;
|
value = m_window;
|
||||||
|
m_ready = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
using fan = dev::pwm<avr::pd5, {.frequency = 50_kHz}>;
|
using fan = dev::pwm<avr::pd5, {.frequency = 50_kHz}>;
|
||||||
|
|
||||||
using serial_t = dev::uart0<{.baud = 115200_Bd, .rx_buffer = 32, .max_baud_error = 2.5_pct}>;
|
// 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{};
|
inline constexpr serial_t serial{};
|
||||||
|
|
||||||
} // namespace app
|
} // namespace app
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <avr/pgmspace.h>
|
|
||||||
|
|
||||||
#include <libavr/libavr.hpp>
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
#include "board.hpp"
|
#include "board.hpp"
|
||||||
@@ -11,7 +9,7 @@
|
|||||||
// The legacy firmware did this with a watchdog reset: `bootloader` armed the
|
// The legacy firmware did this with a watchdog reset: `bootloader` armed the
|
||||||
// watchdog and hung, and the next boot noticed WDRF and jumped to the boot
|
// watchdog and hung, and the next boot noticed WDRF and jumped to the boot
|
||||||
// section. That works for TinySafeBoot and **does not work for pureboot**, which
|
// section. That works for TinySafeBoot and **does not work for pureboot**, which
|
||||||
// deliberately hands straight back to the application on WDRF — an unattended
|
// deliberately 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
|
// board that watchdog-resets in a loop must not sit in a loader instead of
|
||||||
// running. So a reset-based route into pureboot opens no window at all, and on a
|
// running. So a reset-based route into pureboot opens no window at all, and on a
|
||||||
// board whose only way in is the firmware that is a lockout.
|
// board whose only way in is the firmware that is a lockout.
|
||||||
@@ -22,7 +20,7 @@
|
|||||||
// The address is this board's, and it is not the legacy one: the loader lives in
|
// The address is this board's, and it is not the legacy one: the loader lives in
|
||||||
// the top 512 bytes at 0x7e00 (`hfuse d4` puts the boot section at 0x7c00 with
|
// the top 512 bytes at 0x7e00 (`hfuse d4` puts the boot section at 0x7c00 with
|
||||||
// pureboot's staging slot below its own slot). The legacy firmware probed 0x7800
|
// pureboot's staging slot below its own slot). The legacy firmware probed 0x7800
|
||||||
// — a 2 KB boot section's base — which on this board reads erased, so its check
|
// - a 2 KB boot section's base - which on this board reads erased, so its check
|
||||||
// was always false and its `bootloader` command never actually arrived anywhere.
|
// was always false and its `bootloader` command never actually arrived anywhere.
|
||||||
namespace app {
|
namespace app {
|
||||||
|
|
||||||
@@ -32,14 +30,14 @@ class bootloader {
|
|||||||
using guard = dev::watchdog<{.timeout = 16_ms}>;
|
using guard = dev::watchdog<{.timeout = 16_ms}>;
|
||||||
|
|
||||||
// The top 512 bytes. An erased slot reads 0xffff, which is not an
|
// The top 512 bytes. An erased slot reads 0xffff, which is not an
|
||||||
// instruction any loader begins with — so this asks "is a loader installed"
|
// instruction any loader begins with - so this asks "is a loader installed"
|
||||||
// rather than "is it the one I expect", which is the check the legacy
|
// rather than "is it the one I expect", which is the check the legacy
|
||||||
// firmware got wrong in the other direction by testing one specific byte.
|
// firmware got wrong in the other direction by testing one specific byte.
|
||||||
static constexpr std::uint16_t base = 0x7e00;
|
static constexpr std::uint16_t base = 0x7e00;
|
||||||
|
|
||||||
static bool present()
|
static bool present()
|
||||||
{
|
{
|
||||||
return pgm_read_word(base) != 0xffff;
|
return avr::flash_load(reinterpret_cast<const std::uint16_t *>(base)) != 0xffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A function pointer holds a word address on AVR, so the byte address
|
// A function pointer holds a word address on AVR, so the byte address
|
||||||
@@ -52,24 +50,22 @@ class bootloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// Call first thing in main. reset_cause() reads *and clears* MCUSR, which
|
// Call first thing in main. Clearing MCUSR is the whole job: a lingering
|
||||||
// matters on its own: a lingering WDRF forces the watchdog back on at its
|
// WDRF forces the watchdog back on at its shortest timeout, which is a
|
||||||
// shortest timeout. The diversion below is a leftover of the legacy route
|
// reset loop rather than a boot. The cause is not routed on - pureboot
|
||||||
// and is kept only because it is free and cannot hurt — with BOOTRST
|
// peeks WDRF without clearing it and hands back on purpose, so a watchdog
|
||||||
// programmed the loader has already run before this line, so nothing
|
// reset arrives here with the flag still set, and diverting into the
|
||||||
// normally reaches it.
|
// loader on it would reopen the very window that policy closes.
|
||||||
static void handle_reset()
|
static void handle_reset()
|
||||||
{
|
{
|
||||||
auto cause = avr::power::reset_cause();
|
avr::power::consume_reset_cause();
|
||||||
guard::disable();
|
guard::disable();
|
||||||
if (cause.watchdog && present())
|
|
||||||
call(reinterpret_cast<jump_fn>(base / 2));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hand over for real: no reset, so no WDRF for the loader to refuse.
|
// Hand over for real: no reset, so no WDRF for the loader to refuse.
|
||||||
[[noreturn]] static void enter()
|
[[noreturn]] static void enter()
|
||||||
{
|
{
|
||||||
// Interrupts first — the receive vector and the timer live in this
|
// 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's vector table, and once the loader is running there is no
|
||||||
// application to vector into.
|
// application to vector into.
|
||||||
avr::irq::disable();
|
avr::irq::disable();
|
||||||
@@ -77,7 +73,7 @@ class bootloader {
|
|||||||
|
|
||||||
// Release the USART. While TXEN0 is set the peripheral owns PD1, not the
|
// 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
|
// port register, so a loader that bit-bangs the same pin receives
|
||||||
// perfectly and answers into nothing — mute, not deaf, and unverifiable
|
// perfectly and answers into nothing - mute, not deaf, and unverifiable
|
||||||
// from the host. pureboot clears this itself; TinySafeBoot, which is what
|
// from the host. pureboot clears this itself; TinySafeBoot, which is what
|
||||||
// this board still carries, does not. Four bytes make the hand-over work
|
// this board still carries, does not. Four bytes make the hand-over work
|
||||||
// for either one, which is the only reason this route can be tested
|
// for either one, which is the only reason this route can be tested
|
||||||
|
|||||||
@@ -6,71 +6,73 @@
|
|||||||
#include "curve.hpp"
|
#include "curve.hpp"
|
||||||
#include "thermistor.hpp"
|
#include "thermistor.hpp"
|
||||||
|
|
||||||
// Control loop: averaged thermistor samples → temperature → fan duty
|
// Control loop: averaged thermistor samples -> temperature -> fan duty
|
||||||
// through the curve table (auto) or a console-set value (manual).
|
// through the curve table (auto) or a console-set value (manual).
|
||||||
namespace app {
|
namespace app {
|
||||||
|
|
||||||
class controller {
|
class controller {
|
||||||
static inline std::uint16_t adc_average = 0;
|
static inline std::uint16_t m_adc_average = 0;
|
||||||
static inline std::int16_t temp_quarters = 0;
|
static inline std::int16_t m_temp_quarters = 0;
|
||||||
static inline std::uint8_t percent = 100;
|
static inline std::uint8_t m_percent = 100;
|
||||||
static inline bool auto_mode = true;
|
static inline bool m_auto_mode = true;
|
||||||
static inline bool have_data = false;
|
static inline bool m_have_data = false;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static void init()
|
static void init()
|
||||||
{
|
{
|
||||||
fan::duty(avr::percent_t{10000}); // full blast until the first reading
|
fan::set_duty(100_pct); // full blast until the first reading
|
||||||
}
|
}
|
||||||
|
|
||||||
static void poll()
|
static void poll()
|
||||||
{
|
{
|
||||||
std::uint16_t sample;
|
std::uint16_t sample;
|
||||||
if (!sampler::take(sample))
|
if (!sampler::take(sample)) {
|
||||||
return;
|
return;
|
||||||
adc_average = sample;
|
}
|
||||||
temp_quarters = thermistor::quarters(sample);
|
m_adc_average = sample;
|
||||||
have_data = true;
|
m_temp_quarters = thermistor::quarters(sample);
|
||||||
if (auto_mode)
|
m_have_data = true;
|
||||||
percent = curve::duty(static_cast<std::int8_t>((temp_quarters + 2) / 4));
|
if (m_auto_mode) {
|
||||||
fan::duty(avr::percent_t{static_cast<std::uint16_t>(percent * 100)});
|
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 p)
|
static void set_manual(std::uint8_t p)
|
||||||
{
|
{
|
||||||
auto_mode = false;
|
m_auto_mode = false;
|
||||||
percent = p;
|
m_percent = p;
|
||||||
fan::duty(avr::percent_t{static_cast<std::uint16_t>(p * 100)});
|
fan::set_duty(avr::percent_t::of(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void set_automatic()
|
static void set_automatic()
|
||||||
{
|
{
|
||||||
auto_mode = true;
|
m_auto_mode = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool automatic()
|
static bool automatic()
|
||||||
{
|
{
|
||||||
return auto_mode;
|
return m_auto_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool data_available()
|
static bool data_available()
|
||||||
{
|
{
|
||||||
return have_data;
|
return m_have_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::int16_t temperature_quarters()
|
static std::int16_t temperature_quarters()
|
||||||
{
|
{
|
||||||
return temp_quarters;
|
return m_temp_quarters;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::uint16_t last_adc()
|
static std::uint16_t last_adc()
|
||||||
{
|
{
|
||||||
return adc_average;
|
return m_adc_average;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::uint8_t fan_percent()
|
static std::uint8_t fan_percent()
|
||||||
{
|
{
|
||||||
return percent;
|
return m_percent;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,43 +5,55 @@
|
|||||||
|
|
||||||
#include <libavr/flash.hpp>
|
#include <libavr/flash.hpp>
|
||||||
|
|
||||||
// The auto-mode fan curve, tabulated at compile time: a cubic in °C
|
// The auto-mode fan curve, tabulated at compile time as a flash_table of duty
|
||||||
// (0.002246·x³ − 0.09·x² + 0.91·x, zero below 20 °C) as a flash_table of
|
// percent per C: a cubic that is held at zero below the temperature the fan
|
||||||
// duty percent per °C.
|
// starts at.
|
||||||
namespace app::curve {
|
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 {
|
namespace detail {
|
||||||
|
|
||||||
consteval std::uint8_t duty_entry(int celsius)
|
consteval std::uint8_t duty_entry(std::int32_t celsius)
|
||||||
{
|
{
|
||||||
double x = celsius;
|
double x = celsius;
|
||||||
if (x < 20)
|
if (x < start_celsius) {
|
||||||
return 0;
|
return 0;
|
||||||
double duty = 0.002246 * x * x * x - 0.09 * x * x + 0.91 * x;
|
}
|
||||||
if (duty < 0)
|
double duty = cubic_term * x * x * x + square_term * x * x + linear_term * x;
|
||||||
|
if (duty < 0) {
|
||||||
duty = 0;
|
duty = 0;
|
||||||
if (duty > 100)
|
}
|
||||||
|
if (duty > 100) {
|
||||||
duty = 100;
|
duty = 100;
|
||||||
|
}
|
||||||
return static_cast<std::uint8_t>(duty + 0.5);
|
return static_cast<std::uint8_t>(duty + 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline constexpr avr::flash_table<[] {
|
inline constexpr avr::flash_table<[] {
|
||||||
std::array<std::uint8_t, 100> out{};
|
std::array<std::uint8_t, 100> out{};
|
||||||
for (int t = 0; t < 100; ++t)
|
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);
|
out[static_cast<std::size_t>(t)] = duty_entry(t);
|
||||||
|
}
|
||||||
return out;
|
return out;
|
||||||
}()>
|
}()>
|
||||||
table;
|
table;
|
||||||
|
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
|
|
||||||
// Duty percent for a temperature (clamped to the 0..99 °C table window).
|
// Duty percent for a temperature, clamped to the table's own window.
|
||||||
inline std::uint8_t duty(std::int8_t celsius)
|
inline std::uint8_t duty(std::int8_t celsius)
|
||||||
{
|
{
|
||||||
if (celsius < 0)
|
constexpr auto highest = static_cast<std::int8_t>(detail::table.size() - 1);
|
||||||
|
if (celsius < 0) {
|
||||||
celsius = 0;
|
celsius = 0;
|
||||||
if (celsius > 99)
|
}
|
||||||
celsius = 99;
|
if (celsius > highest) {
|
||||||
|
celsius = highest;
|
||||||
|
}
|
||||||
return detail::table[static_cast<std::uint8_t>(celsius)];
|
return detail::table[static_cast<std::uint8_t>(celsius)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "controller.hpp"
|
#include "controller.hpp"
|
||||||
#include "statistics.hpp"
|
#include "statistics.hpp"
|
||||||
#include "terminal.hpp"
|
#include "terminal.hpp"
|
||||||
|
#include "thermistor.hpp"
|
||||||
|
|
||||||
using namespace app;
|
using namespace app;
|
||||||
|
|
||||||
@@ -16,14 +17,15 @@ int main()
|
|||||||
|
|
||||||
avr::init<uptime::ticker, sampler::input, fan, serial_t, statistics>();
|
avr::init<uptime::ticker, sampler::input, fan, serial_t, statistics>();
|
||||||
avr::irq::enable();
|
avr::irq::enable();
|
||||||
sampler::input::start();
|
sampler::thermistor::start();
|
||||||
controller::init();
|
controller::init();
|
||||||
terminal::init();
|
terminal::init();
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
controller::poll();
|
controller::poll();
|
||||||
if (controller::data_available())
|
if (controller::data_available()) {
|
||||||
statistics::record(static_cast<std::int8_t>((controller::temperature_quarters() + 2) / 4));
|
statistics::record(thermistor::whole_degrees(controller::temperature_quarters()));
|
||||||
|
}
|
||||||
terminal::poll();
|
terminal::poll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
#include "board.hpp"
|
#include "board.hpp"
|
||||||
|
|
||||||
// Temperature histogram: one uint32 bucket per °C 0..99, sampled once a
|
// Temperature histogram: one uint32 bucket per C 0..99, sampled once a
|
||||||
// second, written back to EEPROM every 30 minutes (update() only touches
|
// second, written back to EEPROM every 30 minutes (update() only touches
|
||||||
// changed bytes). Erased EEPROM reads back as 0xffffffff — treated as 0.
|
// changed bytes).
|
||||||
namespace app {
|
namespace app {
|
||||||
|
|
||||||
class statistics {
|
class statistics {
|
||||||
@@ -17,11 +17,15 @@ class statistics {
|
|||||||
static constexpr std::uint32_t sample_delay_ms = 1'000;
|
static constexpr std::uint32_t sample_delay_ms = 1'000;
|
||||||
static constexpr std::uint32_t writeback_delay_ms = 1'800'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>;
|
using stored = avr::eeprom::var<std::array<std::uint32_t, range>, 0>;
|
||||||
|
|
||||||
static inline std::array<std::uint32_t, range> histogram{};
|
static inline std::array<std::uint32_t, range> m_histogram{};
|
||||||
static inline std::uint64_t last_sample = 0;
|
static inline std::uint64_t m_last_sample = 0;
|
||||||
static inline std::uint64_t last_writeback = 0;
|
static inline std::uint64_t m_last_writeback = 0;
|
||||||
|
|
||||||
static constexpr std::uint8_t clamp(std::int8_t t)
|
static constexpr std::uint8_t clamp(std::int8_t t)
|
||||||
{
|
{
|
||||||
@@ -33,72 +37,81 @@ class statistics {
|
|||||||
|
|
||||||
static void init()
|
static void init()
|
||||||
{
|
{
|
||||||
histogram = stored::read();
|
m_histogram = stored::read();
|
||||||
for (auto &bucket : histogram)
|
for (auto &bucket : m_histogram) {
|
||||||
if (bucket == 0xffffffff)
|
if (bucket == never_written) {
|
||||||
bucket = 0;
|
bucket = 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void record(std::int8_t celsius)
|
static void record(std::int8_t celsius)
|
||||||
{
|
{
|
||||||
auto now = uptime::millis();
|
auto now = uptime::millis();
|
||||||
if (now >= last_sample + sample_delay_ms) {
|
if (now >= m_last_sample + sample_delay_ms) {
|
||||||
++histogram[clamp(celsius)];
|
++m_histogram[clamp(celsius)];
|
||||||
last_sample = now;
|
m_last_sample = now;
|
||||||
}
|
}
|
||||||
if (now >= last_writeback + writeback_delay_ms) {
|
if (now >= m_last_writeback + writeback_delay_ms) {
|
||||||
save();
|
save();
|
||||||
last_writeback = now;
|
m_last_writeback = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void save()
|
static void save()
|
||||||
{
|
{
|
||||||
stored::update(histogram);
|
stored::update(m_histogram);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void reset()
|
static void reset()
|
||||||
{
|
{
|
||||||
histogram = {};
|
m_histogram = {};
|
||||||
stored::update(histogram);
|
stored::update(m_histogram);
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::uint8_t min_temperature()
|
static std::uint8_t min_temperature()
|
||||||
{
|
{
|
||||||
for (std::uint8_t i = 0; i < range; ++i)
|
for (std::uint8_t i = 0; i < range; ++i) {
|
||||||
if (histogram[i])
|
if (m_histogram[i]) {
|
||||||
return i;
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
return range;
|
return range;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::uint8_t max_temperature()
|
static std::uint8_t max_temperature()
|
||||||
{
|
{
|
||||||
for (std::uint8_t i = range; i > 0; --i)
|
for (std::uint8_t i = range; i > 0; --i) {
|
||||||
if (histogram[i - 1])
|
if (m_histogram[i - 1]) {
|
||||||
return i - 1;
|
return i - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::uint64_t total_samples()
|
static std::uint64_t total_samples()
|
||||||
{
|
{
|
||||||
std::uint64_t total = 0;
|
std::uint64_t total = 0;
|
||||||
for (auto bucket : histogram)
|
for (auto bucket : m_histogram) {
|
||||||
total += bucket;
|
total += bucket;
|
||||||
|
}
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::uint32_t highest_bucket()
|
static std::uint32_t highest_bucket()
|
||||||
{
|
{
|
||||||
std::uint32_t highest = 0;
|
std::uint32_t highest = 0;
|
||||||
for (auto bucket : histogram)
|
for (auto bucket : m_histogram) {
|
||||||
if (bucket > highest)
|
if (bucket > highest) {
|
||||||
highest = bucket;
|
highest = bucket;
|
||||||
|
}
|
||||||
|
}
|
||||||
return highest;
|
return highest;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::uint32_t bucket(std::uint8_t celsius)
|
static std::uint32_t bucket(std::uint8_t celsius)
|
||||||
{
|
{
|
||||||
return histogram[clamp(static_cast<std::int8_t>(celsius))];
|
return m_histogram[clamp(static_cast<std::int8_t>(celsius))];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
335
src/terminal.hpp
335
src/terminal.hpp
@@ -18,44 +18,89 @@
|
|||||||
// until Ctrl+C, and Ctrl+C abandons a half-typed line anywhere else.
|
// until Ctrl+C, and Ctrl+C abandons a half-typed line anywhere else.
|
||||||
namespace app {
|
namespace app {
|
||||||
|
|
||||||
class terminal {
|
// Commands, in the order they are matched - which is the order the original
|
||||||
static constexpr char ctrl_c = 0x03;
|
|
||||||
static constexpr std::uint8_t line_max = 24;
|
|
||||||
static inline char line[line_max]{};
|
|
||||||
static inline std::uint8_t at = 0;
|
|
||||||
static inline bool overflowed = false;
|
|
||||||
static inline bool monitoring = false;
|
|
||||||
static inline std::uint64_t last_monitor = 0;
|
|
||||||
|
|
||||||
// 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
|
// 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,
|
// 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
|
// not set) exactly as it always was, and anything appended to this list cannot
|
||||||
// cannot steal an abbreviation that already meant something else.
|
// steal an abbreviation that already meant something else.
|
||||||
struct command {
|
//
|
||||||
std::string_view name;
|
// A function rather than a variable, so the list exists only while the flash
|
||||||
bool exact; // reset only: an abbreviation must not be able to wipe data
|
// blob below is being built and never as storage of its own.
|
||||||
};
|
consteval auto command_names()
|
||||||
|
{
|
||||||
|
return std::to_array<std::string_view>({
|
||||||
|
"help",
|
||||||
|
"show",
|
||||||
|
"curve",
|
||||||
|
"monitor",
|
||||||
|
"bootloader",
|
||||||
|
"uptime",
|
||||||
|
"statistics",
|
||||||
|
"histogram",
|
||||||
|
"reset",
|
||||||
|
"set",
|
||||||
|
"auto",
|
||||||
|
"version",
|
||||||
|
"save",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static constexpr std::array<command, 13> commands{{
|
class terminal {
|
||||||
{"help", false},
|
static constexpr char ctrl_c = 0x03;
|
||||||
{"show", false},
|
static constexpr char backspace = 0x08;
|
||||||
{"curve", false},
|
static constexpr char del = 0x7f;
|
||||||
{"monitor", false},
|
static inline std::array<char, 24> m_line{};
|
||||||
{"bootloader", false},
|
static inline std::uint8_t m_at = 0;
|
||||||
{"uptime", false},
|
static inline bool m_overflowed = false;
|
||||||
{"statistics", false},
|
static inline bool m_monitoring = false;
|
||||||
{"histogram", false},
|
static inline std::uint64_t m_last_monitor = 0;
|
||||||
{"reset", true},
|
|
||||||
{"set", false},
|
static constexpr std::uint8_t command_count = command_names().size();
|
||||||
{"auto", false},
|
|
||||||
{"version", false},
|
static constexpr std::size_t name_bytes = [] {
|
||||||
{"save", false},
|
std::size_t total = 0;
|
||||||
}};
|
for (auto one : command_names()) {
|
||||||
|
total += one.size() + 1;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}();
|
||||||
|
|
||||||
|
// The names as one NUL-separated blob in flash. Matched 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,
|
||||||
|
// and on this part that was 216 B of 2 KB spent on data that never changes.
|
||||||
|
// Separators rather than an offset table, because an offset table is the
|
||||||
|
// RAM this exists to give back.
|
||||||
|
static constexpr auto names_data = [] {
|
||||||
|
std::array<char, name_bytes> chars{};
|
||||||
|
std::size_t at = 0;
|
||||||
|
for (auto one : command_names()) {
|
||||||
|
for (char c : one) {
|
||||||
|
chars[at++] = c;
|
||||||
|
}
|
||||||
|
chars[at++] = '\0';
|
||||||
|
}
|
||||||
|
return chars;
|
||||||
|
}();
|
||||||
|
|
||||||
|
static constexpr avr::flash_table<names_data> names{};
|
||||||
|
|
||||||
|
// `reset` must be typed in full - an abbreviation must not be able to wipe
|
||||||
|
// data. Found in the list rather than written as an index, so reordering the
|
||||||
|
// commands cannot move the protection onto a different one.
|
||||||
|
static constexpr std::uint8_t exact_command = [] {
|
||||||
|
const auto list = command_names();
|
||||||
|
for (std::uint8_t i = 0; i < list.size(); ++i) {
|
||||||
|
if (list[i] == "reset") {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return static_cast<std::uint8_t>(list.size());
|
||||||
|
}();
|
||||||
|
|
||||||
// Column the descriptions' colons line up in, counted from the start of the
|
// Column the descriptions' colons line up in, counted from the start of the
|
||||||
// name. The longest name is `bootloader` at 10, so 12 leaves it a space and
|
// name. The longest name is `bootloader` at 10, so 12 leaves it a space and
|
||||||
// one dot — the original's layout exactly.
|
// one dot - the original's layout exactly.
|
||||||
static constexpr std::uint8_t help_column = 12;
|
static constexpr std::uint8_t help_column = 12;
|
||||||
|
|
||||||
static void prompt()
|
static void prompt()
|
||||||
@@ -63,42 +108,45 @@ class terminal {
|
|||||||
serial << "> "_P;
|
serial << "> "_P;
|
||||||
}
|
}
|
||||||
|
|
||||||
// `name ....: ` — the dotted label the original used everywhere it printed a
|
// `name ....: ` - the dotted label the original used everywhere it printed a
|
||||||
// list of things, which is what makes a column of values readable without
|
// list of things, which is what makes a column of values readable without
|
||||||
// counting spaces. One renderer for all three users; only the column differs.
|
// counting spaces. Its width comes from the type, so the padding needs no
|
||||||
static void label(std::string_view text, std::uint8_t column)
|
// hand-counted constant.
|
||||||
{
|
|
||||||
serial << text << ' ';
|
|
||||||
for (auto i = text.size() + 1; i < column; ++i)
|
|
||||||
serial << '.';
|
|
||||||
serial << ": "_P;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The same, for a label that is a literal rather than a command name: it
|
|
||||||
// stays in flash, and its width comes from the type, so the padding needs no
|
|
||||||
// hand-counted constant. The command names cannot use this — they are
|
|
||||||
// string_views because they are matched at run time.
|
|
||||||
template <typename Flash>
|
template <typename Flash>
|
||||||
static void label(Flash text, std::uint8_t column)
|
static void label(Flash text, std::uint8_t column)
|
||||||
{
|
{
|
||||||
serial << text << ' ';
|
serial << text << ' ';
|
||||||
for (auto i = Flash::size + 1; i < column; ++i)
|
for (auto i = Flash::size + 1; i < column; ++i) {
|
||||||
serial << '.';
|
serial << '.';
|
||||||
|
}
|
||||||
serial << ": "_P;
|
serial << ": "_P;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void help_row(std::string_view name)
|
// The same shape for a command name, whose width is not a type but the walk
|
||||||
|
// to its separator - so it is counted as it is printed rather than measured
|
||||||
|
// first, which would read the blob twice.
|
||||||
|
static void help_row(std::uint8_t which)
|
||||||
{
|
{
|
||||||
label(name, help_column);
|
auto at = name_start(which);
|
||||||
|
std::uint8_t width = 0;
|
||||||
|
for (char c; (c = names.at(at + width)) != '\0'; ++width) {
|
||||||
|
serial << c;
|
||||||
|
}
|
||||||
|
serial << ' ';
|
||||||
|
for (std::uint8_t i = width + 1; i < help_column; ++i) {
|
||||||
|
serial << '.';
|
||||||
|
}
|
||||||
|
serial << ": "_P;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quarter-°C as a signed decimal with a two-digit fraction. The sign is taken
|
// Quarter- C 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
|
// off first: C++ gives a negative remainder for a negative dividend, so
|
||||||
// `(q % 4) * 25` on -40.25 C yields -25 and prints "-40.-25".
|
// `(q % 4) * 25` on -40.25 C yields -25 and prints "-40.-25".
|
||||||
static void temperature(std::int16_t quarters)
|
static void temperature(std::int16_t quarters)
|
||||||
{
|
{
|
||||||
if (quarters < 0)
|
if (quarters < 0) {
|
||||||
serial << '-';
|
serial << '-';
|
||||||
|
}
|
||||||
auto magnitude = static_cast<std::uint16_t>(quarters < 0 ? -quarters : quarters);
|
auto magnitude = static_cast<std::uint16_t>(quarters < 0 ? -quarters : quarters);
|
||||||
serial << magnitude / 4 << '.' << avr::dec<{.width = 2, .fill = '0'}>((magnitude % 4) * 25);
|
serial << magnitude / 4 << '.' << avr::dec<{.width = 2, .fill = '0'}>((magnitude % 4) * 25);
|
||||||
}
|
}
|
||||||
@@ -106,31 +154,31 @@ class terminal {
|
|||||||
static void help()
|
static void help()
|
||||||
{
|
{
|
||||||
serial << "\r\nFanTemp "_P << version << " command overview\r\n"_P;
|
serial << "\r\nFanTemp "_P << version << " command overview\r\n"_P;
|
||||||
help_row(commands[0].name);
|
help_row(0);
|
||||||
serial << "prints this help message\r\n"_P;
|
serial << "prints this help message\r\n"_P;
|
||||||
help_row(commands[1].name);
|
help_row(1);
|
||||||
serial << "shows current temperature and fan speed\r\n"_P;
|
serial << "shows current temperature and fan speed\r\n"_P;
|
||||||
help_row(commands[2].name);
|
help_row(2);
|
||||||
serial << "shows mapping from temperature to fan speed\r\n"_P;
|
serial << "shows mapping from temperature to fan speed\r\n"_P;
|
||||||
help_row(commands[3].name);
|
help_row(3);
|
||||||
serial << "loops the show command until Ctrl+C is pressed\r\n"_P;
|
serial << "loops the show command until Ctrl+C is pressed\r\n"_P;
|
||||||
help_row(commands[4].name);
|
help_row(4);
|
||||||
serial << "enters the bootloader\r\n"_P;
|
serial << "enters the bootloader\r\n"_P;
|
||||||
help_row(commands[5].name);
|
help_row(5);
|
||||||
serial << "shows system uptime\r\n"_P;
|
serial << "shows system uptime\r\n"_P;
|
||||||
help_row(commands[6].name);
|
help_row(6);
|
||||||
serial << "prints overall statistics like min and max temp\r\n"_P;
|
serial << "prints overall statistics like min and max temp\r\n"_P;
|
||||||
help_row(commands[7].name);
|
help_row(7);
|
||||||
serial << "prints a histogram of the temperature\r\n"_P;
|
serial << "prints a histogram of the temperature\r\n"_P;
|
||||||
help_row(commands[8].name);
|
help_row(8);
|
||||||
serial << "resets statistics to 0 in EEPROM and RAM (no abbreviation)\r\n"_P;
|
serial << "resets statistics to 0 in EEPROM and RAM (no abbreviation)\r\n"_P;
|
||||||
help_row(commands[9].name);
|
help_row(9);
|
||||||
serial << "sets the fan speed to the provided value, 0-100\r\n"_P;
|
serial << "sets the fan speed to the provided value, 0-100\r\n"_P;
|
||||||
help_row(commands[10].name);
|
help_row(10);
|
||||||
serial << "turns on automatic fan control\r\n"_P;
|
serial << "turns on automatic fan control\r\n"_P;
|
||||||
help_row(commands[11].name);
|
help_row(11);
|
||||||
serial << "displays firmware version\r\n"_P;
|
serial << "displays firmware version\r\n"_P;
|
||||||
help_row(commands[12].name);
|
help_row(12);
|
||||||
serial << "writes the statistics to EEPROM now\r\n"_P;
|
serial << "writes the statistics to EEPROM now\r\n"_P;
|
||||||
serial << "commands may be abbreviated: 'up' is uptime\r\n"_P;
|
serial << "commands may be abbreviated: 'up' is uptime\r\n"_P;
|
||||||
}
|
}
|
||||||
@@ -142,9 +190,10 @@ class terminal {
|
|||||||
static std::uint32_t resistance()
|
static std::uint32_t resistance()
|
||||||
{
|
{
|
||||||
auto adc = controller::last_adc();
|
auto adc = controller::last_adc();
|
||||||
if (adc >= 1023)
|
if (adc >= thermistor::adc_full_scale) {
|
||||||
return 0xffffffff; // open circuit: the divider has no solution
|
return open_circuit;
|
||||||
return static_cast<std::uint32_t>(thermistor::series_resistor) * adc / (1023u - adc);
|
}
|
||||||
|
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
|
// One value per line behind a dotted label, as the original had it. A single
|
||||||
@@ -160,10 +209,11 @@ class terminal {
|
|||||||
serial << controller::last_adc() << " / 1023\r\n"_P;
|
serial << controller::last_adc() << " / 1023\r\n"_P;
|
||||||
|
|
||||||
label("Resistance"_P, reading_column);
|
label("Resistance"_P, reading_column);
|
||||||
if (auto ohms = resistance(); ohms == open_circuit)
|
if (auto ohms = resistance(); ohms == open_circuit) {
|
||||||
serial << "open circuit\r\n"_P;
|
serial << "open circuit\r\n"_P;
|
||||||
else
|
} else {
|
||||||
serial << ohms << " Ohm\r\n"_P;
|
serial << ohms << " Ohm\r\n"_P;
|
||||||
|
}
|
||||||
|
|
||||||
label("Temperature"_P, reading_column);
|
label("Temperature"_P, reading_column);
|
||||||
temperature(controller::temperature_quarters());
|
temperature(controller::temperature_quarters());
|
||||||
@@ -171,11 +221,12 @@ class terminal {
|
|||||||
|
|
||||||
label("Fan speed"_P, reading_column);
|
label("Fan speed"_P, reading_column);
|
||||||
serial << controller::fan_percent() << "% "_P;
|
serial << controller::fan_percent() << "% "_P;
|
||||||
if (controller::automatic())
|
if (controller::automatic()) {
|
||||||
serial << "auto\r\n"_P;
|
serial << "auto\r\n"_P;
|
||||||
else
|
} else {
|
||||||
serial << "manual\r\n"_P;
|
serial << "manual\r\n"_P;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Every whole degree from 10 to 60 with a bar, which is the original's and is
|
// 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
|
// the point of the command: the curve is a cubic, and five-degree samples
|
||||||
@@ -187,8 +238,9 @@ class terminal {
|
|||||||
auto duty = curve::duty(static_cast<std::int8_t>(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)
|
serial << avr::dec<{.width = 2, .fill = '0'}>(t) << " C = "_P << avr::dec<{.width = 3, .fill = ' '}>(duty)
|
||||||
<< "% |"_P;
|
<< "% |"_P;
|
||||||
for (std::uint8_t i = 0; i < duty; ++i)
|
for (std::uint8_t i = 0; i < duty; ++i) {
|
||||||
serial << '#';
|
serial << '#';
|
||||||
|
}
|
||||||
serial << "\r\n"_P;
|
serial << "\r\n"_P;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,16 +256,18 @@ class terminal {
|
|||||||
{
|
{
|
||||||
auto empty = statistics::total_samples() == 0;
|
auto empty = statistics::total_samples() == 0;
|
||||||
label("Minimum temperature"_P, stat_column);
|
label("Minimum temperature"_P, stat_column);
|
||||||
if (empty)
|
if (empty) {
|
||||||
serial << "not available\r\n"_P;
|
serial << "not available\r\n"_P;
|
||||||
else
|
} else {
|
||||||
serial << statistics::min_temperature() << " C\r\n"_P;
|
serial << statistics::min_temperature() << " C\r\n"_P;
|
||||||
|
}
|
||||||
|
|
||||||
label("Maximum temperature"_P, stat_column);
|
label("Maximum temperature"_P, stat_column);
|
||||||
if (empty)
|
if (empty) {
|
||||||
serial << "not available\r\n"_P;
|
serial << "not available\r\n"_P;
|
||||||
else
|
} else {
|
||||||
serial << statistics::max_temperature() << " C\r\n"_P;
|
serial << statistics::max_temperature() << " C\r\n"_P;
|
||||||
|
}
|
||||||
|
|
||||||
label("Total samples"_P, stat_column);
|
label("Total samples"_P, stat_column);
|
||||||
serial << static_cast<std::uint32_t>(statistics::total_samples()) << "\r\n"_P;
|
serial << static_cast<std::uint32_t>(statistics::total_samples()) << "\r\n"_P;
|
||||||
@@ -232,8 +286,9 @@ class terminal {
|
|||||||
// neighbouring buckets, which on a distribution this narrow is the whole
|
// neighbouring buckets, which on a distribution this narrow is the whole
|
||||||
// picture.
|
// picture.
|
||||||
std::uint32_t factor = highest / bar_max > 1 ? highest / bar_max : 1;
|
std::uint32_t factor = highest / bar_max > 1 ? highest / bar_max : 1;
|
||||||
while (highest / factor > bar_max)
|
while (highest / factor > bar_max) {
|
||||||
++factor;
|
++factor;
|
||||||
|
}
|
||||||
|
|
||||||
for (std::uint8_t t = statistics::min_temperature(); t <= statistics::max_temperature(); ++t) {
|
for (std::uint8_t t = statistics::min_temperature(); t <= statistics::max_temperature(); ++t) {
|
||||||
auto count = statistics::bucket(t);
|
auto count = statistics::bucket(t);
|
||||||
@@ -241,56 +296,88 @@ class terminal {
|
|||||||
// instead of trailing off the ragged right-hand end of the bars.
|
// 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)
|
serial << avr::dec<{.width = 2, .fill = '0'}>(t) << " C : "_P << avr::dec<{.width = 10, .fill = ' '}>(count)
|
||||||
<< " |"_P;
|
<< " |"_P;
|
||||||
for (std::uint32_t i = 0; i < count / factor; ++i)
|
for (std::uint32_t i = 0; i < count / factor; ++i) {
|
||||||
serial << '#';
|
serial << '#';
|
||||||
|
}
|
||||||
serial << "\r\n"_P;
|
serial << "\r\n"_P;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Abbreviations: the input matches a command when it is a non-empty prefix
|
// Where the given command's name begins in the blob. A walk over the
|
||||||
// of it. `reset` is the exception and must be typed in full.
|
// separators, for the two callers that need to start at one name.
|
||||||
//
|
static constexpr std::uint8_t name_start(std::uint8_t which)
|
||||||
// starts_with, not substr: substr throws std::out_of_range, and one
|
|
||||||
// potentially-throwing call is enough to pull in std::terminate, which does
|
|
||||||
// not exist in a freestanding AVR build. The link fails rather than the
|
|
||||||
// firmware, so this is a build-time trap rather than a runtime one — but it
|
|
||||||
// is a trap, and the whole file avoids substr for that reason.
|
|
||||||
static bool matches(std::string_view input, const command &c)
|
|
||||||
{
|
{
|
||||||
if (input.empty())
|
std::uint8_t at = 0;
|
||||||
return false;
|
for (std::uint8_t i = 0; i < which; ++i) {
|
||||||
if (c.exact)
|
while (names.at(at) != '\0') {
|
||||||
return input == c.name;
|
++at;
|
||||||
return c.name.starts_with(input);
|
}
|
||||||
|
++at;
|
||||||
|
}
|
||||||
|
return at;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Which command a word names, or `command_count` 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)
|
static void dispatch(std::string_view input)
|
||||||
{
|
{
|
||||||
// A line that overflowed the buffer is not a command — it is the tail of
|
// 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.
|
// one. Acting on it is how a truncated `reset` becomes a surprise.
|
||||||
if (overflowed) {
|
if (m_overflowed) {
|
||||||
serial << "input too long, ignored\r\n"_P;
|
serial << "input too long, ignored\r\n"_P;
|
||||||
overflowed = false;
|
m_overflowed = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split on the first space with the (pointer, length) constructor rather
|
// Split on the first space with the (pointer, length) constructor rather
|
||||||
// than substr, which throws — see matches().
|
// than substr, which throws - see matches().
|
||||||
const auto space = input.find(' ');
|
const auto space = input.find(' ');
|
||||||
const auto word = space == std::string_view::npos ? input : std::string_view{input.data(), space};
|
const auto word = space == std::string_view::npos ? input : std::string_view{input.data(), space};
|
||||||
const auto rest = space == std::string_view::npos
|
const auto rest = space == std::string_view::npos
|
||||||
? std::string_view{}
|
? std::string_view{}
|
||||||
: std::string_view{input.data() + space + 1, input.size() - space - 1};
|
: std::string_view{input.data() + space + 1, input.size() - space - 1};
|
||||||
if (word.empty())
|
if (word.empty()) {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
std::uint8_t which = commands.size();
|
|
||||||
for (std::uint8_t i = 0; i < commands.size(); ++i)
|
|
||||||
if (matches(word, commands[i])) {
|
|
||||||
which = i;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto which = lookup(word);
|
||||||
|
|
||||||
switch (which) {
|
switch (which) {
|
||||||
case 0:
|
case 0:
|
||||||
help();
|
help();
|
||||||
@@ -302,7 +389,7 @@ class terminal {
|
|||||||
print_curve();
|
print_curve();
|
||||||
return;
|
return;
|
||||||
case 3:
|
case 3:
|
||||||
monitoring = true;
|
m_monitoring = true;
|
||||||
return;
|
return;
|
||||||
case 4:
|
case 4:
|
||||||
serial << "entering bootloader\r\n"_P;
|
serial << "entering bootloader\r\n"_P;
|
||||||
@@ -331,9 +418,10 @@ class terminal {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
percent = static_cast<std::uint16_t>(percent * 10 + (c - '0'));
|
percent = static_cast<std::uint16_t>(percent * 10 + (c - '0'));
|
||||||
if (percent > 100)
|
if (percent > 100) {
|
||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (valid) {
|
if (valid) {
|
||||||
controller::set_manual(static_cast<std::uint8_t>(percent));
|
controller::set_manual(static_cast<std::uint8_t>(percent));
|
||||||
serial << "fan "_P << percent << " %, manual\r\n"_P;
|
serial << "fan "_P << percent << " %, manual\r\n"_P;
|
||||||
@@ -360,7 +448,10 @@ class terminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static constexpr std::string_view version = "v2.2";
|
// 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;
|
static constexpr std::uint8_t bar_max = 100;
|
||||||
|
|
||||||
// Columns the dotted labels' colons land in, and the curve's span. All four
|
// Columns the dotted labels' colons land in, and the curve's span. All four
|
||||||
@@ -381,17 +472,17 @@ class terminal {
|
|||||||
|
|
||||||
static void poll()
|
static void poll()
|
||||||
{
|
{
|
||||||
if (monitoring) {
|
if (m_monitoring) {
|
||||||
if (uptime::millis() >= last_monitor + 1000) {
|
if (uptime::millis() >= m_last_monitor + 1000) {
|
||||||
show();
|
show();
|
||||||
last_monitor = uptime::millis();
|
m_last_monitor = uptime::millis();
|
||||||
}
|
}
|
||||||
// Ctrl+C only, as the original had it. Stopping on *any* byte reads
|
// 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
|
// well until a host sends a line ending: `monitor\r\n` then stops
|
||||||
// itself on the `\n` it arrived with, one reading in.
|
// itself on the `\n` it arrived with, one reading in.
|
||||||
if (auto in = serial_t::read(); in && *in == ctrl_c) {
|
if (auto in = serial_t::read(); in && *in == ctrl_c) {
|
||||||
serial << "^C\r\n"_P;
|
serial << "^C\r\n"_P;
|
||||||
monitoring = false;
|
m_monitoring = false;
|
||||||
prompt();
|
prompt();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -402,35 +493,35 @@ class terminal {
|
|||||||
// Abandon whatever was typed and start a fresh line, which is
|
// Abandon whatever was typed and start a fresh line, which is
|
||||||
// what Ctrl+C means at every other prompt in the world.
|
// what Ctrl+C means at every other prompt in the world.
|
||||||
serial << "^C\r\n"_P;
|
serial << "^C\r\n"_P;
|
||||||
at = 0;
|
m_at = 0;
|
||||||
overflowed = false;
|
m_overflowed = false;
|
||||||
prompt();
|
prompt();
|
||||||
} else if (c == '\r' || c == '\n') {
|
} else if (c == '\r' || c == '\n') {
|
||||||
serial << "\r\n"_P;
|
serial << "\r\n"_P;
|
||||||
if (at == 0 && !overflowed) {
|
if (m_at == 0 && !m_overflowed) {
|
||||||
prompt(); // a bare Enter just reprompts, no gap needed
|
prompt(); // a bare Enter just reprompts, no gap needed
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
dispatch(std::string_view{line, at});
|
dispatch(std::string_view{m_line.data(), m_at});
|
||||||
at = 0;
|
m_at = 0;
|
||||||
if (!monitoring) {
|
if (!m_monitoring) {
|
||||||
// A blank line between a command's output and the next
|
// A blank line between a command's output and the next
|
||||||
// prompt: without it the answer and the thing you type
|
// prompt: without it the answer and the thing you type
|
||||||
// next run together and a screen of them is unreadable.
|
// next run together and a screen of them is unreadable.
|
||||||
serial << "\r\n"_P;
|
serial << "\r\n"_P;
|
||||||
prompt();
|
prompt();
|
||||||
}
|
}
|
||||||
} else if (c == 0x7f || c == 0x08) {
|
} else if (c == del || c == backspace) {
|
||||||
if (at) {
|
if (m_at) {
|
||||||
--at;
|
--m_at;
|
||||||
serial << "\b \b"_P;
|
serial << "\b \b"_P;
|
||||||
}
|
}
|
||||||
} else if (c >= ' ') {
|
} else if (c >= ' ') {
|
||||||
if (at < line_max) {
|
if (m_at < m_line.size()) {
|
||||||
line[at++] = c;
|
m_line[m_at++] = c;
|
||||||
serial << c; // echo
|
serial << c; // echo
|
||||||
} else {
|
} else {
|
||||||
overflowed = true; // reported when the line is submitted
|
m_overflowed = true; // reported when the line is submitted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,14 @@
|
|||||||
|
|
||||||
// NTC thermistor on a series divider, solved entirely at compile time:
|
// NTC thermistor on a series divider, solved entirely at compile time:
|
||||||
// the Beta equation (logarithm and all) runs consteval into a libavr
|
// the Beta equation (logarithm and all) runs consteval into a libavr
|
||||||
// flash_table — the firmware never does floating point. Raw 10-bit ADC
|
// flash_table - the firmware never does floating point. Raw 10-bit ADC
|
||||||
// counts map to quarter-°C with linear interpolation between table steps.
|
// counts map to quarter- C with linear interpolation between table steps.
|
||||||
namespace app::thermistor {
|
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 series_resistor = 9951;
|
||||||
inline constexpr double nominal_resistance = 9270;
|
inline constexpr double nominal_resistance = 9270;
|
||||||
inline constexpr double beta = 3212;
|
inline constexpr double beta = 3212;
|
||||||
@@ -20,42 +24,47 @@ namespace detail {
|
|||||||
|
|
||||||
consteval double temperature_of(double adc)
|
consteval double temperature_of(double adc)
|
||||||
{
|
{
|
||||||
double resistance = series_resistor * adc / (1023.0 - adc);
|
double resistance = series_resistor * adc / (adc_full_scale - adc);
|
||||||
// __builtin_log constant-folds on the AVR backend, so the table is
|
// __builtin_log constant-folds on the AVR backend, so the table is
|
||||||
// built at compile time with no runtime libm.
|
// built at compile time with no runtime libm.
|
||||||
double steinhart = __builtin_log(resistance / nominal_resistance) / beta + 1.0 / (nominal_temperature + 273.15);
|
double steinhart = __builtin_log(resistance / nominal_resistance) / beta + 1.0 / (nominal_temperature + 273.15);
|
||||||
return 1.0 / steinhart - 273.15;
|
return 1.0 / steinhart - 273.15;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 256 entries over the 10-bit range (steps of 4 counts), quarter-°C,
|
// 256 entries over the 10-bit range (steps of 4 counts), quarter- C,
|
||||||
// clamped to a sane sensor window; entry 256 mirrors 255 so interpolation
|
// clamped to a sane sensor window; entry 256 mirrors 255 so interpolation
|
||||||
// at full scale has a right neighbour.
|
// at full scale has a right neighbour.
|
||||||
consteval std::int16_t quarters_entry(int index)
|
consteval std::int16_t quarters_entry(std::int32_t index)
|
||||||
{
|
{
|
||||||
double adc = index * 4.0;
|
double adc = index * 4.0;
|
||||||
if (adc < 4)
|
if (adc < 4) {
|
||||||
adc = 4;
|
adc = 4;
|
||||||
if (adc > 1019)
|
}
|
||||||
|
if (adc > 1019) {
|
||||||
adc = 1019;
|
adc = 1019;
|
||||||
|
}
|
||||||
double t = temperature_of(adc) * 4.0;
|
double t = temperature_of(adc) * 4.0;
|
||||||
if (t < -40 * 4)
|
if (t < -40 * 4) {
|
||||||
t = -40 * 4;
|
t = -40 * 4;
|
||||||
if (t > 125 * 4)
|
}
|
||||||
|
if (t > 125 * 4) {
|
||||||
t = 125 * 4;
|
t = 125 * 4;
|
||||||
|
}
|
||||||
return static_cast<std::int16_t>(t < 0 ? t - 0.5 : t + 0.5);
|
return static_cast<std::int16_t>(t < 0 ? t - 0.5 : t + 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline constexpr avr::flash_table<[] {
|
inline constexpr avr::flash_table<[] {
|
||||||
std::array<std::int16_t, 257> out{};
|
std::array<std::int16_t, 257> out{};
|
||||||
for (int i = 0; i < 257; ++i)
|
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 < 256 ? i : 255);
|
out[static_cast<std::size_t>(i)] = quarters_entry(i < 256 ? i : 255);
|
||||||
|
}
|
||||||
return out;
|
return out;
|
||||||
}()>
|
}()>
|
||||||
table;
|
table;
|
||||||
|
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
|
|
||||||
// Temperature in quarter-°C from a raw (or averaged) 10-bit sample.
|
// Temperature in quarter- C from a raw (or averaged) 10-bit sample.
|
||||||
inline std::int16_t quarters(std::uint16_t adc)
|
inline std::int16_t quarters(std::uint16_t adc)
|
||||||
{
|
{
|
||||||
std::uint16_t index = adc >> 2; // the 257th entry backs index+1 at full scale
|
std::uint16_t index = adc >> 2; // the 257th entry backs index+1 at full scale
|
||||||
@@ -65,9 +74,12 @@ inline std::int16_t quarters(std::uint16_t adc)
|
|||||||
return static_cast<std::int16_t>(a + ((b - a) * frac) / 4);
|
return static_cast<std::int16_t>(a + ((b - a) * frac) / 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline std::int8_t celsius(std::uint16_t adc)
|
// 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(adc) + 2) / 4);
|
return static_cast<std::int8_t>((quarters + 2) >> 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace app::thermistor
|
} // namespace app::thermistor
|
||||||
|
|||||||
@@ -1,12 +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 8004)
|
||||||
|
|
||||||
|
# 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
|
# 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
|
# route in the emitted image is the only thing standing between a firmware change
|
||||||
# and an unreflashable board. It has been wrong before — see the script.
|
# and an unreflashable board.
|
||||||
find_package(Python3 COMPONENTS Interpreter)
|
# A check whose interpreter this host lacks is registered and *skips*, rather
|
||||||
if(Python3_FOUND)
|
# 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
|
add_test(NAME fantemp.reachability
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/check_reachability.py
|
COMMAND ${_python} ${CMAKE_CURRENT_SOURCE_DIR}/check_reachability.py
|
||||||
--objdump ${CMAKE_OBJDUMP} --elf $<TARGET_FILE:fantemp>
|
--objdump ${CMAKE_OBJDUMP} --elf $<TARGET_FILE:fantemp>
|
||||||
--image $<TARGET_FILE_DIR:fantemp>/fantemp.bin)
|
--image $<TARGET_FILE_DIR:fantemp>/fantemp.bin
|
||||||
else()
|
--loader-base ${PUREBOOT_BASE_HEX})
|
||||||
message(STATUS "Python not found — the reachability check is skipped")
|
|
||||||
endif()
|
libavr_skip_unverified()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
This board has no reset line and no programming header. The single route to the
|
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
|
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
|
gets that route wrong is a board that cannot be reflashed - and the failure is
|
||||||
silent, because everything else still works.
|
silent, because everything else still works.
|
||||||
|
|
||||||
It has been wrong before. The firmware this one replaces probed and jumped to
|
It has been wrong before. The firmware this one replaces probed and jumped to
|
||||||
@@ -15,7 +15,7 @@ hardware, which is what this replaces.
|
|||||||
Three properties, all read out of the disassembly rather than the source:
|
Three 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
|
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,
|
application reaching into it would be overwritten by the loader - or worse,
|
||||||
executed at reset, since BOOTRST points there.
|
executed at reset, since BOOTRST points there.
|
||||||
2. The hand-over targets the loader base. A word address of 0x3f00 is byte
|
2. The hand-over targets the loader base. A word address of 0x3f00 is byte
|
||||||
0x7e00; anything else is the 0x7800 bug again.
|
0x7e00; anything else is the 0x7800 bug again.
|
||||||
@@ -35,8 +35,7 @@ import re
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
BOOT_SECTION = 0x7C00 # hfuse d4: BOOTSZ 512 words
|
BOOT_SECTION = 0x7C00 # hfuse d4: BOOTSZ 512 words - a fuse fact, not a loader one
|
||||||
LOADER_BASE = 0x7E00 # pureboot's 512-byte slot, at the top
|
|
||||||
WDTCSR = 0x60
|
WDTCSR = 0x60
|
||||||
|
|
||||||
|
|
||||||
@@ -45,7 +44,12 @@ def main() -> int:
|
|||||||
parser.add_argument("--objdump", required=True)
|
parser.add_argument("--objdump", required=True)
|
||||||
parser.add_argument("--elf", type=pathlib.Path, required=True)
|
parser.add_argument("--elf", type=pathlib.Path, required=True)
|
||||||
parser.add_argument("--image", 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()
|
args = parser.parse_args()
|
||||||
|
loader_base = args.loader_base
|
||||||
|
|
||||||
failures = []
|
failures = []
|
||||||
|
|
||||||
@@ -60,7 +64,7 @@ def main() -> int:
|
|||||||
text = subprocess.run([args.objdump, "-d", str(args.elf)],
|
text = subprocess.run([args.objdump, "-d", str(args.elf)],
|
||||||
capture_output=True, text=True, check=True).stdout
|
capture_output=True, text=True, check=True).stdout
|
||||||
|
|
||||||
# The address the hand-over actually targets, read at its call sites — not
|
# 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
|
# "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
|
# 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.
|
# passes just as happily on the 0x7800 bug it is supposed to catch.
|
||||||
@@ -81,27 +85,27 @@ def main() -> int:
|
|||||||
if "r24" in held and "r25" in held:
|
if "r24" in held and "r25" in held:
|
||||||
sites.append(held["r25"] << 8 | held["r24"])
|
sites.append(held["r25"] << 8 | held["r24"])
|
||||||
|
|
||||||
want = LOADER_BASE // 2
|
want = loader_base // 2
|
||||||
if not sites:
|
if not sites:
|
||||||
failures.append("no call to bootloader::call with a loaded target — the "
|
failures.append("no call to bootloader::call with a loaded target - the "
|
||||||
"hand-over could not be read out of the image")
|
"hand-over could not be read out of the image")
|
||||||
elif wrong := [a for a in sites if a != want]:
|
elif wrong := [a for a in sites if a != want]:
|
||||||
failures.append(f"the hand-over targets word {[hex(a) for a in wrong]} "
|
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"(byte {[hex(a * 2) for a in wrong]}), not the loader at "
|
||||||
f"0x{LOADER_BASE:04x}")
|
f"0x{loader_base:04x}")
|
||||||
else:
|
else:
|
||||||
print(f" ok all {len(sites)} hand-over site(s) target word 0x{want:04x} "
|
print(f" ok all {len(sites)} hand-over site(s) target word 0x{want:04x} "
|
||||||
f"(byte 0x{LOADER_BASE:04x})")
|
f"(byte 0x{loader_base:04x})")
|
||||||
|
|
||||||
# An icall/ijmp has to exist for that address to be jumped to indirectly.
|
# An icall/ijmp has to exist for that address to be jumped to indirectly.
|
||||||
if not re.search(r"\b(icall|ijmp)\b", text):
|
if not re.search(r"\b(icall|ijmp)\b", text):
|
||||||
failures.append("no icall/ijmp — the hand-over cannot reach across flash")
|
failures.append("no icall/ijmp - the hand-over cannot reach across flash")
|
||||||
else:
|
else:
|
||||||
print(" ok an indirect call exists (a relative one cannot reach)")
|
print(" ok an indirect call exists (a relative one cannot reach)")
|
||||||
|
|
||||||
# What actually reaches WDTCSR, not what the image happens to load somewhere.
|
# 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
|
# 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
|
# *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
|
# clear, which is precisely what the legacy route used and is why this cannot
|
||||||
# be a check for "a prescaler is present".
|
# be a check for "a prescaler is present".
|
||||||
WDCE, WDE = 0x10, 0x08
|
WDCE, WDE = 0x10, 0x08
|
||||||
@@ -114,14 +118,14 @@ def main() -> int:
|
|||||||
values.append(0 if reg == "r1" else held.get(reg))
|
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)]
|
armed = [v for v in values if v is not None and (v & WDE) and not (v & WDCE)]
|
||||||
if armed:
|
if armed:
|
||||||
failures.append(f"WDTCSR is written {[hex(v) for v in armed]} — WDE without "
|
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"WDCE is arming the watchdog, and a reset-based hand-over "
|
||||||
f"opens no pureboot window")
|
f"opens no pureboot window")
|
||||||
elif not values:
|
elif not values:
|
||||||
print(" ok the watchdog is never written")
|
print(" ok the watchdog is never written")
|
||||||
else:
|
else:
|
||||||
print(f" ok WDTCSR writes are {[hex(v) if v is not None else '?' for v in values]}"
|
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")
|
f" - unlock and clear, never an arm")
|
||||||
|
|
||||||
for line in failures:
|
for line in failures:
|
||||||
print(f" FAIL {line}")
|
print(f" FAIL {line}")
|
||||||
|
|||||||
120
test/consteval.cpp
Normal file
120
test/consteval.cpp
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
// 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 < 100; ++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::quarters_entry;
|
||||||
|
|
||||||
|
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 / 4) >= 100); // 25.00 C, in quarters
|
||||||
|
static_assert(quarters_entry(nominal_count / 4 + 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(255) == -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 < 256; ++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
|
||||||
11
tools/check.sh
Executable file
11
tools/check.sh
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# The task gate: every configure preset built, and only then every one tested.
|
||||||
|
#
|
||||||
|
# The loop itself is libavr's (tools/check-presets.sh) because the order in it
|
||||||
|
# is subtle and was got wrong by hand more than once - the mode-identity checks
|
||||||
|
# read the sibling mode's tree, so a build-then-test-per-preset run compares a
|
||||||
|
# fresh image against a stale sibling. Anything this repository needs beyond
|
||||||
|
# the presets is written 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