Compare commits
6
Commits
20b1485b6f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68f0089e8c | ||
|
|
e395c0344f | ||
|
|
44db419c4b | ||
|
|
f50032ee46 | ||
|
|
bc671618a7 | ||
|
|
aafaf25a47 |
No files matched your search
@@ -1,13 +1,10 @@
|
|||||||
# Editor accommodations for the second frontend. No compilation database is
|
# Editor accommodations for the second frontend: only what is true of the
|
||||||
# named here: this repo rides as a submodule in its consumers, and this file
|
# code wherever it is built. The build tree is named in .vscode/settings.json.
|
||||||
# travels with it — a consumer's own database then covers these sources, with
|
|
||||||
# that project's loader flags. The checkout that is opened as a folder names
|
|
||||||
# its build tree in .vscode/settings.json instead.
|
|
||||||
CompileFlags:
|
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
|
||||||
@@ -16,15 +13,12 @@ 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:
|
||||||
# Codegen shaping the loader TUs carry and clang has no spelling for.
|
# Codegen shaping the policy tier carries and clang has no spelling for.
|
||||||
- -fira-algorithm=*
|
|
||||||
- -fno-split-wide-types
|
|
||||||
- -fno-tree-ter
|
- -fno-tree-ter
|
||||||
- -fno-ivopts
|
|
||||||
- -fno-move-loop-invariants
|
- -fno-move-loop-invariants
|
||||||
# The build promotes warnings for the compiler that has to be right about
|
# 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
|
# them; in the editor the flag paints a second frontend's opinions in the
|
||||||
|
|||||||
Vendored
+1
-3
@@ -5,9 +5,7 @@
|
|||||||
"C_Cpp.intelliSenseEngine": "disabled",
|
"C_Cpp.intelliSenseEngine": "disabled",
|
||||||
|
|
||||||
// --query-driver lets clangd ask the cross compiler for its own system
|
// --query-driver lets clangd ask the cross compiler for its own system
|
||||||
// includes and target. The database is named here rather than in .clangd
|
// includes and target.
|
||||||
// because that file travels with the driver into a consumer's submodule,
|
|
||||||
// where a build tree of this repo's own need not exist.
|
|
||||||
"clangd.arguments": [
|
"clangd.arguments": [
|
||||||
"--compile-commands-dir=${workspaceFolder}/build/atmega328p-generated",
|
"--compile-commands-dir=${workspaceFolder}/build/atmega328p-generated",
|
||||||
"--query-driver=**avr-g++*",
|
"--query-driver=**avr-g++*",
|
||||||
|
|||||||
+22
-39
@@ -34,9 +34,8 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
# distribution's compiler speaks in full).
|
# distribution's compiler speaks in full).
|
||||||
#
|
#
|
||||||
# **A host that cannot build it registers those tests anyway and skips
|
# **A host that cannot build it registers those tests anyway and skips
|
||||||
# them.** They used to be left out, which makes the suite a different size
|
# them**: leaving them out would make the suite's size a property of the
|
||||||
# on a different machine - and a suite whose size is a property of the
|
# machine, and such a suite is one nothing can be compared against.
|
||||||
# machine is one nothing can be compared against.
|
|
||||||
set(TSB_DEVICE ${CMAKE_BINARY_DIR}/tsb_device)
|
set(TSB_DEVICE ${CMAKE_BINARY_DIR}/tsb_device)
|
||||||
find_program(_host_cxx NAMES c++ g++)
|
find_program(_host_cxx NAMES c++ g++)
|
||||||
set(_tsb_absent "${LIBAVR_NO_PYTHON}")
|
set(_tsb_absent "${LIBAVR_NO_PYTHON}")
|
||||||
@@ -66,6 +65,11 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
if(_tsb_absent)
|
if(_tsb_absent)
|
||||||
message(STATUS "the protocol tests skip here - ${_tsb_absent}")
|
message(STATUS "the protocol tests skip here - ${_tsb_absent}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# CMakePresets.json is generated; the gate holds it to its generator.
|
||||||
|
libavr_launcher(_presets_python "${LIBAVR_NO_PYTHON}" ${Python3_EXECUTABLE})
|
||||||
|
add_test(NAME presets.generated
|
||||||
|
COMMAND ${_presets_python} ${CMAKE_CURRENT_SOURCE_DIR}/tools/make_presets.py --check)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# The ELF is only a container (symbols, section headers) and is never flashed -
|
# The ELF is only a container (symbols, section headers) and is never flashed -
|
||||||
@@ -82,48 +86,27 @@ function(add_image_outputs name)
|
|||||||
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
|
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
# The TinySafeBoot protocol reimplemented on libavr in variants that trade
|
# The TinySafeBoot protocol reimplemented on libavr in four tiers that trade
|
||||||
# clarity for size. Each links into the ATmega328P boot section (BOOTSZ selects
|
# clarity for size, each with the full oracle feature set; each source's header
|
||||||
# its size; BOOTRST vectors a reset to its base) with -nostartfiles - a polled
|
# says what its tier is allowed, and oracle/README.md compares what they
|
||||||
# loader has no use for the crt or the vector table. The entry sits in
|
# measure. Each links into the ATmega328P boot section (BOOTSZ selects its
|
||||||
# .vectors, laid first, and runs - avr::startup::entry on the policy tier,
|
# size; BOOTRST vectors a reset to its base) with -nostartfiles - a polled
|
||||||
# the experiment tiers' own naked stubs elsewhere, each documented in its
|
# loader has no use for the crt or the vector table - and its entry sits in
|
||||||
# source. The boot base is FLASHEND+1 minus the section size; the linker
|
# .vectors, laid first. The boot base is the flash size minus the section size;
|
||||||
# section-start and the source's boot_bytes agree. tsb_app is
|
# the linker section-start and the source's boot_bytes agree. tsb_app is the
|
||||||
# the application's reset vector, pinned to 0 here so the loaders jump to a
|
# application's reset vector, pinned to 0 here so the loaders jump to a named
|
||||||
# named function; --pmem-wrap-around lets relaxation turn that absolute jump
|
# function; --pmem-wrap-around lets relaxation turn that absolute jump into the
|
||||||
# into the wrapped rjmp AVR's modulo-flash PC actually executes.
|
# wrapped rjmp AVR's modulo-flash PC actually executes.
|
||||||
# All four implement the full oracle feature set (see oracle/README.md):
|
set(TSB_FLASH_KIB 32)
|
||||||
# watchdog bail, one-wire half-duplex, config-page activation timeout, password
|
|
||||||
# gate, emergency erase, config/flash/EEPROM read-write. They differ only in how,
|
|
||||||
# and the size gradient is the cost of that "how".
|
|
||||||
# tsb_asm - the tricks tier's C++ with exactly two routines in asm: the
|
|
||||||
# bounded rx and the page-store loop, the two whose remaining
|
|
||||||
# cost is the C ABI itself. Everything else, bring-up to
|
|
||||||
# dispatch, is C++ on libavr.
|
|
||||||
# tsb_tricks - no asm at all: the whole-loader register allocation lives in
|
|
||||||
# global register variables (Y walks the page pointer), every
|
|
||||||
# helper is a tiny noinline primitive placed by the
|
|
||||||
# global-register store rules, pages stream straight to
|
|
||||||
# SPM/EEPROM.
|
|
||||||
# tsb_pure - pure idiomatic libavr, one function per command, TU-local
|
|
||||||
# (internal linkage), streaming (no SRAM page buffer).
|
|
||||||
# tsb_policy - the policy floor: no inline assembly and no global register
|
|
||||||
# variables, which is philosophy #5's own bound, and the
|
|
||||||
# measured evidence that the 512 B fit is a property of the
|
|
||||||
# mechanisms it bans.
|
|
||||||
#
|
|
||||||
# What each measures is oracle/README.md's table, which is the one place the
|
|
||||||
# four numbers and the hand-written loader's own are compared.
|
|
||||||
#
|
|
||||||
# add_tsb_variant(<name> <boot-section-bytes>)
|
# add_tsb_variant(<name> <boot-section-bytes>)
|
||||||
function(add_tsb_variant name bytes)
|
function(add_tsb_variant name bytes)
|
||||||
math(EXPR base_dec "32768 - ${bytes}")
|
math(EXPR base_dec "${TSB_FLASH_KIB} * 1024 - ${bytes}")
|
||||||
math(EXPR base_hex "${base_dec}" OUTPUT_FORMAT HEXADECIMAL)
|
math(EXPR base_hex "${base_dec}" OUTPUT_FORMAT HEXADECIMAL)
|
||||||
add_executable(${name} tsb/${name}.cpp)
|
add_executable(${name} tsb/${name}.cpp)
|
||||||
target_link_libraries(${name} PRIVATE libavr)
|
target_link_libraries(${name} PRIVATE libavr)
|
||||||
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${base_hex}
|
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${base_hex}
|
||||||
-Wl,--defsym=tsb_app=0 -Wl,--pmem-wrap-around=32k)
|
-Wl,--defsym=tsb_app=0 -Wl,--pmem-wrap-around=${TSB_FLASH_KIB}k)
|
||||||
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
|
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
|
||||||
add_image_outputs(${name})
|
add_image_outputs(${name})
|
||||||
if(PROJECT_IS_TOP_LEVEL)
|
if(PROJECT_IS_TOP_LEVEL)
|
||||||
|
|||||||
+1
-1
Submodule libavr updated: 93d8b0e491...73fde9c243.
+3
-3
@@ -13,9 +13,9 @@ in AVR assembly. It is the size-and-feature bar for the port's `tsb_asm` tier.
|
|||||||
|
|
||||||
## Why this variant
|
## Why this variant
|
||||||
|
|
||||||
The user chose the fixed-baud, hardware-UART variant deliberately: it is the one
|
The fixed-baud, hardware-UART variant is the one whose feature set the port must
|
||||||
whose feature set the port must match. It fits the **complete** TSB feature set
|
match. It fits the **complete** TSB feature set into the 512-byte ATmega boot
|
||||||
into the 512-byte ATmega boot section:
|
section:
|
||||||
|
|
||||||
| Feature | Oracle routine |
|
| Feature | Oracle routine |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
|||||||
+17
-26
@@ -1,12 +1,8 @@
|
|||||||
// simavr "device" for the TSB bootloader: load the boot-linked ELF into the
|
// simavr "device" for the TSB bootloader: load the boot-linked ELF into the
|
||||||
// ATmega328P boot section, enter it (BOOTRST is not modelled, so we set PC to
|
// ATmega328P boot section, enter it (BOOTRST is not modelled, so PC is set to
|
||||||
// the boot base, exactly as simavr's own board_simduino does), and expose
|
// the boot base, exactly as simavr's own board_simduino does), and expose
|
||||||
// UART0 as a pty. A host client (Python pyserial, or the real tsbloader) then
|
// UART0 as a pty. A host client (Python pyserial, or the real tsbloader) then
|
||||||
// speaks the TSB protocol over that pty and actually flashes the device.
|
// speaks the TSB protocol over that pty and actually flashes the device.
|
||||||
//
|
|
||||||
// SPM genuinely writes avr->flash on the mega cores, so on exit (or SIGTERM)
|
|
||||||
// we dump the flash image to a file for a ground-truth cross-check against
|
|
||||||
// what the client read back through the bootloader.
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <csignal>
|
#include <csignal>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@@ -28,19 +24,16 @@ extern "C" {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
// What the loaders are built for: tsb/*.cpp's device clock and the chip's SPM
|
||||||
|
// page, which is also the config page sitting directly below the boot section.
|
||||||
|
constexpr std::uint32_t clock_hz = 16'000'000;
|
||||||
|
constexpr std::uint32_t page_bytes = 128;
|
||||||
|
|
||||||
avr_t *avr;
|
avr_t *avr;
|
||||||
uart_pty_t uart_pty;
|
uart_pty_t uart_pty;
|
||||||
const char *dump_path;
|
|
||||||
|
|
||||||
[[noreturn]] void finish(int)
|
[[noreturn]] void finish(int)
|
||||||
{
|
{
|
||||||
if (dump_path) {
|
|
||||||
std::FILE *f = std::fopen(dump_path, "wb");
|
|
||||||
if (f) {
|
|
||||||
std::fwrite(avr->flash, 1, avr->flashend + 1, f);
|
|
||||||
std::fclose(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uart_pty_stop(&uart_pty);
|
uart_pty_stop(&uart_pty);
|
||||||
_exit(0);
|
_exit(0);
|
||||||
}
|
}
|
||||||
@@ -49,12 +42,11 @@ const char *dump_path;
|
|||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
if (argc < 3) {
|
if (argc != 3) {
|
||||||
std::println(stderr, "usage: {} <tsb.elf> <boot_base_hex> [flash_dump.bin]", argv[0]);
|
std::println(stderr, "usage: {} <tsb.elf> <boot_base_hex>", argv[0]);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
auto boot_base = static_cast<std::uint32_t>(std::strtoul(argv[2], nullptr, 0));
|
const auto boot_base = static_cast<std::uint32_t>(std::strtoul(argv[2], nullptr, 0));
|
||||||
dump_path = argc >= 4 ? argv[3] : nullptr;
|
|
||||||
|
|
||||||
avr = avr_make_mcu_by_name("atmega328p");
|
avr = avr_make_mcu_by_name("atmega328p");
|
||||||
if (!avr) {
|
if (!avr) {
|
||||||
@@ -62,14 +54,14 @@ int main(int argc, char *argv[])
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
avr_init(avr);
|
avr_init(avr);
|
||||||
avr->frequency = 16000000;
|
avr->frequency = clock_hz;
|
||||||
// Real flash powers up erased (0xff); the app region must look erased
|
// Real flash powers up erased (0xff); the app region must look erased
|
||||||
// before the bootloader programs it.
|
// before the bootloader programs it.
|
||||||
std::memset(avr->flash, 0xff, avr->flashend + 1);
|
std::memset(avr->flash, 0xff, avr->flashend + 1);
|
||||||
|
|
||||||
// simavr's ELF loader flattens the flash base to 0 (it expects an app at
|
// simavr's ELF loader flattens the flash base to 0 (it expects an app at
|
||||||
// 0x0), but it hands back the boot code in fw.flash; place it at the boot
|
// 0x0), but it hands back the boot code in fw.flash, so it is placed at the
|
||||||
// section base ourselves and enter there (BOOTRST is not modelled).
|
// boot section base by hand.
|
||||||
elf_firmware_t fw{};
|
elf_firmware_t fw{};
|
||||||
if (elf_read_firmware(argv[1], &fw) != 0) {
|
if (elf_read_firmware(argv[1], &fw) != 0) {
|
||||||
std::println(stderr, "device: cannot read {}", argv[1]);
|
std::println(stderr, "device: cannot read {}", argv[1]);
|
||||||
@@ -88,12 +80,11 @@ int main(int argc, char *argv[])
|
|||||||
avr->pc = boot_base;
|
avr->pc = boot_base;
|
||||||
avr->codeend = avr->flashend;
|
avr->codeend = avr->flashend;
|
||||||
|
|
||||||
// Optional: seed the config page (one page below the boot section) with a
|
// Optional: seed the config page with a hex byte string, so the password
|
||||||
// hex byte string, so the password gate and emergency erase can be tested.
|
// gate and emergency erase can be tested.
|
||||||
// Layout: [appjump lo][appjump hi][timeout][password...][0xff].
|
// Layout: [appjump lo][appjump hi][timeout][password...][0xff].
|
||||||
const char *cfg = std::getenv("TSB_CONFIG");
|
if (const char *cfg = std::getenv("TSB_CONFIG")) {
|
||||||
if (cfg) {
|
const auto app_end = boot_base - page_bytes;
|
||||||
std::uint32_t app_end = boot_base - 128; // config page sits directly below the boot code
|
|
||||||
for (int i = 0; cfg[i] && cfg[i + 1]; i += 2) {
|
for (int i = 0; cfg[i] && cfg[i + 1]; i += 2) {
|
||||||
const std::array pair{cfg[i], cfg[i + 1], '\0'};
|
const std::array pair{cfg[i], cfg[i + 1], '\0'};
|
||||||
avr->flash[app_end + i / 2] = static_cast<std::uint8_t>(std::strtoul(pair.data(), nullptr, 16));
|
avr->flash[app_end + i / 2] = static_cast<std::uint8_t>(std::strtoul(pair.data(), nullptr, 16));
|
||||||
@@ -119,7 +110,7 @@ int main(int argc, char *argv[])
|
|||||||
std::signal(SIGINT, finish);
|
std::signal(SIGINT, finish);
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
int state = avr_run(avr);
|
const auto state = avr_run(avr);
|
||||||
if (state == cpu_Done || state == cpu_Crashed) {
|
if (state == cpu_Done || state == cpu_Crashed) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-5
@@ -23,14 +23,13 @@ class Device:
|
|||||||
page (via the device's TSB_CONFIG hook) so the password gate and emergency
|
page (via the device's TSB_CONFIG hook) so the password gate and emergency
|
||||||
erase are exercisable."""
|
erase are exercisable."""
|
||||||
|
|
||||||
def __init__(self, binary, elf, boot_base, dump="/tmp/tsb_dump.bin", config=None):
|
def __init__(self, binary, elf, boot_base, config=None):
|
||||||
env = dict(os.environ)
|
env = dict(os.environ)
|
||||||
if config is not None:
|
if config is not None:
|
||||||
env["TSB_CONFIG"] = config
|
env["TSB_CONFIG"] = config
|
||||||
self.proc = subprocess.Popen(
|
self.proc = subprocess.Popen(
|
||||||
[binary, elf, boot_base, dump],
|
[binary, elf, boot_base],
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env)
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env)
|
||||||
self.dump = dump
|
|
||||||
self.pty = None
|
self.pty = None
|
||||||
deadline = time.time() + 5
|
deadline = time.time() + 5
|
||||||
while time.time() < deadline:
|
while time.time() < deadline:
|
||||||
@@ -166,7 +165,6 @@ class Host:
|
|||||||
self.s.write(bytes([CONFIRM]))
|
self.s.write(bytes([CONFIRM]))
|
||||||
self._expect(CONFIRM, "emergency mainloop ready")
|
self._expect(CONFIRM, "emergency mainloop ready")
|
||||||
|
|
||||||
|
|
||||||
# A wrong password byte hangs the loader, still draining the line. Two
|
# A wrong password byte hangs the loader, still draining the line. Two
|
||||||
# things must not happen: it must not activate, and it must not fall
|
# things must not happen: it must not activate, and it must not fall
|
||||||
# through to the emergency erase - a byte the gate has already refused
|
# through to the emergency erase - a byte the gate has already refused
|
||||||
@@ -236,7 +234,7 @@ def scenario_emergency(host):
|
|||||||
def scenario_wrong_password(host):
|
def scenario_wrong_password(host):
|
||||||
"""A wrong password byte neither activates the loader nor opens the
|
"""A wrong password byte neither activates the loader nor opens the
|
||||||
emergency erase behind it - the oracle carries a dedicated fix for the
|
emergency erase behind it - the oracle carries a dedicated fix for the
|
||||||
second, and nothing here exercised either half."""
|
second."""
|
||||||
check(host.refuse_password(PW_BYTES[0] ^ 1) == b"", "a wrong password byte draws no reply")
|
check(host.refuse_password(PW_BYTES[0] ^ 1) == b"", "a wrong password byte draws no reply")
|
||||||
check(host.say(0x00) == b"", "a 0 byte after it does not request the erase")
|
check(host.say(0x00) == b"", "a 0 byte after it does not request the erase")
|
||||||
check(host.say(CONFIRM) == b"", "and neither does a confirm")
|
check(host.say(CONFIRM) == b"", "and neither does a confirm")
|
||||||
|
|||||||
@@ -75,12 +75,12 @@ def main():
|
|||||||
rendered = json.dumps(presets, indent=1) + "\n"
|
rendered = json.dumps(presets, indent=1) + "\n"
|
||||||
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "CMakePresets.json")
|
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "CMakePresets.json")
|
||||||
if "--check" in sys.argv[1:]:
|
if "--check" in sys.argv[1:]:
|
||||||
current = open(path).read() if os.path.exists(path) else ""
|
current = open(path, encoding="utf-8").read() if os.path.exists(path) else ""
|
||||||
if current != rendered:
|
if current != rendered:
|
||||||
print("CMakePresets.json does not match its generator - run tools/make_presets.py")
|
print("CMakePresets.json does not match its generator - run tools/make_presets.py")
|
||||||
return 1
|
return 1
|
||||||
return 0
|
return 0
|
||||||
with open(path, "w") as f:
|
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
||||||
f.write(rendered)
|
f.write(rendered)
|
||||||
print(f"{len(CHIPS)} chips, {len(REFLECT_SPOT)} reflect: {os.path.normpath(path)}")
|
print(f"{len(CHIPS)} chips, {len(REFLECT_SPOT)} reflect: {os.path.normpath(path)}")
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
// The TinySafeBoot native-UART fixed-baud protocol, as every tier speaks it:
|
||||||
|
// the link, the handshake and command bytes, the boot geometry and the
|
||||||
|
// device-info block the host reads on activation.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
|
namespace tsb {
|
||||||
|
|
||||||
|
using namespace avr::literals;
|
||||||
|
namespace spm = avr::spm;
|
||||||
|
namespace ee = avr::eeprom;
|
||||||
|
|
||||||
|
constexpr auto clock = 16_MHz;
|
||||||
|
constexpr auto baud_rate = 115200_Bd;
|
||||||
|
|
||||||
|
// Fixed 8N1; the library solves UBRR + U2X from clock and baud.
|
||||||
|
constexpr auto baud = avr::uart::solve_baud(clock, baud_rate, 8, avr::uart::parity::none);
|
||||||
|
|
||||||
|
// One bit time on the wire: the turn-around a shared-line peer needs to stop
|
||||||
|
// driving before this one starts.
|
||||||
|
constexpr auto guard_cycles = static_cast<std::uint32_t>(clock.hz / baud.actual);
|
||||||
|
|
||||||
|
// The loader is purely polled - it never enables interrupts - so every SPM and
|
||||||
|
// EEPROM lock folds to nothing under this posture.
|
||||||
|
constexpr auto off = avr::irq::guard_policy::unused;
|
||||||
|
|
||||||
|
// Strict request/response: every SPM operation is waited out before the next
|
||||||
|
// byte moves, so no flash operation is ever in flight at an EEPROM access -
|
||||||
|
// the write procedure's step 2 has nothing to guard, the omission the
|
||||||
|
// datasheet grants (DS40002061B section 8.6.3).
|
||||||
|
constexpr auto no_spm = ee::spm_interlock::omitted;
|
||||||
|
|
||||||
|
constexpr std::uint8_t confirm = '!';
|
||||||
|
constexpr std::uint8_t request = '?';
|
||||||
|
constexpr std::uint8_t knock = '@';
|
||||||
|
constexpr std::uint8_t activation_knocks = 3;
|
||||||
|
|
||||||
|
namespace command {
|
||||||
|
constexpr std::uint8_t read_flash = 'f';
|
||||||
|
constexpr std::uint8_t write_flash = 'F';
|
||||||
|
constexpr std::uint8_t read_eeprom = 'e';
|
||||||
|
constexpr std::uint8_t write_eeprom = 'E';
|
||||||
|
constexpr std::uint8_t read_config = 'c';
|
||||||
|
constexpr std::uint8_t write_config = 'C';
|
||||||
|
} // namespace command
|
||||||
|
|
||||||
|
// The lockout-proof floor for the receive window: the oracle's F_CPU/1MHz.
|
||||||
|
constexpr auto act_min = static_cast<std::uint8_t>(clock.hz / 1'000'000);
|
||||||
|
// Post-activation window: the host gets seconds, not milliseconds, mid-session.
|
||||||
|
constexpr std::uint8_t comm_window = 200;
|
||||||
|
|
||||||
|
constexpr std::uint16_t page = spm::page_bytes;
|
||||||
|
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
|
||||||
|
|
||||||
|
// The config page (TSB's LASTPAGE), one page below the boot section: the
|
||||||
|
// application's jump vector, then these.
|
||||||
|
consteval std::uint16_t config_page(std::uint16_t boot_bytes)
|
||||||
|
{
|
||||||
|
return spm::flash_bytes - boot_bytes - page;
|
||||||
|
}
|
||||||
|
constexpr std::uint8_t timeout_at = 2;
|
||||||
|
constexpr std::uint8_t password_at = 3;
|
||||||
|
|
||||||
|
// The firmware version stamp, in the encoding the host decodes.
|
||||||
|
consteval std::uint16_t date_stamp(std::uint16_t year, std::uint16_t month, std::uint16_t day)
|
||||||
|
{
|
||||||
|
return year * 512 + month * 32 + day;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BUILDSTATE F3: the native-UART fixed-baud lineage.
|
||||||
|
constexpr std::uint8_t build_state = 0xF3;
|
||||||
|
constexpr std::uint8_t atmega_marker = 0xAA;
|
||||||
|
|
||||||
|
consteval auto info_block(std::uint16_t build_date, std::uint16_t app_end)
|
||||||
|
{
|
||||||
|
const auto low = [](std::uint16_t word) { return static_cast<std::uint8_t>(word); };
|
||||||
|
const auto high = [](std::uint16_t word) { return static_cast<std::uint8_t>(word >> 8); };
|
||||||
|
const std::uint16_t app_end_words = app_end / 2;
|
||||||
|
return std::to_array<std::uint8_t>({
|
||||||
|
'T',
|
||||||
|
'S',
|
||||||
|
'B',
|
||||||
|
low(build_date),
|
||||||
|
high(build_date),
|
||||||
|
build_state,
|
||||||
|
avr::hw::db.signature[0],
|
||||||
|
avr::hw::db.signature[1],
|
||||||
|
avr::hw::db.signature[2],
|
||||||
|
page / 2,
|
||||||
|
low(app_end_words),
|
||||||
|
high(app_end_words),
|
||||||
|
low(eeprom_end),
|
||||||
|
high(eeprom_end),
|
||||||
|
atmega_marker,
|
||||||
|
atmega_marker,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||||
|
{
|
||||||
|
return reinterpret_cast<const std::uint8_t *>(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tsb
|
||||||
+35
-87
@@ -30,80 +30,26 @@
|
|||||||
// line safe: the device drives it only between a received command and its
|
// line safe: the device drives it only between a received command and its
|
||||||
// reply, and releases it (RXEN0 only) whenever it waits.
|
// reply, and releases it (RXEN0 only) whenever it waits.
|
||||||
|
|
||||||
#include <libavr/libavr.hpp>
|
#include "tsb.hpp"
|
||||||
|
|
||||||
#include <avr/io.h> // SP / RAMEND for the crt-free boot entry, SFR addresses for the asm routines
|
#include <avr/io.h> // SPMCSR's I/O address for the page-store routine
|
||||||
|
|
||||||
using namespace avr::literals;
|
|
||||||
namespace spm = avr::spm;
|
|
||||||
namespace ee = avr::eeprom;
|
|
||||||
namespace hw = avr::hw;
|
namespace hw = avr::hw;
|
||||||
|
|
||||||
namespace tsb {
|
namespace tsb {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// The loader is purely polled - it never enables interrupts - so every SPM and
|
// The boot section: 512 B, BOOTSZ=11.
|
||||||
// EEPROM lock folds to nothing under this posture.
|
|
||||||
constexpr auto off = avr::irq::guard_policy::unused;
|
|
||||||
|
|
||||||
// Strict request/response: every SPM operation is waited out before the next
|
|
||||||
// byte moves, so no flash operation is ever in flight at an EEPROM access -
|
|
||||||
// the write procedure's step 2 has nothing to guard, the omission the
|
|
||||||
// datasheet grants (DS40002061B section 8.6.3).
|
|
||||||
constexpr auto no_spm = ee::spm_interlock::omitted;
|
|
||||||
|
|
||||||
constexpr std::uint8_t confirm = '!';
|
|
||||||
constexpr std::uint8_t request = '?';
|
|
||||||
constexpr std::uint8_t knock = '@';
|
|
||||||
|
|
||||||
// Boot geometry for the 512 B boot section (BOOTSZ=11); the page size and the
|
|
||||||
// flash/EEPROM extents are the chip database's to know. app_end is the config
|
|
||||||
// page (TSB's LASTPAGE), one page below the boot section.
|
|
||||||
constexpr std::uint16_t page = spm::page_bytes;
|
|
||||||
constexpr std::uint16_t boot_bytes = 512;
|
constexpr std::uint16_t boot_bytes = 512;
|
||||||
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page;
|
constexpr std::uint16_t app_end = config_page(boot_bytes);
|
||||||
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
|
|
||||||
|
|
||||||
// Lockout-proof floor for the activation window: the oracle's F_CPU/1MHz, so
|
[[gnu::progmem]] constexpr auto info = info_block(date_stamp(26, 7, 20), app_end);
|
||||||
// it follows the clock rather than restating it (rule 41).
|
|
||||||
constexpr auto act_min = static_cast<std::uint8_t>((16_MHz).hz / 1'000'000);
|
|
||||||
// Post-activation window: the host gets seconds, not milliseconds, mid-session.
|
|
||||||
constexpr std::uint8_t comm_window = 200;
|
|
||||||
|
|
||||||
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
|
||||||
|
|
||||||
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
|
||||||
constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd, 8, avr::uart::parity::none);
|
|
||||||
|
|
||||||
// One bit time on the wire: the turn-around a shared-line peer needs to stop
|
|
||||||
// driving before this one starts. Derived from the solved rate, so it follows
|
|
||||||
// the link rather than a count measured against one.
|
|
||||||
constexpr auto guard_cycles = static_cast<std::uint32_t>((16_MHz).hz / baud.actual);
|
|
||||||
|
|
||||||
// The 16-byte device-info block, streamed out on activation.
|
|
||||||
// clang-format off
|
|
||||||
[[gnu::progmem]] constexpr auto info = std::to_array<std::uint8_t>({
|
|
||||||
'T', 'S', 'B',
|
|
||||||
build_date & 0xFF, build_date >> 8,
|
|
||||||
0xF3, // status: native-UART fixed-baud lineage
|
|
||||||
avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2],
|
|
||||||
page / 2, // page size in words
|
|
||||||
(app_end / 2) & 0xFF, (app_end / 2) >> 8,
|
|
||||||
eeprom_end & 0xFF, eeprom_end >> 8,
|
|
||||||
0xAA, 0xAA,
|
|
||||||
});
|
|
||||||
// clang-format on
|
|
||||||
|
|
||||||
register std::uint16_t g_addr asm("r28");
|
register std::uint16_t g_addr asm("r28");
|
||||||
register std::uint8_t g_cnt asm("r16");
|
register std::uint8_t g_cnt asm("r16");
|
||||||
register std::uint8_t g_window asm("r7");
|
register std::uint8_t g_window asm("r7");
|
||||||
register std::uint8_t g_receiving asm("r6");
|
register std::uint8_t g_receiving asm("r6");
|
||||||
|
|
||||||
const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|
||||||
{
|
|
||||||
return reinterpret_cast<const std::uint8_t *>(addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bounded byte receive (asm 1 of 2): release the one-wire line on a direction
|
// Bounded byte receive (asm 1 of 2): release the one-wire line on a direction
|
||||||
// change, poll RXC0 under the oracle's nested X-register countdown seeded from
|
// change, poll RXC0 under the oracle's nested X-register countdown seeded from
|
||||||
// g_window (floored against lockout), byte or 0-on-silence in r24. Z survives
|
// g_window (floored against lockout), byte or 0-on-silence in r24. Z survives
|
||||||
@@ -113,7 +59,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|||||||
std::uint8_t byte;
|
std::uint8_t byte;
|
||||||
asm volatile(" tst %[dir] \n\t" // already receiving? keep the line released
|
asm volatile(" tst %[dir] \n\t" // already receiving? keep the line released
|
||||||
" brne 1f \n\t"
|
" brne 1f \n\t"
|
||||||
" ldi %[b], 0x10 \n\t" // RXEN0 alone: release the line and listen
|
" ldi %[b], %[rxen] \n\t" // the receiver alone: release the line and listen
|
||||||
" sts %[ucsr0b], %[b] \n\t"
|
" sts %[ucsr0b], %[b] \n\t"
|
||||||
" ser %[b] \n\t"
|
" ser %[b] \n\t"
|
||||||
" mov %[dir], %[b] \n\t"
|
" mov %[dir], %[b] \n\t"
|
||||||
@@ -122,7 +68,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|||||||
" clr r26 \n\t"
|
" clr r26 \n\t"
|
||||||
"2: ser %[b] \n\t"
|
"2: ser %[b] \n\t"
|
||||||
"3: lds %[b], %[ucsr0a] \n\t"
|
"3: lds %[b], %[ucsr0a] \n\t"
|
||||||
" sbrc %[b], 7 \n\t" // RXC0
|
" sbrc %[b], %[rxc] \n\t"
|
||||||
" rjmp 4f \n\t"
|
" rjmp 4f \n\t"
|
||||||
" dec %[b] \n\t"
|
" dec %[b] \n\t"
|
||||||
" brne 3b \n\t"
|
" brne 3b \n\t"
|
||||||
@@ -133,8 +79,9 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|||||||
"4: lds %[b], %[udr0] \n\t"
|
"4: lds %[b], %[udr0] \n\t"
|
||||||
"5: \n\t"
|
"5: \n\t"
|
||||||
: [b] "=&d"(byte), [dir] "+r"(g_receiving)
|
: [b] "=&d"(byte), [dir] "+r"(g_receiving)
|
||||||
: [to] "r"(g_window), [actmin] "M"(act_min), [ucsr0a] "n"(_SFR_MEM_ADDR(UCSR0A)),
|
: [to] "r"(g_window), [actmin] "M"(act_min), [rxen] "M"(hw::ucsr0b::rxen0(1).value),
|
||||||
[ucsr0b] "n"(_SFR_MEM_ADDR(UCSR0B)), [udr0] "n"(_SFR_MEM_ADDR(UDR0))
|
[rxc] "I"(std::countr_zero(hw::ucsr0a::rxc0.mask)), [ucsr0a] "n"(hw::ucsr0a::addr),
|
||||||
|
[ucsr0b] "n"(hw::ucsr0b::addr), [udr0] "n"(hw::udr0::addr)
|
||||||
: "r26", "r27", "cc");
|
: "r26", "r27", "cc");
|
||||||
return byte;
|
return byte;
|
||||||
}
|
}
|
||||||
@@ -169,7 +116,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|||||||
// global-register rule, see tsb_tricks.cpp).
|
// global-register rule, see tsb_tricks.cpp).
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t sflash()
|
[[gnu::noinline, gnu::noclone]] std::uint8_t sflash()
|
||||||
{
|
{
|
||||||
std::uint8_t byte = avr::flash_load(flash_ptr(g_addr));
|
const auto byte = avr::flash_load(flash_ptr(g_addr));
|
||||||
++g_addr;
|
++g_addr;
|
||||||
return byte;
|
return byte;
|
||||||
}
|
}
|
||||||
@@ -177,7 +124,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|||||||
// One EEPROM byte <- [g_addr++].
|
// One EEPROM byte <- [g_addr++].
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t eerd()
|
[[gnu::noinline, gnu::noclone]] std::uint8_t eerd()
|
||||||
{
|
{
|
||||||
std::uint8_t byte = ee::read<no_spm>(g_addr);
|
const auto byte = ee::read<no_spm>(g_addr);
|
||||||
++g_addr;
|
++g_addr;
|
||||||
return byte;
|
return byte;
|
||||||
}
|
}
|
||||||
@@ -213,7 +160,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
tsb_app();
|
tsb_app();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step g_addr one page down and erase that page (the decrement lives here -
|
// Step g_addr one page down and erase that page (the decrement lives here -
|
||||||
// the global-register rule).
|
// the global-register rule).
|
||||||
[[gnu::noinline, gnu::noclone]] void erase_below()
|
[[gnu::noinline, gnu::noclone]] void erase_below()
|
||||||
{
|
{
|
||||||
@@ -233,7 +180,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stream one host page into the erased flash page at g_addr (asm 2 of 2): the
|
// Stream one host page into the erased flash page at g_addr (asm 2 of 2): the
|
||||||
// word pair stages in r0:r1 straight from rx (whose register set is known -
|
// word pair stages in r0:r1 straight from rx (whose register set is known -
|
||||||
// the cross-call liveness C++ cannot express), Z walks the page and PGWRT
|
// the cross-call liveness C++ cannot express), Z walks the page and PGWRT
|
||||||
// programs it. g_addr is left at the next page base.
|
// programs it. g_addr is left at the next page base.
|
||||||
[[gnu::noinline, gnu::noclone]] void store_flash()
|
[[gnu::noinline, gnu::noclone]] void store_flash()
|
||||||
@@ -243,8 +190,8 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
"1: rcall %x[rx] \n\t"
|
"1: rcall %x[rx] \n\t"
|
||||||
" mov r0, r24 \n\t" // word low byte
|
" mov r0, r24 \n\t" // word low byte
|
||||||
" rcall %x[rx] \n\t"
|
" rcall %x[rx] \n\t"
|
||||||
" mov r1, r24 \n\t" // word high byte
|
" mov r1, r24 \n\t" // word high byte
|
||||||
" ldi r24, 0x01 \n\t" // SPMEN: buffer the word at Z
|
" ldi r24, %[fill] \n\t" // buffer the word at Z
|
||||||
" out %[spmcsr], r24 \n\t"
|
" out %[spmcsr], r24 \n\t"
|
||||||
" spm \n\t"
|
" spm \n\t"
|
||||||
" clr r1 \n\t"
|
" clr r1 \n\t"
|
||||||
@@ -254,12 +201,13 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
" movw %[base], r30 \n\t" // g_addr = the next page base
|
" movw %[base], r30 \n\t" // g_addr = the next page base
|
||||||
" subi r30, %[pagelo] \n\t" // Z back to this page's base
|
" subi r30, %[pagelo] \n\t" // Z back to this page's base
|
||||||
" sbci r31, %[pagehi] \n\t"
|
" sbci r31, %[pagehi] \n\t"
|
||||||
" ldi r24, 0x05 \n\t" // PGWRT | SPMEN: program the page
|
" ldi r24, %[write] \n\t" // program the page
|
||||||
" out %[spmcsr], r24 \n\t"
|
" out %[spmcsr], r24 \n\t"
|
||||||
" spm \n\t"
|
" spm \n\t"
|
||||||
: [base] "+r"(g_addr)
|
: [base] "+r"(g_addr)
|
||||||
: [rx] "i"(&rx), [spmcsr] "I"(_SFR_IO_ADDR(SPMCSR)), [words] "M"(page / 2), [pagelo] "M"(page & 0xff),
|
: [rx] "i"(&rx), [spmcsr] "I"(_SFR_IO_ADDR(SPMCSR)), [fill] "M"(hw::spmcsr::spmen(1).value),
|
||||||
[pagehi] "M"(page >> 8)
|
[write] "M"(hw::spmcsr::pgwrt(1).value | hw::spmcsr::spmen(1).value), [words] "M"(page / 2),
|
||||||
|
[pagelo] "M"(page & 0xff), [pagehi] "M"(page >> 8)
|
||||||
: "r0", "r1", "r20", "r24", "r26", "r27", "r30", "r31", "cc", "memory");
|
: "r0", "r1", "r20", "r24", "r26", "r27", "r30", "r31", "cc", "memory");
|
||||||
settle();
|
settle();
|
||||||
}
|
}
|
||||||
@@ -284,23 +232,23 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
// same reason.
|
// same reason.
|
||||||
g_receiving = 0;
|
g_receiving = 0;
|
||||||
|
|
||||||
// Activation: 3x'@', each inside the config page's timeout window (rx
|
// Activation: activation_knocks '@', each inside the config page's timeout
|
||||||
// floors it so a corrupt page cannot lock the loader out); anything else -
|
// window (rx floors it so a corrupt page cannot lock the loader out);
|
||||||
// including silence - hands over.
|
// anything else - including silence - hands over.
|
||||||
g_window = avr::flash_load(flash_ptr(app_end + 2));
|
g_window = avr::flash_load(flash_ptr(app_end + timeout_at));
|
||||||
for (std::uint8_t k = 3; k; --k) {
|
for (std::uint8_t k = activation_knocks; k; --k) {
|
||||||
if (rx() != knock) {
|
if (rx() != knock) {
|
||||||
appjump();
|
appjump();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
g_window = comm_window;
|
g_window = comm_window;
|
||||||
|
|
||||||
// Password gate (config page from app_end+3, 0xff-terminated; a blank
|
// Password gate (config page from password_at, 0xff-terminated; a blank
|
||||||
// page is no password). A wrong byte blanks the comparison and drains the
|
// page is no password). A wrong byte blanks the comparison and drains the
|
||||||
// line forever, so a wrong password can never fall through; a 0 requests
|
// line forever, so a wrong password can never fall through; a 0 requests
|
||||||
// emergency erase behind two confirms. On pass the info block goes out;
|
// emergency erase behind two confirms. On pass the info block goes out;
|
||||||
// the emergency path skips it and drops into the command loop.
|
// the emergency path skips it and drops into the command loop.
|
||||||
g_addr = app_end + 3;
|
g_addr = app_end + password_at;
|
||||||
std::uint8_t mask = 0xff;
|
std::uint8_t mask = 0xff;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask;
|
std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask;
|
||||||
@@ -311,7 +259,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
sendf();
|
sendf();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
std::uint8_t got = rx();
|
const auto got = rx();
|
||||||
if (got == 0) {
|
if (got == 0) {
|
||||||
if (mask == 0) {
|
if (mask == 0) {
|
||||||
continue;
|
continue;
|
||||||
@@ -336,7 +284,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
tx(confirm); // Mainloop ready
|
tx(confirm); // Mainloop ready
|
||||||
g_addr = 0;
|
g_addr = 0;
|
||||||
switch (rx()) {
|
switch (rx()) {
|
||||||
case 'f': // read application flash, one page per host '!'
|
case command::read_flash: // one page per host '!'
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (rx() != confirm) {
|
if (rx() != confirm) {
|
||||||
break;
|
break;
|
||||||
@@ -348,13 +296,13 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'F': // erase the application, then take pages behind '?'
|
case command::write_flash: // erase the application, then take pages behind '?'
|
||||||
erase_application(); // leaves g_addr = 0, the write start
|
erase_application(); // leaves g_addr = 0, the write start
|
||||||
while (rcnf() == confirm) {
|
while (rcnf() == confirm) {
|
||||||
store_flash();
|
store_flash();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'e': // read EEPROM, one page per host '!', until the host stops
|
case command::read_eeprom: // one page per host '!', until the host stops
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (rx() != confirm) {
|
if (rx() != confirm) {
|
||||||
break;
|
break;
|
||||||
@@ -365,7 +313,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
} while (--g_cnt);
|
} while (--g_cnt);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'E': // take EEPROM pages behind '?'
|
case command::write_eeprom: // pages behind '?'
|
||||||
while (rcnf() == confirm) {
|
while (rcnf() == confirm) {
|
||||||
g_cnt = page;
|
g_cnt = page;
|
||||||
do {
|
do {
|
||||||
@@ -373,13 +321,13 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
} while (--g_cnt);
|
} while (--g_cnt);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'c': // read the config page
|
case command::read_config:
|
||||||
read_config:
|
read_config:
|
||||||
g_addr = app_end;
|
g_addr = app_end;
|
||||||
g_cnt = page;
|
g_cnt = page;
|
||||||
sendf();
|
sendf();
|
||||||
break;
|
break;
|
||||||
case 'C': // replace the config page, then echo it back to verify
|
case command::write_config: // then echo it back to verify
|
||||||
if (rcnf() != confirm) {
|
if (rcnf() != confirm) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-93
@@ -2,105 +2,51 @@
|
|||||||
//
|
//
|
||||||
// The full TinySafeBoot feature set - watchdog bail, one-wire half-duplex,
|
// The full TinySafeBoot feature set - watchdog bail, one-wire half-duplex,
|
||||||
// config-page activation timeout, password gate, emergency erase, and
|
// config-page activation timeout, password gate, emergency erase, and
|
||||||
// config/flash/EEPROM read-write - under philosophy #5 exactly as pureboot
|
// config/flash/EEPROM read-write - with no assembly and no register variables:
|
||||||
// obeys it: no assembly, no register variables; code, attributes, and flags
|
// code, attributes and flags only. It applies pureboot's size techniques - the
|
||||||
// only. Every lesson pureboot's development produced is applied - the
|
|
||||||
// library's half-duplex serial and startup entry, lean bring-up from reset
|
// library's half-duplex serial and startup entry, lean bring-up from reset
|
||||||
// state, one merged send loop over both memories, oracle-shaped loop bounds,
|
// state, one merged send loop over both memories, oracle-shaped loop bounds,
|
||||||
// locals threaded through noinline primitives, pureboot's codegen flags -
|
// locals threaded through noinline primitives, pureboot's codegen flags - and
|
||||||
// and the result sits below the idiomatic tier and above the 512 B boot
|
// lands between the idiomatic tier and the 512 B section the tricks/asm tiers
|
||||||
// section the tricks/asm tiers reach with the banned mechanisms
|
// reach with the banned mechanisms (oracle/README.md holds all four). The gap
|
||||||
// (oracle/README.md holds all four). This tier exists to keep that gap an
|
// to 512 is the rent of policy-clean C++: helpers that hold a cursor across
|
||||||
// artifact
|
// rx()/tx() pay push/pop and argument threading where a global-register
|
||||||
// rather than a claim: the gap to 512 is the rent of policy-clean C++ -
|
// protocol pays nothing, and both control-flow merges tried (a parametrized
|
||||||
// helpers that hold a cursor across rx()/tx() pay push/pop and argument
|
// paged session, a merged store loop) measured larger than the split cases.
|
||||||
// threading where a global-register protocol pays nothing, and both
|
|
||||||
// control-flow merges tried (a parametrized paged session, a merged store
|
|
||||||
// loop) measured larger than the split cases they replaced. TSB's wire fixes
|
|
||||||
// the per-command loop shapes on the device, so pureboot 5's one-transfer-
|
|
||||||
// loop collapse has no purchase here.
|
|
||||||
//
|
//
|
||||||
// The wire protocol is strict request/response, which is what makes the
|
// The wire protocol is strict request/response, which is what makes the
|
||||||
// shared line safe: the device drives it only between a received command and
|
// shared line safe: the device drives it only between a received command and
|
||||||
// its reply, and releases it (the library's half-duplex choreography)
|
// its reply, and releases it (the library's half-duplex choreography)
|
||||||
// whenever it waits.
|
// whenever it waits.
|
||||||
|
|
||||||
#include <libavr/libavr.hpp>
|
#include "tsb.hpp"
|
||||||
|
|
||||||
using namespace avr::literals;
|
using dev = avr::device<{.clock = tsb::clock}>;
|
||||||
namespace spm = avr::spm;
|
|
||||||
namespace ee = avr::eeprom;
|
|
||||||
|
|
||||||
using dev = avr::device<{.clock = 16_MHz}>;
|
|
||||||
// One-wire: RX and TX share the line, exactly as the native-UART TSB expects.
|
// One-wire: RX and TX share the line, exactly as the native-UART TSB expects.
|
||||||
// 115200 at 16 MHz lands +2.1 % off, past the receiver-tolerance table the
|
// 115200 at 16 MHz lands +2.1 % off, past the receiver-tolerance table the
|
||||||
// solver holds rates to - the oracle's own deployment has run there for a
|
// solver holds rates to - the oracle's own deployment has run there for a
|
||||||
// decade, so the override states that it is meant.
|
// decade, so the override states that it is meant.
|
||||||
using serial_t = dev::uart0<{
|
constexpr avr::uart::config link{
|
||||||
.baud = 115200_Bd,
|
.baud = tsb::baud_rate,
|
||||||
.allow_baud_error = true,
|
.allow_baud_error = true,
|
||||||
.half_duplex = true,
|
.half_duplex = true,
|
||||||
}>;
|
};
|
||||||
|
using serial_t = dev::uart0<link>;
|
||||||
inline constexpr serial_t serial{};
|
inline constexpr serial_t serial{};
|
||||||
|
|
||||||
namespace tsb {
|
namespace tsb {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// The loader is purely polled - it never enables interrupts - so every SPM and
|
// The boot section: 1 KB, BOOTSZ=10.
|
||||||
// EEPROM lock folds to nothing under this posture.
|
|
||||||
constexpr auto off = avr::irq::guard_policy::unused;
|
|
||||||
|
|
||||||
// Strict request/response: every SPM operation is waited out before the next
|
|
||||||
// byte moves, so no flash operation is ever in flight at an EEPROM access -
|
|
||||||
// the write procedure's step 2 has nothing to guard, the omission the
|
|
||||||
// datasheet grants (DS40002061B section 8.6.3).
|
|
||||||
constexpr auto no_spm = ee::spm_interlock::omitted;
|
|
||||||
|
|
||||||
// The handshake bytes, identical across every TSB host.
|
|
||||||
constexpr std::uint8_t confirm = '!';
|
|
||||||
constexpr std::uint8_t request = '?';
|
|
||||||
constexpr std::uint8_t knock = '@';
|
|
||||||
|
|
||||||
// Boot geometry for the 1 KB boot section (BOOTSZ=10); the page size and the
|
|
||||||
// flash/EEPROM extents are the chip database's to know. app_end is the config
|
|
||||||
// page (TSB's LASTPAGE), one page below the boot section.
|
|
||||||
constexpr std::uint16_t page = spm::page_bytes;
|
|
||||||
constexpr std::uint16_t boot_bytes = 1024;
|
constexpr std::uint16_t boot_bytes = 1024;
|
||||||
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page;
|
constexpr std::uint16_t app_end = config_page(boot_bytes);
|
||||||
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
|
|
||||||
|
|
||||||
// Lockout-proof floor for the activation window: the oracle's F_CPU/1MHz, so
|
[[gnu::progmem]] constexpr auto info = info_block(date_stamp(26, 7, 27), app_end);
|
||||||
// it follows the clock rather than restating it (rule 41).
|
|
||||||
constexpr auto act_min = static_cast<std::uint8_t>(dev::clock.hz / 1'000'000);
|
|
||||||
// Post-activation window: the host gets seconds, not milliseconds, mid-session.
|
|
||||||
constexpr std::uint8_t comm_window = 200;
|
|
||||||
|
|
||||||
// Firmware version stamp: YY*512 + MM*32 + DD, the encoding the host decodes.
|
|
||||||
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 27;
|
|
||||||
|
|
||||||
// The 16-byte device-info block, streamed out on activation.
|
|
||||||
// clang-format off
|
|
||||||
[[gnu::progmem]] constexpr auto info = std::to_array<std::uint8_t>({
|
|
||||||
'T', 'S', 'B',
|
|
||||||
build_date & 0xFF, build_date >> 8,
|
|
||||||
0xF3, // status: native-UART fixed-baud lineage
|
|
||||||
avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2],
|
|
||||||
page / 2, // page size in words
|
|
||||||
(app_end / 2) & 0xFF, (app_end / 2) >> 8, // app-flash boundary, words
|
|
||||||
eeprom_end & 0xFF, eeprom_end >> 8,
|
|
||||||
0xAA, 0xAA, // ATmega processor-type marker (bytes 14 == 15)
|
|
||||||
});
|
|
||||||
// clang-format on
|
|
||||||
|
|
||||||
// The receive window, pre-floored where it is set. In .noinit: there is no
|
// The receive window, pre-floored where it is set. In .noinit: there is no
|
||||||
// crt to clear a .bss image, and run() stores it before the first receive.
|
// crt to clear a .bss image, and run() stores it before the first receive.
|
||||||
[[gnu::section(".noinit")]] std::uint8_t window;
|
[[gnu::section(".noinit")]] std::uint8_t window;
|
||||||
|
|
||||||
const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|
||||||
{
|
|
||||||
return reinterpret_cast<const std::uint8_t *>(addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bounded byte receive: poll under nested countdowns, 0 on silence. The 0
|
// Bounded byte receive: poll under nested countdowns, 0 on silence. The 0
|
||||||
// then falls through every compare - not a knock, not a confirm, not a
|
// then falls through every compare - not a knock, not a confirm, not a
|
||||||
// command - so a silent host unwinds the loader to the application from
|
// command - so a silent host unwinds the loader to the application from
|
||||||
@@ -166,8 +112,8 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|||||||
{
|
{
|
||||||
const auto open = spm::page::begin<spm::from::boot_section, off>(at);
|
const auto open = spm::page::begin<spm::from::boot_section, off>(at);
|
||||||
do {
|
do {
|
||||||
std::uint8_t low = rx();
|
const auto low = rx();
|
||||||
std::uint8_t high = rx();
|
const auto high = rx();
|
||||||
spm::fill<off>(open, at, std::bit_cast<std::uint16_t>(std::array{low, high}));
|
spm::fill<off>(open, at, std::bit_cast<std::uint16_t>(std::array{low, high}));
|
||||||
at += 2;
|
at += 2;
|
||||||
} while (static_cast<std::uint8_t>(at) & (page - 1));
|
} while (static_cast<std::uint8_t>(at) & (page - 1));
|
||||||
@@ -214,42 +160,42 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Lean bring-up from reset state: UCSR0C already reads 8N1, UBRR0H reads
|
// Lean bring-up from reset state: UCSR0C already reads 8N1, UBRR0H reads
|
||||||
// 0, and the half-duplex write()/read() raise TXEN0/RXEN0 on first use -
|
// 0, and the half-duplex write()/read() raise TXEN0/RXEN0 on first use -
|
||||||
// only the divisor low byte and U2X0 need a store. The solver still does
|
// only the divisor low byte and U2X0 need a store. The solver still does
|
||||||
// the datasheet work; the asserts pin the reset-state assumptions.
|
// the datasheet work; the asserts pin the reset-state assumptions.
|
||||||
{
|
{
|
||||||
constexpr auto sol = avr::uart::solve_baud(dev::clock, 115200_Bd, 8, avr::uart::parity::none);
|
constexpr auto sol = avr::uart::solve_baud(dev::clock, link.baud, link.data_bits, link.par);
|
||||||
static_assert(sol.u2x && sol.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
static_assert(sol.u2x && sol.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
||||||
avr::hw::ubrr0::write(static_cast<std::uint8_t>(sol.ubrr));
|
avr::hw::ubrr0::write(static_cast<std::uint8_t>(sol.ubrr));
|
||||||
avr::hw::ucsr0a::write(avr::hw::ucsr0a::u2x0(1));
|
avr::hw::ucsr0a::write(avr::hw::ucsr0a::u2x0(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activation: 3x'@', each inside the config page's timeout window
|
// Activation: activation_knocks '@', each inside the config page's timeout
|
||||||
// (floored so a corrupt page cannot lock the loader out); anything else -
|
// window (floored so a corrupt page cannot lock the loader out); anything
|
||||||
// including silence - hands over.
|
// else - including silence - hands over.
|
||||||
window = avr::flash_load(flash_ptr(app_end + 2)) | act_min;
|
window = avr::flash_load(flash_ptr(app_end + timeout_at)) | act_min;
|
||||||
for (std::uint8_t k = 3; k; --k) {
|
for (std::uint8_t k = activation_knocks; k; --k) {
|
||||||
if (rx() != knock) {
|
if (rx() != knock) {
|
||||||
appjump();
|
appjump();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window = comm_window;
|
window = comm_window;
|
||||||
|
|
||||||
// Password gate (config page from app_end+3, 0xff-terminated; a blank
|
// Password gate (config page from password_at, 0xff-terminated; a blank
|
||||||
// page is no password). A wrong byte blanks the comparison and drains the
|
// page is no password). A wrong byte blanks the comparison and drains the
|
||||||
// line forever, so a wrong password can never fall through; a 0 requests
|
// line forever, so a wrong password can never fall through; a 0 requests
|
||||||
// emergency erase behind two confirms. On pass the info block goes out;
|
// emergency erase behind two confirms. On pass the info block goes out;
|
||||||
// the emergency path skips it and drops into the command loop.
|
// the emergency path skips it and drops into the command loop.
|
||||||
std::uint16_t at = app_end + 3;
|
std::uint16_t at = app_end + password_at;
|
||||||
std::uint8_t mask = 0xff;
|
std::uint8_t mask = 0xff;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
std::uint8_t expected = avr::flash_load(flash_ptr(at)) & mask;
|
const std::uint8_t expected = avr::flash_load(flash_ptr(at)) & mask;
|
||||||
++at;
|
++at;
|
||||||
if (expected == 0xff) {
|
if (expected == 0xff) {
|
||||||
send_block(false, reinterpret_cast<std::uint16_t>(info.data()), info.size());
|
send_block(false, reinterpret_cast<std::uint16_t>(info.data()), info.size());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
std::uint8_t got = rx();
|
const auto got = rx();
|
||||||
if (got == 0) {
|
if (got == 0) {
|
||||||
if (mask == 0) {
|
if (mask == 0) {
|
||||||
continue;
|
continue;
|
||||||
@@ -271,9 +217,8 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
tx(confirm); // Mainloop ready
|
tx(confirm); // Mainloop ready
|
||||||
const std::uint8_t command = rx();
|
switch (rx()) {
|
||||||
switch (command) {
|
case command::read_flash: // one page per host '!'
|
||||||
case 'f': // read application flash, one page per host '!'
|
|
||||||
for (std::uint16_t a = 0; a < app_end; a += page) {
|
for (std::uint16_t a = 0; a < app_end; a += page) {
|
||||||
if (rx() != confirm) {
|
if (rx() != confirm) {
|
||||||
break;
|
break;
|
||||||
@@ -281,7 +226,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
send_block(false, a, page);
|
send_block(false, a, page);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'e': // read EEPROM, one page per host '!', until the host stops
|
case command::read_eeprom: // one page per host '!', until the host stops
|
||||||
for (std::uint16_t a = 0;; a += page) {
|
for (std::uint16_t a = 0;; a += page) {
|
||||||
if (rx() != confirm) {
|
if (rx() != confirm) {
|
||||||
break;
|
break;
|
||||||
@@ -289,14 +234,14 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
send_block(true, a, page);
|
send_block(true, a, page);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'F': { // erase the application, then take pages behind '?'
|
case command::write_flash: { // erase the application, then take pages behind '?'
|
||||||
std::uint16_t a = erase_application();
|
std::uint16_t a = erase_application();
|
||||||
for (; rcnf() == confirm; a += page) {
|
for (; rcnf() == confirm; a += page) {
|
||||||
store_flash_page(a);
|
store_flash_page(a);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'E': // take EEPROM pages behind '?', each write host-paced
|
case command::write_eeprom: // pages behind '?', each write host-paced
|
||||||
for (std::uint16_t a = 0; rcnf() == confirm;) {
|
for (std::uint16_t a = 0; rcnf() == confirm;) {
|
||||||
std::uint8_t count = page;
|
std::uint8_t count = page;
|
||||||
do {
|
do {
|
||||||
@@ -305,11 +250,11 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
} while (--count);
|
} while (--count);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'c': // read the config page
|
case command::read_config:
|
||||||
read_config:
|
read_config:
|
||||||
send_block(false, app_end, page);
|
send_block(false, app_end, page);
|
||||||
break;
|
break;
|
||||||
case 'C': // replace the config page, then echo it back to verify
|
case command::write_config: // then echo it back to verify
|
||||||
if (rcnf() != confirm) {
|
if (rcnf() != confirm) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-83
@@ -4,27 +4,20 @@
|
|||||||
// TinySafeBoot native-UART fixed-baud protocol on libavr with the full feature
|
// TinySafeBoot native-UART fixed-baud protocol on libavr with the full feature
|
||||||
// set of the hand-written oracle: a watchdog-reset bail, one-wire half-duplex,
|
// set of the hand-written oracle: a watchdog-reset bail, one-wire half-duplex,
|
||||||
// a config-page activation timeout, the password gate, emergency erase, and
|
// a config-page activation timeout, the password gate, emergency erase, and
|
||||||
// config/flash/EEPROM read-write. This variant is written for clarity -
|
// config/flash/EEPROM read-write. This variant is written for clarity -
|
||||||
// well-factored functions, no compiler-specific size hacks, no inline assembly.
|
// well-factored functions, no compiler-specific size hacks, no inline assembly.
|
||||||
// The one-wire wiring, the flash-resident info block and every SPM/EEPROM lock
|
// The one-wire wiring, the flash-resident info block, every SPM/EEPROM lock and
|
||||||
// are libavr's to handle; the only attribute is the naked reset entry that
|
// the crt-free reset entry are libavr's to handle.
|
||||||
// stands in for the absent C runtime.
|
|
||||||
|
|
||||||
#include <libavr/libavr.hpp>
|
#include "tsb.hpp"
|
||||||
|
|
||||||
#include <avr/io.h> // SP / RAMEND for the crt-free boot entry
|
using dev = avr::device<{.clock = tsb::clock}>;
|
||||||
|
|
||||||
using namespace avr::literals;
|
|
||||||
namespace spm = avr::spm;
|
|
||||||
namespace ee = avr::eeprom;
|
|
||||||
|
|
||||||
using dev = avr::device<{.clock = 16_MHz}>;
|
|
||||||
// One-wire: RX and TX share the line, exactly as the native-UART TSB expects.
|
// One-wire: RX and TX share the line, exactly as the native-UART TSB expects.
|
||||||
// 115200 at 16 MHz lands +2.1 % off, past the receiver-tolerance table the
|
// 115200 at 16 MHz lands +2.1 % off, past the receiver-tolerance table the
|
||||||
// solver holds rates to - the oracle's own deployment has run there for a
|
// solver holds rates to - the oracle's own deployment has run there for a
|
||||||
// decade, so the override states that it is meant.
|
// decade, so the override states that it is meant.
|
||||||
using serial_t = dev::uart0<{
|
using serial_t = dev::uart0<{
|
||||||
.baud = 115200_Bd,
|
.baud = tsb::baud_rate,
|
||||||
.allow_baud_error = true,
|
.allow_baud_error = true,
|
||||||
.half_duplex = true,
|
.half_duplex = true,
|
||||||
}>;
|
}>;
|
||||||
@@ -33,53 +26,15 @@ inline constexpr serial_t serial{};
|
|||||||
namespace tsb {
|
namespace tsb {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// The loader is purely polled - it never enables interrupts - so every SPM and
|
// The boot section: 1 KB, BOOTSZ=10.
|
||||||
// EEPROM lock folds to nothing under this posture.
|
|
||||||
constexpr auto off = avr::irq::guard_policy::unused;
|
|
||||||
|
|
||||||
// Strict request/response: every SPM operation is waited out before the next
|
|
||||||
// byte moves, so no flash operation is ever in flight at an EEPROM access -
|
|
||||||
// the write procedure's step 2 has nothing to guard, the omission the
|
|
||||||
// datasheet grants (DS40002061B section 8.6.3).
|
|
||||||
constexpr auto no_spm = ee::spm_interlock::omitted;
|
|
||||||
|
|
||||||
// The handshake bytes, identical across every TSB host.
|
|
||||||
constexpr std::uint8_t confirm = '!';
|
|
||||||
constexpr std::uint8_t request = '?';
|
|
||||||
constexpr std::uint8_t knock = '@';
|
|
||||||
|
|
||||||
// Boot geometry for the 1 KB boot section (BOOTSZ=10). The page size and the
|
|
||||||
// flash/EEPROM extents are the chip database's to know. app_end is the config
|
|
||||||
// page (the LASTPAGE holding the app-jump vector, activation timeout and
|
|
||||||
// password), one page below the boot section.
|
|
||||||
constexpr std::uint16_t page = spm::page_bytes;
|
|
||||||
constexpr std::uint16_t boot_bytes = 1024;
|
constexpr std::uint16_t boot_bytes = 1024;
|
||||||
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page;
|
constexpr std::uint16_t app_end = config_page(boot_bytes);
|
||||||
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
|
|
||||||
|
|
||||||
// Firmware version stamp: YY*512 + MM*32 + DD, the encoding the host decodes.
|
// A flash_table keeps the info block in progmem with no .data image (there is
|
||||||
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
// no crt to copy one).
|
||||||
|
inline constexpr auto info_data = info_block(date_stamp(26, 7, 20), app_end);
|
||||||
// The 16-byte device-info block the host reads on activation. A flash_table
|
|
||||||
// keeps it in progmem with no .data image (there is no crt to copy one).
|
|
||||||
// clang-format off
|
|
||||||
inline constexpr auto info_data = std::to_array<std::uint8_t>({
|
|
||||||
'T', 'S', 'B',
|
|
||||||
build_date & 0xFF, build_date >> 8,
|
|
||||||
0xF3, // status byte (native-UART fixed-baud lineage)
|
|
||||||
avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2],
|
|
||||||
page / 2, // page size in words
|
|
||||||
(app_end / 2) & 0xFF, (app_end / 2) >> 8, // app-flash boundary, words
|
|
||||||
eeprom_end & 0xFF, eeprom_end >> 8,
|
|
||||||
0xAA, 0xAA, // ATmega processor-type marker (bytes 14 == 15)
|
|
||||||
});
|
|
||||||
// clang-format on
|
|
||||||
using info = avr::flash_table<info_data>;
|
using info = avr::flash_table<info_data>;
|
||||||
|
|
||||||
// The lockout-proof floor for the receive window: the oracle's F_CPU/1MHz, so
|
|
||||||
// it follows the clock rather than restating it.
|
|
||||||
constexpr auto act_min = static_cast<std::uint8_t>(dev::clock.hz / 1'000'000);
|
|
||||||
|
|
||||||
// The receive window, pre-floored where it is set. In .noinit: there is no crt
|
// The receive window, pre-floored where it is set. In .noinit: there is no crt
|
||||||
// to clear a .bss image, and run() stores it before the first receive.
|
// to clear a .bss image, and run() stores it before the first receive.
|
||||||
[[gnu::section(".noinit")]] std::uint8_t window;
|
[[gnu::section(".noinit")]] std::uint8_t window;
|
||||||
@@ -110,11 +65,6 @@ void tx(std::uint8_t byte)
|
|||||||
serial.write(byte);
|
serial.write(byte);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|
||||||
{
|
|
||||||
return reinterpret_cast<const std::uint8_t *>(addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream `count` bytes to the host, from flash (LPM) or from EEPROM.
|
// Stream `count` bytes to the host, from flash (LPM) or from EEPROM.
|
||||||
void send_flash(std::uint16_t addr, std::uint8_t count)
|
void send_flash(std::uint16_t addr, std::uint8_t count)
|
||||||
{
|
{
|
||||||
@@ -144,8 +94,8 @@ void store_flash_page(std::uint16_t addr)
|
|||||||
{
|
{
|
||||||
const auto open = spm::page::begin<spm::from::boot_section, off>(addr);
|
const auto open = spm::page::begin<spm::from::boot_section, off>(addr);
|
||||||
for (std::uint16_t i = 0; i < page; i += 2) {
|
for (std::uint16_t i = 0; i < page; i += 2) {
|
||||||
std::uint8_t lo = rx();
|
const auto lo = rx();
|
||||||
std::uint8_t hi = rx();
|
const auto hi = rx();
|
||||||
spm::fill<off>(open, addr + i, static_cast<std::uint16_t>(lo | (hi << 8)));
|
spm::fill<off>(open, addr + i, static_cast<std::uint16_t>(lo | (hi << 8)));
|
||||||
}
|
}
|
||||||
spm::write_page<spm::from::boot_section, off>(addr); // blocking: waits the write out
|
spm::write_page<spm::from::boot_section, off>(addr); // blocking: waits the write out
|
||||||
@@ -190,8 +140,8 @@ extern "C" [[noreturn]] void tsb_app();
|
|||||||
tsb_app();
|
tsb_app();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'f': stream the application flash back, one page per host '!'. Self-terminates
|
// Stream the application flash back, one page per host '!'. Self-terminates at
|
||||||
// at the application boundary; the host normally stops earlier with a non-'!'.
|
// the application boundary; the host normally stops earlier with a non-'!'.
|
||||||
void read_flash()
|
void read_flash()
|
||||||
{
|
{
|
||||||
for (std::uint16_t a = 0; a < app_end; a += page) {
|
for (std::uint16_t a = 0; a < app_end; a += page) {
|
||||||
@@ -202,7 +152,7 @@ void read_flash()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'e': stream EEPROM back, one page per host '!', until the host stops.
|
// Stream EEPROM back, one page per host '!', until the host stops.
|
||||||
void read_eeprom()
|
void read_eeprom()
|
||||||
{
|
{
|
||||||
for (std::uint16_t a = 0;; a += page) {
|
for (std::uint16_t a = 0;; a += page) {
|
||||||
@@ -213,8 +163,8 @@ void read_eeprom()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'F': erase the whole application first, then take pages the host offers
|
// Erase the whole application first, then take pages the host offers behind
|
||||||
// behind '?'.
|
// '?'.
|
||||||
void write_flash()
|
void write_flash()
|
||||||
{
|
{
|
||||||
erase_application();
|
erase_application();
|
||||||
@@ -223,7 +173,7 @@ void write_flash()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'E': take pages the host offers behind '?' into EEPROM.
|
// Take pages the host offers behind '?' into EEPROM.
|
||||||
void write_eeprom()
|
void write_eeprom()
|
||||||
{
|
{
|
||||||
for (std::uint16_t a = 0; request_confirm(); a += page) {
|
for (std::uint16_t a = 0; request_confirm(); a += page) {
|
||||||
@@ -231,7 +181,7 @@ void write_eeprom()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'C': replace the config page, then echo it back for the host to verify.
|
// Replace the config page, then echo it back for the host to verify.
|
||||||
void write_config()
|
void write_config()
|
||||||
{
|
{
|
||||||
if (!request_confirm()) {
|
if (!request_confirm()) {
|
||||||
@@ -256,7 +206,7 @@ void emergency_erase()
|
|||||||
spm::rww_enable<off>();
|
spm::rww_enable<off>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The password gate. The config page holds the password at app_end+3,
|
// The password gate. The config page holds the password at password_at,
|
||||||
// terminated by 0xff (a blank page means no password). A byte of 0 requests
|
// terminated by 0xff (a blank page means no password). A byte of 0 requests
|
||||||
// emergency erase; a wrong byte hangs the loader, still draining the line, so a
|
// emergency erase; a wrong byte hangs the loader, still draining the line, so a
|
||||||
// wrong password can never fall through to the erase.
|
// wrong password can never fall through to the erase.
|
||||||
@@ -264,12 +214,12 @@ enum class gate : std::uint8_t { pass, emergency };
|
|||||||
|
|
||||||
gate password_gate()
|
gate password_gate()
|
||||||
{
|
{
|
||||||
for (const std::uint8_t *pw = flash_ptr(app_end + 3);; ++pw) {
|
for (const std::uint8_t *pw = flash_ptr(app_end + password_at);; ++pw) {
|
||||||
std::uint8_t expected = avr::flash_load(pw);
|
const auto expected = avr::flash_load(pw);
|
||||||
if (expected == 0xff) {
|
if (expected == 0xff) {
|
||||||
return gate::pass;
|
return gate::pass;
|
||||||
}
|
}
|
||||||
std::uint8_t got = rx();
|
const auto got = rx();
|
||||||
if (got == 0) {
|
if (got == 0) {
|
||||||
return gate::emergency;
|
return gate::emergency;
|
||||||
}
|
}
|
||||||
@@ -291,14 +241,14 @@ gate password_gate()
|
|||||||
|
|
||||||
avr::init<serial_t>();
|
avr::init<serial_t>();
|
||||||
|
|
||||||
// Activation: the host knocks three '@' inside a window whose length is the
|
// Activation: the host knocks activation_knocks times inside a window whose length is the
|
||||||
// config page's timeout byte, floored so a corrupt page can never lock the
|
// config page's timeout byte, floored so a corrupt page can never lock the
|
||||||
// loader out. An idle port times out and boots the application; the same
|
// loader out. An idle port times out and boots the application; the same
|
||||||
// window then bounds every receive of the session.
|
// window then bounds every receive of the session.
|
||||||
window = avr::flash_load(flash_ptr(app_end + 2)) | act_min;
|
window = avr::flash_load(flash_ptr(app_end + timeout_at)) | act_min;
|
||||||
__uint24 idle = static_cast<__uint24>(window) << 16;
|
__uint24 idle = static_cast<__uint24>(window) << 16;
|
||||||
std::uint8_t knocks = 0;
|
std::uint8_t knocks = 0;
|
||||||
while (knocks < 3) {
|
while (knocks < activation_knocks) {
|
||||||
if (auto byte = serial.read()) {
|
if (auto byte = serial.read()) {
|
||||||
knocks = *byte == knock ? knocks + 1 : 0;
|
knocks = *byte == knock ? knocks + 1 : 0;
|
||||||
} else if (--idle == 0) {
|
} else if (--idle == 0) {
|
||||||
@@ -321,22 +271,22 @@ gate password_gate()
|
|||||||
for (;;) {
|
for (;;) {
|
||||||
tx(confirm); // Mainloop ready
|
tx(confirm); // Mainloop ready
|
||||||
switch (rx()) {
|
switch (rx()) {
|
||||||
case 'f':
|
case command::read_flash:
|
||||||
read_flash();
|
read_flash();
|
||||||
break;
|
break;
|
||||||
case 'F':
|
case command::write_flash:
|
||||||
write_flash();
|
write_flash();
|
||||||
break;
|
break;
|
||||||
case 'e':
|
case command::read_eeprom:
|
||||||
read_eeprom();
|
read_eeprom();
|
||||||
break;
|
break;
|
||||||
case 'E':
|
case command::write_eeprom:
|
||||||
write_eeprom();
|
write_eeprom();
|
||||||
break;
|
break;
|
||||||
case 'c':
|
case command::read_config:
|
||||||
send_flash(app_end, page);
|
send_flash(app_end, page);
|
||||||
break;
|
break;
|
||||||
case 'C':
|
case command::write_config:
|
||||||
write_config();
|
write_config();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
+24
-79
@@ -29,80 +29,24 @@
|
|||||||
// line safe: the device drives it only between a received command and its
|
// line safe: the device drives it only between a received command and its
|
||||||
// reply, and releases it (RXEN0 only) whenever it waits.
|
// reply, and releases it (RXEN0 only) whenever it waits.
|
||||||
|
|
||||||
#include <libavr/libavr.hpp>
|
#include "tsb.hpp"
|
||||||
|
|
||||||
#include <avr/io.h> // SP / RAMEND for the crt-free boot entry
|
|
||||||
|
|
||||||
using namespace avr::literals;
|
|
||||||
namespace spm = avr::spm;
|
|
||||||
namespace ee = avr::eeprom;
|
|
||||||
namespace hw = avr::hw;
|
namespace hw = avr::hw;
|
||||||
|
|
||||||
namespace tsb {
|
namespace tsb {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// The loader is purely polled - it never enables interrupts - so every SPM and
|
// The boot section: 1 KB, BOOTSZ=10.
|
||||||
// EEPROM lock folds to nothing under this posture.
|
|
||||||
constexpr auto off = avr::irq::guard_policy::unused;
|
|
||||||
|
|
||||||
// Strict request/response: every SPM operation is waited out before the next
|
|
||||||
// byte moves, so no flash operation is ever in flight at an EEPROM access -
|
|
||||||
// the write procedure's step 2 has nothing to guard, the omission the
|
|
||||||
// datasheet grants (DS40002061B section 8.6.3).
|
|
||||||
constexpr auto no_spm = ee::spm_interlock::omitted;
|
|
||||||
|
|
||||||
constexpr std::uint8_t confirm = '!';
|
|
||||||
constexpr std::uint8_t request = '?';
|
|
||||||
constexpr std::uint8_t knock = '@';
|
|
||||||
|
|
||||||
// Boot geometry for the 1 KB boot section (BOOTSZ=10); the page size and the
|
|
||||||
// flash/EEPROM extents are the chip database's to know. app_end is the config
|
|
||||||
// page (TSB's LASTPAGE), one page below the boot section.
|
|
||||||
constexpr std::uint16_t page = spm::page_bytes;
|
|
||||||
constexpr std::uint16_t boot_bytes = 1024;
|
constexpr std::uint16_t boot_bytes = 1024;
|
||||||
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page;
|
constexpr std::uint16_t app_end = config_page(boot_bytes);
|
||||||
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
|
|
||||||
|
|
||||||
// Lockout-proof floor for the activation window: the oracle's F_CPU/1MHz, so
|
[[gnu::progmem]] constexpr auto info = info_block(date_stamp(26, 7, 20), app_end);
|
||||||
// it follows the clock rather than restating it (rule 41).
|
|
||||||
constexpr auto act_min = static_cast<std::uint8_t>((16_MHz).hz / 1'000'000);
|
|
||||||
// Post-activation window: the host gets seconds, not milliseconds, mid-session.
|
|
||||||
constexpr std::uint8_t comm_window = 200;
|
|
||||||
|
|
||||||
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
|
||||||
|
|
||||||
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
|
||||||
constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd, 8, avr::uart::parity::none);
|
|
||||||
|
|
||||||
// One bit time on the wire: the turn-around a shared-line peer needs to stop
|
|
||||||
// driving before this one starts. Derived from the solved rate, so it follows
|
|
||||||
// the link rather than a count measured against one.
|
|
||||||
constexpr auto guard_cycles = static_cast<std::uint32_t>((16_MHz).hz / baud.actual);
|
|
||||||
|
|
||||||
// The 16-byte device-info block, streamed out on activation.
|
|
||||||
// clang-format off
|
|
||||||
[[gnu::progmem]] constexpr auto info = std::to_array<std::uint8_t>({
|
|
||||||
'T', 'S', 'B',
|
|
||||||
build_date & 0xFF, build_date >> 8,
|
|
||||||
0xF3, // status: native-UART fixed-baud lineage
|
|
||||||
avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2],
|
|
||||||
page / 2, // page size in words
|
|
||||||
(app_end / 2) & 0xFF, (app_end / 2) >> 8,
|
|
||||||
eeprom_end & 0xFF, eeprom_end >> 8,
|
|
||||||
0xAA, 0xAA,
|
|
||||||
});
|
|
||||||
// clang-format on
|
|
||||||
|
|
||||||
register std::uint16_t g_addr asm("r28");
|
register std::uint16_t g_addr asm("r28");
|
||||||
register std::uint8_t g_cnt asm("r16");
|
register std::uint8_t g_cnt asm("r16");
|
||||||
register std::uint8_t g_window asm("r7");
|
register std::uint8_t g_window asm("r7");
|
||||||
register std::uint8_t g_receiving asm("r6");
|
register std::uint8_t g_receiving asm("r6");
|
||||||
|
|
||||||
const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|
||||||
{
|
|
||||||
return reinterpret_cast<const std::uint8_t *>(addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bounded byte receive, the oracle's shape: release the one-wire line on a
|
// Bounded byte receive, the oracle's shape: release the one-wire line on a
|
||||||
// direction change, poll RXC0 under nested countdowns, 0 on silence. The 0
|
// direction change, poll RXC0 under nested countdowns, 0 on silence. The 0
|
||||||
// then falls through every compare - not a knock, not a confirm, not a
|
// then falls through every compare - not a knock, not a confirm, not a
|
||||||
@@ -157,10 +101,11 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|||||||
return rx();
|
return rx();
|
||||||
}
|
}
|
||||||
|
|
||||||
// One flash byte <- [g_addr++] (the advance right before ret - see header).
|
// One flash byte <- [g_addr++] (the advance right before ret - see the top of
|
||||||
|
// this file).
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t sflash()
|
[[gnu::noinline, gnu::noclone]] std::uint8_t sflash()
|
||||||
{
|
{
|
||||||
std::uint8_t byte = avr::flash_load(flash_ptr(g_addr));
|
const auto byte = avr::flash_load(flash_ptr(g_addr));
|
||||||
++g_addr;
|
++g_addr;
|
||||||
return byte;
|
return byte;
|
||||||
}
|
}
|
||||||
@@ -168,7 +113,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|||||||
// One EEPROM byte <- [g_addr++].
|
// One EEPROM byte <- [g_addr++].
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t eerd()
|
[[gnu::noinline, gnu::noclone]] std::uint8_t eerd()
|
||||||
{
|
{
|
||||||
std::uint8_t byte = ee::read<no_spm>(g_addr);
|
const auto byte = ee::read<no_spm>(g_addr);
|
||||||
++g_addr;
|
++g_addr;
|
||||||
return byte;
|
return byte;
|
||||||
}
|
}
|
||||||
@@ -206,7 +151,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
|
|
||||||
// Step g_addr one page down and erase that page. The decrement lives in here,
|
// Step g_addr one page down and erase that page. The decrement lives in here,
|
||||||
// before the erase's own use of it, not in the caller's loop where a following
|
// before the erase's own use of it, not in the caller's loop where a following
|
||||||
// call would get it deleted (see header).
|
// call would get it deleted (see the top of this file).
|
||||||
[[gnu::noinline, gnu::noclone]] void erase_below()
|
[[gnu::noinline, gnu::noclone]] void erase_below()
|
||||||
{
|
{
|
||||||
g_addr -= page;
|
g_addr -= page;
|
||||||
@@ -262,23 +207,23 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
// same reason.
|
// same reason.
|
||||||
g_receiving = 0;
|
g_receiving = 0;
|
||||||
|
|
||||||
// Activation: 3x'@', each inside the config page's timeout window (rx
|
// Activation: activation_knocks '@', each inside the config page's timeout
|
||||||
// floors it so a corrupt page cannot lock the loader out); anything else -
|
// window (rx floors it so a corrupt page cannot lock the loader out);
|
||||||
// including silence - hands over.
|
// anything else - including silence - hands over.
|
||||||
g_window = avr::flash_load(flash_ptr(app_end + 2));
|
g_window = avr::flash_load(flash_ptr(app_end + timeout_at));
|
||||||
for (std::uint8_t k = 3; k; --k) {
|
for (std::uint8_t k = activation_knocks; k; --k) {
|
||||||
if (rx() != knock) {
|
if (rx() != knock) {
|
||||||
appjump();
|
appjump();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
g_window = comm_window;
|
g_window = comm_window;
|
||||||
|
|
||||||
// Password gate (config page from app_end+3, 0xff-terminated; a blank
|
// Password gate (config page from password_at, 0xff-terminated; a blank
|
||||||
// page is no password). A wrong byte blanks the comparison and drains the
|
// page is no password). A wrong byte blanks the comparison and drains the
|
||||||
// line forever, so a wrong password can never fall through; a 0 requests
|
// line forever, so a wrong password can never fall through; a 0 requests
|
||||||
// emergency erase behind two confirms. On pass the info block goes out;
|
// emergency erase behind two confirms. On pass the info block goes out;
|
||||||
// the emergency path skips it and drops into the command loop.
|
// the emergency path skips it and drops into the command loop.
|
||||||
g_addr = app_end + 3;
|
g_addr = app_end + password_at;
|
||||||
std::uint8_t mask = 0xff;
|
std::uint8_t mask = 0xff;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask;
|
std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask;
|
||||||
@@ -289,7 +234,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
sendf();
|
sendf();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
std::uint8_t got = rx();
|
const auto got = rx();
|
||||||
if (got == 0) {
|
if (got == 0) {
|
||||||
if (mask == 0) {
|
if (mask == 0) {
|
||||||
continue;
|
continue;
|
||||||
@@ -314,7 +259,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
tx(confirm); // Mainloop ready
|
tx(confirm); // Mainloop ready
|
||||||
g_addr = 0;
|
g_addr = 0;
|
||||||
switch (rx()) {
|
switch (rx()) {
|
||||||
case 'f': // read application flash, one page per host '!'
|
case command::read_flash: // one page per host '!'
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (rx() != confirm) {
|
if (rx() != confirm) {
|
||||||
break;
|
break;
|
||||||
@@ -326,13 +271,13 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'F': // erase the application, then take pages behind '?'
|
case command::write_flash: // erase the application, then take pages behind '?'
|
||||||
erase_application(); // leaves g_addr = 0, the write start
|
erase_application(); // leaves g_addr = 0, the write start
|
||||||
while (rcnf() == confirm) {
|
while (rcnf() == confirm) {
|
||||||
store_flash();
|
store_flash();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'e': // read EEPROM, one page per host '!', until the host stops
|
case command::read_eeprom: // one page per host '!', until the host stops
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (rx() != confirm) {
|
if (rx() != confirm) {
|
||||||
break;
|
break;
|
||||||
@@ -343,7 +288,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
} while (--g_cnt);
|
} while (--g_cnt);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'E': // take EEPROM pages behind '?'
|
case command::write_eeprom: // pages behind '?'
|
||||||
while (rcnf() == confirm) {
|
while (rcnf() == confirm) {
|
||||||
g_cnt = page;
|
g_cnt = page;
|
||||||
do {
|
do {
|
||||||
@@ -351,13 +296,13 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
} while (--g_cnt);
|
} while (--g_cnt);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'c': // read the config page
|
case command::read_config:
|
||||||
read_config:
|
read_config:
|
||||||
g_addr = app_end;
|
g_addr = app_end;
|
||||||
g_cnt = page;
|
g_cnt = page;
|
||||||
sendf();
|
sendf();
|
||||||
break;
|
break;
|
||||||
case 'C': // replace the config page, then echo it back to verify
|
case command::write_config: // then echo it back to verify
|
||||||
if (rcnf() != confirm) {
|
if (rcnf() != confirm) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user