Compare commits
119 Commits
7d103ca957
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 20b1485b6f | |||
| b759618024 | |||
| 22f17b5a25 | |||
| 243b258d4d | |||
| 9575145f0d | |||
| d8211ff440 | |||
|
|
c004d1bdb9 | ||
| 7172d1fc64 | |||
| 7c1a6a140b | |||
| bf9b86d2fc | |||
| 238fb2919c | |||
| 1541818254 | |||
| 37f9747e26 | |||
| 6ff407e7bb | |||
| cbb3c58300 | |||
| c4b7769f84 | |||
| 735ffab7dc | |||
| be78f38f3f | |||
| d3d8d07905 | |||
| b4243a89a3 | |||
| e4390d2ba8 | |||
| 0cb83ff36f | |||
| 8aa1721709 | |||
| 2655d058f4 | |||
| e9c3897d4f | |||
| 546b1589a3 | |||
| eb213e1025 | |||
| 3e4bfbaf48 | |||
| 579ca81b27 | |||
| 5d520a1ff9 | |||
| f71d76a815 | |||
| 47419400f6 | |||
| bad8b6b43e | |||
| 5d1b4497d4 | |||
| a743ea64a3 | |||
| aa66cfccff | |||
| 459d463283 | |||
| 8e7cc86fb3 | |||
| c8ac61779e | |||
| 4362886c39 | |||
| 0513d07e87 | |||
| d4ab28aa17 | |||
| b60f182105 | |||
| 5bc35a9733 | |||
| 8f6319c068 | |||
| a3ea099105 | |||
| c535c4756c | |||
| 321ff8a4ee | |||
| 531ae6c8dc | |||
| b3f41caf6e | |||
| 7716e1e291 | |||
| 1b18f10f4e | |||
| 52c4cdab32 | |||
| 69f089e53a | |||
| fe0d9f8790 | |||
| 3ce817ea03 | |||
| af0dd15a77 | |||
| a54075e526 | |||
| aec430c2e1 | |||
| 9a8a5b0082 | |||
| 7702c6b700 | |||
| 77dd45aeca | |||
| 433bec3e58 | |||
| e89000f73e | |||
| b0737f7cc0 | |||
| 07f93caba8 | |||
| 45f10f843a | |||
| 34b47048ca | |||
| 392035923f | |||
| f98ed406b8 | |||
| a4da885e36 | |||
| 335e494a31 | |||
| 799709efcf | |||
| 7ae80087b3 | |||
| b477f53ca5 | |||
| 84d3f679c2 | |||
| b5020a1e20 | |||
| 1dbf0089d6 | |||
| 39dfe40dbf | |||
| 8470ce3da0 | |||
| d634741015 | |||
| ab842c8d99 | |||
| a58b59c60f | |||
| ceacb61ba1 | |||
| dd7490a3df | |||
| bc24b63e65 | |||
| 332b244ac3 | |||
| 61b687120c | |||
| ef58d5d363 | |||
| 03bb1f56cf | |||
| 6a5e313530 | |||
| a28fccf475 | |||
| 92503fbb2d | |||
| 8ba023ed6b | |||
| 443fd39d31 | |||
| 08a0d77109 | |||
| 260ab2e4e3 | |||
| e4aaf5bc62 | |||
| 653602058a | |||
| 57d1c009cc | |||
| 06bb06d994 | |||
| 9a9a7ef0e8 | |||
| 4acf358dda | |||
| 11c10f986d | |||
| f587f0a26e | |||
| 27c768a102 | |||
| d989b5b8cd | |||
| 314e422b19 | |||
| 3e0717ef06 | |||
| 812be910c1 | |||
| 34529c8031 | |||
| 06bd56af50 | |||
| 75bb84fa3e | |||
| 778b8a0100 | |||
| 48d11fe8e3 | |||
| bb4784ae0c | |||
| d07586652d | |||
| 3a2f2a23c2 | |||
| 4fa058653e |
@@ -7,8 +7,9 @@ TabWidth: 4
|
|||||||
UseTab: ForIndentation
|
UseTab: ForIndentation
|
||||||
AlignEscapedNewlines: DontAlign
|
AlignEscapedNewlines: DontAlign
|
||||||
AllowShortFunctionsOnASingleLine: Empty
|
AllowShortFunctionsOnASingleLine: Empty
|
||||||
AlwaysBreakTemplateDeclarations: true
|
BreakTemplateDeclarations: Yes
|
||||||
BreakBeforeBraces: Custom
|
BreakBeforeBraces: Custom
|
||||||
BraceWrapping:
|
BraceWrapping:
|
||||||
AfterFunction: true
|
AfterFunction: true
|
||||||
|
InsertBraces: true
|
||||||
...
|
...
|
||||||
|
|||||||
38
.clangd
Normal file
38
.clangd
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Editor accommodations for the second frontend. No compilation database is
|
||||||
|
# named here: this repo rides as a submodule in its consumers, and this file
|
||||||
|
# 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:
|
||||||
|
Add:
|
||||||
|
# clang has no 24-bit integer and GCC's are keywords, not macros, so the
|
||||||
|
# editor needs a stand-in for avr::uint24_t. The next width up is the only
|
||||||
|
# one available — clang rejects _BitInt(24) on this target.
|
||||||
|
- -D__uint24=unsigned long
|
||||||
|
- -D__int24=long
|
||||||
|
# clangd forwards the driver's system includes but not its own header
|
||||||
|
# directory, so <stdint.h> resolves to avr-libc's, which still gates the
|
||||||
|
# limit and constant macros on the C++98 opt-in.
|
||||||
|
- -D__STDC_LIMIT_MACROS
|
||||||
|
- -D__STDC_CONSTANT_MACROS
|
||||||
|
# isr::emit spells a vector number into [[gnu::signal(N)]], which clang
|
||||||
|
# rejects rather than ignores — enough of them in one TU to reach the
|
||||||
|
# default limit of 19 inside the headers and truncate the parse.
|
||||||
|
- -ferror-limit=0
|
||||||
|
Remove:
|
||||||
|
# Codegen shaping the loader TUs carry and clang has no spelling for.
|
||||||
|
- -fira-algorithm=*
|
||||||
|
- -fno-split-wide-types
|
||||||
|
- -fno-tree-ter
|
||||||
|
- -fno-ivopts
|
||||||
|
- -fno-move-loop-invariants
|
||||||
|
# The build promotes warnings for the compiler that has to be right about
|
||||||
|
# them; in the editor the flag paints a second frontend's opinions in the
|
||||||
|
# colour reserved for things that do not compile.
|
||||||
|
- -Werror
|
||||||
|
Diagnostics:
|
||||||
|
Suppress:
|
||||||
|
# clang's AVR `signal` attribute takes no arguments and it knows none of
|
||||||
|
# progmem, naked or OS_main. A misspelling is what the build is for.
|
||||||
|
- attribute_wrong_number_arguments
|
||||||
|
- unknown-attributes
|
||||||
13
.gitattributes
vendored
13
.gitattributes
vendored
@@ -1,8 +1,11 @@
|
|||||||
*.h eol=lf
|
# Line endings are the repository's, not the editing machine's: this checkout
|
||||||
*.hpp eol=lf
|
# is reached from two hosts, and a file rewritten by a Windows tool comes back
|
||||||
*.c eol=lf
|
# with every line changed unless something says otherwise. Naming the source
|
||||||
*.cpp eol=lf
|
# extensions left Markdown, Python, shell and CMake to whatever the writing
|
||||||
.git* eol=lf
|
# tool defaulted to, which is CRLF on one of the two.
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# Atmel Studio writes these and expects them back.
|
||||||
*.vcxproj* eol=crlf
|
*.vcxproj* eol=crlf
|
||||||
*.cppproj eol=crlf
|
*.cppproj eol=crlf
|
||||||
*.sln eol=crlf
|
*.sln eol=crlf
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,6 +12,7 @@ Debug
|
|||||||
|
|
||||||
# CMake / clangd
|
# CMake / clangd
|
||||||
/build/
|
/build/
|
||||||
|
/local/
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
.cache/
|
.cache/
|
||||||
|
|
||||||
|
|||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "libavr"]
|
||||||
|
path = libavr
|
||||||
|
url = ../libavr.git
|
||||||
6
.vscode/extensions.json
vendored
Normal file
6
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"llvm-vs-code-extensions.vscode-clangd",
|
||||||
|
"ms-vscode.cmake-tools"
|
||||||
|
]
|
||||||
|
}
|
||||||
36
.vscode/settings.json
vendored
Normal file
36
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
// clangd is the language server; the cpptools engine would parse every file
|
||||||
|
// a second time and disagree, since nothing tells it about a cross
|
||||||
|
// compiler.
|
||||||
|
"C_Cpp.intelliSenseEngine": "disabled",
|
||||||
|
|
||||||
|
// --query-driver lets clangd ask the cross compiler for its own system
|
||||||
|
// includes and target. The database is named here rather than in .clangd
|
||||||
|
// 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": [
|
||||||
|
"--compile-commands-dir=${workspaceFolder}/build/atmega328p-generated",
|
||||||
|
"--query-driver=**avr-g++*",
|
||||||
|
"--header-insertion=never"
|
||||||
|
],
|
||||||
|
|
||||||
|
// The presets are the build interface, and the toolchain file inside the
|
||||||
|
// libavr submodule is the one place the compiler is chosen. **No prefix is
|
||||||
|
// named here**: a committed file may not name a path that is true of one
|
||||||
|
// machine (libavr guidance rule 50), so the gitignored local/machine.cmake
|
||||||
|
// at this repository's root is where a checkout says where its toolchain
|
||||||
|
// is - one file, and it answers for both hosts.
|
||||||
|
"cmake.useCMakePresets": "always",
|
||||||
|
"cmake.configureOnOpen": true,
|
||||||
|
"cmake.options.statusBarVisibility": "compact",
|
||||||
|
|
||||||
|
"files.watcherExclude": {
|
||||||
|
"**/build/**": true,
|
||||||
|
"**/libavr/**": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"files.associations": {
|
||||||
|
".clangd": "yaml",
|
||||||
|
".clang-format": "yaml"
|
||||||
|
}
|
||||||
|
}
|
||||||
226
CMakeLists.txt
226
CMakeLists.txt
@@ -2,84 +2,119 @@ cmake_minimum_required(VERSION 3.28)
|
|||||||
|
|
||||||
project(tsb_libavr LANGUAGES CXX)
|
project(tsb_libavr LANGUAGES CXX)
|
||||||
|
|
||||||
# libavr from a local checkout (LIBAVR_ROOT) or the forge; the toolchain file
|
# libavr rides as the pinned submodule; LIBAVR_ROOT (cache or environment)
|
||||||
# comes from the same checkout via CMakePresets.json.
|
# overrides it for tandem development against a working tree. The toolchain
|
||||||
include(FetchContent)
|
# file comes from the submodule via CMakePresets.json either way.
|
||||||
if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT})
|
if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT})
|
||||||
set(LIBAVR_ROOT $ENV{LIBAVR_ROOT})
|
set(LIBAVR_ROOT $ENV{LIBAVR_ROOT})
|
||||||
endif()
|
endif()
|
||||||
if(LIBAVR_ROOT)
|
if(NOT LIBAVR_ROOT)
|
||||||
FetchContent_Declare(libavr SOURCE_DIR ${LIBAVR_ROOT})
|
set(LIBAVR_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/libavr)
|
||||||
else()
|
|
||||||
FetchContent_Declare(libavr GIT_REPOSITORY git@git.blackmark.me:avr/libavr.git GIT_TAG main)
|
|
||||||
endif()
|
endif()
|
||||||
FetchContent_MakeAvailable(libavr)
|
if(NOT EXISTS ${LIBAVR_ROOT}/CMakeLists.txt)
|
||||||
|
message(FATAL_ERROR "libavr not found at ${LIBAVR_ROOT} - run: git submodule update --init libavr")
|
||||||
|
endif()
|
||||||
|
add_subdirectory(${LIBAVR_ROOT} libavr-build)
|
||||||
|
|
||||||
|
include(${LIBAVR_ROOT}/cmake/checks.cmake)
|
||||||
|
|
||||||
if(PROJECT_IS_TOP_LEVEL)
|
if(PROJECT_IS_TOP_LEVEL)
|
||||||
add_compile_options(-Werror) # warnings are errors for the port's own code
|
add_compile_options(-Werror) # warnings are errors for the port's own code
|
||||||
enable_testing()
|
enable_testing()
|
||||||
|
|
||||||
|
# Rules 11 and 33 over this repo's own sources. The oracle's assembly needs
|
||||||
|
# no exclusion: it is neither formatted nor ASCII-checked, being in neither
|
||||||
|
# glob, which is the right answer for a vendored reference whose text is
|
||||||
|
# the artifact.
|
||||||
|
libavr_format_test()
|
||||||
|
|
||||||
# The behavioral tests drive the real wire protocols over a simavr pty
|
# The behavioral tests drive the real wire protocols over a simavr pty
|
||||||
# (as the host tools do) and actually flash the device. The runners are
|
# (as the host tools do) and actually flash the device. The runner is a
|
||||||
# host programs built at configure time against libsimavr; if they or
|
# host program built at configure time against libsimavr (C++23 - what the
|
||||||
# Python are missing, only the size tests run.
|
# distribution's compiler speaks in full).
|
||||||
find_program(_host_cc NAMES cc gcc)
|
#
|
||||||
find_package(Python3 COMPONENTS Interpreter)
|
# **A host that cannot build it registers those tests anyway and skips
|
||||||
if(_host_cc AND Python3_FOUND)
|
# them.** They used to be left out, which makes the suite a different size
|
||||||
set(PB_DEVICE ${CMAKE_BINARY_DIR}/pureboot_device)
|
# on a different machine - and a suite whose size is a property of the
|
||||||
|
# machine is one nothing can be compared against.
|
||||||
|
set(TSB_DEVICE ${CMAKE_BINARY_DIR}/tsb_device)
|
||||||
|
find_program(_host_cxx NAMES c++ g++)
|
||||||
|
set(_tsb_absent "${LIBAVR_NO_PYTHON}")
|
||||||
|
if(NOT _host_cxx)
|
||||||
|
set(_tsb_absent "no host C++ compiler on PATH, and the simavr device is a host program")
|
||||||
|
elseif(NOT _tsb_absent)
|
||||||
execute_process(
|
execute_process(
|
||||||
COMMAND ${_host_cc} -O2 -I/usr/include/simavr -I/usr/include/simavr/parts
|
COMMAND ${_host_cxx} -std=c++23 -Wall -Wextra -O2
|
||||||
-o ${PB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pureboot_device.c
|
-I/usr/include/simavr -I/usr/include/simavr/parts
|
||||||
-lsimavr -lsimavrparts -lelf -lutil
|
-o ${TSB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/device.cpp
|
||||||
RESULT_VARIABLE _pbdev_res ERROR_VARIABLE _pbdev_err)
|
-lsimavr -lsimavrparts -lelf
|
||||||
if(NOT _pbdev_res EQUAL 0)
|
RESULT_VARIABLE _dev_res ERROR_VARIABLE _dev_err)
|
||||||
message(STATUS "pureboot_device not built (${_pbdev_err}) — protocol tests skipped")
|
if(NOT _dev_res EQUAL 0)
|
||||||
unset(PB_DEVICE)
|
# One bounded line of it: this becomes a single argument on a
|
||||||
endif()
|
# command line, and the reading has to say what stopped the build
|
||||||
if(LIBAVR_MCU STREQUAL "atmega328p")
|
# rather than that something did.
|
||||||
set(TSB_DEVICE ${CMAKE_BINARY_DIR}/tsb_device)
|
string(REGEX REPLACE "[\r\n\t]+" " " _dev_err "${_dev_err}")
|
||||||
execute_process(
|
string(REPLACE ";" "," _dev_err "${_dev_err}")
|
||||||
COMMAND ${_host_cc} -O2 -I/usr/include/simavr -I/usr/include/simavr/parts
|
string(LENGTH "${_dev_err}" _dev_len)
|
||||||
-o ${TSB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/device.c
|
if(_dev_len GREATER 240)
|
||||||
-lsimavr -lsimavrparts -lelf
|
string(SUBSTRING "${_dev_err}" 0 240 _dev_err)
|
||||||
RESULT_VARIABLE _dev_res ERROR_VARIABLE _dev_err)
|
|
||||||
if(NOT _dev_res EQUAL 0)
|
|
||||||
message(STATUS "tsb_device not built (${_dev_err}) — protocol tests skipped")
|
|
||||||
unset(TSB_DEVICE)
|
|
||||||
endif()
|
endif()
|
||||||
|
set(_tsb_absent "test/device.cpp does not build here: ${_dev_err}")
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
libavr_launcher(_tsb_python "${_tsb_absent}" ${Python3_EXECUTABLE})
|
||||||
|
if(_tsb_absent)
|
||||||
|
message(STATUS "the protocol tests skip here - ${_tsb_absent}")
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# The TinySafeBoot protocol reimplemented on libavr in three variants that trade
|
# The ELF is only a container (symbols, section headers) and is never flashed -
|
||||||
|
# and the host tool's load_image() dispatches on extension, so handing it one
|
||||||
|
# would silently program the header bytes. Every loader image therefore gets
|
||||||
|
# both flashable forms beside it at link time: .hex for avrdude, and .bin for
|
||||||
|
# the host tool's raw path (which is what the reloc and update tests convert to
|
||||||
|
# on the fly). .eeprom is dropped - EEPROM content is its own update.
|
||||||
|
function(add_image_outputs name)
|
||||||
|
add_custom_command(TARGET ${name} POST_BUILD
|
||||||
|
COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom
|
||||||
|
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.hex
|
||||||
|
COMMAND ${CMAKE_OBJCOPY} -O binary -R .eeprom
|
||||||
|
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# The TinySafeBoot protocol reimplemented on libavr in variants that trade
|
||||||
# clarity for size. Each links into the ATmega328P boot section (BOOTSZ selects
|
# clarity for size. Each links into the ATmega328P boot section (BOOTSZ selects
|
||||||
# its size; BOOTRST vectors a reset to its base) with -nostartfiles — a polled
|
# its size; BOOTRST vectors a reset to its base) with -nostartfiles - a polled
|
||||||
# loader has no use for the crt or the vector table. The naked entry sits in
|
# loader has no use for the crt or the vector table. The entry sits in
|
||||||
# .vectors, laid first, and runs. The boot base is FLASHEND+1 minus the section
|
# .vectors, laid first, and runs - avr::startup::entry on the policy tier,
|
||||||
# size; the linker section-start and the source's boot_bytes agree. tsb_app is
|
# the experiment tiers' own naked stubs elsewhere, each documented in its
|
||||||
|
# source. The boot base is FLASHEND+1 minus the section size; the linker
|
||||||
|
# section-start and the source's boot_bytes agree. tsb_app is
|
||||||
# the application's reset vector, pinned to 0 here so the loaders jump to a
|
# the application's reset vector, pinned to 0 here so the loaders jump to a
|
||||||
# named function; --pmem-wrap-around lets relaxation turn that absolute jump
|
# named function; --pmem-wrap-around lets relaxation turn that absolute jump
|
||||||
# into the wrapped rjmp AVR's modulo-flash PC actually executes.
|
# into the wrapped rjmp AVR's modulo-flash PC actually executes.
|
||||||
# All three implement the full oracle feature set (see oracle/README.md):
|
# All four implement the full oracle feature set (see oracle/README.md):
|
||||||
# watchdog bail, one-wire half-duplex, config-page activation timeout, password
|
# watchdog bail, one-wire half-duplex, config-page activation timeout, password
|
||||||
# gate, emergency erase, config/flash/EEPROM read-write. They differ only in how,
|
# gate, emergency erase, config/flash/EEPROM read-write. They differ only in how,
|
||||||
# and the size gradient is the cost of that "how" — see dev/lessons.md.
|
# and the size gradient is the cost of that "how".
|
||||||
# tsb_asm — the tricks tier's C++ with exactly two routines in asm (the
|
# 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
|
# bounded rx and the page-store loop, the two whose remaining
|
||||||
# cost is the C ABI itself): 510 B in the 512 B section the
|
# cost is the C ABI itself. Everything else, bring-up to
|
||||||
# hand-written 500 B oracle occupies. Everything else, from
|
# dispatch, is C++ on libavr.
|
||||||
# bring-up to dispatch, is C++ on libavr.
|
# tsb_tricks - no asm at all: the whole-loader register allocation lives in
|
||||||
# tsb_tricks — no asm at all: the whole-loader register allocation lives in
|
|
||||||
# global register variables (Y walks the page pointer), every
|
# global register variables (Y walks the page pointer), every
|
||||||
# helper is a tiny noinline primitive placed by the
|
# helper is a tiny noinline primitive placed by the
|
||||||
# global-register store rules, pages stream straight to
|
# global-register store rules, pages stream straight to
|
||||||
# SPM/EEPROM, and the bring-up is the two reset-non-default
|
# SPM/EEPROM.
|
||||||
# registers only. 526 B in the 1 KB section (BOOTSZ=10) — 14
|
# tsb_pure - pure idiomatic libavr, one function per command, TU-local
|
||||||
# over the oracle's section, from 168 over at this tier's first
|
# (internal linkage), streaming (no SRAM page buffer).
|
||||||
# floor.
|
# tsb_policy - the policy floor: no inline assembly and no global register
|
||||||
# tsb_pure — pure idiomatic libavr, one function per command, TU-local
|
# variables, which is philosophy #5's own bound, and the
|
||||||
# (internal linkage), streaming (no SRAM page buffer): 836 B in
|
# measured evidence that the 512 B fit is a property of the
|
||||||
# the 1 KB section.
|
# 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)
|
||||||
@@ -90,88 +125,33 @@ function(add_tsb_variant name bytes)
|
|||||||
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=32k)
|
||||||
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})
|
||||||
if(PROJECT_IS_TOP_LEVEL)
|
if(PROJECT_IS_TOP_LEVEL)
|
||||||
add_test(NAME ${name}.size
|
add_test(NAME ${name}.size
|
||||||
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}>
|
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}>
|
||||||
-DLIMIT=${bytes} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
-DLIMIT=${bytes} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
||||||
if(DEFINED TSB_DEVICE)
|
add_test(NAME ${name}.protocol
|
||||||
add_test(NAME ${name}.protocol
|
COMMAND ${_tsb_python} ${CMAKE_CURRENT_SOURCE_DIR}/test/tsbtest.py
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/tsbtest.py
|
${TSB_DEVICE} $<TARGET_FILE:${name}> ${base_hex})
|
||||||
${TSB_DEVICE} $<TARGET_FILE:${name}> ${base_hex})
|
|
||||||
endif()
|
|
||||||
endif()
|
endif()
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
# The tsb tiers reimplement the ATmega328P-only reference protocol; the other
|
# The tiers reimplement the ATmega328P-only reference protocol, so the guard is
|
||||||
# chips build pureboot alone.
|
# the whole of what this repo builds.
|
||||||
if(LIBAVR_MCU STREQUAL "atmega328p")
|
if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||||
add_tsb_variant(tsb_asm 512)
|
add_tsb_variant(tsb_asm 512)
|
||||||
|
add_tsb_variant(tsb_policy 1024)
|
||||||
add_tsb_variant(tsb_pure 1024)
|
add_tsb_variant(tsb_pure 1024)
|
||||||
add_tsb_variant(tsb_tricks 1024)
|
add_tsb_variant(tsb_tricks 1024)
|
||||||
|
# The policy tier's floor needs these two: a loader's loop bodies all
|
||||||
|
# contain calls, which is what makes hoisting an invariant out of one cost
|
||||||
|
# more than it saves. The other tiers keep the flag set their recorded
|
||||||
|
# floors were measured with - none.
|
||||||
|
target_compile_options(tsb_policy PRIVATE -fno-move-loop-invariants -fno-tree-ter)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# pureboot — the pure-constraint port (see pureboot/README.md): one source,
|
# Every test registered above carries the marker a stubbed launcher prints, so
|
||||||
# no inline assembly, no global register variables, every libavr chip, 512
|
# a check this host cannot run reads as Skipped rather than Failed.
|
||||||
# bytes each. The loader owns the top 512 bytes of flash on every chip; the
|
|
||||||
# application entry symbol is address 0 on the mega (reset re-vectors to the
|
|
||||||
# loader through BOOTRST, so word 0 stays the application's own vector) and
|
|
||||||
# the trampoline word just below the loader on the tinies (host-side vector
|
|
||||||
# surgery points it at the application). --pmem-wrap-around models AVR's
|
|
||||||
# modulo-flash PC where the flash is big enough to need it.
|
|
||||||
if(LIBAVR_MCU STREQUAL "attiny13a")
|
|
||||||
set(_pb_flash 1024)
|
|
||||||
set(_pb_wrap "")
|
|
||||||
set(_pb_page 32)
|
|
||||||
set(_pb_hz 9600000)
|
|
||||||
set(_pb_baud 57600)
|
|
||||||
set(_pb_eeprom 64)
|
|
||||||
elseif(LIBAVR_MCU STREQUAL "attiny85")
|
|
||||||
set(_pb_flash 8192)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
|
|
||||||
set(_pb_page 64)
|
|
||||||
set(_pb_hz 8000000)
|
|
||||||
set(_pb_baud 57600)
|
|
||||||
set(_pb_eeprom 512)
|
|
||||||
else()
|
|
||||||
set(_pb_flash 32768)
|
|
||||||
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
|
|
||||||
set(_pb_page 128)
|
|
||||||
set(_pb_hz 16000000)
|
|
||||||
set(_pb_baud 115200)
|
|
||||||
set(_pb_eeprom 1024)
|
|
||||||
endif()
|
|
||||||
math(EXPR _pb_base "${_pb_flash} - 512")
|
|
||||||
math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL)
|
|
||||||
if(LIBAVR_MCU STREQUAL "atmega328p")
|
|
||||||
set(_pb_app 0)
|
|
||||||
else()
|
|
||||||
math(EXPR _pb_app "${_pb_base} - 2")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_executable(pureboot pureboot/pureboot.cpp)
|
|
||||||
target_link_libraries(pureboot PRIVATE libavr)
|
|
||||||
target_link_options(pureboot PRIVATE -nostartfiles -Wl,--section-start=.text=${_pb_base_hex}
|
|
||||||
-Wl,--defsym=pureboot_app=${_pb_app} ${_pb_wrap})
|
|
||||||
add_custom_command(TARGET pureboot POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:pureboot>)
|
|
||||||
if(PROJECT_IS_TOP_LEVEL)
|
if(PROJECT_IS_TOP_LEVEL)
|
||||||
add_test(NAME pureboot.size
|
libavr_skip_unverified()
|
||||||
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot>
|
|
||||||
-DLIMIT=512 -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
|
||||||
|
|
||||||
# The protocol test flashes this fixture through the loader with the real
|
|
||||||
# host tool and expects its banner after the hand-over; a normally linked
|
|
||||||
# application whose reset vector is what the tinies' surgery re-homes.
|
|
||||||
if(DEFINED PB_DEVICE)
|
|
||||||
add_executable(pbapp test/pbapp.cpp)
|
|
||||||
target_link_libraries(pbapp PRIVATE libavr)
|
|
||||||
add_custom_command(TARGET pbapp POST_BUILD
|
|
||||||
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:pbapp> $<TARGET_FILE:pbapp>.bin)
|
|
||||||
add_test(NAME pureboot.protocol
|
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
|
||||||
${PB_DEVICE} $<TARGET_FILE:pureboot> ${LIBAVR_MCU} ${_pb_hz} ${_pb_base_hex}
|
|
||||||
${_pb_page} ${_pb_baud} ${_pb_eeprom} $<TARGET_FILE:pbapp>.bin
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
|
||||||
${CMAKE_BINARY_DIR}/pbtest-work)
|
|
||||||
set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180)
|
|
||||||
endif()
|
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -1,86 +1,84 @@
|
|||||||
{
|
{
|
||||||
"version": 8,
|
"version": 8,
|
||||||
"configurePresets": [
|
"configurePresets": [
|
||||||
{
|
{
|
||||||
"name": "base",
|
"name": "base",
|
||||||
"hidden": true,
|
"hidden": true,
|
||||||
"generator": "Ninja",
|
"generator": "Ninja",
|
||||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||||
"toolchainFile": "$env{LIBAVR_ROOT}/cmake/avr-toolchain.cmake",
|
"toolchainFile": "${sourceDir}/libavr/cmake/avr-toolchain.cmake",
|
||||||
"cacheVariables": {
|
"cacheVariables": {
|
||||||
"CMAKE_BUILD_TYPE": "Release",
|
"CMAKE_BUILD_TYPE": "Release",
|
||||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
||||||
"CMAKE_COLOR_DIAGNOSTICS": "ON"
|
"CMAKE_COLOR_DIAGNOSTICS": "ON"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "atmega328p-generated",
|
"name": "atmega328p-generated",
|
||||||
"inherits": "base",
|
"inherits": "base",
|
||||||
"cacheVariables": { "LIBAVR_MCU": "atmega328p", "LIBAVR_REFLECT": "OFF" }
|
"cacheVariables": {
|
||||||
},
|
"LIBAVR_MCU": "atmega328p",
|
||||||
{
|
"LIBAVR_REFLECT": "OFF"
|
||||||
"name": "atmega328p-reflect",
|
}
|
||||||
"inherits": "base",
|
},
|
||||||
"cacheVariables": { "LIBAVR_MCU": "atmega328p", "LIBAVR_REFLECT": "ON" }
|
{
|
||||||
},
|
"name": "atmega328p-reflect",
|
||||||
{
|
"inherits": "base",
|
||||||
"name": "attiny85-generated",
|
"cacheVariables": {
|
||||||
"inherits": "base",
|
"LIBAVR_MCU": "atmega328p",
|
||||||
"cacheVariables": { "LIBAVR_MCU": "attiny85", "LIBAVR_REFLECT": "OFF" }
|
"LIBAVR_REFLECT": "ON"
|
||||||
},
|
}
|
||||||
{
|
}
|
||||||
"name": "attiny85-reflect",
|
],
|
||||||
"inherits": "base",
|
"buildPresets": [
|
||||||
"cacheVariables": { "LIBAVR_MCU": "attiny85", "LIBAVR_REFLECT": "ON" }
|
{
|
||||||
},
|
"name": "atmega328p-generated",
|
||||||
{
|
"configurePreset": "atmega328p-generated"
|
||||||
"name": "attiny13a-generated",
|
},
|
||||||
"inherits": "base",
|
{
|
||||||
"cacheVariables": { "LIBAVR_MCU": "attiny13a", "LIBAVR_REFLECT": "OFF" }
|
"name": "atmega328p-reflect",
|
||||||
},
|
"configurePreset": "atmega328p-reflect"
|
||||||
{
|
}
|
||||||
"name": "attiny13a-reflect",
|
],
|
||||||
"inherits": "base",
|
"testPresets": [
|
||||||
"cacheVariables": { "LIBAVR_MCU": "attiny13a", "LIBAVR_REFLECT": "ON" }
|
{
|
||||||
}
|
"name": "atmega328p-generated",
|
||||||
],
|
"configurePreset": "atmega328p-generated",
|
||||||
"buildPresets": [
|
"output": {
|
||||||
{ "name": "atmega328p-generated", "configurePreset": "atmega328p-generated" },
|
"outputOnFailure": true
|
||||||
{ "name": "atmega328p-reflect", "configurePreset": "atmega328p-reflect" },
|
}
|
||||||
{ "name": "attiny85-generated", "configurePreset": "attiny85-generated" },
|
}
|
||||||
{ "name": "attiny85-reflect", "configurePreset": "attiny85-reflect" },
|
],
|
||||||
{ "name": "attiny13a-generated", "configurePreset": "attiny13a-generated" },
|
"workflowPresets": [
|
||||||
{ "name": "attiny13a-reflect", "configurePreset": "attiny13a-reflect" }
|
{
|
||||||
],
|
"name": "atmega328p-generated",
|
||||||
"workflowPresets": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"name": "atmega328p-generated",
|
"type": "configure",
|
||||||
"steps": [
|
"name": "atmega328p-generated"
|
||||||
{ "type": "configure", "name": "atmega328p-generated" },
|
},
|
||||||
{ "type": "build", "name": "atmega328p-generated" },
|
{
|
||||||
{ "type": "test", "name": "atmega328p-generated" }
|
"type": "build",
|
||||||
]
|
"name": "atmega328p-generated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "attiny85-generated",
|
"type": "test",
|
||||||
"steps": [
|
"name": "atmega328p-generated"
|
||||||
{ "type": "configure", "name": "attiny85-generated" },
|
}
|
||||||
{ "type": "build", "name": "attiny85-generated" },
|
]
|
||||||
{ "type": "test", "name": "attiny85-generated" }
|
},
|
||||||
]
|
{
|
||||||
},
|
"name": "atmega328p-reflect",
|
||||||
{
|
"steps": [
|
||||||
"name": "attiny13a-generated",
|
{
|
||||||
"steps": [
|
"type": "configure",
|
||||||
{ "type": "configure", "name": "attiny13a-generated" },
|
"name": "atmega328p-reflect"
|
||||||
{ "type": "build", "name": "attiny13a-generated" },
|
},
|
||||||
{ "type": "test", "name": "attiny13a-generated" }
|
{
|
||||||
]
|
"type": "build",
|
||||||
}
|
"name": "atmega328p-reflect"
|
||||||
],
|
}
|
||||||
"testPresets": [
|
]
|
||||||
{ "name": "atmega328p-generated", "configurePreset": "atmega328p-generated", "output": { "outputOnFailure": true } },
|
}
|
||||||
{ "name": "attiny85-generated", "configurePreset": "attiny85-generated", "output": { "outputOnFailure": true } },
|
]
|
||||||
{ "name": "attiny13a-generated", "configurePreset": "attiny13a-generated", "output": { "outputOnFailure": true } }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
67
ide/README.md
Normal file
67
ide/README.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Atmel Studio
|
||||||
|
|
||||||
|
`master` carries `bootloader.atsln`, so this branch does too: `ide/bootloader.atsln`
|
||||||
|
builds the loader from the same source Ninja does, to a **byte-identical
|
||||||
|
`.text`** — the `tsb_asm` tier in its 512-byte section (`check-flags.py` below
|
||||||
|
is what holds the flag sets equal, so the size is Ninja's own). CMake remains
|
||||||
|
the build system; the solution is here so the port opens in Studio as its
|
||||||
|
predecessor did.
|
||||||
|
|
||||||
|
## One project, of four tiers
|
||||||
|
|
||||||
|
A `.cppproj` is one binary at one set of flags. `tsb_asm` is the tier that
|
||||||
|
occupies the same 512-byte section `master`'s `tsb` project targeted, which is
|
||||||
|
the one worth opening in Studio.
|
||||||
|
|
||||||
|
The other three tiers (`tsb_pure`, `tsb_tricks`, `tsb_policy`) are not here.
|
||||||
|
They differ from `tsb_asm` in their source file, their section size, and — for
|
||||||
|
`tsb_policy` — two loop flags; nothing about that is a Studio concern, and what
|
||||||
|
they exist to demonstrate is a size gradient only the CMake size tests measure.
|
||||||
|
Adding one is a copy of `tsb_asm/tsb_asm.cppproj` in its own directory, with its
|
||||||
|
name, its GUID, its source path and its `--section-start` changed (`0x7c00` for
|
||||||
|
the 1 KiB tiers), plus four lines in the solution.
|
||||||
|
|
||||||
|
`avrdevice` is a project property, so each project gets its own directory:
|
||||||
|
Studio builds into `<project dir>/<Configuration>` whatever `OutputDirectory`
|
||||||
|
says, and two projects sharing a directory would share one object file.
|
||||||
|
|
||||||
|
## Debug keeps `-Os`
|
||||||
|
|
||||||
|
Both configurations compile at `-Os`; Debug adds only `-gdwarf-4`. The `.text`
|
||||||
|
is therefore identical in both, which is the point — a loader's section is a
|
||||||
|
**correctness** bound and not a budget. A debug configuration that silently
|
||||||
|
overruns the section is worse than none, and DWARF costs no flash, so the
|
||||||
|
optimisation level stays where correctness needs it.
|
||||||
|
|
||||||
|
## What Studio needs from the machine
|
||||||
|
|
||||||
|
libavr from the **submodule**, found at
|
||||||
|
`$(MSBuildProjectDirectory)\..\..\libavr\include` — correct by construction, and
|
||||||
|
anchored to the project because a plain relative path resolves against the
|
||||||
|
generated makefile's directory (the configuration's output directory), not the
|
||||||
|
project's. There is no `LIBAVR_ROOT` escape hatch: a variable exported in a
|
||||||
|
shell is invisible to Studio launched from the Start menu, and the failure reads
|
||||||
|
as a missing `libavr/libavr.hpp` — which is what the submodule answers.
|
||||||
|
|
||||||
|
A GCC 16.1 toolchain registered as flavour `avr-g++-16.1.0`, nothing older
|
||||||
|
reaching `-std=c++26`.
|
||||||
|
|
||||||
|
## Generating and gating
|
||||||
|
|
||||||
|
One generated file is required before the project will load at all, and one
|
||||||
|
command checks the flags have not drifted (both from libavr's
|
||||||
|
`tools/atmelstudio/`):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python libavr/tools/atmelstudio/componentinfo.py \
|
||||||
|
ide/tsb_asm/tsb_asm.componentinfo.xml --device ATmega328P
|
||||||
|
python libavr/tools/atmelstudio/check-flags.py \
|
||||||
|
--solution ide/bootloader.atsln --project tsb_asm --target tsb_asm \
|
||||||
|
--compile-commands build/atmega328p-generated/compile_commands.json \
|
||||||
|
--log build/as-tsb_asm.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Release is what the gate compares — the presets define no debug build, and
|
||||||
|
Debug differs from Release only in `-gdwarf-4`.
|
||||||
|
|
||||||
|
Legacy (the yazoalfa-era submodules) stays on `master`.
|
||||||
22
ide/bootloader.atsln
Normal file
22
ide/bootloader.atsln
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Atmel Studio Solution File, Format Version 11.00
|
||||||
|
VisualStudioVersion = 14.0.23107.0
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{E66E83B9-2572-4076-B26E-6BE79FF3018A}") = "tsb_asm", "tsb_asm\tsb_asm.cppproj", "{6618D3BE-7EB3-49A2-9113-F128E396FF06}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|AVR = Debug|AVR
|
||||||
|
Release|AVR = Release|AVR
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{6618D3BE-7EB3-49A2-9113-F128E396FF06}.Debug|AVR.ActiveCfg = Debug|AVR
|
||||||
|
{6618D3BE-7EB3-49A2-9113-F128E396FF06}.Debug|AVR.Build.0 = Debug|AVR
|
||||||
|
{6618D3BE-7EB3-49A2-9113-F128E396FF06}.Release|AVR.ActiveCfg = Release|AVR
|
||||||
|
{6618D3BE-7EB3-49A2-9113-F128E396FF06}.Release|AVR.Build.0 = Release|AVR
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
112
ide/tsb_asm/tsb_asm.cppproj
Normal file
112
ide/tsb_asm/tsb_asm.cppproj
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="14.0">
|
||||||
|
<PropertyGroup>
|
||||||
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
|
<ProjectVersion>7.0</ProjectVersion>
|
||||||
|
<ToolchainName>com.Atmel.AVRGCC8.CPP</ToolchainName>
|
||||||
|
<ProjectGuid>6618d3be-7eb3-49a2-9113-f128e396ff06</ProjectGuid>
|
||||||
|
<avrdevice>ATmega328P</avrdevice>
|
||||||
|
<avrdeviceseries>none</avrdeviceseries>
|
||||||
|
<OutputType>Executable</OutputType>
|
||||||
|
<Language>CPP</Language>
|
||||||
|
<OutputFileName>$(MSBuildProjectName)</OutputFileName>
|
||||||
|
<OutputFileExtension>.elf</OutputFileExtension>
|
||||||
|
<OutputDirectory>$(MSBuildProjectDirectory)\$(Configuration)</OutputDirectory>
|
||||||
|
<AssemblyName>tsb_asm</AssemblyName>
|
||||||
|
<Name>tsb_asm</Name>
|
||||||
|
<RootNamespace>tsb_asm</RootNamespace>
|
||||||
|
<ToolchainFlavour>avr-g++-16.1.0</ToolchainFlavour>
|
||||||
|
<KeepTimersRunning>true</KeepTimersRunning>
|
||||||
|
<OverrideVtor>false</OverrideVtor>
|
||||||
|
<CacheFlash>true</CacheFlash>
|
||||||
|
<ProgFlashFromRam>true</ProgFlashFromRam>
|
||||||
|
<RamSnippetAddress>0x20000000</RamSnippetAddress>
|
||||||
|
<UncachedRange />
|
||||||
|
<preserveEEPROM>true</preserveEEPROM>
|
||||||
|
<OverrideVtorValue>exception_table</OverrideVtorValue>
|
||||||
|
<BootSegment>2</BootSegment>
|
||||||
|
<ResetRule>0</ResetRule>
|
||||||
|
<eraseonlaunchrule>0</eraseonlaunchrule>
|
||||||
|
<EraseKey />
|
||||||
|
<AsfFrameworkConfig>
|
||||||
|
<framework-data xmlns="">
|
||||||
|
<options />
|
||||||
|
<configurations />
|
||||||
|
<files />
|
||||||
|
<documentation help="" />
|
||||||
|
<offline-documentation help="" />
|
||||||
|
<dependencies>
|
||||||
|
<content-extension eid="atmel.asf" uuidref="Atmel.ASF" version="3.52.0" />
|
||||||
|
</dependencies>
|
||||||
|
</framework-data>
|
||||||
|
</AsfFrameworkConfig>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
|
<ToolchainSettings>
|
||||||
|
<AvrGccCpp>
|
||||||
|
<avrgcc.common.Device>-mmcu=atmega328p</avrgcc.common.Device>
|
||||||
|
<avrgcc.common.outputfiles.hex>True</avrgcc.common.outputfiles.hex>
|
||||||
|
<avrgcc.common.outputfiles.lss>True</avrgcc.common.outputfiles.lss>
|
||||||
|
<avrgcc.common.outputfiles.eep>True</avrgcc.common.outputfiles.eep>
|
||||||
|
<avrgcc.common.outputfiles.srec>True</avrgcc.common.outputfiles.srec>
|
||||||
|
<avrgcc.common.outputfiles.usersignatures>False</avrgcc.common.outputfiles.usersignatures>
|
||||||
|
<avrgcccpp.compiler.symbols.DefSymbols>
|
||||||
|
<ListValues>
|
||||||
|
<Value>NDEBUG</Value>
|
||||||
|
</ListValues>
|
||||||
|
</avrgcccpp.compiler.symbols.DefSymbols>
|
||||||
|
<avrgcccpp.compiler.directories.IncludePaths>
|
||||||
|
<ListValues>
|
||||||
|
<Value>$(MSBuildProjectDirectory)\..\..\libavr\include</Value>
|
||||||
|
</ListValues>
|
||||||
|
</avrgcccpp.compiler.directories.IncludePaths>
|
||||||
|
<avrgcccpp.compiler.optimization.level>Optimize for size (-Os)</avrgcccpp.compiler.optimization.level>
|
||||||
|
<avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>
|
||||||
|
<avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>
|
||||||
|
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
|
||||||
|
<avrgcccpp.compiler.miscellaneous.OtherFlags>-std=c++26 -Wextra -Werror -mrelax -fno-exceptions -fno-rtti -fno-threadsafe-statics</avrgcccpp.compiler.miscellaneous.OtherFlags>
|
||||||
|
<avrgcccpp.linker.optimization.GarbageCollectUnusedSections>True</avrgcccpp.linker.optimization.GarbageCollectUnusedSections>
|
||||||
|
<avrgcccpp.linker.miscellaneous.LinkerFlags>-mrelax -nostartfiles -Wl,--section-start=.text=0x7e00 -Wl,--defsym=tsb_app=0 -Wl,--pmem-wrap-around=32k</avrgcccpp.linker.miscellaneous.LinkerFlags>
|
||||||
|
</AvrGccCpp>
|
||||||
|
</ToolchainSettings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<ToolchainSettings>
|
||||||
|
<AvrGccCpp>
|
||||||
|
<avrgcc.common.Device>-mmcu=atmega328p</avrgcc.common.Device>
|
||||||
|
<avrgcc.common.outputfiles.hex>True</avrgcc.common.outputfiles.hex>
|
||||||
|
<avrgcc.common.outputfiles.lss>True</avrgcc.common.outputfiles.lss>
|
||||||
|
<avrgcc.common.outputfiles.eep>True</avrgcc.common.outputfiles.eep>
|
||||||
|
<avrgcc.common.outputfiles.srec>True</avrgcc.common.outputfiles.srec>
|
||||||
|
<avrgcc.common.outputfiles.usersignatures>False</avrgcc.common.outputfiles.usersignatures>
|
||||||
|
<avrgcccpp.compiler.symbols.DefSymbols>
|
||||||
|
<ListValues>
|
||||||
|
<Value>DEBUG</Value>
|
||||||
|
</ListValues>
|
||||||
|
</avrgcccpp.compiler.symbols.DefSymbols>
|
||||||
|
<avrgcccpp.compiler.directories.IncludePaths>
|
||||||
|
<ListValues>
|
||||||
|
<Value>$(MSBuildProjectDirectory)\..\..\libavr\include</Value>
|
||||||
|
</ListValues>
|
||||||
|
</avrgcccpp.compiler.directories.IncludePaths>
|
||||||
|
<avrgcccpp.compiler.optimization.level>Optimize for size (-Os)</avrgcccpp.compiler.optimization.level>
|
||||||
|
<avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareFunctionsForGarbageCollection>
|
||||||
|
<avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>True</avrgcccpp.compiler.optimization.PrepareDataForGarbageCollection>
|
||||||
|
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
|
||||||
|
<avrgcccpp.compiler.miscellaneous.OtherFlags>-std=c++26 -Wextra -Werror -mrelax -fno-exceptions -fno-rtti -fno-threadsafe-statics -gdwarf-4</avrgcccpp.compiler.miscellaneous.OtherFlags>
|
||||||
|
<avrgcccpp.linker.optimization.GarbageCollectUnusedSections>True</avrgcccpp.linker.optimization.GarbageCollectUnusedSections>
|
||||||
|
<avrgcccpp.linker.miscellaneous.LinkerFlags>-mrelax -nostartfiles -Wl,--section-start=.text=0x7e00 -Wl,--defsym=tsb_app=0 -Wl,--pmem-wrap-around=32k</avrgcccpp.linker.miscellaneous.LinkerFlags>
|
||||||
|
</AvrGccCpp>
|
||||||
|
</ToolchainSettings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\..\tsb\tsb_asm.cpp">
|
||||||
|
<SubType>compile</SubType>
|
||||||
|
<Link>tsb\tsb_asm.cpp</Link>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="tsb" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(AVRSTUDIO_EXE_PATH)\Vs\Compiler.targets" />
|
||||||
|
</Project>
|
||||||
1
libavr
Submodule
1
libavr
Submodule
Submodule libavr added at 93d8b0e491
@@ -38,11 +38,23 @@ avra -I /usr/share/avra tsb-fixedbaud.asm # after uncommenting .include "m328P
|
|||||||
```
|
```
|
||||||
|
|
||||||
**500 bytes with every feature** — the proof that ≤512 B and full feature parity
|
**500 bytes with every feature** — the proof that ≤512 B and full feature parity
|
||||||
are simultaneously reachable. The port's `tsb_asm` tier meets the same bar at
|
are simultaneously reachable. The port's four tiers reach it from the other
|
||||||
510 B in the same 512 B section, written in C++ on libavr except the two
|
side, and the gradient between them is the cost of the mechanisms each is
|
||||||
routines whose remaining cost is the calling convention itself (the bounded rx
|
allowed:
|
||||||
and the page-store loop); `tsb_tricks` needs no assembly at all at 526 B, and
|
|
||||||
`tsb_pure` stays fully idiomatic at 836 B, both in the 1 KB section.
|
| tier | bytes | section | what it is allowed |
|
||||||
|
|---|---|---|---|
|
||||||
|
| oracle | 500 | 512 B | hand-written assembly, the reference |
|
||||||
|
| `tsb_asm` | 512 | 512 B | C++ on libavr, two routines in asm |
|
||||||
|
| `tsb_tricks` | 528 | 1 KB | no asm; global register variables |
|
||||||
|
| `tsb_policy` | 630 | 1 KB | pureboot's rules: no asm, no register variables |
|
||||||
|
| `tsb_pure` | 776 | 1 KB | idiomatic libavr throughout |
|
||||||
|
|
||||||
|
The two routines `tsb_asm` keeps are the ones whose remaining cost is the
|
||||||
|
calling convention itself: the bounded rx and the page-store loop. It fills
|
||||||
|
its section exactly, with the same one-bit-time turn-around guard the oracle
|
||||||
|
spends six bytes on - every tier implements the whole feature set, which is
|
||||||
|
what makes the column a gradient rather than four different loaders.
|
||||||
|
|
||||||
The oracle targets 20 MHz / 33333 baud; the port targets 16 MHz / 115200 baud
|
The oracle targets 20 MHz / 33333 baud; the port targets 16 MHz / 115200 baud
|
||||||
(what the simavr protocol test drives). Baud and geometry differ, code size and
|
(what the simavr protocol test drives). Baud and geometry differ, code size and
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
# pureboot
|
|
||||||
|
|
||||||
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by
|
|
||||||
constraint: one C++ source, no inline assembly, no global register variables
|
|
||||||
(attributes allowed), built for every chip libavr targets, **512 bytes on
|
|
||||||
each** — 490 B on the ATtiny13A, 510 B on the ATtiny85, 484 B on the
|
|
||||||
ATmega328P. The device speaks primitives; every composite — verify, erase,
|
|
||||||
reset-vector surgery, timeout configuration — lives in the host tool
|
|
||||||
(`pureboot.py`).
|
|
||||||
|
|
||||||
## Link
|
|
||||||
|
|
||||||
| Chip | Serial | Baud | Clock assumed |
|
|
||||||
|---|---|---|---|
|
|
||||||
| ATmega328P | USART0, RXD/TXD = PD0/PD1 | 115200 8N1 | 16 MHz crystal |
|
|
||||||
| ATtiny85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC |
|
|
||||||
| ATtiny13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC |
|
|
||||||
|
|
||||||
The tiny RX pin has its pull-up enabled; TX idles high. All multi-byte
|
|
||||||
quantities on the wire are little-endian.
|
|
||||||
|
|
||||||
## Activation
|
|
||||||
|
|
||||||
Reset enters the loader (BOOTRST on the mega, the patched reset vector on the
|
|
||||||
tinies) — except a watchdog reset, which hands straight to the application
|
|
||||||
(the application owns its watchdog; it must clear WDRF itself, which also
|
|
||||||
releases the WDRF-forced WDE).
|
|
||||||
|
|
||||||
The host then has one activation window per awaited byte to knock: `p` then
|
|
||||||
`b`. Each awaited byte gets a fresh window; any other byte is discarded and
|
|
||||||
awaited again (line noise cannot lock the loader, only delay it). A window
|
|
||||||
expiring with an idle line boots the application.
|
|
||||||
|
|
||||||
The window length in seconds is the **last EEPROM cell** (address
|
|
||||||
`eeprom_size - 1`); `0x00` and the erased `0xff` both mean the 4 s default,
|
|
||||||
so a full EEPROM erase resets the timeout rather than maxing it. The host
|
|
||||||
changes it with the ordinary EEPROM-write command.
|
|
||||||
|
|
||||||
## Session
|
|
||||||
|
|
||||||
After the knock the loader stays in its command loop until `G` or a reset.
|
|
||||||
Before reading each command it waits for any pending EEPROM write to finish
|
|
||||||
and sends the prompt `+` (0x2b) — the prompt is therefore also the completion
|
|
||||||
ack of the previous command. A session is: await `+`, send a command, read
|
|
||||||
its reply, repeat.
|
|
||||||
|
|
||||||
| Cmd | Arguments | Reply |
|
|
||||||
|---|---|---|
|
|
||||||
| `b` | — | the 12-byte info block |
|
|
||||||
| `R` | addr16, n8 | n flash bytes (n = 0 means 256) |
|
|
||||||
| `W` | addr16, then one page of data | — (completion = next prompt) |
|
|
||||||
| `r` | addr16, n8 | n EEPROM bytes (n = 0 means 256) |
|
|
||||||
| `w` | addr16, n8, then n data bytes | `+` per byte, sent once its write has begun |
|
|
||||||
| `F` | — | 4 bytes: low fuse, lock, extended fuse, high fuse |
|
|
||||||
| `G` | — | `+`, then the application runs |
|
|
||||||
| other | — | ignored; the loop re-prompts (send a junk byte, await `+`, to resync) |
|
|
||||||
|
|
||||||
`W` streams exactly one SPM page (size from the info block) into the buffer,
|
|
||||||
then erases and programs; the address must be page-aligned. Pages inside the
|
|
||||||
loader's own 512 bytes are drained but never programmed — a broken host
|
|
||||||
cannot brick the chip. `w` is host-paced: send the next byte only after the
|
|
||||||
previous byte's `+`. `F` returns the bytes in the hardware's Z order; on a chip without an
|
|
||||||
extended fuse byte (the ATtiny13A) that slot carries no meaning. Fuse *writing* does not
|
|
||||||
exist: SPM reaches flash (and, on the mega, lock bits) only — fuse bytes are
|
|
||||||
external-programming territory by hardware.
|
|
||||||
|
|
||||||
The info block (`b`):
|
|
||||||
|
|
||||||
| Offset | Content |
|
|
||||||
|---|---|
|
|
||||||
| 0–2 | `'P'`, `'B'`, protocol version (1) |
|
|
||||||
| 3–5 | device signature |
|
|
||||||
| 6 | SPM page size in bytes |
|
|
||||||
| 7–8 | loader base — application flash ends here |
|
|
||||||
| 9–10 | EEPROM size |
|
|
||||||
| 11 | bit 0 set: host must patch the reset vector (no hardware boot section) |
|
|
||||||
|
|
||||||
Composites are the host's job: verify = read back and compare, erase =
|
|
||||||
write `0xff` (per page for flash, per byte for EEPROM), timeout = EEPROM
|
|
||||||
write to the last cell.
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
**ATmega328P**: program the loader at 0x7e00 with an external programmer;
|
|
||||||
fuses BOOTSZ = 11 (256 words) and BOOTRST programmed. Applications are
|
|
||||||
flashed unmodified — reset re-vectors to the loader in hardware, word 0
|
|
||||||
stays the application's own reset vector, and `G` jumps to 0.
|
|
||||||
|
|
||||||
**Tinies** (no boot section): program the loader at `flash - 512`; erased
|
|
||||||
flash below it walks up into the loader, so a virgin chip activates. When
|
|
||||||
flashing an application the host performs reset-vector surgery: the
|
|
||||||
application's own `rjmp` target is re-encoded as a trampoline `rjmp` in the
|
|
||||||
word just below the loader (`base - 2`, where `G` jumps), and word 0 is
|
|
||||||
rewritten to `rjmp` to the loader base. Every other vector stays the
|
|
||||||
application's. Page 0 is written last, so an interrupted flash leaves word 0
|
|
||||||
erased and the chip still falls through to the loader on the next reset.
|
|
||||||
|
|
||||||
## Host tool
|
|
||||||
|
|
||||||
`pureboot.py` — Python 3, standard library only (termios drives any tty,
|
|
||||||
a USB adapter as well as a simavr pty):
|
|
||||||
|
|
||||||
pureboot.py --port /dev/ttyUSB0 --baud 57600 \
|
|
||||||
--info --fuses --flash app.hex --timeout 10
|
|
||||||
|
|
||||||
Operations run in a fixed order within one session: info, fuses, flash
|
|
||||||
(erase / program / read / verify), EEPROM (erase / program / read / verify),
|
|
||||||
timeout — then the loader hands over to the application; `--stay` keeps the
|
|
||||||
session alive instead, and a later invocation reconnects into it (the knock
|
|
||||||
converges there too). `--flash` and `--eeprom` verify by read-back unless
|
|
||||||
`--no-verify`; images are raw binary, or Intel HEX by extension.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Per chip preset, `ctest` runs the 512-byte size gate and the end-to-end
|
|
||||||
protocol test: a simavr device (`test/pureboot_device.c` — the mega's USART
|
|
||||||
as a pty; on the tinies a cycle-timed GPIO⇄pty bridge for the software UART,
|
|
||||||
plus the SPM/NVM module simavr's tiny cores lack) driven by the real host
|
|
||||||
tool through knock-from-reset, program + verify of both memories, timeout
|
|
||||||
configuration, session reconnect, an external reset through the patched
|
|
||||||
vector, and the hand-over to a fixture application whose banner proves the
|
|
||||||
launch — cross-checked against the simulator's ground-truth memory dumps and
|
|
||||||
an independent decode of the surgery's rjmp words.
|
|
||||||
@@ -1,326 +0,0 @@
|
|||||||
// pureboot — a serial bootloader on libavr, pure by constraint: one C++
|
|
||||||
// source with no inline assembly and no global register variables, built for
|
|
||||||
// every chip libavr targets, 512 bytes on each. The device speaks primitives
|
|
||||||
// — read/program flash, read/write EEPROM, fuse bytes, an info block, run —
|
|
||||||
// and everything composite (verify, erase, reset-vector surgery on the
|
|
||||||
// tinies, timeout configuration) lives in the host tool. Protocol reference:
|
|
||||||
// README.md next to this file.
|
|
||||||
//
|
|
||||||
// Entry: reset lands in avr::startup::entry below (BOOTRST on the mega; the
|
|
||||||
// patched reset vector — or erased flash walking up into the loader — on the
|
|
||||||
// tinies). A watchdog reset hands straight to the application. Otherwise the
|
|
||||||
// host has one activation window — EEPROM's last cell, in seconds — to knock
|
|
||||||
// ("pb"); an idle line boots the application. A session then stays in the
|
|
||||||
// command loop until 'G' hands over or the chip resets.
|
|
||||||
|
|
||||||
#include <libavr/libavr.hpp>
|
|
||||||
|
|
||||||
using namespace avr::literals;
|
|
||||||
namespace spm = avr::spm;
|
|
||||||
namespace ee = avr::eeprom;
|
|
||||||
|
|
||||||
namespace pureboot {
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// Purely polled — interrupts stay off, every guard folds to nothing.
|
|
||||||
constexpr auto off = avr::irq::guard_policy::unused;
|
|
||||||
|
|
||||||
constexpr std::uint8_t ack = '+';
|
|
||||||
|
|
||||||
// Per-chip personality, from the chip database: the clocks the dogfood
|
|
||||||
// boards run (16 MHz crystal on the mega, calibrated RC on the tinies) and
|
|
||||||
// the device signature (compile-time data — the tiny13A cannot even read its
|
|
||||||
// signature row from code).
|
|
||||||
consteval avr::hertz_t clock()
|
|
||||||
{
|
|
||||||
if (avr::hw::db.name == "ATtiny13A")
|
|
||||||
return 9.6_MHz;
|
|
||||||
if (avr::hw::db.name == "ATtiny85")
|
|
||||||
return 8_MHz;
|
|
||||||
return 16_MHz;
|
|
||||||
}
|
|
||||||
|
|
||||||
consteval std::array<std::uint8_t, 3> signature()
|
|
||||||
{
|
|
||||||
if (avr::hw::db.name == "ATtiny13A")
|
|
||||||
return {0x1e, 0x90, 0x07};
|
|
||||||
if (avr::hw::db.name == "ATtiny85")
|
|
||||||
return {0x1e, 0x93, 0x0b};
|
|
||||||
return {0x1e, 0x95, 0x0f};
|
|
||||||
}
|
|
||||||
|
|
||||||
using dev = avr::device<{.clock = clock()}>;
|
|
||||||
|
|
||||||
// Geometry: the loader owns the top 512 bytes of flash; the byte below it is
|
|
||||||
// the trampoline word (the application's relocated reset vector) on chips
|
|
||||||
// without a hardware boot section. The RWWSRE bit marks a separate boot
|
|
||||||
// section — on classic AVR the two capabilities coincide.
|
|
||||||
constexpr std::uint16_t boot_bytes = 512;
|
|
||||||
constexpr std::uint16_t base = static_cast<std::uint16_t>(spm::flash_bytes - boot_bytes);
|
|
||||||
constexpr std::uint16_t page = spm::page_bytes;
|
|
||||||
constexpr bool boot_section = avr::hw::db.field_index("SPMCSR", "RWWSRE") >= 0;
|
|
||||||
|
|
||||||
// The activation timeout lives in EEPROM's last cell, in seconds; the host
|
|
||||||
// rewrites it with the ordinary EEPROM-write command. An unprogrammed cell —
|
|
||||||
// 0x00 or the erased 0xff — means the 4 s default: a stray value can never
|
|
||||||
// floor the window to nothing and lock the loader out, and erasing the whole
|
|
||||||
// EEPROM resets the timeout instead of maxing it to 255 s.
|
|
||||||
constexpr std::uint16_t timeout_cell = avr::hw::db.mem.eeprom_size - 1;
|
|
||||||
constexpr std::uint8_t default_seconds = 4;
|
|
||||||
|
|
||||||
// The 12-byte info block the host reads with the 'b' command; flash-resident
|
|
||||||
// (there is no crt to copy a .data image).
|
|
||||||
inline constexpr std::array<std::uint8_t, 12> info_data = {
|
|
||||||
'P',
|
|
||||||
'B',
|
|
||||||
1, // magic, protocol version
|
|
||||||
signature()[0],
|
|
||||||
signature()[1],
|
|
||||||
signature()[2],
|
|
||||||
static_cast<std::uint8_t>(page),
|
|
||||||
base & 0xff,
|
|
||||||
base >> 8, // app flash ends here; loader base
|
|
||||||
avr::hw::db.mem.eeprom_size & 0xff,
|
|
||||||
avr::hw::db.mem.eeprom_size >> 8,
|
|
||||||
boot_section ? 0 : 1, // bit 0: host must patch the reset vector (no hardware boot section)
|
|
||||||
};
|
|
||||||
using info = avr::flash_table<info_data>;
|
|
||||||
|
|
||||||
// The serial link: the hardware USART where the chip has one, the polled
|
|
||||||
// software UART (no vector — the table belongs to the application) on PB0/PB1
|
|
||||||
// elsewhere. Both are class templates on the clock so only the selected
|
|
||||||
// backend is ever instantiated. pending() is the cheap line test the
|
|
||||||
// activation window polls; rx() then picks the byte up.
|
|
||||||
template <avr::hertz_t C>
|
|
||||||
consteval std::int16_t rxc_field()
|
|
||||||
{
|
|
||||||
return avr::hw::db.field_index("UCSR0A", "RXC0");
|
|
||||||
}
|
|
||||||
|
|
||||||
template <avr::hertz_t C>
|
|
||||||
struct hardware_link {
|
|
||||||
using uart = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>;
|
|
||||||
|
|
||||||
static void init()
|
|
||||||
{
|
|
||||||
avr::init<uart>();
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool pending()
|
|
||||||
{
|
|
||||||
return avr::hw::field_impl<rxc_field<C>()>::test();
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::uint8_t rx()
|
|
||||||
{
|
|
||||||
return uart::read_blocking();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void tx(std::uint8_t byte)
|
|
||||||
{
|
|
||||||
uart::write(byte);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
template <avr::hertz_t C>
|
|
||||||
struct software_link {
|
|
||||||
using rx_t = avr::uart::software_rx_polled<C, avr::pb0, 57600_Bd>;
|
|
||||||
using tx_t = avr::uart::software_tx<C, avr::pb1, 57600_Bd>;
|
|
||||||
|
|
||||||
static void init()
|
|
||||||
{
|
|
||||||
avr::init<rx_t, tx_t>();
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool pending()
|
|
||||||
{
|
|
||||||
return !avr::io::input<avr::pb0>::read(); // a start bit has begun
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::uint8_t rx()
|
|
||||||
{
|
|
||||||
return rx_t::template read_blocking<off>();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void tx(std::uint8_t byte)
|
|
||||||
{
|
|
||||||
tx_t::template write<off>(byte);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
using link = std::conditional_t<avr::hw::db.has_reg("UDR0"), hardware_link<dev::clock>, software_link<dev::clock>>;
|
|
||||||
|
|
||||||
// The application's entry: the linker pins pureboot_app to 0x0000 on the
|
|
||||||
// mega (reset re-vectors here through BOOTRST, so address 0 stays the
|
|
||||||
// application's own vector) and to the trampoline word at base - 2 on the
|
|
||||||
// tinies (--defsym in CMakeLists.txt).
|
|
||||||
extern "C" [[noreturn]] void pureboot_app();
|
|
||||||
|
|
||||||
[[noreturn]] void run_app()
|
|
||||||
{
|
|
||||||
pureboot_app();
|
|
||||||
}
|
|
||||||
|
|
||||||
// One activation tick is 65536 pending() polls — a pin (or flag) test plus a
|
|
||||||
// 16-bit countdown, about 8 cycles. Whole-second precision is all the
|
|
||||||
// timeout cell promises; the seconds count stays a loop bound (a runtime
|
|
||||||
// multiply would drag libgcc's __mulhi3 into the MUL-less tinies).
|
|
||||||
consteval std::uint16_t ticks_per_second()
|
|
||||||
{
|
|
||||||
return static_cast<std::uint16_t>(dev::clock.hz / (65536ull * 8u));
|
|
||||||
}
|
|
||||||
static_assert(ticks_per_second() >= 1);
|
|
||||||
|
|
||||||
bool pending_before(std::uint8_t seconds)
|
|
||||||
{
|
|
||||||
do {
|
|
||||||
std::uint16_t ticks = ticks_per_second();
|
|
||||||
do {
|
|
||||||
std::uint16_t spins = 0; // wraps first, so 65536 polls per tick
|
|
||||||
do {
|
|
||||||
if (link::pending())
|
|
||||||
return true;
|
|
||||||
} while (--spins);
|
|
||||||
} while (--ticks);
|
|
||||||
} while (--seconds);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A knock byte under the activation deadline: an idle line means no host is
|
|
||||||
// there, and the application runs.
|
|
||||||
std::uint8_t rx_deadline(std::uint8_t seconds)
|
|
||||||
{
|
|
||||||
if (!pending_before(seconds))
|
|
||||||
run_app();
|
|
||||||
return link::rx();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::uint16_t rx16()
|
|
||||||
{
|
|
||||||
std::uint8_t low = link::rx();
|
|
||||||
return static_cast<std::uint16_t>(low | (link::rx() << 8));
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::uint8_t *flash_ptr(std::uint16_t address)
|
|
||||||
{
|
|
||||||
return reinterpret_cast<const std::uint8_t *>(address);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The streamers take the count in the wire's 8-bit form: 0 means 256.
|
|
||||||
void send_flash(std::uint16_t address, std::uint8_t count)
|
|
||||||
{
|
|
||||||
do
|
|
||||||
link::tx(avr::flash_load(flash_ptr(address++)));
|
|
||||||
while (--count);
|
|
||||||
}
|
|
||||||
|
|
||||||
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
|
||||||
{
|
|
||||||
do
|
|
||||||
link::tx(ee::read(address++));
|
|
||||||
while (--count);
|
|
||||||
}
|
|
||||||
|
|
||||||
// EEPROM write, host-paced: each ack goes out once the byte's write has
|
|
||||||
// begun, so the next byte arrives while it completes and the following
|
|
||||||
// write's own ready-wait sees an idle line. Nothing is ever missed, on
|
|
||||||
// either serial backend, without a buffer.
|
|
||||||
void store_eeprom(std::uint16_t address, std::uint8_t count)
|
|
||||||
{
|
|
||||||
do {
|
|
||||||
ee::write<off>(address++, link::rx());
|
|
||||||
link::tx(ack);
|
|
||||||
} while (--count);
|
|
||||||
}
|
|
||||||
|
|
||||||
// One flash page: stream the bytes into the SPM buffer as little-endian
|
|
||||||
// words, then erase and program. Addresses in the loader's own 512 bytes
|
|
||||||
// are drained but never programmed — a broken host cannot brick the chip.
|
|
||||||
// On the mega the RWW section is re-enabled so reads work immediately.
|
|
||||||
void program_flash(std::uint16_t address)
|
|
||||||
{
|
|
||||||
for (std::uint16_t i = 0; i < page; i += 2) {
|
|
||||||
std::uint8_t low = link::rx();
|
|
||||||
std::uint8_t high = link::rx();
|
|
||||||
spm::fill<off>(address + i, static_cast<std::uint16_t>(low | (high << 8)));
|
|
||||||
}
|
|
||||||
if (address < base) {
|
|
||||||
spm::erase_page<off>(address);
|
|
||||||
spm::wait();
|
|
||||||
spm::write_page<off>(address);
|
|
||||||
spm::wait();
|
|
||||||
if constexpr (boot_section)
|
|
||||||
spm::rww_enable<off>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The four fuse/lock bytes in the hardware's own Z order: low, lock,
|
|
||||||
// extended, high. Writing fuses is not a thing self-programming can do on
|
|
||||||
// AVR — SPM reaches flash (and boot lock bits) only.
|
|
||||||
void send_fuses()
|
|
||||||
{
|
|
||||||
for (std::uint8_t which = 0; which < 4; ++which)
|
|
||||||
link::tx(spm::read_fuse<off>(static_cast<spm::fuse>(which)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[[noreturn]] void run()
|
|
||||||
{
|
|
||||||
// A watchdog reset belongs to the application (whose watchdog stays
|
|
||||||
// forced on until it clears WDRF) — no activation window in its way.
|
|
||||||
if (avr::hw::mcusr::wdrf.test())
|
|
||||||
run_app();
|
|
||||||
|
|
||||||
link::init();
|
|
||||||
|
|
||||||
std::uint8_t seconds = ee::read(timeout_cell);
|
|
||||||
if (seconds == 0 || seconds == 0xff)
|
|
||||||
seconds = default_seconds;
|
|
||||||
|
|
||||||
// The knock: 'p' then 'b', each under a fresh window; any other byte is
|
|
||||||
// line noise and waits again. Falling out of a window runs the app.
|
|
||||||
while (rx_deadline(seconds) != 'p' || rx_deadline(seconds) != 'b') {
|
|
||||||
}
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
// No prompt while an EEPROM write runs: a pending write blocks SPM
|
|
||||||
// and fuse reads (§26.2.1), and the ack tells the host all is done.
|
|
||||||
ee::wait();
|
|
||||||
link::tx(ack);
|
|
||||||
switch (link::rx()) {
|
|
||||||
case 'b': // info block
|
|
||||||
send_flash(reinterpret_cast<std::uint16_t>(info::storage.data()), info::size());
|
|
||||||
break;
|
|
||||||
case 'R': { // read flash: addr16, n8 (0 = 256)
|
|
||||||
std::uint16_t address = rx16();
|
|
||||||
send_flash(address, link::rx());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'W': // program one flash page: addr16, page bytes
|
|
||||||
program_flash(rx16());
|
|
||||||
break;
|
|
||||||
case 'r': { // read EEPROM: addr16, n8
|
|
||||||
std::uint16_t address = rx16();
|
|
||||||
send_eeprom(address, link::rx());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'w': { // write EEPROM: addr16, n8, then n bytes each acked
|
|
||||||
std::uint16_t address = rx16();
|
|
||||||
store_eeprom(address, link::rx());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'F': // fuse and lock bytes
|
|
||||||
send_fuses();
|
|
||||||
break;
|
|
||||||
case 'G': // hand over to the application
|
|
||||||
link::tx(ack);
|
|
||||||
run_app();
|
|
||||||
default: // unknown bytes are ignored; the loop re-acks
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
} // namespace pureboot
|
|
||||||
|
|
||||||
template struct avr::startup::entry<pureboot::run>;
|
|
||||||
@@ -1,450 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""pureboot host tool — the smart half of the pureboot protocol (README.md).
|
|
||||||
|
|
||||||
The device exposes primitives; this tool composes them: image loading (raw
|
|
||||||
binary or Intel HEX), flash programming with read-back verification, erase as
|
|
||||||
writing 0xff, EEPROM programming, fuse and info readout, activation-timeout
|
|
||||||
configuration, and — on chips without a hardware boot section — the
|
|
||||||
reset-vector surgery that re-homes the application's entry through the
|
|
||||||
trampoline word below the loader, writing page 0 last so an interrupted
|
|
||||||
flash still falls through to the loader.
|
|
||||||
|
|
||||||
Python standard library only; the serial port is driven with termios, so any
|
|
||||||
tty works — a USB adapter as well as a simavr pty.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import select
|
|
||||||
import sys
|
|
||||||
import termios
|
|
||||||
import time
|
|
||||||
|
|
||||||
PROMPT = b"+"
|
|
||||||
PROTOCOL_VERSION = 1
|
|
||||||
|
|
||||||
|
|
||||||
class Error(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- serial ---
|
|
||||||
|
|
||||||
|
|
||||||
class Port:
|
|
||||||
"""A raw serial port with deadline-based reads."""
|
|
||||||
|
|
||||||
def __init__(self, path, baud):
|
|
||||||
self.fd = os.open(path, os.O_RDWR | os.O_NOCTTY)
|
|
||||||
attrs = termios.tcgetattr(self.fd)
|
|
||||||
attrs[0] = 0 # iflag
|
|
||||||
attrs[1] = 0 # oflag
|
|
||||||
attrs[2] = termios.CREAD | termios.CLOCAL | termios.CS8 # cflag
|
|
||||||
attrs[3] = 0 # lflag
|
|
||||||
try:
|
|
||||||
speed = getattr(termios, f"B{baud}")
|
|
||||||
except AttributeError:
|
|
||||||
raise Error(f"unsupported baud rate {baud}") from None
|
|
||||||
attrs[4] = attrs[5] = speed
|
|
||||||
attrs[6][termios.VMIN] = 0
|
|
||||||
attrs[6][termios.VTIME] = 0
|
|
||||||
termios.tcsetattr(self.fd, termios.TCSANOW, attrs)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
os.close(self.fd)
|
|
||||||
|
|
||||||
def write(self, data):
|
|
||||||
os.write(self.fd, data)
|
|
||||||
|
|
||||||
def flush_input(self):
|
|
||||||
termios.tcflush(self.fd, termios.TCIFLUSH)
|
|
||||||
|
|
||||||
def read_available(self, wait):
|
|
||||||
"""Everything that arrives within `wait` seconds of quiet start."""
|
|
||||||
ready, _, _ = select.select([self.fd], [], [], wait)
|
|
||||||
return os.read(self.fd, 4096) if ready else b""
|
|
||||||
|
|
||||||
def read_exact(self, count, timeout):
|
|
||||||
data = b""
|
|
||||||
deadline = time.monotonic() + timeout
|
|
||||||
while len(data) < count:
|
|
||||||
remaining = deadline - time.monotonic()
|
|
||||||
if remaining <= 0:
|
|
||||||
raise Error(f"timeout: got {len(data)} of {count} bytes")
|
|
||||||
ready, _, _ = select.select([self.fd], [], [], remaining)
|
|
||||||
if ready:
|
|
||||||
data += os.read(self.fd, count - len(data))
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------- protocol ---
|
|
||||||
|
|
||||||
|
|
||||||
class Info:
|
|
||||||
"""The 12-byte info block."""
|
|
||||||
|
|
||||||
def __init__(self, raw):
|
|
||||||
if len(raw) != 12 or raw[0:2] != b"PB":
|
|
||||||
raise Error(f"bad info block: {raw.hex()}")
|
|
||||||
if raw[2] != PROTOCOL_VERSION:
|
|
||||||
raise Error(f"protocol version {raw[2]}, tool speaks {PROTOCOL_VERSION}")
|
|
||||||
self.signature = raw[3:6]
|
|
||||||
self.page = raw[6]
|
|
||||||
self.base = raw[7] | (raw[8] << 8)
|
|
||||||
self.eeprom_size = raw[9] | (raw[10] << 8)
|
|
||||||
self.patch_vector = bool(raw[11] & 1)
|
|
||||||
self.flash_size = self.base + 512
|
|
||||||
|
|
||||||
def describe(self):
|
|
||||||
sig = " ".join(f"{b:02x}" for b in self.signature)
|
|
||||||
vector = "host-patched reset vector" if self.patch_vector else "hardware boot section"
|
|
||||||
return (
|
|
||||||
f"signature {sig}, page {self.page} B, "
|
|
||||||
f"app flash {self.base} B (loader at {self.base:#06x}), "
|
|
||||||
f"EEPROM {self.eeprom_size} B, {vector}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Loader:
|
|
||||||
"""A pureboot session. Between commands the loader has prompted `+` and
|
|
||||||
awaits a command byte; every method restores that invariant."""
|
|
||||||
|
|
||||||
def __init__(self, port):
|
|
||||||
self.port = port
|
|
||||||
self.info = None
|
|
||||||
|
|
||||||
def connect(self, wait):
|
|
||||||
"""Knock until the activation window answers, then read the info
|
|
||||||
block. Also converges when the loader already sits in its command
|
|
||||||
loop: the knock bytes are ignored-or-executed there, and the drain
|
|
||||||
absorbs whatever they produced."""
|
|
||||||
self.port.flush_input()
|
|
||||||
deadline = time.monotonic() + wait
|
|
||||||
while True:
|
|
||||||
self.port.write(b"pb")
|
|
||||||
if PROMPT in self.port.read_available(0.4):
|
|
||||||
break
|
|
||||||
if time.monotonic() > deadline:
|
|
||||||
raise Error("no answer — reset the device within its activation window")
|
|
||||||
while self.port.read_available(0.3):
|
|
||||||
pass
|
|
||||||
self.port.write(b"b")
|
|
||||||
self.info = Info(self.port.read_exact(12, 2.0))
|
|
||||||
self._expect_prompt()
|
|
||||||
return self.info
|
|
||||||
|
|
||||||
def _expect_prompt(self, timeout=2.0):
|
|
||||||
byte = self.port.read_exact(1, timeout)
|
|
||||||
if byte != PROMPT:
|
|
||||||
raise Error(f"expected prompt, got {byte.hex()}")
|
|
||||||
|
|
||||||
def _command(self, tx, reply_len=0, timeout=2.0):
|
|
||||||
self.port.write(tx)
|
|
||||||
reply = self.port.read_exact(reply_len, timeout) if reply_len else b""
|
|
||||||
self._expect_prompt(timeout)
|
|
||||||
return reply
|
|
||||||
|
|
||||||
def _stream_read(self, command, address, count):
|
|
||||||
data = b""
|
|
||||||
while count:
|
|
||||||
chunk = min(count, 256)
|
|
||||||
head = bytes((ord(command), address & 0xFF, address >> 8, chunk & 0xFF))
|
|
||||||
data += self._command(head, chunk, 5.0)
|
|
||||||
address += chunk
|
|
||||||
count -= chunk
|
|
||||||
return data
|
|
||||||
|
|
||||||
def read_flash(self, address, count):
|
|
||||||
return self._stream_read("R", address, count)
|
|
||||||
|
|
||||||
def read_eeprom(self, address, count):
|
|
||||||
return self._stream_read("r", address, count)
|
|
||||||
|
|
||||||
def write_page(self, address, data):
|
|
||||||
assert len(data) == self.info.page and address % self.info.page == 0
|
|
||||||
head = bytes((ord("W"), address & 0xFF, address >> 8))
|
|
||||||
self._command(head + data, 0, 2.0)
|
|
||||||
|
|
||||||
def write_eeprom(self, address, data):
|
|
||||||
offset = 0
|
|
||||||
while offset < len(data):
|
|
||||||
chunk = data[offset : offset + 256]
|
|
||||||
head = bytes((ord("w"), address & 0xFF, address >> 8, len(chunk) & 0xFF))
|
|
||||||
self.port.write(head)
|
|
||||||
for byte in chunk:
|
|
||||||
self.port.write(bytes((byte,)))
|
|
||||||
self._expect_prompt() # per-byte ack: the write has begun
|
|
||||||
self._expect_prompt() # the next command prompt
|
|
||||||
address += len(chunk)
|
|
||||||
offset += len(chunk)
|
|
||||||
|
|
||||||
def read_fuses(self):
|
|
||||||
return self._command(b"F", 4, 2.0)
|
|
||||||
|
|
||||||
def run_application(self):
|
|
||||||
self.port.write(b"G")
|
|
||||||
self._expect_prompt()
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- images ---
|
|
||||||
|
|
||||||
|
|
||||||
def load_image(path):
|
|
||||||
"""Raw binary, or Intel HEX by extension (.hex/.ihx/.ihex)."""
|
|
||||||
data = open(path, "rb").read()
|
|
||||||
if not path.lower().endswith((".hex", ".ihx", ".ihex")):
|
|
||||||
if not data:
|
|
||||||
raise Error(f"{path}: empty image")
|
|
||||||
return data
|
|
||||||
memory = {}
|
|
||||||
for number, line in enumerate(data.decode("ascii", "replace").splitlines(), 1):
|
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
if not line.startswith(":"):
|
|
||||||
raise Error(f"{path}:{number}: not an Intel HEX record")
|
|
||||||
record = bytes.fromhex(line[1:])
|
|
||||||
if sum(record) & 0xFF:
|
|
||||||
raise Error(f"{path}:{number}: checksum mismatch")
|
|
||||||
count, address, kind = record[0], (record[1] << 8) | record[2], record[3]
|
|
||||||
payload = record[4 : 4 + count]
|
|
||||||
if kind == 0:
|
|
||||||
for i, byte in enumerate(payload):
|
|
||||||
memory[address + i] = byte
|
|
||||||
elif kind == 1:
|
|
||||||
break
|
|
||||||
elif kind in (2, 4) and not any(payload):
|
|
||||||
continue # a zero base extends nothing
|
|
||||||
elif kind in (3, 5):
|
|
||||||
continue # start address: irrelevant, reset is the entry
|
|
||||||
else:
|
|
||||||
raise Error(f"{path}:{number}: record type {kind} reaches beyond the 16-bit space")
|
|
||||||
if not memory:
|
|
||||||
raise Error(f"{path}: empty image")
|
|
||||||
return bytes(memory.get(i, 0xFF) for i in range(max(memory) + 1))
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------- surgery ---
|
|
||||||
|
|
||||||
|
|
||||||
def rjmp_target(word_address, opcode, flash_words):
|
|
||||||
return (word_address + 1 + (opcode & 0x0FFF)) % flash_words
|
|
||||||
|
|
||||||
|
|
||||||
def rjmp_to(word_address, destination, flash_words):
|
|
||||||
return 0xC000 | ((destination - word_address - 1) % flash_words % 0x1000)
|
|
||||||
|
|
||||||
|
|
||||||
def plan_flash(image, info):
|
|
||||||
"""The pages to program, as {page_address: bytes}, already carrying the
|
|
||||||
reset-vector surgery where the chip needs it. Page 0 must go last —
|
|
||||||
callers get it separated."""
|
|
||||||
page = info.page
|
|
||||||
limit = info.base - (2 if info.patch_vector else 0)
|
|
||||||
if len(image) > limit:
|
|
||||||
raise Error(f"image is {len(image)} B, application flash ends at {limit}")
|
|
||||||
final = bytearray(image) + bytearray([0xFF] * (-len(image) % page))
|
|
||||||
|
|
||||||
if info.patch_vector:
|
|
||||||
flash_words = info.flash_size // 2
|
|
||||||
word0 = final[0] | (final[1] << 8)
|
|
||||||
if word0 & 0xF000 != 0xC000:
|
|
||||||
raise Error(
|
|
||||||
"the image's reset vector is not an rjmp — pureboot's vector "
|
|
||||||
"surgery cannot re-home it (crt-less entry at address 0?)"
|
|
||||||
)
|
|
||||||
entry = rjmp_target(0, word0, flash_words)
|
|
||||||
if entry >= info.base // 2:
|
|
||||||
raise Error(
|
|
||||||
"the image's reset vector already targets the loader — this "
|
|
||||||
"is a read-back of a patched image; flash the original"
|
|
||||||
)
|
|
||||||
trampoline_word = (info.base - 2) // 2
|
|
||||||
patch = rjmp_to(0, info.base // 2, flash_words)
|
|
||||||
final[0], final[1] = patch & 0xFF, patch >> 8
|
|
||||||
trampoline_page = info.base - page
|
|
||||||
if len(final) < trampoline_page + page:
|
|
||||||
final += bytearray([0xFF] * (trampoline_page + page - len(final)))
|
|
||||||
jump = rjmp_to(trampoline_word, entry, flash_words)
|
|
||||||
final[info.base - 2], final[info.base - 1] = jump & 0xFF, jump >> 8
|
|
||||||
|
|
||||||
pages = {a: bytes(final[a : a + page]) for a in range(0, len(final), page)}
|
|
||||||
return pages
|
|
||||||
|
|
||||||
|
|
||||||
def covered(pages, skip_blank):
|
|
||||||
"""Pages in programming order: ascending, page 0 last; optionally
|
|
||||||
dropping all-0xff pages (sound only over erased flash) — never the
|
|
||||||
load-bearing page 0."""
|
|
||||||
rest = [a for a in sorted(pages) if a != 0]
|
|
||||||
if skip_blank:
|
|
||||||
rest = [a for a in rest if pages[a].count(0xFF) != len(pages[a])]
|
|
||||||
return rest + [0]
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------ operations ---
|
|
||||||
|
|
||||||
|
|
||||||
def op_erase_flash(loader):
|
|
||||||
"""0xff over the whole application area. Order does not matter here —
|
|
||||||
every target byte is the same value — and a blank word 0 still falls
|
|
||||||
through to the loader, so an interruption is harmless."""
|
|
||||||
blank = bytes([0xFF] * loader.info.page)
|
|
||||||
for address in range(0, loader.info.base, loader.info.page):
|
|
||||||
loader.write_page(address, blank)
|
|
||||||
print(f"erase: {loader.info.base // loader.info.page} pages")
|
|
||||||
|
|
||||||
|
|
||||||
def op_erase_eeprom(loader):
|
|
||||||
loader.write_eeprom(0, bytes([0xFF] * loader.info.eeprom_size))
|
|
||||||
print(f"erase: {loader.info.eeprom_size} B of EEPROM")
|
|
||||||
|
|
||||||
|
|
||||||
def op_flash(loader, path, erase, verify):
|
|
||||||
image = load_image(path)
|
|
||||||
pages = plan_flash(image, loader.info)
|
|
||||||
if erase:
|
|
||||||
op_erase_flash(loader)
|
|
||||||
order = covered(pages, skip_blank=erase)
|
|
||||||
for address in order:
|
|
||||||
loader.write_page(address, pages[address])
|
|
||||||
print(f"flash: {path}: {len(order)} pages")
|
|
||||||
if verify:
|
|
||||||
verify_pages(loader, pages)
|
|
||||||
|
|
||||||
|
|
||||||
def verify_pages(loader, pages):
|
|
||||||
for address in sorted(pages):
|
|
||||||
got = loader.read_flash(address, loader.info.page)
|
|
||||||
if got != pages[address]:
|
|
||||||
first = next(i for i in range(len(got)) if got[i] != pages[address][i])
|
|
||||||
raise Error(
|
|
||||||
f"verify failed at {address + first:#06x}: "
|
|
||||||
f"wrote {pages[address][first]:02x}, read {got[first]:02x}"
|
|
||||||
)
|
|
||||||
print(f"verify: {len(pages)} pages ok")
|
|
||||||
|
|
||||||
|
|
||||||
def op_verify_flash(loader, path):
|
|
||||||
verify_pages(loader, plan_flash(load_image(path), loader.info))
|
|
||||||
|
|
||||||
|
|
||||||
def op_read_flash(loader, path):
|
|
||||||
data = loader.read_flash(0, loader.info.base)
|
|
||||||
open(path, "wb").write(data)
|
|
||||||
print(f"read flash: {len(data)} B -> {path}")
|
|
||||||
|
|
||||||
|
|
||||||
def op_eeprom(loader, path, erase, verify):
|
|
||||||
image = load_image(path)
|
|
||||||
if len(image) > loader.info.eeprom_size:
|
|
||||||
raise Error(f"EEPROM image is {len(image)} B, device has {loader.info.eeprom_size}")
|
|
||||||
if erase:
|
|
||||||
op_erase_eeprom(loader)
|
|
||||||
loader.write_eeprom(0, image)
|
|
||||||
print(f"eeprom: {path}: {len(image)} B")
|
|
||||||
if verify:
|
|
||||||
got = loader.read_eeprom(0, len(image))
|
|
||||||
if got != image:
|
|
||||||
first = next(i for i in range(len(got)) if got[i] != image[i])
|
|
||||||
raise Error(f"verify failed at EEPROM {first:#06x}: wrote {image[first]:02x}, read {got[first]:02x}")
|
|
||||||
print(f"verify: {len(image)} B ok")
|
|
||||||
|
|
||||||
|
|
||||||
def op_verify_eeprom(loader, path):
|
|
||||||
image = load_image(path)
|
|
||||||
got = loader.read_eeprom(0, len(image))
|
|
||||||
if got != image:
|
|
||||||
first = next(i for i in range(len(got)) if got[i] != image[i])
|
|
||||||
raise Error(f"verify failed at EEPROM {first:#06x}: expected {image[first]:02x}, read {got[first]:02x}")
|
|
||||||
print(f"verify: {len(image)} B of EEPROM ok")
|
|
||||||
|
|
||||||
|
|
||||||
def op_read_eeprom(loader, path):
|
|
||||||
data = loader.read_eeprom(0, loader.info.eeprom_size)
|
|
||||||
open(path, "wb").write(data)
|
|
||||||
print(f"read EEPROM: {len(data)} B -> {path}")
|
|
||||||
|
|
||||||
|
|
||||||
def op_timeout(loader, seconds):
|
|
||||||
loader.write_eeprom(loader.info.eeprom_size - 1, bytes((seconds,)))
|
|
||||||
label = f"{seconds} s" if seconds else "the device default"
|
|
||||||
print(f"activation timeout: {label}")
|
|
||||||
|
|
||||||
|
|
||||||
def op_fuses(loader):
|
|
||||||
low, lock, extended, high = loader.read_fuses()
|
|
||||||
print(f"fuses: low {low:02x} high {high:02x} extended {extended:02x} lock {lock:02x}")
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------- cli ---
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="pureboot host tool", epilog="operations run in the order listed above"
|
|
||||||
)
|
|
||||||
parser.add_argument("--port", required=True, help="serial device (or simavr pty)")
|
|
||||||
parser.add_argument("--baud", type=int, default=115200, help="115200 mega, 57600 tinies")
|
|
||||||
parser.add_argument("--wait", type=float, default=30.0, help="seconds to keep knocking")
|
|
||||||
parser.add_argument("--info", action="store_true", help="print the device info block")
|
|
||||||
parser.add_argument("--fuses", action="store_true", help="read the fuse and lock bytes")
|
|
||||||
parser.add_argument("--erase-flash", action="store_true", help="0xff over the application flash")
|
|
||||||
parser.add_argument("--flash", metavar="FILE", help="program an application (bin or ihex)")
|
|
||||||
parser.add_argument("--no-verify", action="store_true", help="skip read-back after writes")
|
|
||||||
parser.add_argument("--read-flash", metavar="FILE", help="dump the application flash")
|
|
||||||
parser.add_argument("--verify-flash", metavar="FILE", help="compare flash against an image")
|
|
||||||
parser.add_argument("--erase-eeprom", action="store_true", help="0xff over the EEPROM")
|
|
||||||
parser.add_argument("--eeprom", metavar="FILE", help="program the EEPROM (bin or ihex)")
|
|
||||||
parser.add_argument("--read-eeprom", metavar="FILE", help="dump the EEPROM")
|
|
||||||
parser.add_argument("--verify-eeprom", metavar="FILE", help="compare EEPROM against an image")
|
|
||||||
parser.add_argument("--timeout", type=int, metavar="S", help="activation window, 1-254 s (0: default)")
|
|
||||||
parser.add_argument("--stay", action="store_true", help="leave the loader in its session")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.timeout is not None and not 0 <= args.timeout <= 254:
|
|
||||||
parser.error("--timeout must be 0..254 (255 is the erased cell)")
|
|
||||||
|
|
||||||
port = Port(args.port, args.baud)
|
|
||||||
try:
|
|
||||||
loader = Loader(port)
|
|
||||||
info = loader.connect(args.wait)
|
|
||||||
if args.info:
|
|
||||||
print(f"device: {info.describe()}")
|
|
||||||
if args.fuses:
|
|
||||||
op_fuses(loader)
|
|
||||||
if args.flash:
|
|
||||||
op_flash(loader, args.flash, args.erase_flash, not args.no_verify)
|
|
||||||
elif args.erase_flash:
|
|
||||||
op_erase_flash(loader)
|
|
||||||
if args.read_flash:
|
|
||||||
op_read_flash(loader, args.read_flash)
|
|
||||||
if args.verify_flash:
|
|
||||||
op_verify_flash(loader, args.verify_flash)
|
|
||||||
if args.eeprom:
|
|
||||||
op_eeprom(loader, args.eeprom, args.erase_eeprom, not args.no_verify)
|
|
||||||
elif args.erase_eeprom:
|
|
||||||
op_erase_eeprom(loader)
|
|
||||||
if args.read_eeprom:
|
|
||||||
op_read_eeprom(loader, args.read_eeprom)
|
|
||||||
if args.verify_eeprom:
|
|
||||||
op_verify_eeprom(loader, args.verify_eeprom)
|
|
||||||
if args.timeout is not None:
|
|
||||||
op_timeout(loader, args.timeout)
|
|
||||||
if args.stay:
|
|
||||||
print("loader stays in its session (reset to leave)")
|
|
||||||
else:
|
|
||||||
loader.run_application()
|
|
||||||
print("application running")
|
|
||||||
finally:
|
|
||||||
port.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
try:
|
|
||||||
main()
|
|
||||||
except Error as error:
|
|
||||||
print(f"error: {error}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
sys.exit(130)
|
|
||||||
@@ -4,6 +4,9 @@ if(NOT _res EQUAL 0)
|
|||||||
endif()
|
endif()
|
||||||
# avr-size line 2 is "<text> <data> <bss> <dec> <hex> <file>".
|
# avr-size line 2 is "<text> <data> <bss> <dec> <hex> <file>".
|
||||||
string(REGEX MATCH "\n[ \t]*([0-9]+)" _m "${_out}")
|
string(REGEX MATCH "\n[ \t]*([0-9]+)" _m "${_out}")
|
||||||
|
if(NOT _m)
|
||||||
|
message(FATAL_ERROR "could not read a .text size out of ${SIZE_TOOL}'s output for ${ELF}:\n${_out}")
|
||||||
|
endif()
|
||||||
set(_text ${CMAKE_MATCH_1})
|
set(_text ${CMAKE_MATCH_1})
|
||||||
if(_text GREATER LIMIT)
|
if(_text GREATER LIMIT)
|
||||||
message(FATAL_ERROR ".text is ${_text} bytes, over the ${LIMIT}-byte boot section")
|
message(FATAL_ERROR ".text is ${_text} bytes, over the ${LIMIT}-byte boot section")
|
||||||
|
|||||||
@@ -7,103 +7,122 @@
|
|||||||
// SPM genuinely writes avr->flash on the mega cores, so on exit (or SIGTERM)
|
// 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
|
// we dump the flash image to a file for a ground-truth cross-check against
|
||||||
// what the client read back through the bootloader.
|
// what the client read back through the bootloader.
|
||||||
#include <signal.h>
|
#include <array>
|
||||||
#include <stdint.h>
|
#include <csignal>
|
||||||
#include <stdio.h>
|
#include <cstdint>
|
||||||
#include <stdlib.h>
|
#include <cstdio>
|
||||||
#include <string.h>
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <print>
|
||||||
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
// The parts headers (uart_pty.h) carry no C++ linkage guards of their own,
|
||||||
|
// unlike simavr's core headers - the block covers both harmlessly.
|
||||||
|
extern "C" {
|
||||||
#include "avr_uart.h"
|
#include "avr_uart.h"
|
||||||
#include "sim_avr.h"
|
#include "sim_avr.h"
|
||||||
#include "sim_elf.h"
|
#include "sim_elf.h"
|
||||||
#include "uart_pty.h"
|
#include "uart_pty.h"
|
||||||
|
}
|
||||||
|
|
||||||
static avr_t *avr;
|
namespace {
|
||||||
static uart_pty_t uart_pty;
|
|
||||||
static const char *dump_path;
|
|
||||||
|
|
||||||
static void finish(int sig)
|
avr_t *avr;
|
||||||
|
uart_pty_t uart_pty;
|
||||||
|
const char *dump_path;
|
||||||
|
|
||||||
|
[[noreturn]] void finish(int)
|
||||||
{
|
{
|
||||||
(void)sig;
|
|
||||||
if (dump_path) {
|
if (dump_path) {
|
||||||
FILE *f = fopen(dump_path, "wb");
|
std::FILE *f = std::fopen(dump_path, "wb");
|
||||||
if (f) {
|
if (f) {
|
||||||
fwrite(avr->flash, 1, avr->flashend + 1, f);
|
std::fwrite(avr->flash, 1, avr->flashend + 1, f);
|
||||||
fclose(f);
|
std::fclose(f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uart_pty_stop(&uart_pty);
|
uart_pty_stop(&uart_pty);
|
||||||
_exit(0);
|
_exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
if (argc < 3) {
|
if (argc < 3) {
|
||||||
fprintf(stderr, "usage: %s <tsb.elf> <boot_base_hex> [flash_dump.bin]\n", argv[0]);
|
std::println(stderr, "usage: {} <tsb.elf> <boot_base_hex> [flash_dump.bin]", argv[0]);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
uint32_t boot_base = (uint32_t)strtoul(argv[2], NULL, 0);
|
auto boot_base = static_cast<std::uint32_t>(std::strtoul(argv[2], nullptr, 0));
|
||||||
dump_path = argc >= 4 ? argv[3] : NULL;
|
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) {
|
||||||
fprintf(stderr, "device: no ATmega328P core\n");
|
std::println(stderr, "device: no ATmega328P core");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
avr_init(avr);
|
avr_init(avr);
|
||||||
avr->frequency = 16000000;
|
avr->frequency = 16000000;
|
||||||
// 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.
|
||||||
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; place it at the boot
|
||||||
// section base ourselves and enter there (BOOTRST is not modelled).
|
// section base ourselves and enter there (BOOTRST is not modelled).
|
||||||
elf_firmware_t fw = {0};
|
elf_firmware_t fw{};
|
||||||
if (elf_read_firmware(argv[1], &fw) != 0) {
|
if (elf_read_firmware(argv[1], &fw) != 0) {
|
||||||
fprintf(stderr, "device: cannot read %s\n", argv[1]);
|
std::println(stderr, "device: cannot read {}", argv[1]);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
memcpy(avr->flash + boot_base, fw.flash, fw.flashsize);
|
// An image that runs past flash end cannot execute on hardware, and a
|
||||||
|
// naive copy of it would smash the heap beyond avr->flash - after which
|
||||||
|
// the simulation misbehaves in ways that point everywhere but here.
|
||||||
|
// Refuse it loudly instead.
|
||||||
|
if (boot_base + fw.flashsize > avr->flashend + 1) {
|
||||||
|
std::println(stderr, "device: {} B at {:#x} runs past flash end {:#x} - image does not fit its slot",
|
||||||
|
fw.flashsize, boot_base, avr->flashend);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::memcpy(avr->flash + boot_base, fw.flash, fw.flashsize);
|
||||||
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 (one page below the boot section) with a
|
||||||
// hex byte string, so the password gate and emergency erase can be tested.
|
// hex byte string, so the password 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 = getenv("TSB_CONFIG");
|
const char *cfg = std::getenv("TSB_CONFIG");
|
||||||
if (cfg) {
|
if (cfg) {
|
||||||
uint32_t app_end = boot_base - 128; // config page sits directly below the boot code
|
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) {
|
||||||
char b[3] = {cfg[i], cfg[i + 1], 0};
|
const std::array pair{cfg[i], cfg[i + 1], '\0'};
|
||||||
avr->flash[app_end + i / 2] = (uint8_t)strtoul(b, NULL, 16);
|
avr->flash[app_end + i / 2] = static_cast<std::uint8_t>(std::strtoul(pair.data(), nullptr, 16));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// POLL_SLEEP makes simavr usleep(1) on every status-register read while the
|
// POLL_SLEEP makes simavr usleep(1) on every status-register read while the
|
||||||
// UART is idle — a host-CPU-saving hack that models no hardware and paces a
|
// UART is idle - a host-CPU-saving hack that models no hardware and paces a
|
||||||
// tight-polling loader (one that releases TX between bytes, as one-wire does)
|
// tight-polling loader (one that releases TX between bytes, as one-wire does)
|
||||||
// in real time, distorting protocol timing. Clear it so the loader runs at
|
// in real time, distorting protocol timing. Clear it so the loader runs at
|
||||||
// true cycle speed.
|
// true cycle speed.
|
||||||
uint32_t uflags = 0;
|
std::uint32_t uflags = 0;
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &uflags);
|
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &uflags);
|
||||||
uflags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
uflags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &uflags);
|
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &uflags);
|
||||||
|
|
||||||
uart_pty_init(avr, &uart_pty);
|
uart_pty_init(avr, &uart_pty);
|
||||||
uart_pty_connect(&uart_pty, '0');
|
uart_pty_connect(&uart_pty, '0');
|
||||||
printf("TSB_PTY %s\n", uart_pty.pty.slavename);
|
std::println("TSB_PTY {}", uart_pty.pty.slavename);
|
||||||
fflush(stdout);
|
std::fflush(stdout);
|
||||||
|
|
||||||
signal(SIGTERM, finish);
|
std::signal(SIGTERM, finish);
|
||||||
signal(SIGINT, finish);
|
std::signal(SIGINT, finish);
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
int state = avr_run(avr);
|
int state = avr_run(avr);
|
||||||
if (state == cpu_Done || state == cpu_Crashed)
|
if (state == cpu_Done || state == cpu_Crashed) {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finish(0);
|
finish(0);
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
// Test-fixture application for the pureboot protocol test: prints "APP" on
|
|
||||||
// the chip's serial link (the same link the loader uses) and idles — the
|
|
||||||
// proof that the loader's hand-over, and on the tinies the host's
|
|
||||||
// reset-vector surgery, actually launched it. Linked normally (crt, vectors
|
|
||||||
// at 0); on the tinies its reset vector is the rjmp the host re-homes.
|
|
||||||
#include <libavr/libavr.hpp>
|
|
||||||
|
|
||||||
using namespace avr::literals;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
consteval avr::hertz_t clock()
|
|
||||||
{
|
|
||||||
if (avr::hw::db.name == "ATtiny13A")
|
|
||||||
return 9.6_MHz;
|
|
||||||
if (avr::hw::db.name == "ATtiny85")
|
|
||||||
return 8_MHz;
|
|
||||||
return 16_MHz;
|
|
||||||
}
|
|
||||||
|
|
||||||
using dev = avr::device<{.clock = clock()}>;
|
|
||||||
|
|
||||||
template <avr::hertz_t C, bool Hardware = avr::hw::db.has_reg("UDR0")>
|
|
||||||
struct link {
|
|
||||||
using tx_t = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>;
|
|
||||||
static void tx(char c)
|
|
||||||
{
|
|
||||||
tx_t::write(static_cast<std::uint8_t>(c));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
template <avr::hertz_t C>
|
|
||||||
struct link<C, false> {
|
|
||||||
using tx_t = avr::uart::software_tx<C, avr::pb1, 57600_Bd>;
|
|
||||||
static void tx(char c)
|
|
||||||
{
|
|
||||||
tx_t::write(static_cast<std::uint8_t>(c));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int main()
|
|
||||||
{
|
|
||||||
avr::init<typename link<dev::clock>::tx_t>();
|
|
||||||
link<dev::clock>::tx('A');
|
|
||||||
link<dev::clock>::tx('P');
|
|
||||||
link<dev::clock>::tx('P');
|
|
||||||
while (true) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
178
test/pbtest.py
178
test/pbtest.py
@@ -1,178 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""End-to-end pureboot protocol test: spawn the simavr device, then drive it
|
|
||||||
with the real host tool (pureboot.py, as a subprocess over the device's pty)
|
|
||||||
through flash + EEPROM + timeout + fuse + hand-over scenarios, and cross-check
|
|
||||||
the tool's view against the simulator's ground-truth memory dumps.
|
|
||||||
|
|
||||||
Usage: pbtest.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
|
||||||
<baud> <eeprom_size> <app_bin> <tool_py> <workdir>
|
|
||||||
Exits 0 if every scenario passes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import signal
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
def fail(message):
|
|
||||||
print(f"FAIL: {message}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def rjmp_decode(word, at, flash_words):
|
|
||||||
"""Where an rjmp word at word-address `at` lands — deliberately written
|
|
||||||
against the instruction-set definition (12-bit signed offset), not with
|
|
||||||
the host tool's encoder, so an encoding bug cannot verify itself."""
|
|
||||||
if word & 0xF000 != 0xC000:
|
|
||||||
fail(f"word at {at * 2:#06x} is {word:#06x}, not an rjmp")
|
|
||||||
offset = word & 0x0FFF
|
|
||||||
if offset >= 0x800:
|
|
||||||
offset -= 0x1000
|
|
||||||
return (at + 1 + offset) % flash_words
|
|
||||||
|
|
||||||
|
|
||||||
class Device:
|
|
||||||
def __init__(self, binary, elf, mcu, hz, base, page, baud, dump):
|
|
||||||
self.proc = subprocess.Popen(
|
|
||||||
[binary, elf, mcu, hz, base, str(page), str(baud), dump],
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
self.dump = dump
|
|
||||||
self.pty = None
|
|
||||||
deadline = time.time() + 5
|
|
||||||
while time.time() < deadline:
|
|
||||||
line = self.proc.stdout.readline()
|
|
||||||
if not line:
|
|
||||||
break
|
|
||||||
if line.startswith("PB_PTY"):
|
|
||||||
self.pty = line.split()[1]
|
|
||||||
break
|
|
||||||
if not self.pty:
|
|
||||||
self.stop()
|
|
||||||
raise RuntimeError("device did not report a pty")
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self.proc.terminate()
|
|
||||||
try:
|
|
||||||
self.proc.wait(timeout=3)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
self.proc.kill()
|
|
||||||
|
|
||||||
|
|
||||||
def run_tool(tool, pty, baud, *args):
|
|
||||||
result = subprocess.run(
|
|
||||||
[sys.executable, tool, "--port", pty, "--baud", str(baud), "--wait", "20", *args],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=120,
|
|
||||||
)
|
|
||||||
print(result.stdout, end="")
|
|
||||||
if result.returncode != 0:
|
|
||||||
fail(f"tool exited {result.returncode}: {result.stderr.strip()}")
|
|
||||||
return result.stdout
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
(device_bin, elf, mcu, hz, base_hex, page, baud, eeprom_size, app_bin, tool, workdir) = sys.argv[1:]
|
|
||||||
base, page, baud, eeprom_size = int(base_hex, 0), int(page), int(baud), int(eeprom_size)
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
|
||||||
import pureboot as pb
|
|
||||||
|
|
||||||
os.makedirs(workdir, exist_ok=True)
|
|
||||||
ee_image = bytes(range(0xA0, 0xB0))
|
|
||||||
ee_path = os.path.join(workdir, "ee.bin")
|
|
||||||
open(ee_path, "wb").write(ee_image)
|
|
||||||
dump = os.path.join(workdir, "flash_dump.bin")
|
|
||||||
read_flash = os.path.join(workdir, "readback_flash.bin")
|
|
||||||
read_eeprom = os.path.join(workdir, "readback_eeprom.bin")
|
|
||||||
|
|
||||||
# The geometry the host will discover, for computing the expected image.
|
|
||||||
info = pb.Info(
|
|
||||||
bytes([ord("P"), ord("B"), 1, 0, 0, 0, page])
|
|
||||||
+ bytes([base & 0xFF, base >> 8, eeprom_size & 0xFF, eeprom_size >> 8])
|
|
||||||
+ bytes([0 if mcu == "atmega328p" else 1])
|
|
||||||
)
|
|
||||||
|
|
||||||
device = Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump)
|
|
||||||
try:
|
|
||||||
# Session 1: knock from reset, identify, program everything, stay.
|
|
||||||
out = run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin,
|
|
||||||
"--eeprom", ee_path, "--timeout", "8", "--stay")
|
|
||||||
for needed in ("device: signature", "fuses:", "verify:", "activation timeout: 8 s", "stays"):
|
|
||||||
if needed not in out:
|
|
||||||
fail(f"session 1 output lacks {needed!r}")
|
|
||||||
|
|
||||||
# Session 2: reconnect into the live session, verify, dump, hand over
|
|
||||||
# is deferred — the pty must be reopened for the APP banner first.
|
|
||||||
out = run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path,
|
|
||||||
"--read-flash", read_flash, "--read-eeprom", read_eeprom, "--stay")
|
|
||||||
if out.count("verify:") != 2:
|
|
||||||
fail("session 2 did not verify both memories")
|
|
||||||
|
|
||||||
eeprom_back = open(read_eeprom, "rb").read()
|
|
||||||
if eeprom_back[: len(ee_image)] != ee_image:
|
|
||||||
fail("EEPROM read-back mismatch")
|
|
||||||
if eeprom_back[-1] != 8:
|
|
||||||
fail(f"timeout cell reads {eeprom_back[-1]}, expected 8")
|
|
||||||
|
|
||||||
# The expected post-surgery flash, straight from the tool's planner.
|
|
||||||
pages = pb.plan_flash(open(app_bin, "rb").read(), info)
|
|
||||||
flash_back = open(read_flash, "rb").read()
|
|
||||||
for address, data in pages.items():
|
|
||||||
if flash_back[address : address + page] != data:
|
|
||||||
fail(f"flash read-back mismatch in page {address:#06x}")
|
|
||||||
|
|
||||||
# An external reset re-enters through the patched word 0 (tinies; the
|
|
||||||
# runner resets them to address 0 like silicon) or BOOTRST (mega).
|
|
||||||
# The loader must answer a fresh knock, and 'G' must land in the
|
|
||||||
# application, which banners on the same link.
|
|
||||||
device.proc.send_signal(signal.SIGUSR1)
|
|
||||||
port = pb.Port(device.pty, baud)
|
|
||||||
try:
|
|
||||||
loader = pb.Loader(port)
|
|
||||||
loader.connect(15)
|
|
||||||
port.write(b"G")
|
|
||||||
if port.read_exact(1, 5.0) != pb.PROMPT:
|
|
||||||
fail("no ack for G")
|
|
||||||
banner = port.read_exact(3, 5.0)
|
|
||||||
if banner != b"APP":
|
|
||||||
fail(f"application banner was {banner!r}")
|
|
||||||
finally:
|
|
||||||
port.close()
|
|
||||||
finally:
|
|
||||||
device.stop()
|
|
||||||
|
|
||||||
# Ground truth: the simulator's own memories, against the host's view.
|
|
||||||
flash_true = open(dump, "rb").read()
|
|
||||||
if flash_true[:base] != flash_back:
|
|
||||||
fail("host flash read-back differs from the simulator's flash")
|
|
||||||
if flash_true[base] == 0xFF and flash_true[base + 1] == 0xFF:
|
|
||||||
fail("loader region looks erased in the ground-truth dump")
|
|
||||||
|
|
||||||
# The surgery, decoded independently: the patched vector must land on the
|
|
||||||
# loader, the trampoline on the application's own entry.
|
|
||||||
if mcu != "atmega328p":
|
|
||||||
flash_words = (base + 512) // 2
|
|
||||||
app = open(app_bin, "rb").read()
|
|
||||||
word0 = flash_true[0] | (flash_true[1] << 8)
|
|
||||||
if rjmp_decode(word0, 0, flash_words) != base // 2:
|
|
||||||
fail("patched reset vector does not land on the loader base")
|
|
||||||
trampoline = flash_true[base - 2] | (flash_true[base - 1] << 8)
|
|
||||||
original = app[0] | (app[1] << 8)
|
|
||||||
if rjmp_decode(trampoline, (base - 2) // 2, flash_words) != rjmp_decode(original, 0, flash_words):
|
|
||||||
fail("trampoline does not land on the application's own entry")
|
|
||||||
ee_true_path = dump + ".eeprom"
|
|
||||||
if os.path.exists(ee_true_path):
|
|
||||||
ee_true = open(ee_true_path, "rb").read()
|
|
||||||
if ee_true[: len(ee_image)] != ee_image or ee_true[-1] != 8:
|
|
||||||
fail("ground-truth EEPROM does not match what was programmed")
|
|
||||||
|
|
||||||
print("pbtest: all scenarios pass")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,323 +0,0 @@
|
|||||||
// simavr "device" for the pureboot protocol tests, all three chips. Loads
|
|
||||||
// the boot-linked ELF at the loader base, starts execution there (BOOTRST /
|
|
||||||
// the patched vector are not what is under test), and exposes the loader's
|
|
||||||
// serial link as a pty for the real host tool:
|
|
||||||
//
|
|
||||||
// - ATmega328P: the hardware USART0 through simavr's uart_pty.
|
|
||||||
// - Tinies: an 8N1 bridge between a pty and the GPIO software UART
|
|
||||||
// (drives PB0, the loader's RX; decodes PB1, its TX), timed against the
|
|
||||||
// simulated cycle counter.
|
|
||||||
//
|
|
||||||
// simavr's tiny cores decode the SPM opcode but attach no NVM module — SPM
|
|
||||||
// is a silent no-op (the mega's boot section has one, avr_flash). The
|
|
||||||
// missing module is supplied here: the SPM ioctl reads SPMCSR/Z/r1:r0 and
|
|
||||||
// implements buffer fill, page erase, page write, and CTPB, completing
|
|
||||||
// instantly. RFLB's LPM diversion (fuse readout) stays unmodeled, so the
|
|
||||||
// 'F' command answers with flash bytes — the tests assert transport only.
|
|
||||||
//
|
|
||||||
// On exit (or SIGTERM) the flash and EEPROM are dumped to files for a
|
|
||||||
// ground-truth cross-check against what the host read back.
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <pty.h>
|
|
||||||
#include <signal.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <termios.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
#include "avr_eeprom.h"
|
|
||||||
#include "avr_flash.h"
|
|
||||||
#include "avr_ioport.h"
|
|
||||||
#include "avr_uart.h"
|
|
||||||
#include "sim_avr.h"
|
|
||||||
#include "sim_elf.h"
|
|
||||||
#include "sim_io.h"
|
|
||||||
#include "uart_pty.h"
|
|
||||||
|
|
||||||
static avr_t *avr;
|
|
||||||
static uart_pty_t uart_pty;
|
|
||||||
static int use_uart_pty;
|
|
||||||
static const char *dump_path;
|
|
||||||
static uint32_t reset_pc;
|
|
||||||
static volatile sig_atomic_t reset_requested;
|
|
||||||
|
|
||||||
static void request_reset(int sig)
|
|
||||||
{
|
|
||||||
(void)sig;
|
|
||||||
reset_requested = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------- tiny NVM ---
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
avr_io_t io;
|
|
||||||
uint8_t buffer[128];
|
|
||||||
unsigned page;
|
|
||||||
} tiny_nvm_t;
|
|
||||||
|
|
||||||
static tiny_nvm_t nvm;
|
|
||||||
|
|
||||||
static int nvm_ioctl(avr_io_t *io, uint32_t ctl, void *param)
|
|
||||||
{
|
|
||||||
(void)param;
|
|
||||||
if (ctl != AVR_IOCTL_FLASH_SPM)
|
|
||||||
return -1;
|
|
||||||
tiny_nvm_t *n = (tiny_nvm_t *)io;
|
|
||||||
avr_t *mcu = io->avr;
|
|
||||||
uint8_t command = mcu->data[0x57] & 0x1f; // SPMCSR, both tinies
|
|
||||||
uint16_t z = (uint16_t)(mcu->data[30] | (mcu->data[31] << 8));
|
|
||||||
uint32_t page_base = (uint32_t)(z & ~(n->page - 1)) % (mcu->flashend + 1);
|
|
||||||
if (command == 0x01) { // SPMEN alone: buffer fill from r1:r0
|
|
||||||
unsigned offset = z & (n->page - 1) & ~1u;
|
|
||||||
n->buffer[offset] = mcu->data[0];
|
|
||||||
n->buffer[offset + 1] = mcu->data[1];
|
|
||||||
} else if (command == 0x03) { // PGERS
|
|
||||||
memset(mcu->flash + page_base, 0xff, n->page);
|
|
||||||
} else if (command == 0x05) { // PGWRT: programming only clears bits
|
|
||||||
for (unsigned i = 0; i < n->page; i++)
|
|
||||||
mcu->flash[page_base + i] &= n->buffer[i];
|
|
||||||
memset(n->buffer, 0xff, n->page);
|
|
||||||
} else if (command == 0x11) { // CTPB
|
|
||||||
memset(n->buffer, 0xff, n->page);
|
|
||||||
}
|
|
||||||
mcu->data[0x57] &= (uint8_t)~0x1f; // the operation completes instantly
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------- GPIO bridge ---
|
|
||||||
|
|
||||||
static int pty_master = -1;
|
|
||||||
static avr_irq_t *rx_pin; // the loader's RX (PB0), driven from the pty
|
|
||||||
static avr_cycle_count_t bit_cycles;
|
|
||||||
|
|
||||||
static int tx_level = 1, tx_active, tx_bit;
|
|
||||||
static uint8_t tx_shift;
|
|
||||||
|
|
||||||
static avr_cycle_count_t tx_sample(avr_t *mcu, avr_cycle_count_t when, void *param)
|
|
||||||
{
|
|
||||||
(void)mcu;
|
|
||||||
(void)param;
|
|
||||||
tx_shift = (uint8_t)((tx_shift >> 1) | (tx_level ? 0x80 : 0));
|
|
||||||
if (++tx_bit < 8)
|
|
||||||
return when + bit_cycles;
|
|
||||||
if (write(pty_master, &tx_shift, 1) != 1)
|
|
||||||
fprintf(stderr, "device: pty write lost a byte\n");
|
|
||||||
tx_active = 0;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void tx_hook(avr_irq_t *irq, uint32_t value, void *param)
|
|
||||||
{
|
|
||||||
(void)irq;
|
|
||||||
(void)param;
|
|
||||||
int level = value & 1;
|
|
||||||
if (!tx_active && tx_level == 1 && level == 0) { // start edge
|
|
||||||
tx_active = 1;
|
|
||||||
tx_bit = 0;
|
|
||||||
avr_cycle_timer_register(avr, bit_cycles + bit_cycles / 2, tx_sample, NULL);
|
|
||||||
}
|
|
||||||
tx_level = level;
|
|
||||||
}
|
|
||||||
|
|
||||||
static uint8_t rx_queue[8192];
|
|
||||||
static unsigned rx_head, rx_tail; // ring: head = next to send
|
|
||||||
static int rx_active, rx_bit;
|
|
||||||
static uint8_t rx_byte;
|
|
||||||
|
|
||||||
static void rx_start_next(void);
|
|
||||||
|
|
||||||
static avr_cycle_count_t rx_step(avr_t *mcu, avr_cycle_count_t when, void *param)
|
|
||||||
{
|
|
||||||
(void)mcu;
|
|
||||||
(void)param;
|
|
||||||
if (rx_bit < 8) {
|
|
||||||
avr_raise_irq(rx_pin, (rx_byte >> rx_bit) & 1);
|
|
||||||
rx_bit++;
|
|
||||||
return when + bit_cycles;
|
|
||||||
}
|
|
||||||
if (rx_bit == 8) { // stop bit, plus one idle bit of margin
|
|
||||||
avr_raise_irq(rx_pin, 1);
|
|
||||||
rx_bit++;
|
|
||||||
return when + 2 * bit_cycles;
|
|
||||||
}
|
|
||||||
rx_active = 0;
|
|
||||||
rx_start_next();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void rx_start_next(void)
|
|
||||||
{
|
|
||||||
if (rx_active || rx_head == rx_tail)
|
|
||||||
return;
|
|
||||||
rx_byte = rx_queue[rx_head];
|
|
||||||
rx_head = (rx_head + 1) % sizeof(rx_queue);
|
|
||||||
rx_active = 1;
|
|
||||||
rx_bit = 0;
|
|
||||||
avr_raise_irq(rx_pin, 0); // start bit
|
|
||||||
avr_cycle_timer_register(avr, bit_cycles, rx_step, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A reset abandons whatever the bridge was mid-transfer: bytes still queued
|
|
||||||
// for a chip that no longer has the context to receive them meaningfully,
|
|
||||||
// and a decode in progress on a TX line the reset may have already changed.
|
|
||||||
static void bridge_reset(void)
|
|
||||||
{
|
|
||||||
rx_head = rx_tail = 0;
|
|
||||||
rx_active = 0;
|
|
||||||
tx_active = 0;
|
|
||||||
tx_level = 1;
|
|
||||||
avr_raise_irq(rx_pin, 1); // idle line
|
|
||||||
}
|
|
||||||
|
|
||||||
static void poll_pty(void)
|
|
||||||
{
|
|
||||||
uint8_t chunk[256];
|
|
||||||
ssize_t got = read(pty_master, chunk, sizeof(chunk));
|
|
||||||
for (ssize_t i = 0; i < got; i++) {
|
|
||||||
unsigned next = (rx_tail + 1) % sizeof(rx_queue);
|
|
||||||
if (next == rx_head)
|
|
||||||
break; // full: the host will retry on timeout
|
|
||||||
rx_queue[rx_tail] = chunk[i];
|
|
||||||
rx_tail = next;
|
|
||||||
}
|
|
||||||
if (got > 0)
|
|
||||||
rx_start_next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------ main ---
|
|
||||||
|
|
||||||
static void finish(int sig)
|
|
||||||
{
|
|
||||||
(void)sig;
|
|
||||||
if (dump_path) {
|
|
||||||
FILE *f = fopen(dump_path, "wb");
|
|
||||||
if (f) {
|
|
||||||
fwrite(avr->flash, 1, avr->flashend + 1, f);
|
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
avr_eeprom_desc_t ee = {.ee = NULL, .offset = 0, .size = 0};
|
|
||||||
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &ee) == 0 && ee.ee && ee.size) {
|
|
||||||
char path[512];
|
|
||||||
snprintf(path, sizeof(path), "%s.eeprom", dump_path);
|
|
||||||
f = fopen(path, "wb");
|
|
||||||
if (f) {
|
|
||||||
fwrite(ee.ee, 1, ee.size, f);
|
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (use_uart_pty)
|
|
||||||
uart_pty_stop(&uart_pty);
|
|
||||||
_exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
|
||||||
{
|
|
||||||
if (argc != 8) {
|
|
||||||
fprintf(stderr, "usage: %s <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>\n", argv[0]);
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
const char *mcu_name = argv[2];
|
|
||||||
uint32_t base = (uint32_t)strtoul(argv[4], NULL, 0);
|
|
||||||
unsigned page = (unsigned)atoi(argv[5]);
|
|
||||||
unsigned baud = (unsigned)atoi(argv[6]);
|
|
||||||
dump_path = argv[7];
|
|
||||||
use_uart_pty = strcmp(mcu_name, "atmega328p") == 0;
|
|
||||||
|
|
||||||
avr = avr_make_mcu_by_name(mcu_name);
|
|
||||||
if (!avr) {
|
|
||||||
fprintf(stderr, "device: no %s core\n", mcu_name);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
avr_init(avr);
|
|
||||||
avr->frequency = (uint32_t)strtoul(argv[3], NULL, 0);
|
|
||||||
memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased
|
|
||||||
|
|
||||||
elf_firmware_t fw = {0};
|
|
||||||
if (elf_read_firmware(argv[1], &fw) != 0) {
|
|
||||||
fprintf(stderr, "device: cannot read %s\n", argv[1]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
memcpy(avr->flash + base, fw.flash, fw.flashsize);
|
|
||||||
// The mega enters the loader in hardware (BOOTRST, not modeled); the
|
|
||||||
// tinies reset to word 0 like silicon — erased flash walks up into the
|
|
||||||
// loader, and after the host's surgery the patched vector routes there.
|
|
||||||
reset_pc = use_uart_pty ? base : 0;
|
|
||||||
avr->pc = reset_pc;
|
|
||||||
avr->codeend = avr->flashend;
|
|
||||||
|
|
||||||
// Erased EEPROM, as hardware powers up (simavr zeroes it).
|
|
||||||
uint8_t blank[1024];
|
|
||||||
memset(blank, 0xff, sizeof(blank));
|
|
||||||
avr_eeprom_desc_t seed = {.ee = blank, .offset = 0, .size = 0};
|
|
||||||
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &seed) == 0 && seed.size <= sizeof(blank)) {
|
|
||||||
seed.ee = blank;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_EEPROM_SET, &seed);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (use_uart_pty) {
|
|
||||||
// POLL_SLEEP paces an idle-polling loader in host real time (a
|
|
||||||
// no-hardware CPU-saving hack); clear it so cycles run free.
|
|
||||||
uint32_t flags = 0;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &flags);
|
|
||||||
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &flags);
|
|
||||||
uart_pty_init(avr, &uart_pty);
|
|
||||||
uart_pty_connect(&uart_pty, '0');
|
|
||||||
printf("PB_PTY %s\n", uart_pty.pty.slavename);
|
|
||||||
} else {
|
|
||||||
nvm.page = page;
|
|
||||||
memset(nvm.buffer, 0xff, sizeof(nvm.buffer));
|
|
||||||
nvm.io.kind = "tiny_nvm";
|
|
||||||
nvm.io.ioctl = nvm_ioctl;
|
|
||||||
avr_register_io(avr, &nvm.io);
|
|
||||||
|
|
||||||
bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly
|
|
||||||
rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), 0);
|
|
||||||
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), 1), tx_hook, NULL);
|
|
||||||
avr_raise_irq(rx_pin, 1); // idle line
|
|
||||||
|
|
||||||
int slave;
|
|
||||||
struct termios raw;
|
|
||||||
cfmakeraw(&raw);
|
|
||||||
if (openpty(&pty_master, &slave, NULL, &raw, NULL) != 0) {
|
|
||||||
fprintf(stderr, "device: openpty failed\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
fcntl(pty_master, F_SETFL, O_NONBLOCK);
|
|
||||||
printf("PB_PTY %s\n", ttyname(slave));
|
|
||||||
}
|
|
||||||
fflush(stdout);
|
|
||||||
|
|
||||||
signal(SIGTERM, finish);
|
|
||||||
signal(SIGINT, finish);
|
|
||||||
signal(SIGUSR1, request_reset); // an external reset line, for the tests
|
|
||||||
|
|
||||||
long since_poll = 0;
|
|
||||||
for (;;) {
|
|
||||||
int state = avr_run(avr);
|
|
||||||
if (state == cpu_Done || state == cpu_Crashed)
|
|
||||||
break;
|
|
||||||
if (reset_requested) {
|
|
||||||
reset_requested = 0;
|
|
||||||
avr_reset(avr);
|
|
||||||
avr->pc = reset_pc;
|
|
||||||
if (use_uart_pty) { // reset restores the pacing hack; re-clear it
|
|
||||||
uint32_t flags = 0;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &flags);
|
|
||||||
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &flags);
|
|
||||||
} else {
|
|
||||||
bridge_reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!use_uart_pty && ++since_poll >= 2000) {
|
|
||||||
since_poll = 0;
|
|
||||||
poll_pty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finish(0);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -144,7 +144,7 @@ class Host:
|
|||||||
self._expect(CONFIRM, "C end")
|
self._expect(CONFIRM, "C end")
|
||||||
return echo
|
return echo
|
||||||
|
|
||||||
# Activation when the config page carries a password: 3×'@' then the
|
# Activation when the config page carries a password: 3x'@' then the
|
||||||
# password bytes, then the info block + mainloop '!'.
|
# password bytes, then the info block + mainloop '!'.
|
||||||
def activate_password(self, password):
|
def activate_password(self, password):
|
||||||
self.s.reset_input_buffer()
|
self.s.reset_input_buffer()
|
||||||
@@ -167,6 +167,20 @@ class Host:
|
|||||||
self._expect(CONFIRM, "emergency mainloop ready")
|
self._expect(CONFIRM, "emergency mainloop ready")
|
||||||
|
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# through to the emergency erase - a byte the gate has already refused
|
||||||
|
# reaching the erase would let a guess wipe the part.
|
||||||
|
def refuse_password(self, byte):
|
||||||
|
self.s.reset_input_buffer()
|
||||||
|
self.s.write(bytes([KNOCK, KNOCK, KNOCK, byte]))
|
||||||
|
return self.s.read(1)
|
||||||
|
|
||||||
|
def say(self, byte):
|
||||||
|
self.s.write(bytes([byte]))
|
||||||
|
return self.s.read(1)
|
||||||
|
|
||||||
|
|
||||||
def check(cond, msg):
|
def check(cond, msg):
|
||||||
if not cond:
|
if not cond:
|
||||||
raise AssertionError(msg)
|
raise AssertionError(msg)
|
||||||
@@ -181,7 +195,7 @@ PW_BYTES = bytes([0x50, 0x57])
|
|||||||
|
|
||||||
def scenario_roundtrip(host):
|
def scenario_roundtrip(host):
|
||||||
"""Activation + info block + flash/EEPROM/config read-write round-trips, on
|
"""Activation + info block + flash/EEPROM/config read-write round-trips, on
|
||||||
a device with a blank (erased) config page — the usual no-password case."""
|
a device with a blank (erased) config page - the usual no-password case."""
|
||||||
info = host.activate()
|
info = host.activate()
|
||||||
check(info[0:3] == b"TSB", f"magic 'TSB' (got {info[0:3]!r})")
|
check(info[0:3] == b"TSB", f"magic 'TSB' (got {info[0:3]!r})")
|
||||||
check(info[6:9] == bytes([0x1E, 0x95, 0x0F]), f"signature 1E 95 0F (got {info[6:9].hex()})")
|
check(info[6:9] == bytes([0x1E, 0x95, 0x0F]), f"signature 1E 95 0F (got {info[6:9].hex()})")
|
||||||
@@ -219,6 +233,15 @@ def scenario_emergency(host):
|
|||||||
check(host.read_eeprom(1) == b"\xff" * PAGE, "EEPROM wiped")
|
check(host.read_eeprom(1) == b"\xff" * PAGE, "EEPROM wiped")
|
||||||
|
|
||||||
|
|
||||||
|
def scenario_wrong_password(host):
|
||||||
|
"""A wrong password byte neither activates the loader nor opens the
|
||||||
|
emergency erase behind it - the oracle carries a dedicated fix for the
|
||||||
|
second, and nothing here exercised either half."""
|
||||||
|
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(CONFIRM) == b"", "and neither does a confirm")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
binary, elf, boot_base = sys.argv[1], sys.argv[2], sys.argv[3]
|
binary, elf, boot_base = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||||
failures = []
|
failures = []
|
||||||
@@ -229,6 +252,7 @@ def main():
|
|||||||
("round-trip", None, scenario_roundtrip),
|
("round-trip", None, scenario_roundtrip),
|
||||||
("password activation", PW_CONFIG, scenario_password),
|
("password activation", PW_CONFIG, scenario_password),
|
||||||
("emergency erase", PW_CONFIG, scenario_emergency),
|
("emergency erase", PW_CONFIG, scenario_emergency),
|
||||||
|
("wrong password", PW_CONFIG, scenario_wrong_password),
|
||||||
]
|
]
|
||||||
for name, config, fn in groups:
|
for name, config, fn in groups:
|
||||||
print(f"--- {name} ---")
|
print(f"--- {name} ---")
|
||||||
|
|||||||
60
tools/check.sh
Executable file
60
tools/check.sh
Executable file
@@ -0,0 +1,60 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# The port's gate: the generated workflow - build, size tests, and the
|
||||||
|
# simulator-driven protocol suite. --full adds the reflect build, which
|
||||||
|
# compiles the same TUs through libavr's other producer. libavr resolves from
|
||||||
|
# the `libavr/` submodule; LIBAVR_ROOT overrides it for a working tree.
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
full=0
|
||||||
|
[[ "$1" == "--full" ]] && { full=1; shift; }
|
||||||
|
|
||||||
|
# The chip lists come from the presets rather than being spelled a second time
|
||||||
|
# here: a chip added to make_presets.py and missed in a copy of its list would
|
||||||
|
# be a gate that silently never builds it, which is the one failure mode a gate
|
||||||
|
# cannot report. tools/make_presets.py is the single source, CMakePresets.json
|
||||||
|
# is its output, and this reads that.
|
||||||
|
readarray -t WORKFLOWS < <(python3 -c '
|
||||||
|
import json, sys
|
||||||
|
presets = json.load(open("CMakePresets.json"))["workflowPresets"]
|
||||||
|
print("\n".join(p["name"] for p in presets))')
|
||||||
|
if ((${#WORKFLOWS[@]} == 0)); then
|
||||||
|
echo "no workflow presets in CMakePresets.json - run tools/make_presets.py" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
CHIPS=()
|
||||||
|
REFLECT_SPOT=()
|
||||||
|
for workflow in "${WORKFLOWS[@]}"; do
|
||||||
|
case $workflow in
|
||||||
|
*-generated) CHIPS+=("${workflow%-generated}") ;;
|
||||||
|
*-reflect) REFLECT_SPOT+=("${workflow%-reflect}") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Every preset runs even after one goes red, and the gate fails at the end
|
||||||
|
# naming all of them: stopping at the first failure turns a red - a stale size
|
||||||
|
# canary above all - into an alibi for every chip behind it, and a loader can
|
||||||
|
# ship on a chip this gate has not compiled since.
|
||||||
|
red=()
|
||||||
|
run_preset() {
|
||||||
|
echo "==== $1 ===="
|
||||||
|
cmake --workflow --preset "$1" "${@:2}" || red+=("$1")
|
||||||
|
}
|
||||||
|
|
||||||
|
for chip in "${CHIPS[@]}"; do
|
||||||
|
run_preset "$chip-generated" "$@"
|
||||||
|
done
|
||||||
|
|
||||||
|
if ((full)); then
|
||||||
|
for chip in "${REFLECT_SPOT[@]}"; do
|
||||||
|
run_preset "$chip-reflect" "$@"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ((${#red[@]})); then
|
||||||
|
printf '==== red presets ====\n' >&2
|
||||||
|
printf ' %s\n' "${red[@]}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "check: every chip green"
|
||||||
90
tools/make_presets.py
Executable file
90
tools/make_presets.py
Executable file
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Regenerate CMakePresets.json - one uniform pipeline per chip.
|
||||||
|
|
||||||
|
The tiers reimplement the ATmega328P-only reference protocol, so that is the
|
||||||
|
whole chip list. It stays a generated file rather than a hand-written one
|
||||||
|
because the shape - configure, build, test, workflow, and a reflect pair
|
||||||
|
without tests - is the shape a second chip would need too.
|
||||||
|
|
||||||
|
Run from the repo root: tools/make_presets.py - or with --check, which
|
||||||
|
verifies the committed file matches this generator and edits nothing (the
|
||||||
|
ctest entry `presets.generated` runs that, so drift reds the gate).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
CHIPS = [
|
||||||
|
"atmega328p",
|
||||||
|
]
|
||||||
|
|
||||||
|
# The reflect pair: the same chip, built in libavr's other mode.
|
||||||
|
REFLECT_SPOT = [
|
||||||
|
"atmega328p",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
configure = [{
|
||||||
|
"name": "base",
|
||||||
|
"hidden": True,
|
||||||
|
"generator": "Ninja",
|
||||||
|
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||||
|
"toolchainFile": "${sourceDir}/libavr/cmake/avr-toolchain.cmake",
|
||||||
|
"cacheVariables": {
|
||||||
|
"CMAKE_BUILD_TYPE": "Release",
|
||||||
|
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
||||||
|
"CMAKE_COLOR_DIAGNOSTICS": "ON",
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
build, test, workflows = [], [], []
|
||||||
|
|
||||||
|
def add(chip, mode):
|
||||||
|
name = f"{chip}-{mode}"
|
||||||
|
configure.append({
|
||||||
|
"name": name,
|
||||||
|
"inherits": "base",
|
||||||
|
"cacheVariables": {
|
||||||
|
"LIBAVR_MCU": chip,
|
||||||
|
"LIBAVR_REFLECT": "ON" if mode == "reflect" else "OFF",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
build.append({"name": name, "configurePreset": name})
|
||||||
|
steps = [{"type": "configure", "name": name}, {"type": "build", "name": name}]
|
||||||
|
if mode == "generated":
|
||||||
|
test.append({"name": name, "configurePreset": name, "output": {"outputOnFailure": True}})
|
||||||
|
steps.append({"type": "test", "name": name})
|
||||||
|
workflows.append({"name": name, "steps": steps})
|
||||||
|
|
||||||
|
for chip in CHIPS:
|
||||||
|
add(chip, "generated")
|
||||||
|
for chip in REFLECT_SPOT:
|
||||||
|
add(chip, "reflect")
|
||||||
|
|
||||||
|
# CMake rejects unknown fields in the presets root, $comment included, so
|
||||||
|
# the file cannot carry a generated-file marker; the --check ctest is the
|
||||||
|
# whole of rule 10's guard here.
|
||||||
|
presets = {
|
||||||
|
"version": 8,
|
||||||
|
"configurePresets": configure,
|
||||||
|
"buildPresets": build,
|
||||||
|
"testPresets": test,
|
||||||
|
"workflowPresets": workflows,
|
||||||
|
}
|
||||||
|
rendered = json.dumps(presets, indent=1) + "\n"
|
||||||
|
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "CMakePresets.json")
|
||||||
|
if "--check" in sys.argv[1:]:
|
||||||
|
current = open(path).read() if os.path.exists(path) else ""
|
||||||
|
if current != rendered:
|
||||||
|
print("CMakePresets.json does not match its generator - run tools/make_presets.py")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(rendered)
|
||||||
|
print(f"{len(CHIPS)} chips, {len(REFLECT_SPOT)} reflect: {os.path.normpath(path)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
135
tsb/tsb_asm.cpp
135
tsb/tsb_asm.cpp
@@ -1,14 +1,15 @@
|
|||||||
// TinySafeBoot on libavr — tier 3: full feature parity in the 512-byte boot
|
// TinySafeBoot on libavr - tier 3: full feature parity in the 512-byte boot
|
||||||
// section, in C++ except where the C ABI itself is the cost.
|
// section, in C++ except where the C ABI itself is the cost.
|
||||||
//
|
//
|
||||||
// The complete TinySafeBoot feature set — watchdog-reset bail, one-wire
|
// The complete TinySafeBoot feature set - watchdog-reset bail, one-wire
|
||||||
// half-duplex UART, a config-page activation timeout, the password gate,
|
// half-duplex UART, a config-page activation timeout, the password gate,
|
||||||
// emergency erase, and config/flash/EEPROM read-write — at 510 bytes in the
|
// emergency erase, and config/flash/EEPROM read-write - inside the 512-byte
|
||||||
// 512-byte BOOTSZ=11 section the hand-written oracle occupies (500 B). This tier used to be one
|
// BOOTSZ=11 section the hand-written oracle occupies (oracle/README.md holds
|
||||||
// monolithic inline-asm routine; it is now the tricks tier's C++ (same
|
// what each tier measures, in one table rather than four). The
|
||||||
// register protocol, same structure — see tsb_tricks.cpp, including the
|
// body is the tricks tier's C++ (same register protocol, same structure - see
|
||||||
// global-register miscompile rules) with exactly two routines kept in
|
// tsb_tricks.cpp, including the global-register miscompile rules) with exactly
|
||||||
// assembly, the two whose remaining cost *is* the calling convention:
|
// two routines kept in assembly, the two whose remaining cost *is* the calling
|
||||||
|
// convention:
|
||||||
//
|
//
|
||||||
// rx the bounded receive: C++ must re-floor the timeout window on every
|
// rx the bounded receive: C++ must re-floor the timeout window on every
|
||||||
// call (the global-register-store miscompile) and split it across
|
// call (the global-register-store miscompile) and split it across
|
||||||
@@ -16,12 +17,12 @@
|
|||||||
// countdown.
|
// countdown.
|
||||||
// store the page-store loop: C++ cannot hold the receive byte pair and the
|
// store the page-store loop: C++ cannot hold the receive byte pair and the
|
||||||
// walked Z pointer across the rx calls without call-saved staging
|
// walked Z pointer across the rx calls without call-saved staging
|
||||||
// (push/pop + a Y→Z copy per word); the asm calls rx knowing exactly
|
// (push/pop + a Y->Z copy per word); the asm calls rx knowing exactly
|
||||||
// which registers it touches and walks Z live across the whole page.
|
// which registers it touches and walks Z live across the whole page.
|
||||||
//
|
//
|
||||||
// Everything else — bring-up, activation, password gate, emergency erase,
|
// Everything else - bring-up, activation, password gate, emergency erase,
|
||||||
// dispatch, every SPM/EEPROM/flash primitive, every geometry/baud/info
|
// dispatch, every SPM/EEPROM/flash primitive, every geometry/baud/info
|
||||||
// constant — is C++ on libavr, and the two asm routines splice into the same
|
// constant - is C++ on libavr, and the two asm routines splice into the same
|
||||||
// global-register protocol the C++ uses (g_addr in Y, g_cnt in r16, g_window
|
// global-register protocol the C++ uses (g_addr in Y, g_cnt in r16, g_window
|
||||||
// in r7, g_receiving in r6), so calls cross the boundary with no marshalling.
|
// in r7, g_receiving in r6), so calls cross the boundary with no marshalling.
|
||||||
//
|
//
|
||||||
@@ -41,10 +42,16 @@ namespace hw = avr::hw;
|
|||||||
namespace tsb {
|
namespace tsb {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// The loader is purely polled — it never enables interrupts — so every SPM and
|
// The loader is purely polled - it never enables interrupts - so every SPM and
|
||||||
// EEPROM lock folds to nothing under this posture.
|
// EEPROM lock folds to nothing under this posture.
|
||||||
constexpr auto off = avr::irq::guard_policy::unused;
|
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 confirm = '!';
|
||||||
constexpr std::uint8_t request = '?';
|
constexpr std::uint8_t request = '?';
|
||||||
constexpr std::uint8_t knock = '@';
|
constexpr std::uint8_t knock = '@';
|
||||||
@@ -57,28 +64,34 @@ 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 = spm::flash_bytes - boot_bytes - page;
|
||||||
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
|
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).
|
// Lockout-proof floor for the activation window: the oracle's F_CPU/1MHz, so
|
||||||
constexpr std::uint8_t act_min = 16;
|
// 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.
|
// Post-activation window: the host gets seconds, not milliseconds, mid-session.
|
||||||
constexpr std::uint8_t comm_window = 200;
|
constexpr std::uint8_t comm_window = 200;
|
||||||
|
|
||||||
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
||||||
|
|
||||||
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
||||||
constexpr auto baud = avr::uart::detail::solve_baud(16_MHz, 115200_Bd);
|
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.
|
// The 16-byte device-info block, streamed out on activation.
|
||||||
// clang-format off
|
// clang-format off
|
||||||
[[gnu::progmem]] constexpr std::uint8_t info[16] = {
|
[[gnu::progmem]] constexpr auto info = std::to_array<std::uint8_t>({
|
||||||
'T', 'S', 'B',
|
'T', 'S', 'B',
|
||||||
build_date & 0xFF, build_date >> 8,
|
build_date & 0xFF, build_date >> 8,
|
||||||
0xF3, // status: native-UART fixed-baud lineage
|
0xF3, // status: native-UART fixed-baud lineage
|
||||||
0x1E, 0x95, 0x0F, // ATmega328P signature
|
avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2],
|
||||||
page / 2, // page size in words
|
page / 2, // page size in words
|
||||||
(app_end / 2) & 0xFF, (app_end / 2) >> 8,
|
(app_end / 2) & 0xFF, (app_end / 2) >> 8,
|
||||||
eeprom_end & 0xFF, eeprom_end >> 8,
|
eeprom_end & 0xFF, eeprom_end >> 8,
|
||||||
0xAA, 0xAA,
|
0xAA, 0xAA,
|
||||||
};
|
});
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
register std::uint16_t g_addr asm("r28");
|
register std::uint16_t g_addr asm("r28");
|
||||||
@@ -94,7 +107,7 @@ const std::uint8_t *flash_ptr(std::uint16_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
|
||||||
// — the property the store's word loop rides on.
|
// - the property the store's word loop rides on.
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t rx()
|
[[gnu::noinline, gnu::noclone]] std::uint8_t rx()
|
||||||
{
|
{
|
||||||
std::uint8_t byte;
|
std::uint8_t byte;
|
||||||
@@ -115,7 +128,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|||||||
" brne 3b \n\t"
|
" brne 3b \n\t"
|
||||||
" sbiw r26, 1 \n\t"
|
" sbiw r26, 1 \n\t"
|
||||||
" brcc 2b \n\t"
|
" brcc 2b \n\t"
|
||||||
" clr %[b] \n\t" // silence → 0, which no compare accepts
|
" clr %[b] \n\t" // silence -> 0, which no compare accepts
|
||||||
" rjmp 5f \n\t"
|
" rjmp 5f \n\t"
|
||||||
"4: lds %[b], %[udr0] \n\t"
|
"4: lds %[b], %[udr0] \n\t"
|
||||||
"5: \n\t"
|
"5: \n\t"
|
||||||
@@ -129,14 +142,13 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|||||||
// One-wire transmit: take the line (TXEN0 alone) on a direction change with a
|
// One-wire transmit: take the line (TXEN0 alone) on a direction change with a
|
||||||
// turn-around guard, put the byte out, hold the line until the whole frame is
|
// turn-around guard, put the byte out, hold the line until the whole frame is
|
||||||
// out (TXC0, not UDRE0), W1C TXC0 by storing the sampled status back (keeps
|
// out (TXC0, not UDRE0), W1C TXC0 by storing the sampled status back (keeps
|
||||||
// U2X0). Plain C++ — it compiles *smaller* than the oracle's routine.
|
// U2X0). Plain C++ - it compiles *smaller* than the oracle's routine.
|
||||||
[[gnu::noinline, gnu::noclone]] void tx(std::uint8_t byte)
|
[[gnu::noinline, gnu::noclone]] void tx(std::uint8_t byte)
|
||||||
{
|
{
|
||||||
if (g_receiving) {
|
if (g_receiving) {
|
||||||
g_receiving = 0;
|
g_receiving = 0;
|
||||||
hw::ucsr0b::write(hw::ucsr0b::txen0(1));
|
hw::ucsr0b::write(hw::ucsr0b::txen0(1));
|
||||||
for (std::uint8_t guard = 46; guard; --guard)
|
avr::delay::cycles<guard_cycles>();
|
||||||
;
|
|
||||||
}
|
}
|
||||||
hw::udr0::write(byte);
|
hw::udr0::write(byte);
|
||||||
std::uint8_t status;
|
std::uint8_t status;
|
||||||
@@ -153,7 +165,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|||||||
return rx();
|
return rx();
|
||||||
}
|
}
|
||||||
|
|
||||||
// One flash byte ← [g_addr++] (the advance right before ret — the
|
// One flash byte <- [g_addr++] (the advance right before ret - the
|
||||||
// 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()
|
||||||
{
|
{
|
||||||
@@ -162,18 +174,18 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|||||||
return byte;
|
return byte;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(g_addr);
|
std::uint8_t byte = ee::read<no_spm>(g_addr);
|
||||||
++g_addr;
|
++g_addr;
|
||||||
return byte;
|
return byte;
|
||||||
}
|
}
|
||||||
|
|
||||||
// One EEPROM byte → [g_addr++].
|
// One EEPROM byte -> [g_addr++].
|
||||||
[[gnu::noinline, gnu::noclone]] void eewr(std::uint8_t byte)
|
[[gnu::noinline, gnu::noclone]] void eewr(std::uint8_t byte)
|
||||||
{
|
{
|
||||||
ee::write<off>(g_addr, byte);
|
ee::write<off, no_spm>(g_addr, byte);
|
||||||
++g_addr;
|
++g_addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +197,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|||||||
} while (--g_cnt);
|
} while (--g_cnt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait out a running SPM op, then re-open the RWW section — after every page
|
// Wait out a running SPM op, then re-open the RWW section - after every page
|
||||||
// op and before handing over, as the oracle does.
|
// op and before handing over, as the oracle does.
|
||||||
[[gnu::noinline, gnu::noclone]] void settle()
|
[[gnu::noinline, gnu::noclone]] void settle()
|
||||||
{
|
{
|
||||||
@@ -201,12 +213,12 @@ 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()
|
||||||
{
|
{
|
||||||
g_addr -= page;
|
g_addr -= page;
|
||||||
spm::erase_page<off>(g_addr);
|
spm::command<off>(spm::op::erase, g_addr);
|
||||||
settle();
|
settle();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +233,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()
|
||||||
@@ -256,14 +268,15 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
{
|
{
|
||||||
// A watchdog reset hands straight back to the application, as the
|
// A watchdog reset hands straight back to the application, as the
|
||||||
// reference loader does, rather than re-entering the bootloader.
|
// reference loader does, rather than re-entering the bootloader.
|
||||||
if (hw::mcusr::wdrf.test())
|
if (hw::mcusr::wdrf.test()) {
|
||||||
appjump();
|
appjump();
|
||||||
|
}
|
||||||
|
|
||||||
// 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 rx()/tx() raise RXEN0/TXEN0 on first use — only the divisor low
|
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use - only the divisor low
|
||||||
// byte and U2X0 need a store. The library still does the datasheet work.
|
// byte and U2X0 need a store. The library still does the datasheet work.
|
||||||
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
||||||
hw::reg<"UBRR0">::write(static_cast<std::uint8_t>(baud.ubrr));
|
hw::ubrr0::write(static_cast<std::uint8_t>(baud.ubrr));
|
||||||
hw::ucsr0a::write(hw::ucsr0a::u2x0(1));
|
hw::ucsr0a::write(hw::ucsr0a::u2x0(1));
|
||||||
// General-purpose registers are undefined at power-on (no crt zeroes them);
|
// General-purpose registers are undefined at power-on (no crt zeroes them);
|
||||||
// the direction latch must start "not receiving" so the first rx() enables
|
// the direction latch must start "not receiving" so the first rx() enables
|
||||||
@@ -271,13 +284,15 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
// same reason.
|
// same reason.
|
||||||
g_receiving = 0;
|
g_receiving = 0;
|
||||||
|
|
||||||
// Activation: 3×'@', each inside the config page's timeout window (rx
|
// Activation: 3x'@', each inside the config page's timeout window (rx
|
||||||
// floors it so a corrupt page cannot lock the loader out); anything else —
|
// floors it so a corrupt page cannot lock the loader out); anything else -
|
||||||
// including silence — hands over.
|
// including silence - hands over.
|
||||||
g_window = avr::flash_load(flash_ptr(app_end + 2));
|
g_window = avr::flash_load(flash_ptr(app_end + 2));
|
||||||
for (std::uint8_t k = 3; k; --k)
|
for (std::uint8_t k = 3; 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 app_end+3, 0xff-terminated; a blank
|
||||||
@@ -291,17 +306,19 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask;
|
std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask;
|
||||||
++g_addr;
|
++g_addr;
|
||||||
if (expected == 0xff) {
|
if (expected == 0xff) {
|
||||||
g_addr = reinterpret_cast<std::uint16_t>(&info[0]);
|
g_addr = reinterpret_cast<std::uint16_t>(info.data());
|
||||||
g_cnt = sizeof(info);
|
g_cnt = sizeof(info);
|
||||||
sendf();
|
sendf();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
std::uint8_t got = rx();
|
std::uint8_t got = rx();
|
||||||
if (got == 0) {
|
if (got == 0) {
|
||||||
if (mask == 0)
|
if (mask == 0) {
|
||||||
continue;
|
continue;
|
||||||
if (rcnf() != confirm || rcnf() != confirm)
|
}
|
||||||
|
if (rcnf() != confirm || rcnf() != confirm) {
|
||||||
appjump();
|
appjump();
|
||||||
|
}
|
||||||
erase_application(); // leaves g_addr = 0 for the EEPROM walk
|
erase_application(); // leaves g_addr = 0 for the EEPROM walk
|
||||||
do {
|
do {
|
||||||
eewr(0xff);
|
eewr(0xff);
|
||||||
@@ -310,8 +327,9 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
erase_below();
|
erase_below();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (got != expected)
|
if (got != expected) {
|
||||||
mask = 0;
|
mask = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
@@ -320,23 +338,27 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
switch (rx()) {
|
switch (rx()) {
|
||||||
case 'f': // read application flash, one page per host '!'
|
case 'f': // read application flash, one page per host '!'
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (rx() != confirm)
|
if (rx() != confirm) {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
g_cnt = page;
|
g_cnt = page;
|
||||||
sendf();
|
sendf();
|
||||||
if (g_addr >= app_end)
|
if (g_addr >= app_end) {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'F': // erase the application, then take pages behind '?'
|
case 'F': // 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 'e': // read EEPROM, one page per host '!', until the host stops
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (rx() != confirm)
|
if (rx() != confirm) {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
g_cnt = page;
|
g_cnt = page;
|
||||||
do {
|
do {
|
||||||
tx(eerd());
|
tx(eerd());
|
||||||
@@ -358,8 +380,9 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
sendf();
|
sendf();
|
||||||
break;
|
break;
|
||||||
case 'C': // replace the config page, then echo it back to verify
|
case 'C': // replace the config page, then echo it back to verify
|
||||||
if (rcnf() != confirm)
|
if (rcnf() != confirm) {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
g_addr = app_end + page;
|
g_addr = app_end + page;
|
||||||
erase_below(); // leaves g_addr = app_end, the store target
|
erase_below(); // leaves g_addr = app_end, the store target
|
||||||
store_flash();
|
store_flash();
|
||||||
@@ -373,14 +396,6 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
} // namespace
|
} // namespace
|
||||||
} // namespace tsb
|
} // namespace tsb
|
||||||
|
|
||||||
// Reset lands here: BOOTRST vectors to the boot section base and .vectors is
|
// Reset lands at the boot section base (BOOTRST): the entry stub in .vectors
|
||||||
// laid first, so this is the first instruction executed. No crt ran, so set
|
// is laid first and does the one line of crt a crt-less image needs.
|
||||||
// the stack pointer before anything is called.
|
template struct avr::startup::entry<tsb::run, avr::startup::stack::hardware>;
|
||||||
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
|
|
||||||
{
|
|
||||||
SP = RAMEND;
|
|
||||||
// The one line of crt this loader needs: compiled code assumes
|
|
||||||
// __zero_reg__ (r1) is 0, and power-on registers are undefined.
|
|
||||||
asm volatile("clr __zero_reg__");
|
|
||||||
tsb::run();
|
|
||||||
}
|
|
||||||
|
|||||||
329
tsb/tsb_policy.cpp
Normal file
329
tsb/tsb_policy.cpp
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
// TinySafeBoot on libavr - the policy floor: pureboot's rules, measured.
|
||||||
|
//
|
||||||
|
// The full TinySafeBoot feature set - watchdog bail, one-wire half-duplex,
|
||||||
|
// config-page activation timeout, password gate, emergency erase, and
|
||||||
|
// config/flash/EEPROM read-write - under philosophy #5 exactly as pureboot
|
||||||
|
// obeys it: no assembly, no register variables; code, attributes, and flags
|
||||||
|
// only. Every lesson pureboot's development produced is applied - the
|
||||||
|
// 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,
|
||||||
|
// locals threaded through noinline primitives, pureboot's codegen flags -
|
||||||
|
// and the result sits below the idiomatic tier and above the 512 B boot
|
||||||
|
// section the tricks/asm tiers reach with the banned mechanisms
|
||||||
|
// (oracle/README.md holds all four). This tier exists to keep that gap an
|
||||||
|
// artifact
|
||||||
|
// rather than a claim: the gap to 512 is the rent of policy-clean C++ -
|
||||||
|
// helpers that hold a cursor across rx()/tx() pay push/pop and argument
|
||||||
|
// 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
|
||||||
|
// shared line safe: the device drives it only between a received command and
|
||||||
|
// its reply, and releases it (the library's half-duplex choreography)
|
||||||
|
// whenever it waits.
|
||||||
|
|
||||||
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
|
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.
|
||||||
|
// 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
|
||||||
|
// decade, so the override states that it is meant.
|
||||||
|
using serial_t = dev::uart0<{
|
||||||
|
.baud = 115200_Bd,
|
||||||
|
.allow_baud_error = true,
|
||||||
|
.half_duplex = true,
|
||||||
|
}>;
|
||||||
|
inline constexpr serial_t serial{};
|
||||||
|
|
||||||
|
namespace tsb {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// 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 app_end = spm::flash_bytes - boot_bytes - page;
|
||||||
|
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
|
||||||
|
// 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
|
||||||
|
// crt to clear a .bss image, and run() stores it before the first receive.
|
||||||
|
[[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
|
||||||
|
// 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
|
||||||
|
// anywhere, and a mid-session cable pull cannot wedge it. The line release on
|
||||||
|
// a direction change is the serial backend's.
|
||||||
|
[[gnu::noinline]] std::uint8_t rx()
|
||||||
|
{
|
||||||
|
std::uint16_t outer = static_cast<std::uint16_t>(window) << 8;
|
||||||
|
do {
|
||||||
|
std::uint8_t fine = 0;
|
||||||
|
do {
|
||||||
|
if (auto byte = serial.read()) {
|
||||||
|
return *byte;
|
||||||
|
}
|
||||||
|
} while (--fine);
|
||||||
|
} while (--outer);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One-wire transmit: the backend takes the line with a turn-around guard and
|
||||||
|
// holds it until the whole frame is out.
|
||||||
|
[[gnu::noinline]] void tx(std::uint8_t byte)
|
||||||
|
{
|
||||||
|
serial.write(byte);
|
||||||
|
}
|
||||||
|
|
||||||
|
// '?', then hand back the host's reply for the callers' one-byte compare.
|
||||||
|
[[gnu::noinline]] std::uint8_t rcnf()
|
||||||
|
{
|
||||||
|
tx(request);
|
||||||
|
return rx();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The one send loop: the info block, the config page, application flash and
|
||||||
|
// EEPROM pages all stream through here.
|
||||||
|
[[gnu::noinline]] void send_block(bool eep, std::uint16_t at, std::uint8_t count)
|
||||||
|
{
|
||||||
|
do {
|
||||||
|
tx(eep ? ee::read<no_spm>(at) : avr::flash_load(flash_ptr(at)));
|
||||||
|
++at;
|
||||||
|
} while (--count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One EEPROM byte in - shared by the emergency wipe and the 'E' stream.
|
||||||
|
[[gnu::noinline]] void eeput(std::uint16_t at, std::uint8_t value)
|
||||||
|
{
|
||||||
|
ee::write<off, no_spm>(at, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait out a running SPM op, then re-open the RWW section - after every page
|
||||||
|
// op and before handing over, as the oracle does.
|
||||||
|
[[gnu::noinline]] void settle()
|
||||||
|
{
|
||||||
|
spm::wait();
|
||||||
|
spm::rww_enable<off>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// One host page straight into the erased flash page at `at` - through the SPM
|
||||||
|
// word buffer (low byte then high), no SRAM staging - then committed. `at`
|
||||||
|
// names a page base, so the cursor's low byte reaching the boundary ends the
|
||||||
|
// walk.
|
||||||
|
[[gnu::noinline]] void store_flash_page(std::uint16_t at)
|
||||||
|
{
|
||||||
|
const auto open = spm::page::begin<spm::from::boot_section, off>(at);
|
||||||
|
do {
|
||||||
|
std::uint8_t low = rx();
|
||||||
|
std::uint8_t high = rx();
|
||||||
|
spm::fill<off>(open, at, std::bit_cast<std::uint16_t>(std::array{low, high}));
|
||||||
|
at += 2;
|
||||||
|
} while (static_cast<std::uint8_t>(at) & (page - 1));
|
||||||
|
spm::command<off>(spm::op::write, at - page);
|
||||||
|
settle();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --defsym=tsb_app=0
|
||||||
|
|
||||||
|
[[noreturn]] void appjump()
|
||||||
|
{
|
||||||
|
settle();
|
||||||
|
tsb_app();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step one page down and erase it - the erase shared by the whole-app walk,
|
||||||
|
// the config rewrite and the emergency wipe; hands the stepped address back.
|
||||||
|
[[gnu::noinline]] std::uint16_t erase_below(std::uint16_t at)
|
||||||
|
{
|
||||||
|
at -= page;
|
||||||
|
spm::command<off>(spm::op::erase, at);
|
||||||
|
settle();
|
||||||
|
return at;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erase the whole application, top-down like the oracle: the loop bound is a
|
||||||
|
// compare with zero, and the returned 0 is the address every caller wants
|
||||||
|
// next.
|
||||||
|
[[gnu::noinline]] std::uint16_t erase_application()
|
||||||
|
{
|
||||||
|
std::uint16_t at = app_end;
|
||||||
|
do {
|
||||||
|
at = erase_below(at);
|
||||||
|
} while (at != 0);
|
||||||
|
return at;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[noreturn]] void run()
|
||||||
|
{
|
||||||
|
// A watchdog reset hands straight back to the application, as the
|
||||||
|
// reference loader does, rather than re-entering the bootloader.
|
||||||
|
if (avr::hw::mcusr::wdrf.test()) {
|
||||||
|
appjump();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 -
|
||||||
|
// only the divisor low byte and U2X0 need a store. The solver still does
|
||||||
|
// 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);
|
||||||
|
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::ucsr0a::write(avr::hw::ucsr0a::u2x0(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activation: 3x'@', each inside the config page's timeout window
|
||||||
|
// (floored so a corrupt page cannot lock the loader out); anything else -
|
||||||
|
// including silence - hands over.
|
||||||
|
window = avr::flash_load(flash_ptr(app_end + 2)) | act_min;
|
||||||
|
for (std::uint8_t k = 3; k; --k) {
|
||||||
|
if (rx() != knock) {
|
||||||
|
appjump();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window = comm_window;
|
||||||
|
|
||||||
|
// Password gate (config page from app_end+3, 0xff-terminated; a blank
|
||||||
|
// 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
|
||||||
|
// emergency erase behind two confirms. On pass the info block goes out;
|
||||||
|
// the emergency path skips it and drops into the command loop.
|
||||||
|
std::uint16_t at = app_end + 3;
|
||||||
|
std::uint8_t mask = 0xff;
|
||||||
|
for (;;) {
|
||||||
|
std::uint8_t expected = avr::flash_load(flash_ptr(at)) & mask;
|
||||||
|
++at;
|
||||||
|
if (expected == 0xff) {
|
||||||
|
send_block(false, reinterpret_cast<std::uint16_t>(info.data()), info.size());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::uint8_t got = rx();
|
||||||
|
if (got == 0) {
|
||||||
|
if (mask == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rcnf() != confirm || rcnf() != confirm) {
|
||||||
|
appjump();
|
||||||
|
}
|
||||||
|
std::uint16_t a = erase_application();
|
||||||
|
do {
|
||||||
|
eeput(a, 0xff);
|
||||||
|
} while (++a <= eeprom_end);
|
||||||
|
erase_below(app_end + page);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (got != expected) {
|
||||||
|
mask = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
tx(confirm); // Mainloop ready
|
||||||
|
const std::uint8_t command = rx();
|
||||||
|
switch (command) {
|
||||||
|
case 'f': // read application flash, one page per host '!'
|
||||||
|
for (std::uint16_t a = 0; a < app_end; a += page) {
|
||||||
|
if (rx() != confirm) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
send_block(false, a, page);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'e': // read EEPROM, one page per host '!', until the host stops
|
||||||
|
for (std::uint16_t a = 0;; a += page) {
|
||||||
|
if (rx() != confirm) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
send_block(true, a, page);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'F': { // erase the application, then take pages behind '?'
|
||||||
|
std::uint16_t a = erase_application();
|
||||||
|
for (; rcnf() == confirm; a += page) {
|
||||||
|
store_flash_page(a);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'E': // take EEPROM pages behind '?', each write host-paced
|
||||||
|
for (std::uint16_t a = 0; rcnf() == confirm;) {
|
||||||
|
std::uint8_t count = page;
|
||||||
|
do {
|
||||||
|
eeput(a, rx());
|
||||||
|
++a;
|
||||||
|
} while (--count);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'c': // read the config page
|
||||||
|
read_config:
|
||||||
|
send_block(false, app_end, page);
|
||||||
|
break;
|
||||||
|
case 'C': // replace the config page, then echo it back to verify
|
||||||
|
if (rcnf() != confirm) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
store_flash_page(erase_below(app_end + page));
|
||||||
|
goto read_config;
|
||||||
|
default: // 'q' or any other byte runs the application
|
||||||
|
appjump();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace tsb
|
||||||
|
|
||||||
|
// Reset lands at the boot section base (BOOTRST): the entry stub in .vectors
|
||||||
|
// is laid first and does the one line of crt a crt-less image needs.
|
||||||
|
template struct avr::startup::entry<tsb::run, avr::startup::stack::hardware>;
|
||||||
149
tsb/tsb_pure.cpp
149
tsb/tsb_pure.cpp
@@ -1,10 +1,10 @@
|
|||||||
// TinySafeBoot on libavr — tier 1: pure, idiomatic C++.
|
// TinySafeBoot on libavr - tier 1: pure, idiomatic C++.
|
||||||
//
|
//
|
||||||
// A serial flash bootloader for the ATmega328P boot section, reimplementing the
|
// A serial flash bootloader for the ATmega328P boot section, reimplementing the
|
||||||
// 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 and every SPM/EEPROM lock
|
||||||
// are libavr's to handle; the only attribute is the naked reset entry that
|
// are libavr's to handle; the only attribute is the naked reset entry that
|
||||||
@@ -20,16 +20,29 @@ namespace ee = avr::eeprom;
|
|||||||
|
|
||||||
using dev = avr::device<{.clock = 16_MHz}>;
|
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.
|
||||||
using serial_t = dev::uart0<{.baud = 115200_Bd, .max_baud_error = 3_pct, .half_duplex = true}>;
|
// 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
|
||||||
|
// decade, so the override states that it is meant.
|
||||||
|
using serial_t = dev::uart0<{
|
||||||
|
.baud = 115200_Bd,
|
||||||
|
.allow_baud_error = true,
|
||||||
|
.half_duplex = true,
|
||||||
|
}>;
|
||||||
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 loader is purely polled - it never enables interrupts - so every SPM and
|
||||||
// EEPROM lock folds to nothing under this posture.
|
// EEPROM lock folds to nothing under this posture.
|
||||||
constexpr auto off = avr::irq::guard_policy::unused;
|
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.
|
// The handshake bytes, identical across every TSB host.
|
||||||
constexpr std::uint8_t confirm = '!';
|
constexpr std::uint8_t confirm = '!';
|
||||||
constexpr std::uint8_t request = '?';
|
constexpr std::uint8_t request = '?';
|
||||||
@@ -50,26 +63,48 @@ constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
|||||||
// The 16-byte device-info block the host reads on activation. A flash_table
|
// 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).
|
// keeps it in progmem with no .data image (there is no crt to copy one).
|
||||||
// clang-format off
|
// clang-format off
|
||||||
inline constexpr std::array<std::uint8_t, 16> info_data = {
|
inline constexpr auto info_data = std::to_array<std::uint8_t>({
|
||||||
'T', 'S', 'B',
|
'T', 'S', 'B',
|
||||||
build_date & 0xFF, build_date >> 8,
|
build_date & 0xFF, build_date >> 8,
|
||||||
0xF3, // status byte (native-UART fixed-baud lineage)
|
0xF3, // status byte (native-UART fixed-baud lineage)
|
||||||
0x1E, 0x95, 0x0F, // ATmega328P signature
|
avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2],
|
||||||
page / 2, // page size in words
|
page / 2, // page size in words
|
||||||
(app_end / 2) & 0xFF, (app_end / 2) >> 8, // app-flash boundary, words
|
(app_end / 2) & 0xFF, (app_end / 2) >> 8, // app-flash boundary, words
|
||||||
eeprom_end & 0xFF, eeprom_end >> 8,
|
eeprom_end & 0xFF, eeprom_end >> 8,
|
||||||
0xAA, 0xAA, // ATmega processor-type marker (bytes 14 == 15)
|
0xAA, 0xAA, // ATmega processor-type marker (bytes 14 == 15)
|
||||||
};
|
});
|
||||||
// clang-format on
|
// clang-format on
|
||||||
using info = avr::flash_table<info_data>;
|
using info = avr::flash_table<info_data>;
|
||||||
|
|
||||||
// Blocking byte read/write over the one-wire line: read() releases the line to
|
// The lockout-proof floor for the receive window: the oracle's F_CPU/1MHz, so
|
||||||
// the receiver, write() takes it and holds it until the frame is out.
|
// 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
|
||||||
|
// to clear a .bss image, and run() stores it before the first receive.
|
||||||
|
[[gnu::section(".noinit")]] std::uint8_t window;
|
||||||
|
|
||||||
|
// Bounded byte read over the one-wire line - read() releases the line to the
|
||||||
|
// receiver - answering 0 on silence. That 0 falls through every compare below:
|
||||||
|
// not a knock, not a confirm, not a command, so a silent host unwinds the
|
||||||
|
// loader to the application from anywhere and a mid-session cable pull cannot
|
||||||
|
// wedge it. The oracle lists that timeout among its own fixes, and a blocking
|
||||||
|
// read is how a tier loses it.
|
||||||
std::uint8_t rx()
|
std::uint8_t rx()
|
||||||
{
|
{
|
||||||
return serial.read_blocking();
|
std::uint16_t outer = static_cast<std::uint16_t>(window) << 8;
|
||||||
|
do {
|
||||||
|
std::uint8_t fine = 0;
|
||||||
|
do {
|
||||||
|
if (auto byte = serial.read()) {
|
||||||
|
return *byte;
|
||||||
|
}
|
||||||
|
} while (--fine);
|
||||||
|
} while (--outer);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// write() takes the line and holds it until the frame is out.
|
||||||
void tx(std::uint8_t byte)
|
void tx(std::uint8_t byte)
|
||||||
{
|
{
|
||||||
serial.write(byte);
|
serial.write(byte);
|
||||||
@@ -83,14 +118,16 @@ const std::uint8_t *flash_ptr(std::uint16_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)
|
||||||
{
|
{
|
||||||
while (count--)
|
while (count--) {
|
||||||
tx(avr::flash_load(flash_ptr(addr++)));
|
tx(avr::flash_load(flash_ptr(addr++)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void send_eeprom(std::uint16_t addr, std::uint8_t count)
|
void send_eeprom(std::uint16_t addr, std::uint8_t count)
|
||||||
{
|
{
|
||||||
while (count--)
|
while (count--) {
|
||||||
tx(ee::read(addr++));
|
tx(ee::read<no_spm>(addr++));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompt the host with '?' and report whether it answered '!'.
|
// Prompt the host with '?' and report whether it answered '!'.
|
||||||
@@ -101,32 +138,33 @@ bool request_confirm()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stream one page from the host straight into the already-erased flash page at
|
// Stream one page from the host straight into the already-erased flash page at
|
||||||
// `addr`, filling the SPM word buffer low byte then high — no SRAM staging, so
|
// `addr`, filling the SPM word buffer low byte then high - no SRAM staging, so
|
||||||
// receiving and programming are the same loop.
|
// receiving and programming are the same loop.
|
||||||
void store_flash_page(std::uint16_t addr)
|
void store_flash_page(std::uint16_t 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();
|
std::uint8_t lo = rx();
|
||||||
std::uint8_t hi = rx();
|
std::uint8_t hi = rx();
|
||||||
spm::fill<off>(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<off>(addr);
|
spm::write_page<spm::from::boot_section, off>(addr); // blocking: waits the write out
|
||||||
spm::wait();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream one page from the host straight into EEPROM, byte by byte.
|
// Stream one page from the host straight into EEPROM, byte by byte.
|
||||||
void store_eeprom_page(std::uint16_t addr)
|
void store_eeprom_page(std::uint16_t addr)
|
||||||
{
|
{
|
||||||
for (std::uint16_t i = 0; i < page; ++i)
|
for (std::uint16_t i = 0; i < page; ++i) {
|
||||||
ee::write<off>(addr + i, rx());
|
ee::write<off, no_spm>(addr + i, rx());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Erase one flash page and wait it out — the erase step shared by the whole-app
|
// Erase one flash page, waited out by the blocking spelling - the erase step
|
||||||
// erase, the config-page rewrite and the emergency wipe.
|
// shared by the whole-app erase, the config-page rewrite and the emergency
|
||||||
|
// wipe.
|
||||||
void erase_page(std::uint16_t addr)
|
void erase_page(std::uint16_t addr)
|
||||||
{
|
{
|
||||||
spm::erase_page<off>(addr);
|
spm::erase_page<spm::from::boot_section, off>(addr);
|
||||||
spm::wait();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Erase the whole application, one page at a time, top-down as the reference
|
// Erase the whole application, one page at a time, top-down as the reference
|
||||||
@@ -157,8 +195,9 @@ extern "C" [[noreturn]] void tsb_app();
|
|||||||
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) {
|
||||||
if (rx() != confirm)
|
if (rx() != confirm) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
send_flash(a, page);
|
send_flash(a, page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,8 +206,9 @@ void read_flash()
|
|||||||
void read_eeprom()
|
void read_eeprom()
|
||||||
{
|
{
|
||||||
for (std::uint16_t a = 0;; a += page) {
|
for (std::uint16_t a = 0;; a += page) {
|
||||||
if (rx() != confirm)
|
if (rx() != confirm) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
send_eeprom(a, page);
|
send_eeprom(a, page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,22 +218,25 @@ void read_eeprom()
|
|||||||
void write_flash()
|
void write_flash()
|
||||||
{
|
{
|
||||||
erase_application();
|
erase_application();
|
||||||
for (std::uint16_t a = 0; request_confirm(); a += page)
|
for (std::uint16_t a = 0; request_confirm(); a += page) {
|
||||||
store_flash_page(a);
|
store_flash_page(a);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'E': take pages the host offers behind '?' into EEPROM.
|
// 'E': 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) {
|
||||||
store_eeprom_page(a);
|
store_eeprom_page(a);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'C': replace the config page, then echo it back for the host to verify.
|
// 'C': 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()) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
erase_page(app_end);
|
erase_page(app_end);
|
||||||
store_flash_page(app_end);
|
store_flash_page(app_end);
|
||||||
spm::rww_enable<off>();
|
spm::rww_enable<off>();
|
||||||
@@ -206,8 +249,9 @@ void write_config()
|
|||||||
void emergency_erase()
|
void emergency_erase()
|
||||||
{
|
{
|
||||||
erase_application();
|
erase_application();
|
||||||
for (std::uint16_t a = 0; a <= eeprom_end; ++a)
|
for (std::uint16_t a = 0; a <= eeprom_end; ++a) {
|
||||||
ee::write<off>(a, 0xff);
|
ee::write<off, no_spm>(a, 0xff);
|
||||||
|
}
|
||||||
erase_page(app_end);
|
erase_page(app_end);
|
||||||
spm::rww_enable<off>();
|
spm::rww_enable<off>();
|
||||||
}
|
}
|
||||||
@@ -222,14 +266,18 @@ 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 + 3);; ++pw) {
|
||||||
std::uint8_t expected = avr::flash_load(pw);
|
std::uint8_t expected = avr::flash_load(pw);
|
||||||
if (expected == 0xff)
|
if (expected == 0xff) {
|
||||||
return gate::pass;
|
return gate::pass;
|
||||||
|
}
|
||||||
std::uint8_t got = rx();
|
std::uint8_t got = rx();
|
||||||
if (got == 0)
|
if (got == 0) {
|
||||||
return gate::emergency;
|
return gate::emergency;
|
||||||
if (got != expected)
|
}
|
||||||
for (;;)
|
if (got != expected) {
|
||||||
|
for (;;) {
|
||||||
rx();
|
rx();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,21 +285,25 @@ gate password_gate()
|
|||||||
{
|
{
|
||||||
// A watchdog reset hands straight back to the application, as the reference
|
// A watchdog reset hands straight back to the application, as the reference
|
||||||
// loader does, rather than re-entering the bootloader.
|
// loader does, rather than re-entering the bootloader.
|
||||||
if (avr::hw::mcusr::wdrf.test())
|
if (avr::hw::mcusr::wdrf.test()) {
|
||||||
appjump();
|
appjump();
|
||||||
|
}
|
||||||
|
|
||||||
avr::init<serial_t>();
|
avr::init<serial_t>();
|
||||||
|
|
||||||
// Activation: the host knocks three '@' inside a window whose length is the
|
// Activation: the host knocks three '@' 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.
|
// loader out. An idle port times out and boots the application; the same
|
||||||
__uint24 idle = static_cast<__uint24>(avr::flash_load(flash_ptr(app_end + 2)) | 16) << 16;
|
// window then bounds every receive of the session.
|
||||||
|
window = avr::flash_load(flash_ptr(app_end + 2)) | act_min;
|
||||||
|
__uint24 idle = static_cast<__uint24>(window) << 16;
|
||||||
std::uint8_t knocks = 0;
|
std::uint8_t knocks = 0;
|
||||||
while (knocks < 3) {
|
while (knocks < 3) {
|
||||||
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) {
|
||||||
appjump();
|
appjump();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (password_gate()) {
|
switch (password_gate()) {
|
||||||
@@ -259,8 +311,9 @@ gate password_gate()
|
|||||||
send_flash(reinterpret_cast<std::uint16_t>(info::storage.data()), info::size());
|
send_flash(reinterpret_cast<std::uint16_t>(info::storage.data()), info::size());
|
||||||
break;
|
break;
|
||||||
case gate::emergency:
|
case gate::emergency:
|
||||||
if (!request_confirm() || !request_confirm())
|
if (!request_confirm() || !request_confirm()) {
|
||||||
appjump();
|
appjump();
|
||||||
|
}
|
||||||
emergency_erase();
|
emergency_erase();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -295,14 +348,6 @@ gate password_gate()
|
|||||||
} // namespace
|
} // namespace
|
||||||
} // namespace tsb
|
} // namespace tsb
|
||||||
|
|
||||||
// Reset lands here: BOOTRST vectors to the boot section base and .vectors is
|
// Reset lands at the boot section base (BOOTRST): the entry stub in .vectors
|
||||||
// laid first, so this is the first instruction executed. No crt ran, so set the
|
// is laid first and does the one line of crt a crt-less image needs.
|
||||||
// stack pointer before anything is called.
|
template struct avr::startup::entry<tsb::run, avr::startup::stack::hardware>;
|
||||||
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
|
|
||||||
{
|
|
||||||
SP = RAMEND;
|
|
||||||
// The one line of crt this loader needs: compiled code assumes
|
|
||||||
// __zero_reg__ (r1) is 0, and power-on registers are undefined.
|
|
||||||
asm volatile("clr __zero_reg__");
|
|
||||||
tsb::run();
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
// TinySafeBoot on libavr — tier 2: C++ with compiler trickery, no assembly.
|
// TinySafeBoot on libavr - tier 2: C++ with compiler trickery, no assembly.
|
||||||
//
|
//
|
||||||
// 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 — in pure C++, 526 bytes: 14 over the 512-byte
|
// config/flash/EEPROM read-write - in pure C++, a little over the 512-byte boot
|
||||||
// boot section the hand-written oracle fits, from 168 over at this tier's first
|
// section the hand-written oracle fits (oracle/README.md holds what each tier
|
||||||
// floor. The structure mirrors the oracle's: a handful of tiny noinline
|
// measures). The structure mirrors the oracle's: a handful of tiny noinline
|
||||||
// primitives sharing one whole-loader register allocation, expressed as global
|
// primitives sharing one whole-loader register allocation, expressed as global
|
||||||
// register variables so no helper ever saves, spills, or reloads any of it.
|
// register variables so no helper ever saves, spills, or reloads any of it.
|
||||||
//
|
//
|
||||||
// The register protocol (all call-saved, so calls preserve them by ABI):
|
// The register protocol (all call-saved, so calls preserve them by ABI):
|
||||||
// Y (r28:r29) g_addr the walked flash/EEPROM address — adiw-able
|
// Y (r28:r29) g_addr the walked flash/EEPROM address - adiw-able
|
||||||
// r16 g_cnt byte countdown of the running block — ldi-able
|
// r16 g_cnt byte countdown of the running block - ldi-able
|
||||||
// r7 g_window rx timeout, roughly 30 ms units at 16 MHz
|
// r7 g_window rx timeout, roughly 30 ms units at 16 MHz
|
||||||
// r6 g_receiving one-wire direction latch, cleared at bring-up
|
// r6 g_receiving one-wire direction latch, cleared at bring-up
|
||||||
// (power-on registers are undefined)
|
// (power-on registers are undefined)
|
||||||
@@ -18,10 +18,10 @@
|
|||||||
// GCC 16.1 miscompiles stores into global register variables: an update whose
|
// GCC 16.1 miscompiles stores into global register variables: an update whose
|
||||||
// remaining uses all hide inside callees is deleted whenever a CALL follows it
|
// remaining uses all hide inside callees is deleted whenever a CALL follows it
|
||||||
// before any jump/ret (the backend's liveness walk lumps fixed registers with
|
// before any jump/ret (the backend's liveness walk lumps fixed registers with
|
||||||
// call-clobbered ones — minimal repro in libavr's
|
// call-clobbered ones - minimal repro in libavr's
|
||||||
// local/scratch/probes/gcc-avr-globalreg-repro.cpp, lessons.md entry). Every
|
// test/upstream/gcc-avr-globalreg-repro.cpp). Every
|
||||||
// g_* update below therefore sits where a *local* read or a jump/ret follows
|
// g_* update below therefore sits where a *local* read or a jump/ret follows
|
||||||
// it — the helpers advance g_addr immediately before returning, and rx()
|
// it - the helpers advance g_addr immediately before returning, and rx()
|
||||||
// re-floors the window on every call instead of storing the floored value
|
// re-floors the window on every call instead of storing the floored value
|
||||||
// once. The layout is load-bearing; do not "simplify" it.
|
// once. The layout is load-bearing; do not "simplify" it.
|
||||||
//
|
//
|
||||||
@@ -41,10 +41,16 @@ namespace hw = avr::hw;
|
|||||||
namespace tsb {
|
namespace tsb {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// The loader is purely polled — it never enables interrupts — so every SPM and
|
// The loader is purely polled - it never enables interrupts - so every SPM and
|
||||||
// EEPROM lock folds to nothing under this posture.
|
// EEPROM lock folds to nothing under this posture.
|
||||||
constexpr auto off = avr::irq::guard_policy::unused;
|
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 confirm = '!';
|
||||||
constexpr std::uint8_t request = '?';
|
constexpr std::uint8_t request = '?';
|
||||||
constexpr std::uint8_t knock = '@';
|
constexpr std::uint8_t knock = '@';
|
||||||
@@ -57,28 +63,34 @@ 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 = spm::flash_bytes - boot_bytes - page;
|
||||||
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
|
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).
|
// Lockout-proof floor for the activation window: the oracle's F_CPU/1MHz, so
|
||||||
constexpr std::uint8_t act_min = 16;
|
// 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.
|
// Post-activation window: the host gets seconds, not milliseconds, mid-session.
|
||||||
constexpr std::uint8_t comm_window = 200;
|
constexpr std::uint8_t comm_window = 200;
|
||||||
|
|
||||||
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
||||||
|
|
||||||
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
||||||
constexpr auto baud = avr::uart::detail::solve_baud(16_MHz, 115200_Bd);
|
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.
|
// The 16-byte device-info block, streamed out on activation.
|
||||||
// clang-format off
|
// clang-format off
|
||||||
[[gnu::progmem]] constexpr std::uint8_t info[16] = {
|
[[gnu::progmem]] constexpr auto info = std::to_array<std::uint8_t>({
|
||||||
'T', 'S', 'B',
|
'T', 'S', 'B',
|
||||||
build_date & 0xFF, build_date >> 8,
|
build_date & 0xFF, build_date >> 8,
|
||||||
0xF3, // status: native-UART fixed-baud lineage
|
0xF3, // status: native-UART fixed-baud lineage
|
||||||
0x1E, 0x95, 0x0F, // ATmega328P signature
|
avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2],
|
||||||
page / 2, // page size in words
|
page / 2, // page size in words
|
||||||
(app_end / 2) & 0xFF, (app_end / 2) >> 8,
|
(app_end / 2) & 0xFF, (app_end / 2) >> 8,
|
||||||
eeprom_end & 0xFF, eeprom_end >> 8,
|
eeprom_end & 0xFF, eeprom_end >> 8,
|
||||||
0xAA, 0xAA,
|
0xAA, 0xAA,
|
||||||
};
|
});
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
register std::uint16_t g_addr asm("r28");
|
register std::uint16_t g_addr asm("r28");
|
||||||
@@ -93,8 +105,8 @@ const std::uint8_t *flash_ptr(std::uint16_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
|
||||||
// command — so a silent host unwinds the loader to the application from
|
// command - so a silent host unwinds the loader to the application from
|
||||||
// anywhere, and a mid-session cable pull cannot wedge it.
|
// anywhere, and a mid-session cable pull cannot wedge it.
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t rx()
|
[[gnu::noinline, gnu::noclone]] std::uint8_t rx()
|
||||||
{
|
{
|
||||||
@@ -102,24 +114,25 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|||||||
g_receiving = 1;
|
g_receiving = 1;
|
||||||
hw::ucsr0b::write(hw::ucsr0b::rxen0(1)); // RXEN0 alone: release and listen
|
hw::ucsr0b::write(hw::ucsr0b::rxen0(1)); // RXEN0 alone: release and listen
|
||||||
}
|
}
|
||||||
// act_min ORs in here, per call, not once into g_window at setup — the
|
// act_min ORs in here, per call, not once into g_window at setup - the
|
||||||
// one placement the global-register-store miscompile cannot delete.
|
// one placement the global-register-store miscompile cannot delete.
|
||||||
std::uint16_t outer = static_cast<std::uint16_t>(g_window | act_min) << 8;
|
std::uint16_t outer = static_cast<std::uint16_t>(g_window | act_min) << 8;
|
||||||
do {
|
do {
|
||||||
std::uint8_t fine = 0;
|
std::uint8_t fine = 0;
|
||||||
do {
|
do {
|
||||||
auto status = hw::ucsr0a::read();
|
auto status = hw::ucsr0a::read();
|
||||||
if (status & hw::ucsr0a::rxc0(1).value)
|
if (status & hw::ucsr0a::rxc0(1).value) {
|
||||||
return hw::udr0::read();
|
return hw::udr0::read();
|
||||||
|
}
|
||||||
} while (--fine);
|
} while (--fine);
|
||||||
} while (--outer);
|
} while (--outer);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// One-wire transmit: take the line (TXEN0 alone — the receiver must be off
|
// One-wire transmit: take the line (TXEN0 alone - the receiver must be off
|
||||||
// while driving) on a direction change, with a turn-around guard so a shorted
|
// while driving) on a direction change, with a turn-around guard so a shorted
|
||||||
// peer can switch first; then hold the line until the whole frame is out
|
// peer can switch first; then hold the line until the whole frame is out
|
||||||
// (TXC0, not UDRE0 — the stop bit must be on the wire before a caller may
|
// (TXC0, not UDRE0 - the stop bit must be on the wire before a caller may
|
||||||
// release the line), and W1C TXC0 by storing the sampled status back, which
|
// release the line), and W1C TXC0 by storing the sampled status back, which
|
||||||
// keeps U2X0.
|
// keeps U2X0.
|
||||||
[[gnu::noinline, gnu::noclone]] void tx(std::uint8_t byte)
|
[[gnu::noinline, gnu::noclone]] void tx(std::uint8_t byte)
|
||||||
@@ -127,8 +140,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|||||||
if (g_receiving) {
|
if (g_receiving) {
|
||||||
g_receiving = 0;
|
g_receiving = 0;
|
||||||
hw::ucsr0b::write(hw::ucsr0b::txen0(1));
|
hw::ucsr0b::write(hw::ucsr0b::txen0(1));
|
||||||
for (std::uint8_t guard = 46; guard; --guard)
|
avr::delay::cycles<guard_cycles>();
|
||||||
;
|
|
||||||
}
|
}
|
||||||
hw::udr0::write(byte);
|
hw::udr0::write(byte);
|
||||||
std::uint8_t status;
|
std::uint8_t status;
|
||||||
@@ -145,7 +157,7 @@ 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 header).
|
||||||
[[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));
|
std::uint8_t byte = avr::flash_load(flash_ptr(g_addr));
|
||||||
@@ -153,18 +165,18 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|||||||
return byte;
|
return byte;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(g_addr);
|
std::uint8_t byte = ee::read<no_spm>(g_addr);
|
||||||
++g_addr;
|
++g_addr;
|
||||||
return byte;
|
return byte;
|
||||||
}
|
}
|
||||||
|
|
||||||
// One EEPROM byte → [g_addr++].
|
// One EEPROM byte -> [g_addr++].
|
||||||
[[gnu::noinline, gnu::noclone]] void eewr(std::uint8_t byte)
|
[[gnu::noinline, gnu::noclone]] void eewr(std::uint8_t byte)
|
||||||
{
|
{
|
||||||
ee::write<off>(g_addr, byte);
|
ee::write<off, no_spm>(g_addr, byte);
|
||||||
++g_addr;
|
++g_addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +188,7 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|||||||
} while (--g_cnt);
|
} while (--g_cnt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait out a running SPM op, then re-open the RWW section — after every page
|
// Wait out a running SPM op, then re-open the RWW section - after every page
|
||||||
// op and before handing over, as the oracle does.
|
// op and before handing over, as the oracle does.
|
||||||
[[gnu::noinline, gnu::noclone]] void settle()
|
[[gnu::noinline, gnu::noclone]] void settle()
|
||||||
{
|
{
|
||||||
@@ -198,12 +210,12 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
[[gnu::noinline, gnu::noclone]] void erase_below()
|
[[gnu::noinline, gnu::noclone]] void erase_below()
|
||||||
{
|
{
|
||||||
g_addr -= page;
|
g_addr -= page;
|
||||||
spm::erase_page<off>(g_addr);
|
spm::command<off>(spm::op::erase, g_addr);
|
||||||
settle();
|
settle();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Erase the whole application, top-down like the oracle: the loop bound is a
|
// Erase the whole application, top-down like the oracle: the loop bound is a
|
||||||
// compare with zero, and g_addr = 0 — the value every caller wants next — is
|
// compare with zero, and g_addr = 0 - the value every caller wants next - is
|
||||||
// handed back for free.
|
// handed back for free.
|
||||||
[[gnu::noinline, gnu::noclone]] void erase_application()
|
[[gnu::noinline, gnu::noclone]] void erase_application()
|
||||||
{
|
{
|
||||||
@@ -214,18 +226,19 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stream one host page into the erased flash page at g_addr (SPM word buffer,
|
// Stream one host page into the erased flash page at g_addr (SPM word buffer,
|
||||||
// low byte then high) — no SRAM staging, receive and program are one loop.
|
// low byte then high) - no SRAM staging, receive and program are one loop.
|
||||||
// g_addr is left at the next page base.
|
// g_addr is left at the next page base.
|
||||||
[[gnu::noinline, gnu::noclone]] void store_flash()
|
[[gnu::noinline, gnu::noclone]] void store_flash()
|
||||||
{
|
{
|
||||||
|
const auto open = spm::page::begin<spm::from::boot_section, off>(g_addr);
|
||||||
g_cnt = page / 2;
|
g_cnt = page / 2;
|
||||||
do {
|
do {
|
||||||
std::uint16_t word = rx();
|
std::uint16_t word = rx();
|
||||||
word |= static_cast<std::uint16_t>(rx()) << 8;
|
word |= static_cast<std::uint16_t>(rx()) << 8;
|
||||||
spm::fill<off>(g_addr, word);
|
spm::fill<off>(open, g_addr, word);
|
||||||
g_addr += 2;
|
g_addr += 2;
|
||||||
} while (--g_cnt);
|
} while (--g_cnt);
|
||||||
spm::write_page<off>(g_addr - page);
|
spm::command<off>(spm::op::write, g_addr - page);
|
||||||
settle();
|
settle();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,14 +246,15 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
{
|
{
|
||||||
// A watchdog reset hands straight back to the application, as the
|
// A watchdog reset hands straight back to the application, as the
|
||||||
// reference loader does, rather than re-entering the bootloader.
|
// reference loader does, rather than re-entering the bootloader.
|
||||||
if (hw::mcusr::wdrf.test())
|
if (hw::mcusr::wdrf.test()) {
|
||||||
appjump();
|
appjump();
|
||||||
|
}
|
||||||
|
|
||||||
// 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 rx()/tx() raise RXEN0/TXEN0 on first use — only the divisor low
|
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use - only the divisor low
|
||||||
// byte and U2X0 need a store. The library still does the datasheet work.
|
// byte and U2X0 need a store. The library still does the datasheet work.
|
||||||
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
||||||
hw::reg<"UBRR0">::write(static_cast<std::uint8_t>(baud.ubrr));
|
hw::ubrr0::write(static_cast<std::uint8_t>(baud.ubrr));
|
||||||
hw::ucsr0a::write(hw::ucsr0a::u2x0(1));
|
hw::ucsr0a::write(hw::ucsr0a::u2x0(1));
|
||||||
// General-purpose registers are undefined at power-on (no crt zeroes them);
|
// General-purpose registers are undefined at power-on (no crt zeroes them);
|
||||||
// the direction latch must start "not receiving" so the first rx() enables
|
// the direction latch must start "not receiving" so the first rx() enables
|
||||||
@@ -248,13 +262,15 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
// same reason.
|
// same reason.
|
||||||
g_receiving = 0;
|
g_receiving = 0;
|
||||||
|
|
||||||
// Activation: 3×'@', each inside the config page's timeout window (rx
|
// Activation: 3x'@', each inside the config page's timeout window (rx
|
||||||
// floors it so a corrupt page cannot lock the loader out); anything else —
|
// floors it so a corrupt page cannot lock the loader out); anything else -
|
||||||
// including silence — hands over.
|
// including silence - hands over.
|
||||||
g_window = avr::flash_load(flash_ptr(app_end + 2));
|
g_window = avr::flash_load(flash_ptr(app_end + 2));
|
||||||
for (std::uint8_t k = 3; k; --k)
|
for (std::uint8_t k = 3; 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 app_end+3, 0xff-terminated; a blank
|
||||||
@@ -268,17 +284,19 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask;
|
std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask;
|
||||||
++g_addr;
|
++g_addr;
|
||||||
if (expected == 0xff) {
|
if (expected == 0xff) {
|
||||||
g_addr = reinterpret_cast<std::uint16_t>(&info[0]);
|
g_addr = reinterpret_cast<std::uint16_t>(info.data());
|
||||||
g_cnt = sizeof(info);
|
g_cnt = sizeof(info);
|
||||||
sendf();
|
sendf();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
std::uint8_t got = rx();
|
std::uint8_t got = rx();
|
||||||
if (got == 0) {
|
if (got == 0) {
|
||||||
if (mask == 0)
|
if (mask == 0) {
|
||||||
continue;
|
continue;
|
||||||
if (rcnf() != confirm || rcnf() != confirm)
|
}
|
||||||
|
if (rcnf() != confirm || rcnf() != confirm) {
|
||||||
appjump();
|
appjump();
|
||||||
|
}
|
||||||
erase_application(); // leaves g_addr = 0 for the EEPROM walk
|
erase_application(); // leaves g_addr = 0 for the EEPROM walk
|
||||||
do {
|
do {
|
||||||
eewr(0xff);
|
eewr(0xff);
|
||||||
@@ -287,8 +305,9 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
erase_below();
|
erase_below();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (got != expected)
|
if (got != expected) {
|
||||||
mask = 0;
|
mask = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
@@ -297,23 +316,27 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
switch (rx()) {
|
switch (rx()) {
|
||||||
case 'f': // read application flash, one page per host '!'
|
case 'f': // read application flash, one page per host '!'
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (rx() != confirm)
|
if (rx() != confirm) {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
g_cnt = page;
|
g_cnt = page;
|
||||||
sendf();
|
sendf();
|
||||||
if (g_addr >= app_end)
|
if (g_addr >= app_end) {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'F': // erase the application, then take pages behind '?'
|
case 'F': // 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 'e': // read EEPROM, one page per host '!', until the host stops
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (rx() != confirm)
|
if (rx() != confirm) {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
g_cnt = page;
|
g_cnt = page;
|
||||||
do {
|
do {
|
||||||
tx(eerd());
|
tx(eerd());
|
||||||
@@ -335,8 +358,9 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
sendf();
|
sendf();
|
||||||
break;
|
break;
|
||||||
case 'C': // replace the config page, then echo it back to verify
|
case 'C': // replace the config page, then echo it back to verify
|
||||||
if (rcnf() != confirm)
|
if (rcnf() != confirm) {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
g_addr = app_end + page;
|
g_addr = app_end + page;
|
||||||
erase_below(); // leaves g_addr = app_end, the store target
|
erase_below(); // leaves g_addr = app_end, the store target
|
||||||
store_flash();
|
store_flash();
|
||||||
@@ -350,14 +374,6 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
} // namespace
|
} // namespace
|
||||||
} // namespace tsb
|
} // namespace tsb
|
||||||
|
|
||||||
// Reset lands here: BOOTRST vectors to the boot section base and .vectors is
|
// Reset lands at the boot section base (BOOTRST): the entry stub in .vectors
|
||||||
// laid first, so this is the first instruction executed. No crt ran, so set
|
// is laid first and does the one line of crt a crt-less image needs.
|
||||||
// the stack pointer before anything is called.
|
template struct avr::startup::entry<tsb::run, avr::startup::stack::hardware>;
|
||||||
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
|
|
||||||
{
|
|
||||||
SP = RAMEND;
|
|
||||||
// The one line of crt this loader needs: compiled code assumes
|
|
||||||
// __zero_reg__ (r1) is 0, and power-on registers are undefined.
|
|
||||||
asm volatile("clr __zero_reg__");
|
|
||||||
tsb::run();
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user