Compare commits
6
Commits
78c9071f0c
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b493426298 | ||
|
|
f8e136e8ad | ||
|
|
51535953b9 | ||
|
|
999d087eb4 | ||
|
|
c58ff23e90 | ||
|
|
e44b45dd1e |
No files matched your search
@@ -5,7 +5,7 @@ CompileFlags:
|
|||||||
Add:
|
Add:
|
||||||
# clang has no 24-bit integer and GCC's are keywords, not macros, so the
|
# 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
|
# 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.
|
# one available - clang rejects _BitInt(24) on this target.
|
||||||
- -D__uint24=unsigned long
|
- -D__uint24=unsigned long
|
||||||
- -D__int24=long
|
- -D__int24=long
|
||||||
# clangd forwards the driver's system includes but not its own header
|
# clangd forwards the driver's system includes but not its own header
|
||||||
@@ -14,7 +14,7 @@ CompileFlags:
|
|||||||
- -D__STDC_LIMIT_MACROS
|
- -D__STDC_LIMIT_MACROS
|
||||||
- -D__STDC_CONSTANT_MACROS
|
- -D__STDC_CONSTANT_MACROS
|
||||||
# isr::emit spells a vector number into [[gnu::signal(N)]], which clang
|
# 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
|
# rejects rather than ignores - enough of them in one TU to reach the
|
||||||
# default limit of 19 inside the headers and truncate the parse.
|
# default limit of 19 inside the headers and truncate the parse.
|
||||||
- -ferror-limit=0
|
- -ferror-limit=0
|
||||||
Remove:
|
Remove:
|
||||||
|
|||||||
+2
-7
@@ -1,12 +1,7 @@
|
|||||||
# Line endings are the repository's, not the editing machine's: this checkout
|
# Line endings are the repository's, not the editing tool's: a file rewritten
|
||||||
# is reached from two hosts, and a file rewritten by a Windows tool comes back
|
# by a Windows tool must not come back with every line changed.
|
||||||
# with every line changed unless something says otherwise. Naming the source
|
|
||||||
# extensions left Markdown, Python, shell and CMake to whatever the writing
|
|
||||||
# tool defaulted to, which is CRLF on one of the two.
|
|
||||||
* text=auto eol=lf
|
* text=auto eol=lf
|
||||||
|
|
||||||
# Atmel Studio writes these and expects them back.
|
# 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
|
||||||
+6
-8
@@ -37,14 +37,12 @@ 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
|
# The board's loader, built from the pin, so the one image this deployment
|
||||||
# pureboot for its geometry alone left the one image this deployment cannot be
|
# cannot be recovered without is rebuilt by the commit that names it. Every
|
||||||
# recovered without outside the repository that pins it. Every parameter is
|
# parameter is pureboot's own default for this chip - USART0 at 115200 on the
|
||||||
# pureboot's own default for this chip - USART0 at 115200 on the 16 MHz crystal
|
# 16 MHz crystal `src/board.hpp` declares. It goes on over the wire
|
||||||
# `src/board.hpp` declares - and that is measured rather than assumed, the
|
# (`--update-loader`), which is why there is no flash target beside it: the
|
||||||
# image being byte-for-byte the 386 B the board's slot reads back. It goes on
|
# board has no programming header.
|
||||||
# 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)
|
pureboot_add_loader(pureboot)
|
||||||
|
|
||||||
enable_testing()
|
enable_testing()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ 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
|
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
|
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
|
command that cannot be abbreviated, because `r` should not be able to wipe the
|
||||||
histogram. `save` (new) forces a writeback, which otherwise happens every 30
|
histogram. `save` forces a writeback, which otherwise happens every 30
|
||||||
minutes and on the way into the bootloader.
|
minutes and on the way into the bootloader.
|
||||||
|
|
||||||
`show`, `statistics` and the histogram print one value per line behind a dotted
|
`show`, `statistics` and the histogram print one value per line behind a dotted
|
||||||
@@ -28,9 +28,8 @@ 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.
|
without a graph show none of its shape.
|
||||||
|
|
||||||
**Ctrl+C** abandons a half-typed line and gives a fresh prompt, echoing `^C`, and
|
**Ctrl+C** abandons a half-typed line and gives a fresh prompt, echoing `^C`, and
|
||||||
it is what stops `monitor`. Stopping on *any* byte, which is what the port did
|
it is what stops `monitor`. Stopping on *any* byte would not do: a host's line
|
||||||
first, reads well right up until a host sends a line ending: `monitor\r\n` then
|
ending would stop `monitor\r\n` on the `\n` it arrived with, one reading in.
|
||||||
stopped itself on the `\n` it arrived with, one reading in.
|
|
||||||
|
|
||||||
## Reaching the bootloader
|
## Reaching the bootloader
|
||||||
|
|
||||||
@@ -47,9 +46,10 @@ stopped itself on the `\n` it arrived with, one reading in.
|
|||||||
that bit-bangs the same pin receives perfectly and answers into nothing.
|
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`),
|
`ctest` reads all three back out of the emitted image (`test/check_reachability.py`),
|
||||||
because none of them is visible from the source alone and the failure mode is an
|
with the image's clearance below the boot section, because none of them is
|
||||||
unreflashable board. Both the address and the watchdog checks are red-proven
|
visible from the source alone and the failure mode is an unreflashable board.
|
||||||
against the legacy behaviour they exist to catch.
|
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
|
The Steinhart–Hart math of the legacy firmware (runtime doubles + libm
|
||||||
log) is gone: the Beta equation and the cubic fan curve are evaluated
|
log) is gone: the Beta equation and the cubic fan curve are evaluated
|
||||||
@@ -66,7 +66,7 @@ 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
|
The firmware is **7836 B** of flash, byte-identical between the generated and
|
||||||
reflect modes, and `ctest` holds it to that number.
|
reflect modes, and `ctest` holds it to that number.
|
||||||
|
|
||||||
## Atmel Studio
|
## Atmel Studio
|
||||||
|
|||||||
+2
-2
@@ -16,8 +16,8 @@ and re-propose work the owner has already refused.
|
|||||||
### The millisecond clock stays 64 bits
|
### The millisecond clock stays 64 bits
|
||||||
|
|
||||||
Narrowing `uptime::millis()` and the three timestamps that hold its value from
|
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
|
`uint64_t` to `uint32_t` is **602 B of flash, 7.7 % of the image** (7838 B down
|
||||||
to 7396), plus 16 B of RAM.
|
to 7236), plus 16 B of RAM.
|
||||||
|
|
||||||
**Refused, owner-stated: this board runs continuously and an uptime that
|
**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
|
restarts every 49.7 days is not acceptable.** That is what a 32-bit
|
||||||
|
|||||||
+1
-1
Submodule libavr updated: 93d8b0e491...73fde9c243.
+1
-1
Submodule pureboot updated: 26a86213ca...9d6b81c024.
+10
-37
@@ -4,24 +4,12 @@
|
|||||||
|
|
||||||
#include "board.hpp"
|
#include "board.hpp"
|
||||||
|
|
||||||
// Reaching the resident bootloader from the console.
|
// Reaching the resident bootloader from the console. pureboot hands straight
|
||||||
//
|
// back to the application on WDRF - an unattended board that watchdog-resets in
|
||||||
// The legacy firmware did this with a watchdog reset: `bootloader` armed the
|
// a loop must not sit in a loader instead of running - so a reset-based route
|
||||||
// watchdog and hung, and the next boot noticed WDRF and jumped to the boot
|
// opens no window at all, and on a board whose only way in is the firmware that
|
||||||
// section. That works for TinySafeBoot and **does not work for pureboot**, which
|
// is a lockout. This route jumps instead, with the reset flags already clear,
|
||||||
// deliberately hands straight back to the application on WDRF - an unattended
|
// so the loader starts as if from a clean power-on and opens its window.
|
||||||
// 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
|
|
||||||
// board whose only way in is the firmware that is a lockout.
|
|
||||||
//
|
|
||||||
// This route therefore never resets. It jumps, with the reset flags already
|
|
||||||
// clear, so the loader starts as if from a clean power-on and opens its window.
|
|
||||||
//
|
|
||||||
// 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
|
|
||||||
// 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
|
|
||||||
// was always false and its `bootloader` command never actually arrived anywhere.
|
|
||||||
namespace app {
|
namespace app {
|
||||||
|
|
||||||
class bootloader {
|
class bootloader {
|
||||||
@@ -29,17 +17,10 @@ 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: `hfuse d4` puts the boot section at 0x7c00, with
|
||||||
// instruction any loader begins with - so this asks "is a loader installed"
|
// pureboot's staging slot below its own slot.
|
||||||
// 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.
|
|
||||||
static constexpr std::uint16_t base = 0x7e00;
|
static constexpr std::uint16_t base = 0x7e00;
|
||||||
|
|
||||||
static bool present()
|
|
||||||
{
|
|
||||||
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
|
||||||
// halves. [[gnu::noipa]] keeps the call indirect: a constant target folds
|
// halves. [[gnu::noipa]] keeps the call indirect: a constant target folds
|
||||||
// into a relative call that cannot reach across flash.
|
// into a relative call that cannot reach across flash.
|
||||||
@@ -73,20 +54,12 @@ 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. pureboot releases it itself; this
|
||||||
// from the host. pureboot clears this itself; TinySafeBoot, which is what
|
// keeps the hand-over correct for a loader that does not.
|
||||||
// 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
|
|
||||||
// before the loader is replaced.
|
|
||||||
avr::hw::ucsr0b::write(0);
|
avr::hw::ucsr0b::write(0);
|
||||||
|
|
||||||
call(reinterpret_cast<jump_fn>(base / 2));
|
call(reinterpret_cast<jump_fn>(base / 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool available()
|
|
||||||
{
|
|
||||||
return present();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace app
|
} // namespace app
|
||||||
+3
-3
@@ -38,11 +38,11 @@ class controller {
|
|||||||
fan::set_duty(avr::percent_t::of(m_percent));
|
fan::set_duty(avr::percent_t::of(m_percent));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void set_manual(std::uint8_t p)
|
static void set_manual(std::uint8_t percent)
|
||||||
{
|
{
|
||||||
m_auto_mode = false;
|
m_auto_mode = false;
|
||||||
m_percent = p;
|
m_percent = percent;
|
||||||
fan::set_duty(avr::percent_t::of(p));
|
fan::set_duty(avr::percent_t::of(percent));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void set_automatic()
|
static void set_automatic()
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
#include <libavr/flash.hpp>
|
#include <libavr/flash.hpp>
|
||||||
|
|
||||||
// The auto-mode fan curve, tabulated at compile time as a flash_table of duty
|
// The auto-mode fan curve, tabulated at compile time as a flash_table of duty
|
||||||
// percent per C: a cubic that is held at zero below the temperature the fan
|
// percent per degree C: a cubic that is held at zero below the temperature the fan
|
||||||
// starts at.
|
// starts at.
|
||||||
namespace app::curve {
|
namespace app::curve {
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
#include "board.hpp"
|
#include "board.hpp"
|
||||||
|
|
||||||
// Temperature histogram: one uint32 bucket per C 0..99, sampled once a
|
// Temperature histogram: one uint32 bucket per degree 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).
|
// changed bytes).
|
||||||
namespace app {
|
namespace app {
|
||||||
|
|||||||
+130
-140
@@ -3,6 +3,7 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include <libavr/libavr.hpp>
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
@@ -18,6 +19,11 @@
|
|||||||
// 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 {
|
||||||
|
|
||||||
|
struct command {
|
||||||
|
std::string_view name;
|
||||||
|
std::string_view summary;
|
||||||
|
};
|
||||||
|
|
||||||
// Commands, in the order they are matched - which is the order the original
|
// 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,
|
||||||
@@ -25,57 +31,71 @@ namespace app {
|
|||||||
// steal an abbreviation that already meant something else.
|
// steal an abbreviation that already meant something else.
|
||||||
//
|
//
|
||||||
// A function rather than a variable, so the list exists only while the flash
|
// A function rather than a variable, so the list exists only while the flash
|
||||||
// blob below is being built and never as storage of its own.
|
// blobs below are being built and never as storage of its own.
|
||||||
consteval auto command_names()
|
consteval auto commands()
|
||||||
{
|
{
|
||||||
return std::to_array<std::string_view>({
|
return std::to_array<command>({
|
||||||
"help",
|
{"help", "prints this help message"},
|
||||||
"show",
|
{"show", "shows current temperature and fan speed"},
|
||||||
"curve",
|
{"curve", "shows mapping from temperature to fan speed"},
|
||||||
"monitor",
|
{"monitor", "loops the show command until Ctrl+C is pressed"},
|
||||||
"bootloader",
|
{"bootloader", "enters the bootloader"},
|
||||||
"uptime",
|
{"uptime", "shows system uptime"},
|
||||||
"statistics",
|
{"statistics", "prints overall statistics like min and max temp"},
|
||||||
"histogram",
|
{"histogram", "prints a histogram of the temperature"},
|
||||||
"reset",
|
{"reset", "resets statistics to 0 in EEPROM and RAM (no abbreviation)"},
|
||||||
"set",
|
{"set", "sets the fan speed to the provided value, 0-100"},
|
||||||
"auto",
|
{"auto", "turns on automatic fan control"},
|
||||||
"version",
|
{"version", "displays firmware version"},
|
||||||
"save",
|
{"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 {
|
class terminal {
|
||||||
static constexpr char ctrl_c = 0x03;
|
static constexpr char ctrl_c = 0x03;
|
||||||
static constexpr char backspace = 0x08;
|
static constexpr char backspace = 0x08;
|
||||||
static constexpr char del = 0x7f;
|
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::array<char, 24> m_line{};
|
||||||
static inline std::uint8_t m_at = 0;
|
static inline std::uint8_t m_at = 0;
|
||||||
static inline bool m_overflowed = false;
|
static inline bool m_overflowed = false;
|
||||||
static inline bool m_monitoring = false;
|
static inline bool m_monitoring = false;
|
||||||
static inline std::uint64_t m_last_monitor = 0;
|
static inline std::uint64_t m_last_monitor = 0;
|
||||||
|
|
||||||
static constexpr std::uint8_t command_count = command_names().size();
|
static constexpr std::uint8_t command_count = commands().size();
|
||||||
|
|
||||||
static constexpr std::size_t name_bytes = [] {
|
// One column of the command table as a NUL-separated blob in flash. Matched
|
||||||
std::size_t total = 0;
|
// and printed at run time, which is not a reason to sit in RAM on a machine
|
||||||
for (auto one : command_names()) {
|
// with two address spaces: a table of `string_view` puts the characters
|
||||||
total += one.size() + 1;
|
// *and* its own pointers there. Separators rather than an offset table,
|
||||||
}
|
// because an offset table is the RAM this exists to give back.
|
||||||
return total;
|
template <std::string_view command::*Column>
|
||||||
}();
|
static constexpr auto joined = [] {
|
||||||
|
constexpr auto bytes = [] {
|
||||||
// The names as one NUL-separated blob in flash. Matched at run time, which
|
std::size_t total = 0;
|
||||||
// is not a reason to sit in RAM on a machine with two address spaces: a
|
for (const auto &one : commands()) {
|
||||||
// table of `string_view` puts the characters *and* its own pointers there,
|
total += (one.*Column).size() + 1;
|
||||||
// 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
|
return total;
|
||||||
// RAM this exists to give back.
|
}();
|
||||||
static constexpr auto names_data = [] {
|
std::array<char, bytes> chars{};
|
||||||
std::array<char, name_bytes> chars{};
|
|
||||||
std::size_t at = 0;
|
std::size_t at = 0;
|
||||||
for (auto one : command_names()) {
|
for (const auto &one : commands()) {
|
||||||
for (char c : one) {
|
for (char c : one.*Column) {
|
||||||
chars[at++] = c;
|
chars[at++] = c;
|
||||||
}
|
}
|
||||||
chars[at++] = '\0';
|
chars[at++] = '\0';
|
||||||
@@ -83,103 +103,86 @@ class terminal {
|
|||||||
return chars;
|
return chars;
|
||||||
}();
|
}();
|
||||||
|
|
||||||
static constexpr avr::flash_table<names_data> names{};
|
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
|
// `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
|
// data.
|
||||||
// commands cannot move the protection onto a different one.
|
static constexpr std::uint8_t exact_command = command_index("reset");
|
||||||
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
|
// One space and at least one dot after the longest name, the original's
|
||||||
// name. The longest name is `bootloader` at 10, so 12 leaves it a space and
|
// layout exactly.
|
||||||
// one dot - the original's layout exactly.
|
static constexpr std::uint8_t help_column = [] {
|
||||||
static constexpr std::uint8_t help_column = 12;
|
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()
|
static void prompt()
|
||||||
{
|
{
|
||||||
serial << "> "_P;
|
serial << "> "_P;
|
||||||
}
|
}
|
||||||
|
|
||||||
// `name ....: ` - the dotted label the original used everywhere it printed a
|
// The tail of `name ....: ` - the dotted label the original used everywhere
|
||||||
// list of things, which is what makes a column of values readable without
|
// it printed a list of things, which is what makes a column of values
|
||||||
// counting spaces. Its width comes from the type, so the padding needs no
|
// readable without counting spaces.
|
||||||
// hand-counted constant.
|
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>
|
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) {
|
pad(Flash::size, column);
|
||||||
serial << '.';
|
|
||||||
}
|
|
||||||
serial << ": "_P;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The same shape for a command name, whose width is not a type but the walk
|
// Prints one entry of a blob, from `at` to its separator, and answers its
|
||||||
// to its separator - so it is counted as it is printed rather than measured
|
// width - counted as it is printed rather than measured first, which would
|
||||||
// first, which would read the blob twice.
|
// read the blob twice.
|
||||||
static void help_row(std::uint8_t which)
|
template <typename Blob>
|
||||||
|
static std::uint8_t print_entry(Blob blob, std::uint16_t at)
|
||||||
{
|
{
|
||||||
auto at = name_start(which);
|
|
||||||
std::uint8_t width = 0;
|
std::uint8_t width = 0;
|
||||||
for (char c; (c = names.at(at + width)) != '\0'; ++width) {
|
for (char c; (c = blob.at(at + width)) != '\0'; ++width) {
|
||||||
serial << c;
|
serial << c;
|
||||||
}
|
}
|
||||||
serial << ' ';
|
return width;
|
||||||
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-degrees as a signed decimal with a two-digit fraction. The sign is
|
||||||
// off first: C++ gives a negative remainder for a negative dividend, so
|
// taken 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".
|
// the fraction of -40.25 C would print as "-40.-25".
|
||||||
static void temperature(std::int16_t quarters)
|
static void temperature(std::int16_t quarters)
|
||||||
{
|
{
|
||||||
|
constexpr auto per_degree = thermistor::quarters_per_degree;
|
||||||
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 / per_degree << '.'
|
||||||
|
<< avr::dec<{.width = 2, .fill = '0'}>((magnitude % per_degree) * (100 / per_degree));
|
||||||
}
|
}
|
||||||
|
|
||||||
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(0);
|
std::uint16_t name = 0;
|
||||||
serial << "prints this help message\r\n"_P;
|
std::uint16_t summary = 0;
|
||||||
help_row(1);
|
for (std::uint8_t i = 0; i < command_count; ++i) {
|
||||||
serial << "shows current temperature and fan speed\r\n"_P;
|
const auto width = print_entry(names, name);
|
||||||
help_row(2);
|
name += width + 1;
|
||||||
serial << "shows mapping from temperature to fan speed\r\n"_P;
|
pad(width, help_column);
|
||||||
help_row(3);
|
summary += print_entry(summaries, summary) + 1;
|
||||||
serial << "loops the show command until Ctrl+C is pressed\r\n"_P;
|
serial << "\r\n"_P;
|
||||||
help_row(4);
|
}
|
||||||
serial << "enters the bootloader\r\n"_P;
|
|
||||||
help_row(5);
|
|
||||||
serial << "shows system uptime\r\n"_P;
|
|
||||||
help_row(6);
|
|
||||||
serial << "prints overall statistics like min and max temp\r\n"_P;
|
|
||||||
help_row(7);
|
|
||||||
serial << "prints a histogram of the temperature\r\n"_P;
|
|
||||||
help_row(8);
|
|
||||||
serial << "resets statistics to 0 in EEPROM and RAM (no abbreviation)\r\n"_P;
|
|
||||||
help_row(9);
|
|
||||||
serial << "sets the fan speed to the provided value, 0-100\r\n"_P;
|
|
||||||
help_row(10);
|
|
||||||
serial << "turns on automatic fan control\r\n"_P;
|
|
||||||
help_row(11);
|
|
||||||
serial << "displays firmware version\r\n"_P;
|
|
||||||
help_row(12);
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +209,7 @@ class terminal {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
label("ADC value"_P, reading_column);
|
label("ADC value"_P, reading_column);
|
||||||
serial << controller::last_adc() << " / 1023\r\n"_P;
|
serial << controller::last_adc() << " / "_P << thermistor::adc_full_scale << "\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) {
|
||||||
@@ -247,9 +250,12 @@ class terminal {
|
|||||||
|
|
||||||
static void print_uptime()
|
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);
|
auto seconds = static_cast<std::uint32_t>(uptime::millis() / 1000);
|
||||||
serial << seconds / 86400 << "d "_P << (seconds / 3600) % 24 << "h "_P << (seconds / 60) % 60 << "m "_P
|
serial << seconds / day << "d "_P << seconds % day / hour << "h "_P << seconds % hour / minute << "m "_P
|
||||||
<< seconds % 60 << "s\r\n"_P;
|
<< seconds % minute << "s\r\n"_P;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void print_statistics()
|
static void print_statistics()
|
||||||
@@ -303,24 +309,10 @@ class terminal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Where the given command's name begins in the blob. A walk over the
|
|
||||||
// separators, for the two callers that need to start at one name.
|
|
||||||
static constexpr std::uint8_t name_start(std::uint8_t which)
|
|
||||||
{
|
|
||||||
std::uint8_t at = 0;
|
|
||||||
for (std::uint8_t i = 0; i < which; ++i) {
|
|
||||||
while (names.at(at) != '\0') {
|
|
||||||
++at;
|
|
||||||
}
|
|
||||||
++at;
|
|
||||||
}
|
|
||||||
return at;
|
|
||||||
}
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// Which command a word names, or `command_count` for none. The input
|
// Which command a word names, or `no_command` for none. The input matches
|
||||||
// matches when it is a non-empty prefix of a name; `reset` is the exception
|
// when it is a non-empty prefix of a name; `reset` is the exception and must
|
||||||
// and must be typed in full.
|
// be typed in full.
|
||||||
//
|
//
|
||||||
// One pass over the blob answers both, because the names sit in it in match
|
// 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
|
// order: each name is compared as it is walked, and the walk to its
|
||||||
@@ -365,8 +357,8 @@ class terminal {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split on the first space with the (pointer, length) constructor rather
|
// Split on the first space with the (pointer, length) constructor:
|
||||||
// than substr, which throws - see matches().
|
// substr checks its bounds by throwing, which this build cannot.
|
||||||
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
|
||||||
@@ -376,40 +368,38 @@ class terminal {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto which = lookup(word);
|
switch (lookup(word)) {
|
||||||
|
case command_index("help"):
|
||||||
switch (which) {
|
|
||||||
case 0:
|
|
||||||
help();
|
help();
|
||||||
return;
|
return;
|
||||||
case 1:
|
case command_index("show"):
|
||||||
show();
|
show();
|
||||||
return;
|
return;
|
||||||
case 2:
|
case command_index("curve"):
|
||||||
print_curve();
|
print_curve();
|
||||||
return;
|
return;
|
||||||
case 3:
|
case command_index("monitor"):
|
||||||
m_monitoring = true;
|
m_monitoring = true;
|
||||||
return;
|
return;
|
||||||
case 4:
|
case command_index("bootloader"):
|
||||||
serial << "entering bootloader\r\n"_P;
|
serial << "entering bootloader\r\n"_P;
|
||||||
statistics::save();
|
statistics::save();
|
||||||
serial.drain();
|
serial.drain();
|
||||||
bootloader::enter();
|
bootloader::enter();
|
||||||
case 5:
|
case command_index("uptime"):
|
||||||
print_uptime();
|
print_uptime();
|
||||||
return;
|
return;
|
||||||
case 6:
|
case command_index("statistics"):
|
||||||
print_statistics();
|
print_statistics();
|
||||||
return;
|
return;
|
||||||
case 7:
|
case command_index("histogram"):
|
||||||
print_histogram();
|
print_histogram();
|
||||||
return;
|
return;
|
||||||
case 8:
|
case command_index("reset"):
|
||||||
statistics::reset();
|
statistics::reset();
|
||||||
serial << "statistics cleared in EEPROM and RAM\r\n"_P;
|
serial << "statistics cleared in EEPROM and RAM\r\n"_P;
|
||||||
return;
|
return;
|
||||||
case 9: {
|
case command_index("set"): {
|
||||||
std::uint16_t percent = 0;
|
std::uint16_t percent = 0;
|
||||||
bool valid = !rest.empty();
|
bool valid = !rest.empty();
|
||||||
for (char c : rest) {
|
for (char c : rest) {
|
||||||
@@ -430,14 +420,14 @@ class terminal {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 10:
|
case command_index("auto"):
|
||||||
controller::set_automatic();
|
controller::set_automatic();
|
||||||
serial << "automatic fan control\r\n"_P;
|
serial << "automatic fan control\r\n"_P;
|
||||||
return;
|
return;
|
||||||
case 11:
|
case command_index("version"):
|
||||||
serial << "FanTemp "_P << version << " on libavr\r\n"_P;
|
serial << "FanTemp "_P << version << " on libavr\r\n"_P;
|
||||||
return;
|
return;
|
||||||
case 12:
|
case command_index("save"):
|
||||||
statistics::save();
|
statistics::save();
|
||||||
serial << "statistics written to EEPROM\r\n"_P;
|
serial << "statistics written to EEPROM\r\n"_P;
|
||||||
return;
|
return;
|
||||||
@@ -473,7 +463,7 @@ class terminal {
|
|||||||
static void poll()
|
static void poll()
|
||||||
{
|
{
|
||||||
if (m_monitoring) {
|
if (m_monitoring) {
|
||||||
if (uptime::millis() >= m_last_monitor + 1000) {
|
if (uptime::millis() >= m_last_monitor + monitor_interval_ms) {
|
||||||
show();
|
show();
|
||||||
m_last_monitor = uptime::millis();
|
m_last_monitor = uptime::millis();
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-23
@@ -8,7 +8,7 @@
|
|||||||
// 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-degrees 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
|
// The converter's top count: this board reads the divider at the ADC's
|
||||||
@@ -20,43 +20,58 @@ inline constexpr double nominal_resistance = 9270;
|
|||||||
inline constexpr double beta = 3212;
|
inline constexpr double beta = 3212;
|
||||||
inline constexpr double nominal_temperature = 25;
|
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 {
|
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)
|
consteval double temperature_of(double adc)
|
||||||
{
|
{
|
||||||
double resistance = series_resistor * adc / (adc_full_scale - 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 =
|
||||||
return 1.0 / steinhart - 273.15;
|
__builtin_log(resistance / nominal_resistance) / beta + 1.0 / (nominal_temperature + kelvin_offset);
|
||||||
|
return 1.0 / steinhart - kelvin_offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 256 entries over the 10-bit range (steps of 4 counts), quarter- C,
|
// One entry per step, in quarter-degrees, with the reading kept a step clear of
|
||||||
// clamped to a sane sensor window; entry 256 mirrors 255 so interpolation
|
// both rails, where the divider has no solution.
|
||||||
// at full scale has a right neighbour.
|
|
||||||
consteval std::int16_t quarters_entry(std::int32_t index)
|
consteval std::int16_t quarters_entry(std::int32_t index)
|
||||||
{
|
{
|
||||||
double adc = index * 4.0;
|
double adc = index * double{counts_per_step};
|
||||||
if (adc < 4) {
|
if (adc < counts_per_step) {
|
||||||
adc = 4;
|
adc = counts_per_step;
|
||||||
}
|
}
|
||||||
if (adc > 1019) {
|
if (adc > adc_full_scale - counts_per_step) {
|
||||||
adc = 1019;
|
adc = adc_full_scale - counts_per_step;
|
||||||
}
|
}
|
||||||
double t = temperature_of(adc) * 4.0;
|
double t = temperature_of(adc) * quarters_per_degree;
|
||||||
if (t < -40 * 4) {
|
if (t < coldest * quarters_per_degree) {
|
||||||
t = -40 * 4;
|
t = coldest * quarters_per_degree;
|
||||||
}
|
}
|
||||||
if (t > 125 * 4) {
|
if (t > hottest * quarters_per_degree) {
|
||||||
t = 125 * 4;
|
t = hottest * quarters_per_degree;
|
||||||
}
|
}
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One entry past the last step, mirroring it, so interpolation at full scale
|
||||||
|
// has a right neighbour.
|
||||||
inline constexpr avr::flash_table<[] {
|
inline constexpr avr::flash_table<[] {
|
||||||
std::array<std::int16_t, 257> out{};
|
std::array<std::int16_t, steps + 1> out{};
|
||||||
for (std::int32_t i = 0; i < static_cast<std::int32_t>(out.size()); ++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 < steps ? i : steps - 1);
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}()>
|
}()>
|
||||||
@@ -64,14 +79,14 @@ inline constexpr avr::flash_table<[] {
|
|||||||
|
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
|
|
||||||
// Temperature in quarter- C from a raw (or averaged) 10-bit sample.
|
// Temperature in quarter-degrees 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 / detail::counts_per_step;
|
||||||
std::uint8_t frac = adc & 3;
|
std::uint8_t frac = adc % detail::counts_per_step;
|
||||||
auto a = detail::table[index];
|
auto a = detail::table[index];
|
||||||
auto b = detail::table[static_cast<std::uint16_t>(index + 1)];
|
auto b = detail::table[static_cast<std::uint16_t>(index + 1)];
|
||||||
return static_cast<std::int16_t>(a + ((b - a) * frac) / 4);
|
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
|
// Whole degrees from quarter-degrees, rounded to nearest. The shift is the
|
||||||
@@ -79,7 +94,7 @@ inline std::int16_t quarters(std::uint16_t adc)
|
|||||||
// -3.00 C as -2, where an arithmetic shift floors and reads it as -3.
|
// -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)
|
inline constexpr std::int8_t whole_degrees(std::int16_t quarters)
|
||||||
{
|
{
|
||||||
return static_cast<std::int8_t>((quarters + 2) >> 2);
|
return static_cast<std::int8_t>((quarters + quarters_per_degree / 2) >> quarter_bits);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace app::thermistor
|
} // namespace app::thermistor
|
||||||
+1
-1
@@ -2,7 +2,7 @@ libavr_format_test()
|
|||||||
|
|
||||||
# The image must stay below the boot section at 0x7e00, and the README states
|
# 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.
|
# what it measures; a library advance that moves it says nothing on its own.
|
||||||
libavr_size_claim_test(fantemp 8004)
|
libavr_size_claim_test(fantemp 7836)
|
||||||
|
|
||||||
# The README says the two modes emit the same image, and only a tree with
|
# The README says the two modes emit the same image, and only a tree with
|
||||||
# both built can say whether they do.
|
# both built can say whether they do.
|
||||||
|
|||||||
+21
-15
@@ -6,25 +6,20 @@ 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
|
Four properties, all read out of the disassembly rather than the source:
|
||||||
`0x7800`, the base of a 2 KB boot section, while the board's loader sits at
|
|
||||||
`0x7e00`; `check()` therefore read an erased byte, was false, and the command
|
|
||||||
never arrived anywhere. Nothing about that is visible short of trying it on the
|
|
||||||
hardware, which is what this replaces.
|
|
||||||
|
|
||||||
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; 0x7800, a 2 KB boot section's base, reads erased on this board.
|
||||||
3. The hand-over does not arm the watchdog. pureboot hands straight back on
|
3. The hand-over releases the USART first. While TXEN0 is set the peripheral
|
||||||
WDRF by design, so a reset-based route reaches it and opens no window. The
|
owns PD1, so a loader that bit-bangs the pin answers into nothing.
|
||||||
legacy firmware's route was exactly that, and it is the one change that
|
4. The hand-over does not arm the watchdog. pureboot hands straight back on
|
||||||
cannot be walked back from the host.
|
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
|
check_reachability.py --objdump avr-objdump --elf fantemp --image fantemp.bin \
|
||||||
|
--loader-base 0x7e00
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -36,6 +31,7 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
BOOT_SECTION = 0x7C00 # hfuse d4: BOOTSZ 512 words - a fuse fact, not a loader one
|
BOOT_SECTION = 0x7C00 # hfuse d4: BOOTSZ 512 words - a fuse fact, not a loader one
|
||||||
|
UCSR0B = 0xC1
|
||||||
WDTCSR = 0x60
|
WDTCSR = 0x60
|
||||||
|
|
||||||
|
|
||||||
@@ -73,17 +69,21 @@ def main() -> int:
|
|||||||
# loads the *word* address into a register pair immediately before it.
|
# loads the *word* address into a register pair immediately before it.
|
||||||
lines = text.splitlines()
|
lines = text.splitlines()
|
||||||
helper = re.compile(r"\b(?:r?call)\b.*<_ZN3app10bootloader4call")
|
helper = re.compile(r"\b(?:r?call)\b.*<_ZN3app10bootloader4call")
|
||||||
sites = []
|
release = re.compile(rf"\bsts\s+0x00{UCSR0B:02X},\s*r1\b", re.I)
|
||||||
|
sites, unreleased = [], 0
|
||||||
for index, line in enumerate(lines):
|
for index, line in enumerate(lines):
|
||||||
if not helper.search(line):
|
if not helper.search(line):
|
||||||
continue
|
continue
|
||||||
held: dict[str, int] = {}
|
held: dict[str, int] = {}
|
||||||
for back in lines[max(0, index - 8):index]:
|
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):
|
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)
|
held[m.group(1)] = int(m.group(2), 16)
|
||||||
# The AVR ABI passes the pointer in r25:r24.
|
# The AVR ABI passes the pointer in r25:r24.
|
||||||
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"])
|
||||||
|
if not any(release.search(back) for back in window):
|
||||||
|
unreleased += 1
|
||||||
|
|
||||||
want = loader_base // 2
|
want = loader_base // 2
|
||||||
if not sites:
|
if not sites:
|
||||||
@@ -97,6 +97,12 @@ def main() -> int:
|
|||||||
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})")
|
||||||
|
|
||||||
|
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.
|
# 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")
|
||||||
|
|||||||
+7
-5
@@ -43,7 +43,7 @@ static_assert(duty_entry(99) == 100);
|
|||||||
// Monotone across the whole table: a warmer reading never asks for less air.
|
// Monotone across the whole table: a warmer reading never asks for less air.
|
||||||
consteval bool curve_rises()
|
consteval bool curve_rises()
|
||||||
{
|
{
|
||||||
for (std::int32_t t = 1; t < 100; ++t) {
|
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)) {
|
if (duty_entry(t) < duty_entry(t - 1)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -57,23 +57,25 @@ static_assert(curve_rises());
|
|||||||
// thermistor's nominal resistance at
|
// thermistor's nominal resistance at
|
||||||
// adc = full_scale * nominal / (series + nominal), and the equation's own
|
// adc = full_scale * nominal / (series + nominal), and the equation's own
|
||||||
// definition puts that count at the nominal temperature.
|
// 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::quarters_entry;
|
||||||
|
using app::thermistor::detail::steps;
|
||||||
|
|
||||||
inline constexpr std::int32_t nominal_count =
|
inline constexpr std::int32_t nominal_count =
|
||||||
static_cast<std::int32_t>(app::thermistor::adc_full_scale * app::thermistor::nominal_resistance /
|
static_cast<std::int32_t>(app::thermistor::adc_full_scale * app::thermistor::nominal_resistance /
|
||||||
(app::thermistor::series_resistor + 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 / counts_per_step) >= 100); // 25.00 C, in quarters
|
||||||
static_assert(quarters_entry(nominal_count / 4 + 1) < 100); // and the step below it
|
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.
|
// Both clamps, at the ends the divider cannot leave.
|
||||||
static_assert(quarters_entry(0) == 125 * 4);
|
static_assert(quarters_entry(0) == 125 * 4);
|
||||||
static_assert(quarters_entry(255) == -40 * 4);
|
static_assert(quarters_entry(steps - 1) == -40 * 4);
|
||||||
|
|
||||||
// An NTC on this divider falls with the count: more counts is more resistance
|
// An NTC on this divider falls with the count: more counts is more resistance
|
||||||
// is a colder sensor, over every step of the table.
|
// is a colder sensor, over every step of the table.
|
||||||
consteval bool thermistor_falls()
|
consteval bool thermistor_falls()
|
||||||
{
|
{
|
||||||
for (std::int32_t i = 1; i < 256; ++i) {
|
for (std::int32_t i = 1; i < steps; ++i) {
|
||||||
if (quarters_entry(i) > quarters_entry(i - 1)) {
|
if (quarters_entry(i) > quarters_entry(i - 1)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-4
@@ -1,11 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# The task gate: every configure preset built, and only then every one tested.
|
# 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
|
# The loop is libavr's (tools/check-presets.sh): the mode-identity checks read
|
||||||
# is subtle and was got wrong by hand more than once - the mode-identity checks
|
# the sibling mode's tree, so a build-then-test-per-preset run would compare a
|
||||||
# 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
|
# 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.
|
# the presets goes after the call, where every tree is built and tested.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||||
"${LIBAVR_ROOT:-libavr}/tools/check-presets.sh" "$@"
|
"${LIBAVR_ROOT:-libavr}/tools/check-presets.sh" "$@"
|
||||||
Reference in new issue
Block a user