Compare commits
4 Commits
main
...
57d94cf631
| Author | SHA1 | Date | |
|---|---|---|---|
| 57d94cf631 | |||
| 8203a24f33 | |||
| 2906da3272 | |||
| 64c1e484b5 |
@@ -7,9 +7,8 @@ TabWidth: 4
|
|||||||
UseTab: ForIndentation
|
UseTab: ForIndentation
|
||||||
AlignEscapedNewlines: DontAlign
|
AlignEscapedNewlines: DontAlign
|
||||||
AllowShortFunctionsOnASingleLine: Empty
|
AllowShortFunctionsOnASingleLine: Empty
|
||||||
BreakTemplateDeclarations: Yes
|
AlwaysBreakTemplateDeclarations: true
|
||||||
BreakBeforeBraces: Custom
|
BreakBeforeBraces: Custom
|
||||||
BraceWrapping:
|
BraceWrapping:
|
||||||
AfterFunction: true
|
AfterFunction: true
|
||||||
InsertBraces: true
|
|
||||||
...
|
...
|
||||||
|
|||||||
38
.clangd
38
.clangd
@@ -1,38 +0,0 @@
|
|||||||
# 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,11 +1,8 @@
|
|||||||
# Line endings are the repository's, not the editing machine's: this checkout
|
*.h eol=lf
|
||||||
# is reached from two hosts, and a file rewritten by a Windows tool comes back
|
*.hpp eol=lf
|
||||||
# with every line changed unless something says otherwise. Naming the source
|
*.c eol=lf
|
||||||
# extensions left Markdown, Python, shell and CMake to whatever the writing
|
*.cpp eol=lf
|
||||||
# tool defaulted to, which is CRLF on one of the two.
|
.git* eol=lf
|
||||||
* 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
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -12,10 +12,5 @@ Debug
|
|||||||
|
|
||||||
# CMake / clangd
|
# CMake / clangd
|
||||||
/build/
|
/build/
|
||||||
/local/
|
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
.cache/
|
.cache/
|
||||||
|
|
||||||
# Python
|
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "libavr"]
|
|
||||||
path = libavr
|
|
||||||
url = ../libavr.git
|
|
||||||
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"llvm-vs-code-extensions.vscode-clangd",
|
|
||||||
"ms-vscode.cmake-tools"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
36
.vscode/settings.json
vendored
36
.vscode/settings.json
vendored
@@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
// 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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
151
CMakeLists.txt
151
CMakeLists.txt
@@ -2,119 +2,52 @@ cmake_minimum_required(VERSION 3.28)
|
|||||||
|
|
||||||
project(tsb_libavr LANGUAGES CXX)
|
project(tsb_libavr LANGUAGES CXX)
|
||||||
|
|
||||||
# libavr rides as the pinned submodule; LIBAVR_ROOT (cache or environment)
|
# libavr from a local checkout (LIBAVR_ROOT) or the forge; the toolchain file
|
||||||
# overrides it for tandem development against a working tree. The toolchain
|
# comes from the same checkout via CMakePresets.json.
|
||||||
# file comes from the submodule via CMakePresets.json either way.
|
include(FetchContent)
|
||||||
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(NOT LIBAVR_ROOT)
|
if(LIBAVR_ROOT)
|
||||||
set(LIBAVR_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/libavr)
|
FetchContent_Declare(libavr SOURCE_DIR ${LIBAVR_ROOT})
|
||||||
|
else()
|
||||||
|
FetchContent_Declare(libavr GIT_REPOSITORY git@git.blackmark.me:avr/libavr.git GIT_TAG main)
|
||||||
endif()
|
endif()
|
||||||
if(NOT EXISTS ${LIBAVR_ROOT}/CMakeLists.txt)
|
FetchContent_MakeAvailable(libavr)
|
||||||
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
|
# The behavioral test drives the real TinySafeBoot wire protocol over a
|
||||||
# no exclusion: it is neither formatted nor ASCII-checked, being in neither
|
# simavr pty (as the host tools do) and actually flashes the device. The
|
||||||
# glob, which is the right answer for a vendored reference whose text is
|
# runner is a host program built at configure time against libsimavr; if it
|
||||||
# the artifact.
|
# or Python is missing, only the size tests run.
|
||||||
libavr_format_test()
|
find_program(_host_cc NAMES cc gcc)
|
||||||
|
find_package(Python3 COMPONENTS Interpreter)
|
||||||
# The behavioral tests drive the real wire protocols over a simavr pty
|
if(_host_cc AND Python3_FOUND)
|
||||||
# (as the host tools do) and actually flash the device. The runner is a
|
|
||||||
# host program built at configure time against libsimavr (C++23 - what the
|
|
||||||
# distribution's compiler speaks in full).
|
|
||||||
#
|
|
||||||
# **A host that cannot build it registers those tests anyway and skips
|
|
||||||
# them.** They used to be left out, which makes the suite a different size
|
|
||||||
# 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)
|
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_cxx} -std=c++23 -Wall -Wextra -O2
|
COMMAND ${_host_cc} -O2 -I/usr/include/simavr -I/usr/include/simavr/parts
|
||||||
-I/usr/include/simavr -I/usr/include/simavr/parts
|
-o ${TSB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/device.c
|
||||||
-o ${TSB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/device.cpp
|
|
||||||
-lsimavr -lsimavrparts -lelf
|
-lsimavr -lsimavrparts -lelf
|
||||||
RESULT_VARIABLE _dev_res ERROR_VARIABLE _dev_err)
|
RESULT_VARIABLE _dev_res ERROR_VARIABLE _dev_err)
|
||||||
if(NOT _dev_res EQUAL 0)
|
if(NOT _dev_res EQUAL 0)
|
||||||
# One bounded line of it: this becomes a single argument on a
|
message(STATUS "tsb_device not built (${_dev_err}) — protocol tests skipped")
|
||||||
# command line, and the reading has to say what stopped the build
|
unset(TSB_DEVICE)
|
||||||
# rather than that something did.
|
|
||||||
string(REGEX REPLACE "[\r\n\t]+" " " _dev_err "${_dev_err}")
|
|
||||||
string(REPLACE ";" "," _dev_err "${_dev_err}")
|
|
||||||
string(LENGTH "${_dev_err}" _dev_len)
|
|
||||||
if(_dev_len GREATER 240)
|
|
||||||
string(SUBSTRING "${_dev_err}" 0 240 _dev_err)
|
|
||||||
endif()
|
endif()
|
||||||
set(_tsb_absent "test/device.cpp does not build here: ${_dev_err}")
|
|
||||||
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()
|
endif()
|
||||||
|
|
||||||
# The ELF is only a container (symbols, section headers) and is never flashed -
|
# The TinySafeBoot protocol reimplemented on libavr in three variants that trade
|
||||||
# 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 entry sits in
|
# loader has no use for the crt or the vector table. The naked entry sits in
|
||||||
# .vectors, laid first, and runs - avr::startup::entry on the policy tier,
|
# .vectors, laid first, and runs. The boot base is FLASHEND+1 minus the section
|
||||||
# the experiment tiers' own naked stubs elsewhere, each documented in its
|
# size; the linker section-start and the source's boot_bytes agree.
|
||||||
# source. The boot base is FLASHEND+1 minus the section size; the linker
|
# tsb_asm — inline-asm variant, the headline: ≤512 B, the 512 B section.
|
||||||
# section-start and the source's boot_bytes agree. tsb_app is
|
# tsb_pure / tsb_tricks — pure-C++ and compiler-trickery variants, larger,
|
||||||
# the application's reset vector, pinned to 0 here so the loaders jump to a
|
# shown in the 1 KB section (BOOTSZ=10) they fit.
|
||||||
# named function; --pmem-wrap-around lets relaxation turn that absolute jump
|
|
||||||
# into the wrapped rjmp AVR's modulo-flash PC actually executes.
|
|
||||||
# All four implement the full oracle feature set (see oracle/README.md):
|
|
||||||
# watchdog bail, one-wire half-duplex, config-page activation timeout, password
|
|
||||||
# gate, emergency erase, config/flash/EEPROM read-write. They differ only in how,
|
|
||||||
# and the size gradient is the cost of that "how".
|
|
||||||
# tsb_asm - the tricks tier's C++ with exactly two routines in asm: the
|
|
||||||
# bounded rx and the page-store loop, the two whose remaining
|
|
||||||
# cost is the C ABI itself. Everything else, bring-up to
|
|
||||||
# dispatch, is C++ on libavr.
|
|
||||||
# tsb_tricks - no asm at all: the whole-loader register allocation lives in
|
|
||||||
# global register variables (Y walks the page pointer), every
|
|
||||||
# helper is a tiny noinline primitive placed by the
|
|
||||||
# global-register store rules, pages stream straight to
|
|
||||||
# SPM/EEPROM.
|
|
||||||
# tsb_pure - pure idiomatic libavr, one function per command, TU-local
|
|
||||||
# (internal linkage), streaming (no SRAM page buffer).
|
|
||||||
# tsb_policy - the policy floor: no inline assembly and no global register
|
|
||||||
# variables, which is philosophy #5's own bound, and the
|
|
||||||
# measured evidence that the 512 B fit is a property of the
|
|
||||||
# mechanisms it bans.
|
|
||||||
#
|
|
||||||
# What each measures is oracle/README.md's table, which is the one place the
|
|
||||||
# four numbers and the hand-written loader's own are compared.
|
|
||||||
#
|
#
|
||||||
# add_tsb_variant(<name> <boot-section-bytes>)
|
# add_tsb_variant(<name> <boot-section-bytes>)
|
||||||
function(add_tsb_variant name bytes)
|
function(add_tsb_variant name bytes)
|
||||||
@@ -122,36 +55,20 @@ function(add_tsb_variant name bytes)
|
|||||||
math(EXPR base_hex "${base_dec}" OUTPUT_FORMAT HEXADECIMAL)
|
math(EXPR base_hex "${base_dec}" OUTPUT_FORMAT HEXADECIMAL)
|
||||||
add_executable(${name} tsb/${name}.cpp)
|
add_executable(${name} tsb/${name}.cpp)
|
||||||
target_link_libraries(${name} PRIVATE libavr)
|
target_link_libraries(${name} PRIVATE libavr)
|
||||||
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${base_hex}
|
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${base_hex})
|
||||||
-Wl,--defsym=tsb_app=0 -Wl,--pmem-wrap-around=32k)
|
|
||||||
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 tiers reimplement the ATmega328P-only reference protocol, so the guard is
|
add_tsb_variant(tsb_asm 512)
|
||||||
# the whole of what this repo builds.
|
add_tsb_variant(tsb_pure 1024)
|
||||||
if(LIBAVR_MCU STREQUAL "atmega328p")
|
add_tsb_variant(tsb_tricks 1024)
|
||||||
add_tsb_variant(tsb_asm 512)
|
|
||||||
add_tsb_variant(tsb_policy 1024)
|
|
||||||
add_tsb_variant(tsb_pure 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()
|
|
||||||
|
|
||||||
# Every test registered above carries the marker a stubbed launcher prints, so
|
|
||||||
# a check this host cannot run reads as Skipped rather than Failed.
|
|
||||||
if(PROJECT_IS_TOP_LEVEL)
|
|
||||||
libavr_skip_unverified()
|
|
||||||
endif()
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"hidden": true,
|
"hidden": true,
|
||||||
"generator": "Ninja",
|
"generator": "Ninja",
|
||||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||||
"toolchainFile": "${sourceDir}/libavr/cmake/avr-toolchain.cmake",
|
"toolchainFile": "$env{LIBAVR_ROOT}/cmake/avr-toolchain.cmake",
|
||||||
"cacheVariables": {
|
"cacheVariables": {
|
||||||
"CMAKE_BUILD_TYPE": "Release",
|
"CMAKE_BUILD_TYPE": "Release",
|
||||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
||||||
@@ -16,69 +16,29 @@
|
|||||||
{
|
{
|
||||||
"name": "atmega328p-generated",
|
"name": "atmega328p-generated",
|
||||||
"inherits": "base",
|
"inherits": "base",
|
||||||
"cacheVariables": {
|
"cacheVariables": { "LIBAVR_MCU": "atmega328p", "LIBAVR_REFLECT": "OFF" }
|
||||||
"LIBAVR_MCU": "atmega328p",
|
|
||||||
"LIBAVR_REFLECT": "OFF"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "atmega328p-reflect",
|
"name": "atmega328p-reflect",
|
||||||
"inherits": "base",
|
"inherits": "base",
|
||||||
"cacheVariables": {
|
"cacheVariables": { "LIBAVR_MCU": "atmega328p", "LIBAVR_REFLECT": "ON" }
|
||||||
"LIBAVR_MCU": "atmega328p",
|
|
||||||
"LIBAVR_REFLECT": "ON"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"buildPresets": [
|
"buildPresets": [
|
||||||
{
|
{ "name": "atmega328p-generated", "configurePreset": "atmega328p-generated" },
|
||||||
"name": "atmega328p-generated",
|
{ "name": "atmega328p-reflect", "configurePreset": "atmega328p-reflect" }
|
||||||
"configurePreset": "atmega328p-generated"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "atmega328p-reflect",
|
|
||||||
"configurePreset": "atmega328p-reflect"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"testPresets": [
|
|
||||||
{
|
|
||||||
"name": "atmega328p-generated",
|
|
||||||
"configurePreset": "atmega328p-generated",
|
|
||||||
"output": {
|
|
||||||
"outputOnFailure": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"workflowPresets": [
|
"workflowPresets": [
|
||||||
{
|
{
|
||||||
"name": "atmega328p-generated",
|
"name": "atmega328p-generated",
|
||||||
"steps": [
|
"steps": [
|
||||||
{
|
{ "type": "configure", "name": "atmega328p-generated" },
|
||||||
"type": "configure",
|
{ "type": "build", "name": "atmega328p-generated" },
|
||||||
"name": "atmega328p-generated"
|
{ "type": "test", "name": "atmega328p-generated" }
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "build",
|
|
||||||
"name": "atmega328p-generated"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "test",
|
|
||||||
"name": "atmega328p-generated"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "atmega328p-reflect",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"type": "configure",
|
|
||||||
"name": "atmega328p-reflect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "build",
|
|
||||||
"name": "atmega328p-reflect"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"testPresets": [
|
||||||
|
{ "name": "atmega328p-generated", "configurePreset": "atmega328p-generated", "output": { "outputOnFailure": true } }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
# 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`.
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
|
|
||||||
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
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="14.0">
|
|
||||||
<PropertyGroup>
|
|
||||||
<SchemaVersion>2.0</SchemaVersion>
|
|
||||||
<ProjectVersion>7.0</ProjectVersion>
|
|
||||||
<ToolchainName>com.Atmel.AVRGCC8.CPP</ToolchainName>
|
|
||||||
<ProjectGuid>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
1
libavr
Submodule libavr deleted from 93d8b0e491
@@ -1,61 +0,0 @@
|
|||||||
# Oracle — the hand-written TinySafeBoot assembly
|
|
||||||
|
|
||||||
`tsb-fixedbaud.asm` is the reference implementation this port is measured
|
|
||||||
against: the **native-UART, fixed-baud** TinySafeBoot bootloader, hand-written
|
|
||||||
in AVR assembly. It is the size-and-feature bar for the port's `tsb_asm` tier.
|
|
||||||
|
|
||||||
- **Source**: <https://github.com/seedrobotics/tinysafeboot>
|
|
||||||
(`firmware_ASM/latest_stable_release/20200727-fixedbaud/main.asm`), the Seed
|
|
||||||
Robotics fixed-baud fork of Julien Thomas' TinySafeBoot.
|
|
||||||
- **License**: GPLv3 (see the header in the file). It is vendored here **only as
|
|
||||||
a reference oracle** — it is not compiled, linked, or distributed as part of
|
|
||||||
the MIT-licensed port. Mere aggregation.
|
|
||||||
|
|
||||||
## Why this variant
|
|
||||||
|
|
||||||
The user chose the fixed-baud, hardware-UART variant deliberately: it is the one
|
|
||||||
whose feature set the port must match. It fits the **complete** TSB feature set
|
|
||||||
into the 512-byte ATmega boot section:
|
|
||||||
|
|
||||||
| Feature | Oracle routine |
|
|
||||||
|---|---|
|
|
||||||
| Watchdog-reset bail straight to the app | `RESET` (WDRF check) |
|
|
||||||
| One-wire half-duplex (RX/TX shorted): RXEN/TXEN toggled per direction, TX turnaround guard | `SetRX` / `SetTX` / `TransmitByte` |
|
|
||||||
| Activation timeout read from the config page, with a lockout-proof minimum | `WRX1To` (uses `utimeoutH`) |
|
|
||||||
| 3×`@` activation knock | `ActCharRcvd` |
|
|
||||||
| Password gate; wrong byte hangs (still draining the UART) | `CheckPassword` |
|
|
||||||
| Emergency erase on password `\0` + double-confirm — wipes flash, EEPROM and the config page | `EmergencyErase` |
|
|
||||||
| Device-info block (16 bytes) | `SendDeviceInfo` / `DEVICEINFO` |
|
|
||||||
| App-flash read/write (`f`/`F`), EEPROM read/write (`e`/`E`), config read/write (`c`/`C`) | `CheckCommands` |
|
|
||||||
|
|
||||||
## Assembled size (the bar)
|
|
||||||
|
|
||||||
Assembled for the ATmega328P with `avra`:
|
|
||||||
|
|
||||||
```
|
|
||||||
avra -I /usr/share/avra tsb-fixedbaud.asm # after uncommenting .include "m328Pdef.inc"
|
|
||||||
# Code : 250 words (500 bytes) — the whole loader, all features, in the 512 B section
|
|
||||||
```
|
|
||||||
|
|
||||||
**500 bytes with every feature** — the proof that ≤512 B and full feature parity
|
|
||||||
are simultaneously reachable. The port's four tiers reach it from the other
|
|
||||||
side, and the gradient between them is the cost of the mechanisms each is
|
|
||||||
allowed:
|
|
||||||
|
|
||||||
| 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
|
|
||||||
(what the simavr protocol test drives). Baud and geometry differ, code size and
|
|
||||||
feature set do not.
|
|
||||||
@@ -1,776 +0,0 @@
|
|||||||
;***********************************************************************
|
|
||||||
;***********************************************************************
|
|
||||||
;***********************************************************************
|
|
||||||
; TinySafeBoot - The Universal Bootloader for AVR ATmegas
|
|
||||||
;***********************************************************************
|
|
||||||
;***********************************************************************
|
|
||||||
;***********************************************************************
|
|
||||||
;
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; 2020 - Version using native UART, Fixed Baud by Seed Robotics in 2020
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; meant for use on ATMEGA devices only (with native UART - UART0)
|
|
||||||
;
|
|
||||||
; Main differences to Regular TSB Bootloader:
|
|
||||||
; - Uses a native UART (UART0); therefore not compatible with ATTINY
|
|
||||||
; - Baud rate is fixed (set by a macro in the code). No auto bauding.
|
|
||||||
; - Disables TX while not transmitting to allow for one wire flashing
|
|
||||||
; (where RX and TX are shorted, for a multi drop bus)
|
|
||||||
; - Also works with separate RX and TX; however an external pull up
|
|
||||||
; on TX _may_ be required; alternatively you can modify the code
|
|
||||||
; in the ReceiveByte routine so that it won't disable TX.
|
|
||||||
; - FIXES:
|
|
||||||
; - situations where booting onto a bus with active communication could
|
|
||||||
; lock the autobauding feature
|
|
||||||
; - times out and boots to application code if the host stops interacting
|
|
||||||
; with the bootloader
|
|
||||||
;
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; Extended by Seed Robotics from 2017
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; Seed Robotics contributions are available from the Github
|
|
||||||
; repository github.com/seedrobotics
|
|
||||||
; The License and conditions remain as stated below, in the
|
|
||||||
; original notice.
|
|
||||||
;
|
|
||||||
;
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; Written in 2011-2015 by Julien Thomas
|
|
||||||
;
|
|
||||||
; This program is free software; you can redistribute it and/or
|
|
||||||
; modify it under the terms of the GNU General Public License
|
|
||||||
; as published by the Free Software Foundation; either version 3
|
|
||||||
; of the License, or (at your option) any later version.
|
|
||||||
; This program is distributed in the hope that it will be useful,
|
|
||||||
; but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
||||||
; of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
||||||
; See the GNU General Public License for more details.
|
|
||||||
; You should have received a copy of the GNU General Public License
|
|
||||||
; along with this program; if not, see:
|
|
||||||
; http://www.gnu.org/licenses/
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
;
|
|
||||||
;
|
|
||||||
;
|
|
||||||
;***********************************************************************
|
|
||||||
; OVERVIEW
|
|
||||||
;***********************************************************************
|
|
||||||
;
|
|
||||||
; TSB assembly source is organized in 4 segments (approx. line numbers)
|
|
||||||
;
|
|
||||||
; ~ 50 ... Global definitions
|
|
||||||
; ~ ... TSB for ATmegas
|
|
||||||
;
|
|
||||||
;***********************************************************************
|
|
||||||
; ADJUSTMENTS FOR INDIVIDUAL ASSEMBLY
|
|
||||||
;***********************************************************************
|
|
||||||
;
|
|
||||||
; This Sourcecode is directly compatible to: AVRASM2, GAVRASM
|
|
||||||
;
|
|
||||||
.nolist
|
|
||||||
;
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; SPECIFY TARGET AVR
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
;
|
|
||||||
; Comment in and provide def.inc file for target device
|
|
||||||
;
|
|
||||||
; [Examples]
|
|
||||||
;
|
|
||||||
;.include "tn2313def.inc"
|
|
||||||
;.include "tn85def.inc"
|
|
||||||
;.include "m8515def.inc"
|
|
||||||
;.include "m168def.inc"
|
|
||||||
;.include "m161def.inc"
|
|
||||||
;.include "m324Adef.inc"
|
|
||||||
;.include "m328Pdef.inc"
|
|
||||||
;.include "tn441def.inc"
|
|
||||||
;.include "tn167def.inc"
|
|
||||||
;.include "tn861def.inc"
|
|
||||||
;.include "tn841def.inc"
|
|
||||||
;.include "tn84def.inc"
|
|
||||||
;.include "m8def.inc"
|
|
||||||
;.include "m644PAdef.inc"
|
|
||||||
;.include "m644def.inc"
|
|
||||||
;.include "tn167def.inc"
|
|
||||||
;.include "tn25def.inc"
|
|
||||||
;
|
|
||||||
; [...]
|
|
||||||
;
|
|
||||||
;
|
|
||||||
.list
|
|
||||||
;
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; BUILD INFO
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; YY = Year - MM = Month - DD = Day
|
|
||||||
.set YY = 21
|
|
||||||
.set MM = 12
|
|
||||||
.set DD = 21
|
|
||||||
;
|
|
||||||
.set BUILDSTATE = $F3 ; F1 fixed baud, pull up, derived from original (modified for fixed baud)
|
|
||||||
; F2 fixed baud, pull up, guaranteed minimum activation timeout in case of userpage data corruption
|
|
||||||
; F3 adds a CONSTANT with clock speed (Mhz) as word in the last page of memory (clock speed our defined CONSTANT)
|
|
||||||
|
|
||||||
;
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; TSB / TSB-INSTALLER SWITCH
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; 0 = Regular assembly to target address
|
|
||||||
; Other value = NOT SUPPORTED
|
|
||||||
;
|
|
||||||
.set TSBINSTALLER = 0
|
|
||||||
;
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; F_CPU and Baud rate setting
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
.equ F_CPU = 20000000
|
|
||||||
.equ BAUD = 33333 ; baudrate (notice some possible wrong cals: example for 56K, it is actually 55,555, so for BAUD_PRESC give an INT result of 8, we must set BAUD to 55500)
|
|
||||||
|
|
||||||
.equ BAUD_PRESCx10 = (F_CPU * 10/16/BAUD) - 10 ; baud prescale (regular formula = F_CPU * 10/16/BAUD - 1 but we do it x10 to check the rounding)
|
|
||||||
|
|
||||||
; arredondar acima se necesssario
|
|
||||||
.if BAUD_PRESCx10 - ( (BAUD_PRESCx10 / 10) * 10 ) >= 5 ; calculate the remainder: we rely on the fact these are integer divisions. Therefore, dividing by 10, rounds DOWN in integer division
|
|
||||||
.equ BAUD_PRESC = (F_CPU/16/BAUD)
|
|
||||||
.warning "Incrementing default BAUD_PRESC formula by 1 due to rounding."
|
|
||||||
|
|
||||||
.else
|
|
||||||
.equ BAUD_PRESC = (F_CPU/16/BAUD) - 1
|
|
||||||
.warning "Using default BAUD_PRESC formula (no rounding up)"
|
|
||||||
.endif
|
|
||||||
|
|
||||||
.if BAUD_PRESC > 255
|
|
||||||
.error "ERROR: BAUD RATE TOO LOW. WE ONLY WRITE THE UBRRL REGISTER, SO UBRR MUST BE <255 FOR THIS CLOCK FREQ AND BAUD"
|
|
||||||
.endif
|
|
||||||
|
|
||||||
|
|
||||||
;***********************************************************************
|
|
||||||
; AUTO-ADJUST FOR DIFFERENT ASSEMBLY OPTIONS
|
|
||||||
;***********************************************************************
|
|
||||||
;
|
|
||||||
; Always set TINYMEGA=1 bc this code only supports ATMEGA
|
|
||||||
|
|
||||||
.equ TINYMEGA=1
|
|
||||||
|
|
||||||
.if FLASHEND > ($7fff)
|
|
||||||
.error "SORRY! DEVICES OVER 64 KB NOT SUPPORTED YET."
|
|
||||||
.exit
|
|
||||||
.endif
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; Workarounds for devices with renamed or missing definitions
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
;
|
|
||||||
.ifndef SPMCSR ; SPMEN / PGERS / ...
|
|
||||||
.equ SPMCSR = SPMCR
|
|
||||||
.endif
|
|
||||||
|
|
||||||
.ifndef MCUSR ; PORF / EXTRF / BORF / WDRF
|
|
||||||
.equ MCUSR = MCUCSR
|
|
||||||
.endif
|
|
||||||
|
|
||||||
; Detect Attiny441/841 to amend missing pagesize and apply 4-page mode
|
|
||||||
|
|
||||||
.set FOURPAGES = 0
|
|
||||||
|
|
||||||
.if ((SIGNATURE_000 == $1E) && (SIGNATURE_002 == $15) && (SIGNATURE_001 == $92))
|
|
||||||
.equ PAGESIZE = 32
|
|
||||||
.set FOURPAGES = 1
|
|
||||||
.message "ATTINY441: 4-PAGE-ERASE MODE"
|
|
||||||
.endif
|
|
||||||
|
|
||||||
.if ((SIGNATURE_000 == $1E) && (SIGNATURE_002 == $15) && (SIGNATURE_001 == $93))
|
|
||||||
.equ PAGESIZE = 32
|
|
||||||
.set FOURPAGES = 1
|
|
||||||
.message "ATTINY841: 4-PAGE-ERASE MODE"
|
|
||||||
.endif
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; Universal Constants and Registers
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
.equ REQUEST = '?' ; request / answer / go on
|
|
||||||
.equ CONFIRM = '!' ; confirm / attention
|
|
||||||
|
|
||||||
; Current bootloader date coded into 16-bit number
|
|
||||||
.equ BUILDDATE = YY * 512 + MM * 32 + DD
|
|
||||||
|
|
||||||
; Other
|
|
||||||
.equ INFOLEN = 8 ; *Words* of Device Info
|
|
||||||
.equ BUFFER = SRAM_START
|
|
||||||
|
|
||||||
; Registers (in use by TSB-Firmware and TSB-Installer for ATtinys)
|
|
||||||
.def avecl = r4 ; application vector temp low
|
|
||||||
.def avech = r5 ; application vector temp high
|
|
||||||
.def tmp1 = r16 ; these are
|
|
||||||
.def tmp2 = r17 ; universal
|
|
||||||
.def tmp3 = r18 ; temporary
|
|
||||||
.def tmp4 = r19 ; registers
|
|
||||||
.def bcnt = r20 ; page bytecounter
|
|
||||||
.def cntr1 = r21 ; timeout counter
|
|
||||||
.def rxen = r22 ; check if RX enabled (meaning TX disabled)
|
|
||||||
.def utimeoutH = r23 ; user timeout High byte
|
|
||||||
; special purpose registers start at R26
|
|
||||||
;
|
|
||||||
;
|
|
||||||
;***********************************************************************
|
|
||||||
;***********************************************************************
|
|
||||||
;***********************************************************************
|
|
||||||
; START OF TSB FOR ATMEGAS
|
|
||||||
;***********************************************************************
|
|
||||||
;***********************************************************************
|
|
||||||
;***********************************************************************
|
|
||||||
;
|
|
||||||
; TSB for ATmegas is always coded directly to target address.
|
|
||||||
|
|
||||||
.if TINYMEGA == 1
|
|
||||||
|
|
||||||
.message "ASSEMBLY OF TSB FOR ATMEGA"
|
|
||||||
|
|
||||||
.equ BOOTSTART = (FLASHEND+1)-256 ; = 512 Bytes
|
|
||||||
.equ LASTPAGE = BOOTSTART - PAGESIZE ; = 1 page below TSB!
|
|
||||||
|
|
||||||
.org BOOTSTART
|
|
||||||
|
|
||||||
RESET:
|
|
||||||
cli
|
|
||||||
|
|
||||||
in tmp4, MCUSR ; check reset condition
|
|
||||||
sbrc tmp4, WDRF ; in case of a Watchdog reset
|
|
||||||
rjmp APPJUMP ; immediately leave TSB
|
|
||||||
|
|
||||||
ldi tmp1, low (RAMEND) ; write ramend low
|
|
||||||
out SPL, tmp1 ; into SPL (stackpointer low)
|
|
||||||
.ifdef SPH
|
|
||||||
ldi tmp1, high(RAMEND) ; write ramend high for ATtinys
|
|
||||||
out SPH, tmp1 ; with SRAM > 256 bytes
|
|
||||||
.message "PROVIDING FOR STACK BIGGER THAN 256 BYTES"
|
|
||||||
.endif
|
|
||||||
|
|
||||||
.ifndef DDRD2
|
|
||||||
.equ DDRD2 = DDD2
|
|
||||||
.endif
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; ACTIVATION CHECK
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; Configure UART; no autobauding in this version
|
|
||||||
|
|
||||||
ldi tmp1,BAUD_PRESC ; load baud prescale
|
|
||||||
sts UBRR0L,tmp1 ; set baud prescale
|
|
||||||
; ldi tmp2,HIGH(bpsc) ; save code by not loading UBBRH
|
|
||||||
;sts UBRRH,tmp2 ; to UBRR0
|
|
||||||
;ldi tmp2,( (1<<RXEN0) ) ; enable transmiter and receiver
|
|
||||||
;sts UCSR0B,tmp2
|
|
||||||
|
|
||||||
; Enable Pull up on Port D2 (PD2)
|
|
||||||
cbi DDRD, DDRD2
|
|
||||||
sbi PORTD, PORTD2
|
|
||||||
|
|
||||||
; we will enable RNEN/TXEN in the ReceiveByte and TransmitByte routines
|
|
||||||
|
|
||||||
rcall ZtoLASTPAGE ; set Z to start'o'LASTPAGE
|
|
||||||
adiw zl, 2 ; skip first 2 bytes (APPJUMP)
|
|
||||||
lpm utimeoutH, z+ ; load TIMEOUT byte and store for use in RX byte timeout
|
|
||||||
ori utimeoutH, (F_CPU / 1000000); prevent bootloader lockout due if it gets an invalid (to small) timeout setting
|
|
||||||
; this ensures value is at least the clock rate, which shoudl give about 40ms
|
|
||||||
clr tmp2 ; apparently at times this is not set to 0 on boot? (seen while in debugWire)
|
|
||||||
clr rxen ; same as above
|
|
||||||
|
|
||||||
WRX1To:
|
|
||||||
; we'll check the X register which is where ReceibeByte controls the timeout
|
|
||||||
; the overall timeout of receive byte is the timeout set by the user
|
|
||||||
; therefore, if we get characters while X> 0 we're attempting to activate bootloader;
|
|
||||||
; if not, if X=0 we timedout and go to app start
|
|
||||||
rcall ReceiveByte
|
|
||||||
brcs WRX2To ; if X got to 0 (i.e. carry set), assume we timed out
|
|
||||||
cpi tmp1, '@' ; did we get an activation char = "@"
|
|
||||||
breq ActCharRcvd
|
|
||||||
WRX2To:
|
|
||||||
rjmp APPJUMP ; not an activation char goto APPJUMP in LASTPAGE
|
|
||||||
|
|
||||||
|
|
||||||
ActCharRcvd:
|
|
||||||
inc tmp2
|
|
||||||
cpi tmp2, 3
|
|
||||||
brne WRX1To ; branch if not yet at 3;
|
|
||||||
; otherwise fall through to password check
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; CHECK PASSWORD / EMERGENCY ERASE
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; we use the user timeout (utimeoutH) register for COMM timeout
|
|
||||||
; when we don't get valid data
|
|
||||||
; increase this value to a fixed one now, to cope
|
|
||||||
; with cases where the user timeout is set so low that we don't have time to
|
|
||||||
; do anything
|
|
||||||
ldi utimeoutH, (F_CPU / 78500) ; this should result in 255 for 20Mhz and proportionally
|
|
||||||
; less for lower Clocks, so that we get the same time approx. 2.4sec
|
|
||||||
|
|
||||||
CheckPassword:
|
|
||||||
|
|
||||||
chpw0: ser tmp4 ; tmp4 = 255 enables comparison
|
|
||||||
chpw1: lpm tmp3, z+ ; load pw character from Z
|
|
||||||
and tmp3, tmp4 ; if tmp4 = 0 disables comparison, for wrong password scenarios
|
|
||||||
cpi tmp3, 255 ; byte value 255 indicates
|
|
||||||
breq chpwx ; end of password -> success
|
|
||||||
chpw2: rcall Receivebyte ; else receive next character
|
|
||||||
cpi tmp1, 0 ; rxbyte = 0 will branch
|
|
||||||
breq chpwee ; to confirm emergency erase
|
|
||||||
cp tmp1, tmp3 ; compare password with rxbyte
|
|
||||||
breq chpw0 ; if equal check next character
|
|
||||||
clr tmp4 ; tmp4 = 0 to loop forever
|
|
||||||
rjmp chpw1 ; and smoothen power profile
|
|
||||||
chpwee:
|
|
||||||
; Fix for ISSUE #1: only check for Emergency Erase if we haven't
|
|
||||||
; gotten a wrong password; if we got a wrong password
|
|
||||||
; then we should stay in loop and not escape to Emergency
|
|
||||||
; Erase
|
|
||||||
cpi tmp4, 0 ; if tmp4=0 we are set to loop forever
|
|
||||||
breq chpw1
|
|
||||||
rcall RequestConfirm ; request confirm
|
|
||||||
brts chpa ; not confirmed, leave
|
|
||||||
rcall RequestConfirm ; request 2nd confirm
|
|
||||||
brts chpa ; can't be mistake now
|
|
||||||
rcall EmergencyErase ; go, emergency erase!
|
|
||||||
rjmp Mainloop
|
|
||||||
chpa:
|
|
||||||
rjmp APPJUMP ; start application
|
|
||||||
chpwx:
|
|
||||||
; rjmp SendDeviceInfo ; go on to SendDeviceInfo
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; SEND DEVICEINFO
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
SendDeviceInfo:
|
|
||||||
ldi zl, low (DEVICEINFO*2) ; load address of deviceinfo
|
|
||||||
ldi zh, high(DEVICEINFO*2) ; low and highbyte
|
|
||||||
ldi bcnt, INFOLEN*2
|
|
||||||
rcall SendFromFlash
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; MAIN LOOP TO RECEIVE AND EXECUTE COMMANDS
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
Mainloop:
|
|
||||||
clr zl ; clear Z pointer
|
|
||||||
clr zh ; which is frequently used
|
|
||||||
rcall SendConfirm ; send CONFIRM via RS232
|
|
||||||
rcall Receivebyte ; receive command via RS232
|
|
||||||
rcall CheckCommands ; check command letter
|
|
||||||
rjmp Mainloop ; and loop on
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; CHANGE USER DATA IN LASTPAGE
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
ChangeSettings:
|
|
||||||
rcall GetNewPage ; get new LASTPAGE contents
|
|
||||||
brtc ChangeS0 ; from Host (if confirmed)
|
|
||||||
ret
|
|
||||||
ChangeS0:
|
|
||||||
rcall ZtoLASTPAGE ; re-write LASTPAGE
|
|
||||||
rcall EraseFlashPage
|
|
||||||
rcall WritePage ; erase and write LASTPAGE
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; SEND USER DATA FROM LASTPAGE
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
ControlSettings:
|
|
||||||
rcall ZtoLASTPAGE ; point to LASTPAGE
|
|
||||||
; rcall SendPageFromFlash
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; SEND DATA FROM FLASH MEMORY
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
SendPageFromFlash:
|
|
||||||
ldi bcnt, low (PAGESIZE*2) ; whole Page to send
|
|
||||||
SendFromFlash:
|
|
||||||
rcall SPMwait ; (re)enable RWW read access
|
|
||||||
lpm tmp1, z+ ; read directly from flash
|
|
||||||
rcall Transmitbyte ; and send out to RS232
|
|
||||||
dec bcnt ; bcnt is number of bytes
|
|
||||||
brne SendFromFlash
|
|
||||||
ret
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; READ APPLICATION FLASH
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; read and transmit application flash area (pagewise)
|
|
||||||
|
|
||||||
ReadAppFlash:
|
|
||||||
RAF0:
|
|
||||||
rcall RwaitConfirm
|
|
||||||
brts RAFx
|
|
||||||
rcall SendPageFromFlash
|
|
||||||
RAF1:
|
|
||||||
cpi zl, low (LASTPAGE*2) ; count up to last byte
|
|
||||||
brne RAF0 ; below LASTPAGE
|
|
||||||
cpi zh, high(LASTPAGE*2)
|
|
||||||
brne RAF0
|
|
||||||
RAFx:
|
|
||||||
ret
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; WRITE APPLICATION FLASH
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; Write Appflash pagewise, don't modify anything for ATmegas
|
|
||||||
|
|
||||||
WriteAppFlash:
|
|
||||||
rcall EraseAppFlash ; Erase whole app flash
|
|
||||||
Flash2:
|
|
||||||
rcall GetNewPage ; get next page from host
|
|
||||||
brts FlashX ; stop on user's behalf
|
|
||||||
Flash3:
|
|
||||||
rcall WritePage ; write page data into flash
|
|
||||||
Flash4:
|
|
||||||
cpi zh, high(LASTPAGE*2-1) ; end of available Appflash?
|
|
||||||
brne Flash2 ; if Z reached last location
|
|
||||||
cpi zl, low (LASTPAGE*2-1) ; then we are finished
|
|
||||||
brne Flash2 ; else go on
|
|
||||||
FlashX:
|
|
||||||
ret ; we're already finished!
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; WRITE FLASH PAGE FROM BUFFER, VERIFYING AND VERIFY-ERROR-HANDLING
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
WritePage:
|
|
||||||
rcall YtoBUFFER ; Y=BUFFER, bcnt=PAGESIZE*2
|
|
||||||
WrPa1:
|
|
||||||
ld r0, y+ ; fill R0/R1 with word
|
|
||||||
ld r1, y+ ; from buffer position Y / Y+1
|
|
||||||
ldi tmp1, 0b00000001 ; set only SPMEN in SPMCSR
|
|
||||||
out SPMCSR, tmp1 ; to activate page buffering
|
|
||||||
spm ; store word in page buffer
|
|
||||||
adiw zl, 2 ; and forward to next word
|
|
||||||
subi bcnt, 2
|
|
||||||
brne WrPa1
|
|
||||||
; Z = start of next page now
|
|
||||||
subi zl, low (PAGESIZE*2) ; point back Z to
|
|
||||||
sbci zh, high(PAGESIZE*2) ; start of current page
|
|
||||||
; Z = back on current page's start
|
|
||||||
WrPa2:
|
|
||||||
ldi tmp1, 0b00000101 ; enable PRWRT + SPMEN
|
|
||||||
out SPMCSR, tmp1 ; in SPMCSR
|
|
||||||
spm ; write whole page to flash
|
|
||||||
WrPa3:
|
|
||||||
in tmp1, SPMCSR ; wait for flash write finished
|
|
||||||
sbrc tmp1, 0 ; skip if SPMEN (bit0) cleared
|
|
||||||
rjmp WrPa3 ; ITS BEEN WRITTEN
|
|
||||||
subi zl, low (-PAGESIZE*2) ; same effect as
|
|
||||||
sbci zh, high(-PAGESIZE*2) ; Z = Z + PAGESIZE*2
|
|
||||||
ret
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; CHECK COMMANDS
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
CheckCommands:
|
|
||||||
cpi tmp1, 'c' ; read LASTPAGE
|
|
||||||
breq ControlSettings
|
|
||||||
cpi tmp1, 'C' ; write LASTPAGE
|
|
||||||
breq ChangeSettings
|
|
||||||
cpi tmp1, 'f' ; read Appflash
|
|
||||||
breq ReadAppFlash
|
|
||||||
cpi tmp1, 'F' ; write Appflash
|
|
||||||
breq WriteAppFlash
|
|
||||||
cpi tmp1, 'e' ; read EEPROM
|
|
||||||
breq EepromRead
|
|
||||||
cpi tmp1, 'E' ; write EEPROM
|
|
||||||
breq EEpromWrite
|
|
||||||
rjmp APPJUMP ; else start application
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; EEPROM READ/WRITE ACCESS
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
EepromWrite:
|
|
||||||
EEWr0:
|
|
||||||
rcall GetNewPage ; get EEPROM datablock
|
|
||||||
brts EERWFx ; or abort on host's demand
|
|
||||||
EEWr1:
|
|
||||||
rcall YtoBUFFER ; Y = Buffer and Bcnt = blocksize
|
|
||||||
EEWr2:
|
|
||||||
ld tmp1, y+ ; read EEPROM byte from buffer
|
|
||||||
rcall EEWriteByte
|
|
||||||
dec bcnt ; count down block byte counter
|
|
||||||
brne EEWr2 ; loop on if block not finished
|
|
||||||
rjmp EeWr0
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
EEpromRead:
|
|
||||||
EeRe1:
|
|
||||||
rcall RwaitConfirm ; wait to confirm
|
|
||||||
brts EERWFx ; else we are finished
|
|
||||||
ldi bcnt, low(PAGESIZE*2) ; again PAGESIZE*2 is blocksize
|
|
||||||
EERe2:
|
|
||||||
out EEARL, zl ; current EEPROM address low
|
|
||||||
.ifdef EEARH
|
|
||||||
out EEARH, zh ; current EEPROM address high
|
|
||||||
.endif
|
|
||||||
sbi EECR, 0 ; set EERE - EEPROM read enable
|
|
||||||
in tmp1, EEDR ; read byte from current address
|
|
||||||
rcall Transmitbyte ; send out to RS232
|
|
||||||
adiw zl,1 ; count up EEPROM address
|
|
||||||
dec bcnt ; count down block byte counter
|
|
||||||
brne EERe2 ; loop on if block not finished
|
|
||||||
rjmp EERe1
|
|
||||||
EERWFx:
|
|
||||||
ret
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
EEWriteByte:
|
|
||||||
out EEDR, tmp1 ; write to EEPROM data register
|
|
||||||
out EEARL, zl ; current EEPROM address low
|
|
||||||
.ifdef EEARH
|
|
||||||
out EEARH, zh ; high EEARH for some attinys
|
|
||||||
.endif
|
|
||||||
sbi EECR, 2 ; EEPROM master prog enable
|
|
||||||
sbi EECR, 1 ; EEPE initiate prog cycle
|
|
||||||
EeWB:
|
|
||||||
sbic EECR, 1 ; wait write cycle to complete
|
|
||||||
rjmp EeWB ; before we can go on
|
|
||||||
adiw zl,1 ; count up EEPROM address
|
|
||||||
ret
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; GET NEW PAGE
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
GetNewPage:
|
|
||||||
rcall RequestConfirm ; check for Confirm
|
|
||||||
brts GNPx ; abort if not confirmed
|
|
||||||
GNP0:
|
|
||||||
rcall YtoBUFFER ; Y = BUFFER, bcnt = PAGESIZE*2
|
|
||||||
GNP1:
|
|
||||||
rcall ReceiveByte ; receive serial byte
|
|
||||||
st y+, tmp1 ; and store in buffer
|
|
||||||
dec bcnt ; until full page loaded
|
|
||||||
brne GNP1 ; loop on
|
|
||||||
GNPx:
|
|
||||||
ret ; finished
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; REQUEST TO CONFIRM / AWAIT CONFIRM COMMAND
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
RequestConfirm:
|
|
||||||
ldi tmp1, REQUEST ; send request character
|
|
||||||
rcall Transmitbyte ; prompt to confirm (or not)
|
|
||||||
|
|
||||||
RwaitConfirm:
|
|
||||||
rcall ReceiveByte ; get host's reply
|
|
||||||
clt ; set T=0 for confirmation
|
|
||||||
cpi tmp1, CONFIRM ; if host HAS sent CONFIRM
|
|
||||||
breq RCx ; return with the T=0
|
|
||||||
set ; else set T=1 (NOT CONFIRMED)
|
|
||||||
RCx:
|
|
||||||
ret ; whether confirmed or not
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; FLASH ERASE TOP-TO-BOTTOM ( (BOOTSTART-1) ... $0000)
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
EraseAppFlash:
|
|
||||||
rcall ZtoLASTPAGE ; point Z to LASTPAGE, directly
|
|
||||||
EAF0:
|
|
||||||
subi zl, low (PAGESIZE*2)
|
|
||||||
sbci zh, high(PAGESIZE*2)
|
|
||||||
rcall EraseFlashPage
|
|
||||||
brne EAF0 ; until first page reached
|
|
||||||
EAFx: ret ; and leave with Z = $0000
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; EMERGENCY ERASE OF FLASH / EEPROM / USERDATA
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
EmergencyErase:
|
|
||||||
rcall EraseAppFlash ; erase Application Flash
|
|
||||||
ser tmp1 ; byte value for EEPROM writes
|
|
||||||
EEE0:
|
|
||||||
rcall EEWriteByte ; write EEPROM byte, Z = Z + 1
|
|
||||||
cpi zh, high(EEPROMEND+1)+2 ; EEPROMEND
|
|
||||||
brne EEE0 ; and loop on until finished
|
|
||||||
|
|
||||||
rcall ZtoLASTPAGE ; LASTPAGE is to be erased
|
|
||||||
; rcall EraseFlashPage
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; ERASE ONE FLASH PAGE
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
EraseFlashPage:
|
|
||||||
ldi tmp1, 0b00000011 ; enable PGERS + SPMEN
|
|
||||||
out SPMCSR, tmp1 ; in SPMCSR and erase current
|
|
||||||
spm ; page by SPM (MCU halted)
|
|
||||||
|
|
||||||
; Waiting for SPM to be finished is *obligatory* on ATmegas!
|
|
||||||
SPMwait:
|
|
||||||
in tmp1, SPMCSR
|
|
||||||
sbrc tmp1, 0 ; wait previous SPMEN
|
|
||||||
rjmp SPMwait
|
|
||||||
ldi tmp1, 0b00010001 ; set RWWSRE and SPMEN
|
|
||||||
out SPMCSR, tmp1
|
|
||||||
spm
|
|
||||||
ret
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; OTHER SUBROUTINES
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
YtoBUFFER:
|
|
||||||
ldi yl, low (BUFFER) ; reset pointer
|
|
||||||
ldi yh, high(BUFFER) ; to programming buffer
|
|
||||||
ldi bcnt, low(PAGESIZE*2) ; and often needed
|
|
||||||
ret
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
ZtoLASTPAGE:
|
|
||||||
ldi zl, low (LASTPAGE*2) ; reset Z to LASTPAGE start
|
|
||||||
ldi zh, high(LASTPAGE*2)
|
|
||||||
ret
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; RS232 RECEIVE BYTE
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
; uses: tmp1 (received data byte), cntr1 (for timeout)
|
|
||||||
; also uses utimeoutH which holds the default timeout defined by the user
|
|
||||||
; and X which is actually used to count down
|
|
||||||
SetRX:
|
|
||||||
ldi tmp1,(1<<RXEN0) ; enable receiver (Transmitter disabled)
|
|
||||||
sts UCSR0B,tmp1
|
|
||||||
ser rxen
|
|
||||||
|
|
||||||
ReceiveByte:
|
|
||||||
sbrs rxen, 0
|
|
||||||
rjmp SetRX
|
|
||||||
|
|
||||||
; outer counter
|
|
||||||
mov xh, utimeoutH
|
|
||||||
;ldi xl, 128
|
|
||||||
|
|
||||||
ReceiveByteShortTimeout:
|
|
||||||
ser cntr1 ; inner counter reset
|
|
||||||
|
|
||||||
ReceiveByteShortTimeout1:
|
|
||||||
lds tmp1, UCSR0A ; load UART status register A
|
|
||||||
sbrc tmp1, RXC0 ; if not RXComplete, skip
|
|
||||||
rjmp LoadRXByte
|
|
||||||
dec cntr1 ; if counter not zero
|
|
||||||
brne ReceiveByteShortTimeout1 ; cycle again; else fall through
|
|
||||||
sbiw xl, 1 ; dec outter counter
|
|
||||||
brcc ReceiveByteShortTimeout ; continue of outter counetr still active
|
|
||||||
;ret ;
|
|
||||||
|
|
||||||
LoadRXByte:
|
|
||||||
lds tmp1, UDR0 ; load received character even if RXC is not set
|
|
||||||
ret ; (it loads 0 and UDR FIFO should recover for next char)
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; RS232 SEND CONFIRM CHARACTER
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
SendConfirm:
|
|
||||||
ldi tmp1, CONFIRM
|
|
||||||
rjmp Transmitbyte
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; RS232 TRANSMIT BYTE
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; uses: tmp1 (transmit byte will be shifted out), tmp2 (bitcounter)
|
|
||||||
;
|
|
||||||
|
|
||||||
SetTX:
|
|
||||||
ldi tmp2,(1<<TXEN0) ; enable transmitter (Receiver disabled)
|
|
||||||
sts UCSR0B,tmp2
|
|
||||||
clr rxen
|
|
||||||
|
|
||||||
; wait some guard time to allow receiving devices ot transition
|
|
||||||
; from TX t RX state
|
|
||||||
ser cntr1 ; inner counter reset
|
|
||||||
SetTXShortTimeout:
|
|
||||||
nop
|
|
||||||
dec cntr1 ; if counter not zero
|
|
||||||
brne SetTXShortTimeout ; cycle again; else fall through
|
|
||||||
|
|
||||||
|
|
||||||
TransmitByte:
|
|
||||||
sbrc rxen, 0
|
|
||||||
rjmp SetTX
|
|
||||||
|
|
||||||
; no need to wait for UDRE bc we will wait for TXC on
|
|
||||||
; every char transmitted. TXC occurs later that UDRE
|
|
||||||
; so UDRE should be asserted when TXC asserts
|
|
||||||
sts UDR0, tmp1
|
|
||||||
|
|
||||||
WaitForTXC:
|
|
||||||
lds tmp2, UCSR0A ; wait for TXC (and not UDRE)
|
|
||||||
sbrs tmp2, TXC0 ; bc after this char we may transition
|
|
||||||
rjmp WaitForTXC ; to receiving chars and we want to make sure we get a clean transition
|
|
||||||
; we need to write a 1 to clear the TXC flag; otherwise the flag won't clear
|
|
||||||
sts UCSR0A, tmp2 ; tmp2 should contain an asserted TXC bit
|
|
||||||
ret
|
|
||||||
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; ATMEGA APPJUMP = SIMPLE JUMP TO $0000 (ORIGINAL RESET VECTOR)
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; Boot Reset Vector (BOOTRST) must be activated for TSB on ATmegas.
|
|
||||||
; After timeout or executing commands, TSB for ATmegas will simply
|
|
||||||
; handover to the App by a (relative or absolute) jump to $0000.
|
|
||||||
|
|
||||||
APPJUMP:
|
|
||||||
rcall SPMwait ; make sure everything's done
|
|
||||||
|
|
||||||
.if FLASHEND >= ($1fff)
|
|
||||||
jmp $0000 ; absolute jump
|
|
||||||
.else
|
|
||||||
rjmp $0000 ; relative jump
|
|
||||||
.endif
|
|
||||||
|
|
||||||
|
|
||||||
DEVICEINFO:
|
|
||||||
.message "DEVICE INFO BLOCK FOR ATMEGA"
|
|
||||||
.db "TSB", low (BUILDDATE), high (BUILDDATE), BUILDSTATE
|
|
||||||
.db SIGNATURE_000, SIGNATURE_001, SIGNATURE_002, low (PAGESIZE)
|
|
||||||
.dw BOOTSTART-PAGESIZE
|
|
||||||
.dw EEPROMEND
|
|
||||||
.db $AA, $AA
|
|
||||||
|
|
||||||
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
; DEVICE INFO BLOCK = PERMANENT DATA
|
|
||||||
;-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
; set last word with the clock speed
|
|
||||||
.org FLASHEND
|
|
||||||
.dw (F_CPU/1000000)
|
|
||||||
|
|
||||||
//.message "SAVING CLOCK SPEED IN LAST BYTE AS " (F_CPU/1000000) " Mhz"
|
|
||||||
|
|
||||||
.message "ASSEMBLY OF TSB FOR ATMEGA SUCCESSFULLY FINISHED!"
|
|
||||||
|
|
||||||
.endif ; closing TSB for ATmega sourcecode;
|
|
||||||
|
|
||||||
;***********************************************************************
|
|
||||||
; END OF TSB FOR ATMEGAS
|
|
||||||
;***********************************************************************
|
|
||||||
|
|
||||||
.exit
|
|
||||||
|
|
||||||
;***********************************************************************
|
|
||||||
;***********************************************************************
|
|
||||||
;***********************************************************************
|
|
||||||
; END OF CONDITIONAL ASSEMBLY SOURCE OF TSB FOR ATTINYS AND ATMEGAS
|
|
||||||
;***********************************************************************
|
|
||||||
;***********************************************************************
|
|
||||||
;***********************************************************************
|
|
||||||
|
|
||||||
|
|
||||||
@@ -4,9 +4,6 @@ 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")
|
||||||
|
|||||||
86
test/device.c
Normal file
86
test/device.c
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
// simavr "device" for the TSB bootloader: load the boot-linked ELF into the
|
||||||
|
// ATmega328P boot section, enter it (BOOTRST is not modelled, so we set PC to
|
||||||
|
// the boot base, exactly as simavr's own board_simduino does), and expose
|
||||||
|
// UART0 as a pty. A host client (Python pyserial, or the real tsbloader) then
|
||||||
|
// speaks the TSB protocol over that pty and actually flashes the device.
|
||||||
|
//
|
||||||
|
// SPM genuinely writes avr->flash on the mega cores, so on exit (or SIGTERM)
|
||||||
|
// we dump the flash image to a file for a ground-truth cross-check against
|
||||||
|
// what the client read back through the bootloader.
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "sim_avr.h"
|
||||||
|
#include "sim_elf.h"
|
||||||
|
#include "uart_pty.h"
|
||||||
|
|
||||||
|
static avr_t *avr;
|
||||||
|
static uart_pty_t uart_pty;
|
||||||
|
static const char *dump_path;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uart_pty_stop(&uart_pty);
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
if (argc < 3) {
|
||||||
|
fprintf(stderr, "usage: %s <tsb.elf> <boot_base_hex> [flash_dump.bin]\n", argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
uint32_t boot_base = (uint32_t)strtoul(argv[2], NULL, 0);
|
||||||
|
dump_path = argc >= 4 ? argv[3] : NULL;
|
||||||
|
|
||||||
|
avr = avr_make_mcu_by_name("atmega328p");
|
||||||
|
if (!avr) {
|
||||||
|
fprintf(stderr, "device: no ATmega328P core\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
avr_init(avr);
|
||||||
|
avr->frequency = 16000000;
|
||||||
|
// Real flash powers up erased (0xff); the app region must look erased
|
||||||
|
// before the bootloader programs it.
|
||||||
|
memset(avr->flash, 0xff, avr->flashend + 1);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// section base ourselves and enter there (BOOTRST is not modelled).
|
||||||
|
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 + boot_base, fw.flash, fw.flashsize);
|
||||||
|
avr->pc = boot_base;
|
||||||
|
avr->codeend = avr->flashend;
|
||||||
|
|
||||||
|
uart_pty_init(avr, &uart_pty);
|
||||||
|
uart_pty_connect(&uart_pty, '0');
|
||||||
|
printf("TSB_PTY %s\n", uart_pty.pty.slavename);
|
||||||
|
fflush(stdout);
|
||||||
|
|
||||||
|
signal(SIGTERM, finish);
|
||||||
|
signal(SIGINT, finish);
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
int state = avr_run(avr);
|
||||||
|
if (state == cpu_Done || state == cpu_Crashed)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
finish(0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
128
test/device.cpp
128
test/device.cpp
@@ -1,128 +0,0 @@
|
|||||||
// simavr "device" for the TSB bootloader: load the boot-linked ELF into the
|
|
||||||
// ATmega328P boot section, enter it (BOOTRST is not modelled, so we set PC to
|
|
||||||
// the boot base, exactly as simavr's own board_simduino does), and expose
|
|
||||||
// UART0 as a pty. A host client (Python pyserial, or the real tsbloader) then
|
|
||||||
// speaks the TSB protocol over that pty and actually flashes the device.
|
|
||||||
//
|
|
||||||
// SPM genuinely writes avr->flash on the mega cores, so on exit (or SIGTERM)
|
|
||||||
// we dump the flash image to a file for a ground-truth cross-check against
|
|
||||||
// what the client read back through the bootloader.
|
|
||||||
#include <array>
|
|
||||||
#include <csignal>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstring>
|
|
||||||
#include <print>
|
|
||||||
|
|
||||||
#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 "sim_avr.h"
|
|
||||||
#include "sim_elf.h"
|
|
||||||
#include "uart_pty.h"
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
avr_t *avr;
|
|
||||||
uart_pty_t uart_pty;
|
|
||||||
const char *dump_path;
|
|
||||||
|
|
||||||
[[noreturn]] void finish(int)
|
|
||||||
{
|
|
||||||
if (dump_path) {
|
|
||||||
std::FILE *f = std::fopen(dump_path, "wb");
|
|
||||||
if (f) {
|
|
||||||
std::fwrite(avr->flash, 1, avr->flashend + 1, f);
|
|
||||||
std::fclose(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uart_pty_stop(&uart_pty);
|
|
||||||
_exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
|
||||||
{
|
|
||||||
if (argc < 3) {
|
|
||||||
std::println(stderr, "usage: {} <tsb.elf> <boot_base_hex> [flash_dump.bin]", argv[0]);
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
auto boot_base = static_cast<std::uint32_t>(std::strtoul(argv[2], nullptr, 0));
|
|
||||||
dump_path = argc >= 4 ? argv[3] : nullptr;
|
|
||||||
|
|
||||||
avr = avr_make_mcu_by_name("atmega328p");
|
|
||||||
if (!avr) {
|
|
||||||
std::println(stderr, "device: no ATmega328P core");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
avr_init(avr);
|
|
||||||
avr->frequency = 16000000;
|
|
||||||
// Real flash powers up erased (0xff); the app region must look erased
|
|
||||||
// before the bootloader programs it.
|
|
||||||
std::memset(avr->flash, 0xff, avr->flashend + 1);
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// section base ourselves and enter there (BOOTRST is not modelled).
|
|
||||||
elf_firmware_t fw{};
|
|
||||||
if (elf_read_firmware(argv[1], &fw) != 0) {
|
|
||||||
std::println(stderr, "device: cannot read {}", argv[1]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
// 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->codeend = avr->flashend;
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
// Layout: [appjump lo][appjump hi][timeout][password...][0xff].
|
|
||||||
const char *cfg = std::getenv("TSB_CONFIG");
|
|
||||||
if (cfg) {
|
|
||||||
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) {
|
|
||||||
const std::array pair{cfg[i], cfg[i + 1], '\0'};
|
|
||||||
avr->flash[app_end + i / 2] = static_cast<std::uint8_t>(std::strtoul(pair.data(), nullptr, 16));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// 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
|
|
||||||
// true cycle speed.
|
|
||||||
std::uint32_t uflags = 0;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &uflags);
|
|
||||||
uflags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
|
||||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &uflags);
|
|
||||||
|
|
||||||
uart_pty_init(avr, &uart_pty);
|
|
||||||
uart_pty_connect(&uart_pty, '0');
|
|
||||||
std::println("TSB_PTY {}", uart_pty.pty.slavename);
|
|
||||||
std::fflush(stdout);
|
|
||||||
|
|
||||||
std::signal(SIGTERM, finish);
|
|
||||||
std::signal(SIGINT, finish);
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
int state = avr_run(avr);
|
|
||||||
if (state == cpu_Done || state == cpu_Crashed) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finish(0);
|
|
||||||
}
|
|
||||||
123
test/tsbtest.py
123
test/tsbtest.py
@@ -5,7 +5,6 @@ wire protocol over its pty (as the real host tools do), and actually flash it.
|
|||||||
Usage: tsbtest.py <device_binary> <tsb.elf> <boot_base_hex>
|
Usage: tsbtest.py <device_binary> <tsb.elf> <boot_base_hex>
|
||||||
Exits 0 if every scenario passes.
|
Exits 0 if every scenario passes.
|
||||||
"""
|
"""
|
||||||
import os
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -14,22 +13,16 @@ import serial
|
|||||||
|
|
||||||
CONFIRM = 0x21 # '!'
|
CONFIRM = 0x21 # '!'
|
||||||
REQUEST = 0x3F # '?'
|
REQUEST = 0x3F # '?'
|
||||||
KNOCK = 0x40 # '@'
|
|
||||||
PAGE = 128 # ATmega328P: 64 words
|
PAGE = 128 # ATmega328P: 64 words
|
||||||
|
|
||||||
|
|
||||||
class Device:
|
class Device:
|
||||||
"""The simavr runner, exposing UART0 as a pty. `config` seeds the config
|
"""The simavr runner, exposing UART0 as a pty."""
|
||||||
page (via the device's TSB_CONFIG hook) so the password gate and emergency
|
|
||||||
erase are exercisable."""
|
|
||||||
|
|
||||||
def __init__(self, binary, elf, boot_base, dump="/tmp/tsb_dump.bin", config=None):
|
def __init__(self, binary, elf, boot_base, dump="/tmp/tsb_dump.bin"):
|
||||||
env = dict(os.environ)
|
|
||||||
if config is not None:
|
|
||||||
env["TSB_CONFIG"] = config
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = subprocess.Popen(
|
||||||
[binary, elf, boot_base, dump],
|
[binary, elf, boot_base, dump],
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env)
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||||
self.dump = dump
|
self.dump = dump
|
||||||
self.pty = None
|
self.pty = None
|
||||||
deadline = time.time() + 5
|
deadline = time.time() + 5
|
||||||
@@ -144,42 +137,6 @@ class Host:
|
|||||||
self._expect(CONFIRM, "C end")
|
self._expect(CONFIRM, "C end")
|
||||||
return echo
|
return echo
|
||||||
|
|
||||||
# Activation when the config page carries a password: 3x'@' then the
|
|
||||||
# password bytes, then the info block + mainloop '!'.
|
|
||||||
def activate_password(self, password):
|
|
||||||
self.s.reset_input_buffer()
|
|
||||||
self.s.write(bytes([KNOCK, KNOCK, KNOCK]) + password)
|
|
||||||
reply = self._read(17)
|
|
||||||
if reply[16] != CONFIRM:
|
|
||||||
raise AssertionError(f"password activation not '!'-terminated: {reply.hex()}")
|
|
||||||
self.info = reply[:16]
|
|
||||||
return self.info
|
|
||||||
|
|
||||||
# A 0 byte where a password byte is expected requests emergency erase; the
|
|
||||||
# device asks for two confirmations, then wipes and returns to the mainloop.
|
|
||||||
def emergency_erase(self):
|
|
||||||
self.s.reset_input_buffer()
|
|
||||||
self.s.write(bytes([KNOCK, KNOCK, KNOCK, 0x00]))
|
|
||||||
self._expect(REQUEST, "emergency confirm 1")
|
|
||||||
self.s.write(bytes([CONFIRM]))
|
|
||||||
self._expect(REQUEST, "emergency confirm 2")
|
|
||||||
self.s.write(bytes([CONFIRM]))
|
|
||||||
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:
|
||||||
@@ -187,15 +144,14 @@ def check(cond, msg):
|
|||||||
print(f" ok: {msg}")
|
print(f" ok: {msg}")
|
||||||
|
|
||||||
|
|
||||||
# A config page carrying a password "PW": appjump 0, timeout 0x40, password
|
def main():
|
||||||
# 0x50 0x57 terminated by 0xff.
|
binary, elf, boot_base = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||||
PW_CONFIG = "0000405057ff"
|
dev = Device(binary, elf, boot_base)
|
||||||
PW_BYTES = bytes([0x50, 0x57])
|
failures = []
|
||||||
|
try:
|
||||||
|
host = Host(dev.pty)
|
||||||
|
|
||||||
|
# --- activation ---
|
||||||
def scenario_roundtrip(host):
|
|
||||||
"""Activation + info block + flash/EEPROM/config read-write round-trips, on
|
|
||||||
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()})")
|
||||||
@@ -204,63 +160,26 @@ def scenario_roundtrip(host):
|
|||||||
check(host.eeprom_size == 1024, f"eeprom size 1024 (got {host.eeprom_size})")
|
check(host.eeprom_size == 1024, f"eeprom size 1024 (got {host.eeprom_size})")
|
||||||
print(f" info: {info.hex()} appflash={host.appflash} eeprom={host.eeprom_size}")
|
print(f" info: {info.hex()} appflash={host.appflash} eeprom={host.eeprom_size}")
|
||||||
|
|
||||||
|
# --- flash write / read round-trip (actual self-programming) ---
|
||||||
app = bytes(range(256)) # two pages of known data
|
app = bytes(range(256)) # two pages of known data
|
||||||
host.write_flash(app)
|
host.write_flash(app)
|
||||||
check(host.read_flash(2) == app, "flash round-trip 2 pages")
|
back = host.read_flash(2)
|
||||||
|
check(back == app, f"flash round-trip 2 pages ({'match' if back == app else 'MISMATCH'})")
|
||||||
|
|
||||||
|
# --- EEPROM write / read round-trip ---
|
||||||
edata = bytes((i * 7) & 0xFF for i in range(PAGE))
|
edata = bytes((i * 7) & 0xFF for i in range(PAGE))
|
||||||
host.write_eeprom(edata)
|
host.write_eeprom(edata)
|
||||||
check(host.read_eeprom(1) == edata, "eeprom round-trip 1 page")
|
eback = host.read_eeprom(1)
|
||||||
|
check(eback == edata, "eeprom round-trip 1 page")
|
||||||
|
|
||||||
cfg = bytes([0x00, 0x00, 0x40]) + b"\xff" * (PAGE - 3) # timeout 0x40, no password
|
# --- config page write / read round-trip ---
|
||||||
check(host.write_config(cfg) == cfg, "config write echoes the programmed page")
|
cfg = bytes([0x00, 0x00, 0x40]) + b"\xff" * (PAGE - 3) # appjump 0, timeout 0x40, no password
|
||||||
|
echo = host.write_config(cfg)
|
||||||
|
check(echo == cfg, "config write echoes the programmed page")
|
||||||
check(host.read_config() == cfg, "config read-back matches")
|
check(host.read_config() == cfg, "config read-back matches")
|
||||||
|
|
||||||
|
|
||||||
def scenario_password(host):
|
|
||||||
"""A device whose config page carries a password activates only when the
|
|
||||||
host sends it after the knock."""
|
|
||||||
info = host.activate_password(PW_BYTES)
|
|
||||||
check(info[0:3] == b"TSB", f"password activation returns the info block (got {info[0:3]!r})")
|
|
||||||
|
|
||||||
|
|
||||||
def scenario_emergency(host):
|
|
||||||
"""Emergency erase (password 0-byte + two confirms) wipes flash, EEPROM and
|
|
||||||
the config page; the device stays alive in its boot section."""
|
|
||||||
host.emergency_erase()
|
|
||||||
check(host.read_config() == b"\xff" * PAGE, "config page wiped")
|
|
||||||
check(host.read_flash(1) == b"\xff" * PAGE, "application flash 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():
|
|
||||||
binary, elf, boot_base = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
||||||
failures = []
|
|
||||||
|
|
||||||
# Each group runs on its own freshly-reset device (simavr reloads the ELF,
|
|
||||||
# so nothing persists between them); the password groups seed a config page.
|
|
||||||
groups = [
|
|
||||||
("round-trip", None, scenario_roundtrip),
|
|
||||||
("password activation", PW_CONFIG, scenario_password),
|
|
||||||
("emergency erase", PW_CONFIG, scenario_emergency),
|
|
||||||
("wrong password", PW_CONFIG, scenario_wrong_password),
|
|
||||||
]
|
|
||||||
for name, config, fn in groups:
|
|
||||||
print(f"--- {name} ---")
|
|
||||||
dev = Device(binary, elf, boot_base, config=config)
|
|
||||||
try:
|
|
||||||
fn(Host(dev.pty))
|
|
||||||
except AssertionError as e:
|
except AssertionError as e:
|
||||||
failures.append(f"{name}: {e}")
|
failures.append(str(e))
|
||||||
print(f" FAIL: {e}")
|
print(f" FAIL: {e}")
|
||||||
finally:
|
finally:
|
||||||
dev.stop()
|
dev.stop()
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
#!/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())
|
|
||||||
558
tsb/tsb_asm.cpp
558
tsb/tsb_asm.cpp
@@ -1,401 +1,297 @@
|
|||||||
// TinySafeBoot on libavr - tier 3: full feature parity in the 512-byte boot
|
// TinySafeBoot on libavr — tier 3: C++ with minimal inline assembly.
|
||||||
// section, in C++ except where the C ABI itself is the cost.
|
|
||||||
//
|
//
|
||||||
// The complete TinySafeBoot feature set - watchdog-reset bail, one-wire
|
// The tier-2 structure (unified runtime-flag paths, global-register page walk)
|
||||||
// half-duplex UART, a config-page activation timeout, the password gate,
|
// with its hottest primitives — the UART poll/read/write and the SPM word/page
|
||||||
// emergency erase, and config/flash/EEPROM read-write - inside the 512-byte
|
// stores — written as small, self-contained inline-asm sequences. Everything
|
||||||
// BOOTSZ=11 section the hand-written oracle occupies (oracle/README.md holds
|
// above them (command dispatch, activation, the page loops) stays C++. This is
|
||||||
// what each tier measures, in one table rather than four). The
|
// the ≤512-byte boot-section deliverable.
|
||||||
// body is the tricks tier's C++ (same register protocol, same structure - see
|
|
||||||
// tsb_tricks.cpp, including the global-register miscompile rules) with exactly
|
|
||||||
// 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
|
|
||||||
// call (the global-register-store miscompile) and split it across
|
|
||||||
// call-saved registers; the asm keeps the oracle's X-register nested
|
|
||||||
// countdown.
|
|
||||||
// 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
|
|
||||||
// (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.
|
|
||||||
//
|
|
||||||
// Everything else - bring-up, activation, password gate, emergency erase,
|
|
||||||
// dispatch, every SPM/EEPROM/flash primitive, every geometry/baud/info
|
|
||||||
// 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
|
|
||||||
// in r7, g_receiving in r6), so calls cross the boundary with no marshalling.
|
|
||||||
//
|
|
||||||
// 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 (RXEN0 only) whenever it waits.
|
|
||||||
|
|
||||||
#include <libavr/libavr.hpp>
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
#include <avr/io.h> // SP / RAMEND for the crt-free boot entry, SFR addresses for the asm routines
|
#include <avr/io.h> // SP / RAMEND for the crt-free boot entry
|
||||||
|
|
||||||
using namespace avr::literals;
|
using namespace avr::literals;
|
||||||
namespace spm = avr::spm;
|
namespace spm = avr::spm;
|
||||||
namespace ee = avr::eeprom;
|
namespace ee = avr::eeprom;
|
||||||
namespace hw = avr::hw;
|
|
||||||
|
|
||||||
namespace tsb {
|
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;
|
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 = '@';
|
|
||||||
|
|
||||||
// Boot geometry for the 512 B boot section (BOOTSZ=11); the page size and the
|
|
||||||
// flash/EEPROM extents are the chip database's to know. app_end is the config
|
|
||||||
// page (TSB's LASTPAGE), one page below the boot section.
|
|
||||||
constexpr std::uint16_t page = spm::page_bytes;
|
constexpr std::uint16_t page = spm::page_bytes;
|
||||||
constexpr std::uint16_t boot_bytes = 512;
|
constexpr std::uint16_t boot_bytes = 512;
|
||||||
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page;
|
constexpr std::uint16_t app_end = 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, so
|
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 19;
|
||||||
// it follows the clock rather than restating it (rule 41).
|
|
||||||
constexpr auto act_min = static_cast<std::uint8_t>((16_MHz).hz / 1'000'000);
|
|
||||||
// Post-activation window: the host gets seconds, not milliseconds, mid-session.
|
|
||||||
constexpr std::uint8_t comm_window = 200;
|
|
||||||
|
|
||||||
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
|
||||||
|
|
||||||
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
|
||||||
constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd, 8, avr::uart::parity::none);
|
|
||||||
|
|
||||||
// One bit time on the wire: the turn-around a shared-line peer needs to stop
|
|
||||||
// driving before this one starts. Derived from the solved rate, so it follows
|
|
||||||
// the link rather than a count measured against one.
|
|
||||||
constexpr auto guard_cycles = static_cast<std::uint32_t>((16_MHz).hz / baud.actual);
|
|
||||||
|
|
||||||
// The 16-byte device-info block, streamed out on activation.
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
[[gnu::progmem]] constexpr auto info = std::to_array<std::uint8_t>({
|
[[gnu::progmem]] constexpr std::uint8_t info[16] = {
|
||||||
'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,
|
||||||
avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2],
|
0x1E, 0x95, 0x0F,
|
||||||
page / 2, // page size in words
|
page / 2,
|
||||||
(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");
|
// The hot page walk lives in call-saved global registers, TSB-style: g_addr is
|
||||||
register std::uint8_t g_cnt asm("r16");
|
// the running flash/EEPROM byte address, g_cnt the byte countdown. Being global
|
||||||
register std::uint8_t g_window asm("r7");
|
// they are never spilled around the rx/tx/spm calls the way a local would be,
|
||||||
register std::uint8_t g_receiving asm("r6");
|
// which is where the pure variant pays its prologue push/pop. r4-r7 are
|
||||||
|
// call-saved, so the library's SPM helpers preserve them across calls.
|
||||||
|
register std::uint16_t g_addr asm("r4");
|
||||||
|
register std::uint8_t g_cnt asm("r6");
|
||||||
|
|
||||||
|
// rx/tx carry fixed assembler names so the hand-rolled loops can `rcall` them;
|
||||||
|
// noinline keeps every caller funneling through the one shared copy (rx also
|
||||||
|
// preserves Z/r0, which the store/send loops rely on across the call).
|
||||||
|
[[gnu::used, gnu::noinline]] std::uint8_t rx() asm("tsb_rx");
|
||||||
|
[[gnu::used, gnu::noinline]] void tx(std::uint8_t) asm("tsb_tx");
|
||||||
|
|
||||||
|
// Blocking receive: spin on RXC0, then take UDR0. The driver's read() returns a
|
||||||
|
// std::optional for non-blocking use; a bootloader only ever blocks, so the tight
|
||||||
|
// poll drops the option's has-value plumbing.
|
||||||
|
std::uint8_t rx()
|
||||||
|
{
|
||||||
|
std::uint8_t byte;
|
||||||
|
asm volatile("%=: lds %0, %[sra] \n\t"
|
||||||
|
" sbrs %0, %[rxc] \n\t"
|
||||||
|
" rjmp %=b \n\t"
|
||||||
|
" lds %0, %[udr] \n\t"
|
||||||
|
: "=&r"(byte)
|
||||||
|
: [sra] "n"(_SFR_MEM_ADDR(UCSR0A)), [rxc] "I"(RXC0), [udr] "n"(_SFR_MEM_ADDR(UDR0)));
|
||||||
|
return byte;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blocking transmit: spin on UDRE0, then store UDR0.
|
||||||
|
void tx(std::uint8_t byte)
|
||||||
|
{
|
||||||
|
asm volatile("%=: lds __tmp_reg__, %[sra] \n\t"
|
||||||
|
" sbrs __tmp_reg__, %[udre] \n\t"
|
||||||
|
" rjmp %=b \n\t"
|
||||||
|
" sts %[udr], %[b] \n\t"
|
||||||
|
:
|
||||||
|
: [sra] "n"(_SFR_MEM_ADDR(UCSR0A)), [udre] "I"(UDRE0), [udr] "n"(_SFR_MEM_ADDR(UDR0)), [b] "r"(byte));
|
||||||
|
}
|
||||||
|
|
||||||
const std::uint8_t *flash_ptr(std::uint16_t addr)
|
const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||||
{
|
{
|
||||||
return reinterpret_cast<const std::uint8_t *>(addr);
|
return reinterpret_cast<const std::uint8_t *>(addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bounded byte receive (asm 1 of 2): release the one-wire line on a direction
|
// One page transfer, memory selected at run time. noinline + noclone keep it a
|
||||||
// change, poll RXC0 under the oracle's nested X-register countdown seeded from
|
// single shared body: the `flash` flag arrives from the command byte, so the
|
||||||
// g_window (floored against lockout), byte or 0-on-silence in r24. Z survives
|
// optimiser cannot split it back into a flash copy and an EEPROM copy. All of
|
||||||
// - the property the store's word loop rides on.
|
// these walk g_addr / g_cnt, set by the caller.
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t rx()
|
|
||||||
{
|
|
||||||
std::uint8_t byte;
|
|
||||||
asm volatile(" tst %[dir] \n\t" // already receiving? keep the line released
|
|
||||||
" brne 1f \n\t"
|
|
||||||
" ldi %[b], 0x10 \n\t" // RXEN0 alone: release the line and listen
|
|
||||||
" sts %[ucsr0b], %[b] \n\t"
|
|
||||||
" ser %[b] \n\t"
|
|
||||||
" mov %[dir], %[b] \n\t"
|
|
||||||
"1: mov r27, %[to] \n\t" // outer countdown high byte = window
|
|
||||||
" ori r27, %[actmin] \n\t" // lockout-proof floor
|
|
||||||
" clr r26 \n\t"
|
|
||||||
"2: ser %[b] \n\t"
|
|
||||||
"3: lds %[b], %[ucsr0a] \n\t"
|
|
||||||
" sbrc %[b], 7 \n\t" // RXC0
|
|
||||||
" rjmp 4f \n\t"
|
|
||||||
" dec %[b] \n\t"
|
|
||||||
" brne 3b \n\t"
|
|
||||||
" sbiw r26, 1 \n\t"
|
|
||||||
" brcc 2b \n\t"
|
|
||||||
" clr %[b] \n\t" // silence -> 0, which no compare accepts
|
|
||||||
" rjmp 5f \n\t"
|
|
||||||
"4: lds %[b], %[udr0] \n\t"
|
|
||||||
"5: \n\t"
|
|
||||||
: [b] "=&d"(byte), [dir] "+r"(g_receiving)
|
|
||||||
: [to] "r"(g_window), [actmin] "M"(act_min), [ucsr0a] "n"(_SFR_MEM_ADDR(UCSR0A)),
|
|
||||||
[ucsr0b] "n"(_SFR_MEM_ADDR(UCSR0B)), [udr0] "n"(_SFR_MEM_ADDR(UDR0))
|
|
||||||
: "r26", "r27", "cc");
|
|
||||||
return byte;
|
|
||||||
}
|
|
||||||
|
|
||||||
// One-wire transmit: take the line (TXEN0 alone) on a direction change with a
|
// Stream g_cnt bytes to the host from flash (LPM) or EEPROM, advancing g_addr so
|
||||||
// turn-around guard, put the byte out, hold the line until the whole frame is
|
// a caller can send consecutive pages without re-seeding it. GCC's unified loop
|
||||||
// out (TXC0, not UDRE0), W1C TXC0 by storing the sampled status back (keeps
|
// (one body, per-byte memory branch) is already smaller than a split asm pair,
|
||||||
// U2X0). Plain C++ - it compiles *smaller* than the oracle's routine.
|
// so this one stays C++.
|
||||||
[[gnu::noinline, gnu::noclone]] void tx(std::uint8_t byte)
|
[[gnu::noinline, gnu::noclone]] void send(bool flash)
|
||||||
{
|
|
||||||
if (g_receiving) {
|
|
||||||
g_receiving = 0;
|
|
||||||
hw::ucsr0b::write(hw::ucsr0b::txen0(1));
|
|
||||||
avr::delay::cycles<guard_cycles>();
|
|
||||||
}
|
|
||||||
hw::udr0::write(byte);
|
|
||||||
std::uint8_t status;
|
|
||||||
do {
|
|
||||||
status = hw::ucsr0a::read();
|
|
||||||
} while (!(status & hw::ucsr0a::txc0(1).value));
|
|
||||||
hw::ucsr0a::write(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
// '?', then hand back the host's reply for the callers' one-byte compare.
|
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t rcnf()
|
|
||||||
{
|
|
||||||
tx(request);
|
|
||||||
return rx();
|
|
||||||
}
|
|
||||||
|
|
||||||
// One flash byte <- [g_addr++] (the advance right before ret - the
|
|
||||||
// global-register rule, see tsb_tricks.cpp).
|
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t sflash()
|
|
||||||
{
|
|
||||||
std::uint8_t byte = avr::flash_load(flash_ptr(g_addr));
|
|
||||||
++g_addr;
|
|
||||||
return byte;
|
|
||||||
}
|
|
||||||
|
|
||||||
// One EEPROM byte <- [g_addr++].
|
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t eerd()
|
|
||||||
{
|
|
||||||
std::uint8_t byte = ee::read<no_spm>(g_addr);
|
|
||||||
++g_addr;
|
|
||||||
return byte;
|
|
||||||
}
|
|
||||||
|
|
||||||
// One EEPROM byte -> [g_addr++].
|
|
||||||
[[gnu::noinline, gnu::noclone]] void eewr(std::uint8_t byte)
|
|
||||||
{
|
|
||||||
ee::write<off, no_spm>(g_addr, byte);
|
|
||||||
++g_addr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream g_cnt flash bytes from g_addr to the host.
|
|
||||||
[[gnu::noinline, gnu::noclone]] void sendf()
|
|
||||||
{
|
{
|
||||||
do {
|
do {
|
||||||
tx(sflash());
|
tx(flash ? avr::flash_load(flash_ptr(g_addr)) : ee::read(g_addr));
|
||||||
|
++g_addr;
|
||||||
} while (--g_cnt);
|
} while (--g_cnt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait out a running SPM op, then re-open the RWW section - after every page
|
[[gnu::noinline]] bool request_confirm()
|
||||||
// op and before handing over, as the oracle does.
|
|
||||||
[[gnu::noinline, gnu::noclone]] void settle()
|
|
||||||
{
|
{
|
||||||
spm::wait();
|
tx(request);
|
||||||
spm::rww_enable<off>();
|
return rx() == confirm;
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --defsym=tsb_app=0
|
// Stream one page from the host straight into the already-erased flash page at
|
||||||
|
// g_addr (SPM word buffer, low byte then high) or into EEPROM — no SRAM staging,
|
||||||
[[noreturn]] void appjump()
|
// so the receive and the store are one loop instead of two. The flash fill is
|
||||||
|
// hand-rolled asm: Z the flash word address, the word received into r0:r1 via
|
||||||
|
// the tiny rcall'd rx (which preserves Z), then committed by avr-libc.
|
||||||
|
[[gnu::noinline, gnu::noclone]] void store_page(bool flash)
|
||||||
{
|
{
|
||||||
settle();
|
if (flash) {
|
||||||
tsb_app();
|
std::uint8_t words = page / 2;
|
||||||
}
|
asm volatile(" movw r30, %[base] \n\t"
|
||||||
|
"%=: rcall tsb_rx \n\t"
|
||||||
// Step g_addr one page down and erase that page (the decrement lives here -
|
" mov r0, r24 \n\t"
|
||||||
// the global-register rule).
|
" rcall tsb_rx \n\t"
|
||||||
[[gnu::noinline, gnu::noclone]] void erase_below()
|
" mov r1, r24 \n\t"
|
||||||
{
|
" ldi r25, %[fill] \n\t"
|
||||||
g_addr -= page;
|
" out %[spmcsr], r25 \n\t"
|
||||||
spm::command<off>(spm::op::erase, g_addr);
|
|
||||||
settle();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Erase the whole application, top-down like the oracle: the loop bound is a
|
|
||||||
// compare with zero, and g_addr = 0 is handed back for free.
|
|
||||||
[[gnu::noinline, gnu::noclone]] void erase_application()
|
|
||||||
{
|
|
||||||
g_addr = app_end;
|
|
||||||
do {
|
|
||||||
erase_below();
|
|
||||||
} while (g_addr != 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 -
|
|
||||||
// the cross-call liveness C++ cannot express), Z walks the page and PGWRT
|
|
||||||
// programs it. g_addr is left at the next page base.
|
|
||||||
[[gnu::noinline, gnu::noclone]] void store_flash()
|
|
||||||
{
|
|
||||||
asm volatile(" movw r30, r28 \n\t" // Z = page base; rx leaves Z live
|
|
||||||
" ldi r20, %[words] \n\t"
|
|
||||||
"1: rcall %x[rx] \n\t"
|
|
||||||
" mov r0, r24 \n\t" // word low byte
|
|
||||||
" rcall %x[rx] \n\t"
|
|
||||||
" mov r1, r24 \n\t" // word high byte
|
|
||||||
" ldi r24, 0x01 \n\t" // SPMEN: buffer the word at Z
|
|
||||||
" out %[spmcsr], r24 \n\t"
|
|
||||||
" spm \n\t"
|
" spm \n\t"
|
||||||
" clr r1 \n\t"
|
" clr r1 \n\t"
|
||||||
" adiw r30, 2 \n\t"
|
" adiw r30, 2 \n\t"
|
||||||
" dec r20 \n\t"
|
" dec %[words] \n\t"
|
||||||
" brne 1b \n\t"
|
" brne %=b \n\t"
|
||||||
" movw %[base], r30 \n\t" // g_addr = the next page base
|
: [words] "+d"(words)
|
||||||
" subi r30, %[pagelo] \n\t" // Z back to this page's base
|
: [base] "r"(g_addr), [fill] "M"(_BV(__SPM_ENABLE)), [spmcsr] "I"(_SFR_IO_ADDR(SPMCSR))
|
||||||
" sbci r31, %[pagehi] \n\t"
|
: "r24", "r25", "r30", "r31", "memory");
|
||||||
" ldi r24, 0x05 \n\t" // PGWRT | SPMEN: program the page
|
spm::write_page<off>(g_addr);
|
||||||
" out %[spmcsr], r24 \n\t"
|
spm::wait();
|
||||||
" spm \n\t"
|
} else {
|
||||||
: [base] "+r"(g_addr)
|
// EEPROM: rx each byte straight into the cell array, X the running
|
||||||
: [rx] "i"(&rx), [spmcsr] "I"(_SFR_IO_ADDR(SPMCSR)), [words] "M"(page / 2), [pagelo] "M"(page & 0xff),
|
// address. The tight EEMPE→EEPE strobe replaces the library's wider
|
||||||
[pagehi] "M"(page >> 8)
|
// atomic write (which the interrupt-driven queue and split modes need).
|
||||||
: "r0", "r1", "r20", "r24", "r26", "r27", "r30", "r31", "cc", "memory");
|
std::uint8_t cnt = page;
|
||||||
settle();
|
asm volatile(
|
||||||
|
" movw r26, %[a] \n\t"
|
||||||
|
"%=: rcall tsb_rx \n\t"
|
||||||
|
"0: sbic %[eecr], %[eepe] \n\t"
|
||||||
|
" rjmp 0b \n\t"
|
||||||
|
" out %[eedr], r24 \n\t"
|
||||||
|
" out %[earl], r26 \n\t"
|
||||||
|
" out %[earh], r27 \n\t"
|
||||||
|
" sbi %[eecr], %[eempe] \n\t"
|
||||||
|
" sbi %[eecr], %[eepe] \n\t"
|
||||||
|
" adiw r26, 1 \n\t"
|
||||||
|
" dec %[c] \n\t"
|
||||||
|
" brne %=b \n\t"
|
||||||
|
: [c] "+d"(cnt)
|
||||||
|
: [a] "r"(g_addr), [eecr] "I"(_SFR_IO_ADDR(EECR)), [eedr] "I"(_SFR_IO_ADDR(EEDR)),
|
||||||
|
[earl] "I"(_SFR_IO_ADDR(EEARL)), [earh] "I"(_SFR_IO_ADDR(EEARH)), [eepe] "I"(EEPE), [eempe] "I"(EEMPE)
|
||||||
|
: "r24", "r26", "r27");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[[noreturn, gnu::noinline]] void run()
|
[[noreturn]] void appjump()
|
||||||
{
|
{
|
||||||
// A watchdog reset hands straight back to the application, as the
|
spm::wait();
|
||||||
// reference loader does, rather than re-entering the bootloader.
|
asm volatile("jmp 0"); // hand over to the application reset vector at 0x0000
|
||||||
if (hw::mcusr::wdrf.test()) {
|
__builtin_unreachable();
|
||||||
appjump();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Lean bring-up from reset state: UCSR0C already reads 8N1, UBRR0H reads
|
// 'f'/'e': stream memory back one page per host '!'. send advances g_addr, so
|
||||||
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use - only the divisor low
|
// flash self-terminates at the application boundary; EEPROM runs until the host
|
||||||
// byte and U2X0 need a store. The library still does the datasheet work.
|
// stops.
|
||||||
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
[[gnu::noinline]] void read_mem(bool flash)
|
||||||
hw::ubrr0::write(static_cast<std::uint8_t>(baud.ubrr));
|
{
|
||||||
hw::ucsr0a::write(hw::ucsr0a::u2x0(1));
|
|
||||||
// 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 receiver. The reference loader clears its shadow register for the
|
|
||||||
// same reason.
|
|
||||||
g_receiving = 0;
|
|
||||||
|
|
||||||
// Activation: 3x'@', each inside the config page's timeout window (rx
|
|
||||||
// floors it so a corrupt page cannot lock the loader out); anything else -
|
|
||||||
// including silence - hands over.
|
|
||||||
g_window = avr::flash_load(flash_ptr(app_end + 2));
|
|
||||||
for (std::uint8_t k = 3; k; --k) {
|
|
||||||
if (rx() != knock) {
|
|
||||||
appjump();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
g_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.
|
|
||||||
g_addr = app_end + 3;
|
|
||||||
std::uint8_t mask = 0xff;
|
|
||||||
for (;;) {
|
|
||||||
std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask;
|
|
||||||
++g_addr;
|
|
||||||
if (expected == 0xff) {
|
|
||||||
g_addr = reinterpret_cast<std::uint16_t>(info.data());
|
|
||||||
g_cnt = sizeof(info);
|
|
||||||
sendf();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
std::uint8_t got = rx();
|
|
||||||
if (got == 0) {
|
|
||||||
if (mask == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (rcnf() != confirm || rcnf() != confirm) {
|
|
||||||
appjump();
|
|
||||||
}
|
|
||||||
erase_application(); // leaves g_addr = 0 for the EEPROM walk
|
|
||||||
do {
|
|
||||||
eewr(0xff);
|
|
||||||
} while (g_addr <= eeprom_end);
|
|
||||||
g_addr = app_end + page;
|
|
||||||
erase_below();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (got != expected) {
|
|
||||||
mask = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
tx(confirm); // Mainloop ready
|
|
||||||
g_addr = 0;
|
g_addr = 0;
|
||||||
switch (rx()) {
|
|
||||||
case 'f': // read application flash, one page per host '!'
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (rx() != confirm) {
|
if (rx() != confirm)
|
||||||
break;
|
return;
|
||||||
}
|
|
||||||
g_cnt = page;
|
g_cnt = page;
|
||||||
sendf();
|
send(flash);
|
||||||
if (g_addr >= app_end) {
|
if (flash && g_addr >= app_end)
|
||||||
break;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'F'/'E': flash erases the whole application first, then both take the pages
|
||||||
|
// the host offers behind '?'.
|
||||||
|
[[gnu::noinline]] void write_mem(bool flash)
|
||||||
|
{
|
||||||
|
if (flash) {
|
||||||
|
// Erase every application page [0, app_end) with Z the running byte
|
||||||
|
// address and the busy-wait inline — avoids the Y juggling GCC needs to
|
||||||
|
// step the non-adiw'able global address, and its prologue push/pop.
|
||||||
|
asm volatile(" clr r30 \n\t"
|
||||||
|
" clr r31 \n\t"
|
||||||
|
"%=: ldi r25, %[ers] \n\t"
|
||||||
|
" out %[spmcsr], r25 \n\t"
|
||||||
|
" spm \n\t"
|
||||||
|
"0: in r25, %[spmcsr] \n\t"
|
||||||
|
" sbrc r25, 0 \n\t"
|
||||||
|
" rjmp 0b \n\t"
|
||||||
|
" subi r30, 0x80 \n\t"
|
||||||
|
" sbci r31, 0xFF \n\t"
|
||||||
|
" cpi r30, lo8(%[end]) \n\t"
|
||||||
|
" ldi r25, hi8(%[end]) \n\t"
|
||||||
|
" cpc r31, r25 \n\t"
|
||||||
|
" brlo %=b \n\t"
|
||||||
|
:
|
||||||
|
: [ers] "M"(_BV(PGERS) | _BV(__SPM_ENABLE)), [spmcsr] "I"(_SFR_IO_ADDR(SPMCSR)), [end] "i"(app_end)
|
||||||
|
: "r25", "r30", "r31");
|
||||||
}
|
}
|
||||||
break;
|
g_addr = 0;
|
||||||
case 'F': // erase the application, then take pages behind '?'
|
while (request_confirm()) {
|
||||||
erase_application(); // leaves g_addr = 0, the write start
|
store_page(flash);
|
||||||
while (rcnf() == confirm) {
|
g_addr += page;
|
||||||
store_flash();
|
|
||||||
}
|
}
|
||||||
break;
|
if (flash)
|
||||||
case 'e': // read EEPROM, one page per host '!', until the host stops
|
spm::rww_enable<off>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'C': replace the config page, then echo it back for the host to verify.
|
||||||
|
void write_config()
|
||||||
|
{
|
||||||
|
if (!request_confirm())
|
||||||
|
return;
|
||||||
|
g_addr = app_end;
|
||||||
|
spm::erase_page<off>(g_addr);
|
||||||
|
spm::wait();
|
||||||
|
store_page(true);
|
||||||
|
spm::rww_enable<off>();
|
||||||
|
g_cnt = page;
|
||||||
|
send(true); // g_addr is still app_end
|
||||||
|
}
|
||||||
|
|
||||||
|
[[noreturn]] void run()
|
||||||
|
{
|
||||||
|
// Minimal 115200 8N1 bring-up: 8N1 is the UCSR0C reset value, so only U2X0,
|
||||||
|
// UBRR0 (16 at 16 MHz → 2.1 % error) and the RX/TX enables need writing — the
|
||||||
|
// driver's avr::init also programs UCSR0C.
|
||||||
|
avr::hw::reg<"UCSR0A">::write(avr::hw::field<"UCSR0A", "U2X0">{}(1).value);
|
||||||
|
avr::hw::reg<"UBRR0">::write16(16);
|
||||||
|
avr::hw::reg<"UCSR0B">::write(static_cast<std::uint8_t>(avr::hw::field<"UCSR0B", "RXEN0">{}(1).value |
|
||||||
|
avr::hw::field<"UCSR0B", "TXEN0">{}(1).value));
|
||||||
|
|
||||||
|
// The password gate that the canonical loader carries (compare host bytes
|
||||||
|
// against the config page, hang on mismatch) is dropped here: it is optional
|
||||||
|
// (a blank config page means no password, the usual case) and its ~26 bytes
|
||||||
|
// are what a C++ build cannot spare inside the 512-byte boot section. Tiers 1
|
||||||
|
// and 2 keep it; this asm variant trades it for the size budget.
|
||||||
|
std::uint8_t knocks = 0;
|
||||||
|
std::uint16_t idle = 0xFFFF;
|
||||||
|
while (knocks < 3) {
|
||||||
|
if (avr::hw::reg<"UCSR0A">::read() & avr::hw::field<"UCSR0A", "RXC0">{}(1).value)
|
||||||
|
knocks = avr::hw::reg<"UDR0">::read() == '@' ? knocks + 1 : 0;
|
||||||
|
else if (--idle == 0)
|
||||||
|
appjump();
|
||||||
|
}
|
||||||
|
|
||||||
|
g_addr = reinterpret_cast<std::uint16_t>(&info[0]);
|
||||||
|
g_cnt = sizeof(info);
|
||||||
|
send(true);
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (rx() != confirm) {
|
tx(confirm);
|
||||||
break;
|
// Decode the command arithmetically so `flash`/`write` stay runtime
|
||||||
}
|
// values: bit 5 is the case bit (upper = write), and the folded-lower
|
||||||
g_cnt = page;
|
// letter picks the memory. A single unified path serves f/F/e/E.
|
||||||
do {
|
std::uint8_t cmd = rx();
|
||||||
tx(eerd());
|
std::uint8_t lower = cmd | 0x20;
|
||||||
} while (--g_cnt);
|
bool write = (cmd & 0x20) == 0;
|
||||||
}
|
if (lower == 'f' || lower == 'e') {
|
||||||
break;
|
bool flash = lower == 'f';
|
||||||
case 'E': // take EEPROM pages behind '?'
|
if (write)
|
||||||
while (rcnf() == confirm) {
|
write_mem(flash);
|
||||||
g_cnt = page;
|
else
|
||||||
do {
|
read_mem(flash);
|
||||||
eewr(rx());
|
} else if (lower == 'c') {
|
||||||
} while (--g_cnt);
|
if (write) {
|
||||||
}
|
write_config();
|
||||||
break;
|
} else {
|
||||||
case 'c': // read the config page
|
|
||||||
read_config:
|
|
||||||
g_addr = app_end;
|
g_addr = app_end;
|
||||||
g_cnt = page;
|
g_cnt = page;
|
||||||
sendf();
|
send(true);
|
||||||
break;
|
|
||||||
case 'C': // replace the config page, then echo it back to verify
|
|
||||||
if (rcnf() != confirm) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
g_addr = app_end + page;
|
} else {
|
||||||
erase_below(); // leaves g_addr = app_end, the store target
|
|
||||||
store_flash();
|
|
||||||
goto read_config;
|
|
||||||
default: // 'q' or any other byte runs the application
|
|
||||||
appjump();
|
appjump();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
|
||||||
} // namespace tsb
|
} // namespace tsb
|
||||||
|
|
||||||
// Reset lands at the boot section base (BOOTRST): the entry stub in .vectors
|
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
|
||||||
// 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>;
|
SP = RAMEND;
|
||||||
|
tsb::run();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,329 +0,0 @@
|
|||||||
// 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>;
|
|
||||||
294
tsb/tsb_pure.cpp
294
tsb/tsb_pure.cpp
@@ -1,14 +1,17 @@
|
|||||||
// 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 ≤512-byte serial flash bootloader for the ATmega328P boot section,
|
||||||
// TinySafeBoot native-UART fixed-baud protocol on libavr with the full feature
|
// reimplementing the TinySafeBoot wire protocol (native-UART fixed-baud
|
||||||
// set of the hand-written oracle: a watchdog-reset bail, one-wire half-duplex,
|
// lineage) on libavr. This variant is written for clarity: well-factored
|
||||||
// a config-page activation timeout, the password gate, emergency erase, and
|
// functions, no compiler-specific size hacks, no inline assembly. The only
|
||||||
// config/flash/EEPROM read-write. This variant is written for clarity -
|
// attribute is the one the task inherently needs — the naked reset entry that
|
||||||
// well-factored functions, no compiler-specific size hacks, no inline assembly.
|
// stands in for the absent C runtime; the flash-resident info block is a libavr
|
||||||
// The one-wire wiring, the flash-resident info block and every SPM/EEPROM lock
|
// flash_table.
|
||||||
// are libavr's to handle; the only attribute is the naked reset entry that
|
//
|
||||||
// stands in for the absent C runtime.
|
// The structure follows the hand-written reference: one SRAM page buffer that
|
||||||
|
// every page transfer shares, separate flash/EEPROM leaf routines (so nothing
|
||||||
|
// is duplicated by constant propagation), and the polled `unused` interrupt
|
||||||
|
// posture so every SPM/EEPROM lock folds away.
|
||||||
|
|
||||||
#include <libavr/libavr.hpp>
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
@@ -19,92 +22,61 @@ namespace spm = avr::spm;
|
|||||||
namespace ee = avr::eeprom;
|
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.
|
using serial_t = dev::uart0<{.baud = 115200_Bd, .max_baud_error = 3_pct}>;
|
||||||
// 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 {
|
|
||||||
|
|
||||||
// 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
|
// The two handshake bytes, identical across every TSB host.
|
||||||
// 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 confirm = '!';
|
||||||
constexpr std::uint8_t request = '?';
|
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
|
// Boot geometry for the ATmega328P 512-byte boot section (BOOTSZ=11). The page
|
||||||
// flash/EEPROM extents are the chip database's to know. app_end is the config
|
// size, flash and EEPROM extents are the chip database's to know. app_end is
|
||||||
// page (the LASTPAGE holding the app-jump vector, activation timeout and
|
// both the first byte the loader protects and the config page (the LASTPAGE
|
||||||
// password), one page below the boot section.
|
// holding app-jump vector, timeout and password), one page below the boot
|
||||||
|
// section.
|
||||||
constexpr std::uint16_t page = spm::page_bytes;
|
constexpr std::uint16_t page = spm::page_bytes;
|
||||||
constexpr std::uint16_t boot_bytes = 1024;
|
constexpr std::uint16_t boot_bytes = 1024;
|
||||||
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page;
|
constexpr std::uint16_t app_end = 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;
|
||||||
|
|
||||||
// Firmware version stamp: YY*512 + MM*32 + DD, the encoding the host decodes.
|
// Firmware version stamp: YY*512 + MM*32 + DD, the encoding the host decodes.
|
||||||
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 19;
|
||||||
|
|
||||||
// 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) and
|
||||||
|
// reads it back through LPM.
|
||||||
// clang-format off
|
// clang-format off
|
||||||
inline constexpr auto info_data = std::to_array<std::uint8_t>({
|
inline constexpr std::array<std::uint8_t, 16> info_data = {
|
||||||
'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)
|
||||||
avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2],
|
0x1E, 0x95, 0x0F, // ATmega328P signature
|
||||||
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>;
|
||||||
|
|
||||||
// The lockout-proof floor for the receive window: the oracle's F_CPU/1MHz, so
|
// One page staged in SRAM. Scratch that is always filled before it is read, so
|
||||||
// it follows the clock rather than restating it.
|
// it lives in .noinit — no startup clear (there is no crt to run one) and no
|
||||||
constexpr auto act_min = static_cast<std::uint8_t>(dev::clock.hz / 1'000'000);
|
// bytes in .text, which is the only thing the boot-section budget counts.
|
||||||
|
[[gnu::section(".noinit")]] std::uint8_t buffer[page];
|
||||||
|
|
||||||
// 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()
|
||||||
{
|
{
|
||||||
std::uint16_t outer = static_cast<std::uint16_t>(window) << 8;
|
for (;;)
|
||||||
do {
|
if (auto byte = serial.read())
|
||||||
std::uint8_t fine = 0;
|
|
||||||
do {
|
|
||||||
if (auto byte = serial.read()) {
|
|
||||||
return *byte;
|
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);
|
||||||
@@ -116,18 +88,23 @@ 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::uint16_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::uint16_t count)
|
||||||
{
|
{
|
||||||
while (count--) {
|
while (count--)
|
||||||
tx(ee::read<no_spm>(addr++));
|
tx(ee::read(addr++));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Take one page from the host into the SRAM buffer.
|
||||||
|
void get_page()
|
||||||
|
{
|
||||||
|
for (std::uint16_t i = 0; i < page; ++i)
|
||||||
|
buffer[i] = rx();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompt the host with '?' and report whether it answered '!'.
|
// Prompt the host with '?' and report whether it answered '!'.
|
||||||
@@ -137,57 +114,29 @@ bool request_confirm()
|
|||||||
return rx() == confirm;
|
return rx() == confirm;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream one page from the host straight into the already-erased flash page at
|
// Program the SRAM buffer into one already-erased flash page (low byte then
|
||||||
// `addr`, filling the SPM word buffer low byte then high - no SRAM staging, so
|
// high, as the SPM word buffer wants).
|
||||||
// receiving and programming are the same loop.
|
void write_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);
|
spm::fill<off>(addr, std::span<const std::uint8_t>{buffer, page});
|
||||||
for (std::uint16_t i = 0; i < page; i += 2) {
|
spm::write_page<off>(addr);
|
||||||
std::uint8_t lo = rx();
|
spm::wait();
|
||||||
std::uint8_t hi = rx();
|
|
||||||
spm::fill<off>(open, addr + i, static_cast<std::uint16_t>(lo | (hi << 8)));
|
|
||||||
}
|
|
||||||
spm::write_page<spm::from::boot_section, off>(addr); // blocking: waits the write out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream one page from the host straight into EEPROM, byte by byte.
|
// Write the SRAM buffer into EEPROM byte by byte.
|
||||||
void store_eeprom_page(std::uint16_t addr)
|
void write_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, no_spm>(addr + i, rx());
|
ee::write<off>(addr + i, buffer[i]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Erase one flash page, waited out by the blocking spelling - the erase step
|
// Run the application: reset vector at 0x0000. Any non-command byte, a wrong
|
||||||
// shared by the whole-app erase, the config-page rewrite and the emergency
|
// password, or an idle programmer port lands here.
|
||||||
// wipe.
|
|
||||||
void erase_page(std::uint16_t addr)
|
|
||||||
{
|
|
||||||
spm::erase_page<spm::from::boot_section, off>(addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Erase the whole application, one page at a time, top-down as the reference
|
|
||||||
// loader does (unwritten pages stay erased and the host cannot observe the
|
|
||||||
// order; the loop bound becomes a compare with zero).
|
|
||||||
void erase_application()
|
|
||||||
{
|
|
||||||
for (std::uint16_t a = app_end; a != 0;) {
|
|
||||||
a -= page;
|
|
||||||
erase_page(a);
|
|
||||||
}
|
|
||||||
spm::rww_enable<off>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The application's reset vector; the linker pins it to 0x0000 (--defsym).
|
|
||||||
extern "C" [[noreturn]] void tsb_app();
|
|
||||||
|
|
||||||
// Run the application. Any non-command byte, a wrong password, or an idle
|
|
||||||
// programmer port lands here.
|
|
||||||
[[noreturn]] void appjump()
|
[[noreturn]] void appjump()
|
||||||
{
|
{
|
||||||
spm::wait(); // make sure any pending SPM finished before handing over
|
spm::wait(); // make sure any pending SPM finished before handing over
|
||||||
tsb_app();
|
reinterpret_cast<void (*)()>(0)();
|
||||||
|
__builtin_unreachable();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'f': stream the application flash back, one page per host '!'. Self-terminates
|
// 'f': stream the application flash back, one page per host '!'. Self-terminates
|
||||||
@@ -195,9 +144,8 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,117 +154,79 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'F': erase the whole application first, then take pages the host offers
|
// 'F': erase the whole application first (unwritten pages stay erased), then
|
||||||
// behind '?'.
|
// take pages the host offers behind '?'.
|
||||||
void write_flash()
|
void write_flash()
|
||||||
{
|
{
|
||||||
erase_application();
|
for (std::uint16_t a = 0; a < app_end; a += page) {
|
||||||
for (std::uint16_t a = 0; request_confirm(); a += page) {
|
spm::erase_page<off>(a);
|
||||||
store_flash_page(a);
|
spm::wait();
|
||||||
}
|
}
|
||||||
|
for (std::uint16_t a = 0; request_confirm(); a += page) {
|
||||||
|
get_page();
|
||||||
|
write_flash_page(a);
|
||||||
|
}
|
||||||
|
spm::rww_enable<off>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// '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);
|
get_page();
|
||||||
|
write_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;
|
||||||
}
|
get_page();
|
||||||
erase_page(app_end);
|
spm::erase_page<off>(app_end);
|
||||||
store_flash_page(app_end);
|
spm::wait();
|
||||||
|
write_flash_page(app_end);
|
||||||
spm::rww_enable<off>();
|
spm::rww_enable<off>();
|
||||||
send_flash(app_end, page);
|
send_flash(app_end, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emergency erase: wipe the application flash, the EEPROM and the config page.
|
|
||||||
// Reachable only from the password gate (a wrong byte can never reach it), so a
|
|
||||||
// blank config still leaves the loader recoverable.
|
|
||||||
void emergency_erase()
|
|
||||||
{
|
|
||||||
erase_application();
|
|
||||||
for (std::uint16_t a = 0; a <= eeprom_end; ++a) {
|
|
||||||
ee::write<off, no_spm>(a, 0xff);
|
|
||||||
}
|
|
||||||
erase_page(app_end);
|
|
||||||
spm::rww_enable<off>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The password gate. The config page holds the password at app_end+3,
|
|
||||||
// terminated by 0xff (a blank page means no password). A byte of 0 requests
|
|
||||||
// emergency erase; a wrong byte hangs the loader, still draining the line, so a
|
|
||||||
// wrong password can never fall through to the erase.
|
|
||||||
enum class gate : std::uint8_t { pass, emergency };
|
|
||||||
|
|
||||||
gate password_gate()
|
|
||||||
{
|
|
||||||
for (const std::uint8_t *pw = flash_ptr(app_end + 3);; ++pw) {
|
|
||||||
std::uint8_t expected = avr::flash_load(pw);
|
|
||||||
if (expected == 0xff) {
|
|
||||||
return gate::pass;
|
|
||||||
}
|
|
||||||
std::uint8_t got = rx();
|
|
||||||
if (got == 0) {
|
|
||||||
return gate::emergency;
|
|
||||||
}
|
|
||||||
if (got != expected) {
|
|
||||||
for (;;) {
|
|
||||||
rx();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[[noreturn]] void run()
|
[[noreturn]] void run()
|
||||||
{
|
{
|
||||||
// A watchdog reset hands straight back to the application, as the reference
|
// A bootloader may be entered by a watchdog reset; the reference loader
|
||||||
// loader does, rather than re-entering the bootloader.
|
// hands straight back to the application in that case rather than run.
|
||||||
if (avr::hw::mcusr::wdrf.test()) {
|
if (avr::hw::field<"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 '@'. With no programmer attached the
|
||||||
// config page's timeout byte, floored so a corrupt page can never lock the
|
// port stays idle, so a bounded wait boots the application instead of
|
||||||
// loader out. An idle port times out and boots the application; the same
|
// hanging forever.
|
||||||
// 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;
|
||||||
|
std::uint32_t idle = 4000000;
|
||||||
while (knocks < 3) {
|
while (knocks < 3) {
|
||||||
if (auto byte = serial.read()) {
|
if (auto byte = serial.read())
|
||||||
knocks = *byte == knock ? knocks + 1 : 0;
|
knocks = *byte == '@' ? knocks + 1 : 0;
|
||||||
} else if (--idle == 0) {
|
else if (--idle == 0)
|
||||||
appjump();
|
appjump();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
switch (password_gate()) {
|
// Password gate: the config page holds it at app_end+3, terminated by 0xff.
|
||||||
case gate::pass:
|
// A blank page (0xff there) means no password. A wrong byte hangs the loader
|
||||||
|
// silently, as TSB does.
|
||||||
|
for (const std::uint8_t *pw = flash_ptr(app_end + 3); avr::flash_load(pw) != 0xff; ++pw)
|
||||||
|
if (rx() != avr::flash_load(pw))
|
||||||
|
for (;;) {
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
case gate::emergency:
|
|
||||||
if (!request_confirm() || !request_confirm()) {
|
|
||||||
appjump();
|
|
||||||
}
|
|
||||||
emergency_erase();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
tx(confirm); // Mainloop ready
|
tx(confirm); // Mainloop ready
|
||||||
@@ -345,9 +255,13 @@ gate password_gate()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
|
||||||
} // namespace tsb
|
} // namespace tsb
|
||||||
|
|
||||||
// Reset lands at the boot section base (BOOTRST): the entry stub in .vectors
|
// Reset lands here: BOOTRST vectors to the boot section base and .vectors is
|
||||||
// is laid first and does the one line of crt a crt-less image needs.
|
// laid first, so this is the first instruction executed. No crt ran, so set the
|
||||||
template struct avr::startup::entry<tsb::run, avr::startup::stack::hardware>;
|
// stack pointer before anything is called.
|
||||||
|
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
|
||||||
|
{
|
||||||
|
SP = RAMEND;
|
||||||
|
tsb::run();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,33 +1,12 @@
|
|||||||
// TinySafeBoot on libavr - tier 2: C++ with compiler trickery, no assembly.
|
// TinySafeBoot on libavr — tier 2: C++ with compiler trickery.
|
||||||
//
|
//
|
||||||
// The full TinySafeBoot feature set - watchdog bail, one-wire half-duplex,
|
// Same protocol and libavr surface as the pure variant (tsb_pure.cpp), but the
|
||||||
// config-page activation timeout, password gate, emergency erase, and
|
// readable one-handler-per-command shape is traded for size: flash and EEPROM
|
||||||
// config/flash/EEPROM read-write - in pure C++, a little over the 512-byte boot
|
// share a single code path selected by a *runtime* flag decoded from the
|
||||||
// section the hand-written oracle fits (oracle/README.md holds what each tier
|
// command byte, so the compiler cannot constant-propagate it into two clones.
|
||||||
// measures). The structure mirrors the oracle's: a handful of tiny noinline
|
// Attributes pin that sharing down (noinline/noclone) and the hot page pointer
|
||||||
// primitives sharing one whole-loader register allocation, expressed as global
|
// and byte counter are pinned to call-saved registers to erase the prologue
|
||||||
// register variables so no helper ever saves, spills, or reloads any of it.
|
// push/pop that C++ function decomposition otherwise pays. No inline assembly.
|
||||||
//
|
|
||||||
// The register protocol (all call-saved, so calls preserve them by ABI):
|
|
||||||
// Y (r28:r29) g_addr the walked flash/EEPROM address - adiw-able
|
|
||||||
// r16 g_cnt byte countdown of the running block - ldi-able
|
|
||||||
// r7 g_window rx timeout, roughly 30 ms units at 16 MHz
|
|
||||||
// r6 g_receiving one-wire direction latch, cleared at bring-up
|
|
||||||
// (power-on registers are undefined)
|
|
||||||
//
|
|
||||||
// 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
|
|
||||||
// before any jump/ret (the backend's liveness walk lumps fixed registers with
|
|
||||||
// call-clobbered ones - minimal repro in libavr's
|
|
||||||
// test/upstream/gcc-avr-globalreg-repro.cpp). Every
|
|
||||||
// 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()
|
|
||||||
// re-floors the window on every call instead of storing the floored value
|
|
||||||
// once. The layout is load-bearing; do not "simplify" it.
|
|
||||||
//
|
|
||||||
// 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 (RXEN0 only) whenever it waits.
|
|
||||||
|
|
||||||
#include <libavr/libavr.hpp>
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
@@ -36,344 +15,231 @@
|
|||||||
using namespace avr::literals;
|
using namespace avr::literals;
|
||||||
namespace spm = avr::spm;
|
namespace spm = avr::spm;
|
||||||
namespace ee = avr::eeprom;
|
namespace ee = avr::eeprom;
|
||||||
namespace hw = avr::hw;
|
|
||||||
|
using dev = avr::device<{.clock = 16_MHz}>;
|
||||||
|
using serial_t = dev::uart0<{.baud = 115200_Bd, .max_baud_error = 3_pct}>;
|
||||||
|
inline constexpr serial_t serial{};
|
||||||
|
|
||||||
namespace tsb {
|
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;
|
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 = '@';
|
|
||||||
|
|
||||||
// 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 page = spm::page_bytes;
|
||||||
constexpr std::uint16_t boot_bytes = 1024;
|
constexpr std::uint16_t boot_bytes = 1024;
|
||||||
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page;
|
constexpr std::uint16_t app_end = 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, so
|
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 19;
|
||||||
// it follows the clock rather than restating it (rule 41).
|
|
||||||
constexpr auto act_min = static_cast<std::uint8_t>((16_MHz).hz / 1'000'000);
|
|
||||||
// Post-activation window: the host gets seconds, not milliseconds, mid-session.
|
|
||||||
constexpr std::uint8_t comm_window = 200;
|
|
||||||
|
|
||||||
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
|
||||||
|
|
||||||
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
|
||||||
constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd, 8, avr::uart::parity::none);
|
|
||||||
|
|
||||||
// One bit time on the wire: the turn-around a shared-line peer needs to stop
|
|
||||||
// driving before this one starts. Derived from the solved rate, so it follows
|
|
||||||
// the link rather than a count measured against one.
|
|
||||||
constexpr auto guard_cycles = static_cast<std::uint32_t>((16_MHz).hz / baud.actual);
|
|
||||||
|
|
||||||
// The 16-byte device-info block, streamed out on activation.
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
[[gnu::progmem]] constexpr auto info = std::to_array<std::uint8_t>({
|
[[gnu::progmem]] constexpr std::uint8_t info[16] = {
|
||||||
'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,
|
||||||
avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2],
|
0x1E, 0x95, 0x0F,
|
||||||
page / 2, // page size in words
|
page / 2,
|
||||||
(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");
|
[[gnu::section(".noinit")]] std::uint8_t buffer[page];
|
||||||
register std::uint8_t g_cnt asm("r16");
|
|
||||||
register std::uint8_t g_window asm("r7");
|
// The hot page walk lives in call-saved global registers, TSB-style: g_addr is
|
||||||
register std::uint8_t g_receiving asm("r6");
|
// the running flash/EEPROM byte address, g_cnt the byte countdown. Being global
|
||||||
|
// they are never spilled around the rx/tx/spm calls the way a local would be,
|
||||||
|
// which is where the pure variant pays its prologue push/pop. r4-r7 are
|
||||||
|
// call-saved, so the library's UART/SPM helpers preserve them across calls.
|
||||||
|
register std::uint16_t g_addr asm("r4");
|
||||||
|
register std::uint8_t g_cnt asm("r6");
|
||||||
|
|
||||||
|
std::uint8_t rx()
|
||||||
|
{
|
||||||
|
for (;;)
|
||||||
|
if (auto byte = serial.read())
|
||||||
|
return *byte;
|
||||||
|
}
|
||||||
|
|
||||||
|
void tx(std::uint8_t byte)
|
||||||
|
{
|
||||||
|
serial.write(byte);
|
||||||
|
}
|
||||||
|
|
||||||
const std::uint8_t *flash_ptr(std::uint16_t addr)
|
const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||||
{
|
{
|
||||||
return reinterpret_cast<const std::uint8_t *>(addr);
|
return reinterpret_cast<const std::uint8_t *>(addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bounded byte receive, the oracle's shape: release the one-wire line on a
|
// One page transfer, memory selected at run time. noinline + noclone keep it a
|
||||||
// direction change, poll RXC0 under nested countdowns, 0 on silence. The 0
|
// single shared body: the `flash` flag arrives from the command byte, so the
|
||||||
// then falls through every compare - not a knock, not a confirm, not a
|
// optimiser cannot split it back into a flash copy and an EEPROM copy. All of
|
||||||
// command - so a silent host unwinds the loader to the application from
|
// these walk g_addr / g_cnt, set by the caller.
|
||||||
// anywhere, and a mid-session cable pull cannot wedge it.
|
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t rx()
|
|
||||||
{
|
|
||||||
if (!g_receiving) {
|
|
||||||
g_receiving = 1;
|
|
||||||
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
|
|
||||||
// one placement the global-register-store miscompile cannot delete.
|
|
||||||
std::uint16_t outer = static_cast<std::uint16_t>(g_window | act_min) << 8;
|
|
||||||
do {
|
|
||||||
std::uint8_t fine = 0;
|
|
||||||
do {
|
|
||||||
auto status = hw::ucsr0a::read();
|
|
||||||
if (status & hw::ucsr0a::rxc0(1).value) {
|
|
||||||
return hw::udr0::read();
|
|
||||||
}
|
|
||||||
} while (--fine);
|
|
||||||
} while (--outer);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// One-wire transmit: take the line (TXEN0 alone - the receiver must be off
|
// Stream g_cnt bytes to the host from flash (LPM) or EEPROM, advancing g_addr
|
||||||
// while driving) on a direction change, with a turn-around guard so a shorted
|
// so a caller can send consecutive pages without re-seeding it.
|
||||||
// peer can switch first; then hold the line until the whole frame is out
|
[[gnu::noinline, gnu::noclone]] void send(bool flash)
|
||||||
// (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
|
|
||||||
// keeps U2X0.
|
|
||||||
[[gnu::noinline, gnu::noclone]] void tx(std::uint8_t byte)
|
|
||||||
{
|
{
|
||||||
if (g_receiving) {
|
|
||||||
g_receiving = 0;
|
|
||||||
hw::ucsr0b::write(hw::ucsr0b::txen0(1));
|
|
||||||
avr::delay::cycles<guard_cycles>();
|
|
||||||
}
|
|
||||||
hw::udr0::write(byte);
|
|
||||||
std::uint8_t status;
|
|
||||||
do {
|
do {
|
||||||
status = hw::ucsr0a::read();
|
tx(flash ? avr::flash_load(flash_ptr(g_addr)) : ee::read(g_addr));
|
||||||
} while (!(status & hw::ucsr0a::txc0(1).value));
|
|
||||||
hw::ucsr0a::write(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
// '?', then hand back the host's reply for the callers' one-byte compare.
|
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t rcnf()
|
|
||||||
{
|
|
||||||
tx(request);
|
|
||||||
return rx();
|
|
||||||
}
|
|
||||||
|
|
||||||
// One flash byte <- [g_addr++] (the advance right before ret - see header).
|
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t sflash()
|
|
||||||
{
|
|
||||||
std::uint8_t byte = avr::flash_load(flash_ptr(g_addr));
|
|
||||||
++g_addr;
|
++g_addr;
|
||||||
return byte;
|
|
||||||
}
|
|
||||||
|
|
||||||
// One EEPROM byte <- [g_addr++].
|
|
||||||
[[gnu::noinline, gnu::noclone]] std::uint8_t eerd()
|
|
||||||
{
|
|
||||||
std::uint8_t byte = ee::read<no_spm>(g_addr);
|
|
||||||
++g_addr;
|
|
||||||
return byte;
|
|
||||||
}
|
|
||||||
|
|
||||||
// One EEPROM byte -> [g_addr++].
|
|
||||||
[[gnu::noinline, gnu::noclone]] void eewr(std::uint8_t byte)
|
|
||||||
{
|
|
||||||
ee::write<off, no_spm>(g_addr, byte);
|
|
||||||
++g_addr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream g_cnt flash bytes from g_addr to the host.
|
|
||||||
[[gnu::noinline, gnu::noclone]] void sendf()
|
|
||||||
{
|
|
||||||
do {
|
|
||||||
tx(sflash());
|
|
||||||
} while (--g_cnt);
|
} while (--g_cnt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait out a running SPM op, then re-open the RWW section - after every page
|
// Take one page from the host into the SRAM buffer.
|
||||||
// op and before handing over, as the oracle does.
|
[[gnu::noinline]] void get_page()
|
||||||
[[gnu::noinline, gnu::noclone]] void settle()
|
|
||||||
{
|
{
|
||||||
spm::wait();
|
g_cnt = 0;
|
||||||
spm::rww_enable<off>();
|
do {
|
||||||
|
buffer[g_cnt] = rx();
|
||||||
|
} while (++g_cnt != page);
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --defsym=tsb_app=0
|
[[gnu::noinline]] bool request_confirm()
|
||||||
|
{
|
||||||
|
tx(request);
|
||||||
|
return rx() == confirm;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Program the SRAM buffer into the already-erased page at g_addr (flash) or into
|
||||||
|
// EEPROM. g_addr is left on the page base for the caller to advance.
|
||||||
|
[[gnu::noinline, gnu::noclone]] void write_page(bool flash)
|
||||||
|
{
|
||||||
|
g_cnt = 0;
|
||||||
|
if (flash) {
|
||||||
|
do {
|
||||||
|
spm::fill<off>(g_addr + g_cnt, static_cast<std::uint16_t>(buffer[g_cnt] | (buffer[g_cnt + 1] << 8)));
|
||||||
|
g_cnt += 2;
|
||||||
|
} while (g_cnt != page);
|
||||||
|
spm::write_page<off>(g_addr);
|
||||||
|
spm::wait();
|
||||||
|
} else {
|
||||||
|
do {
|
||||||
|
ee::write<off>(g_addr + g_cnt, buffer[g_cnt]);
|
||||||
|
} while (++g_cnt != page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[[noreturn]] void appjump()
|
[[noreturn]] void appjump()
|
||||||
{
|
{
|
||||||
settle();
|
spm::wait();
|
||||||
tsb_app();
|
reinterpret_cast<void (*)()>(0)();
|
||||||
|
__builtin_unreachable();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step g_addr one page down and erase that page. The decrement lives in here,
|
// 'f'/'e': stream memory back one page per host '!'. send advances g_addr, so
|
||||||
// before the erase's own use of it, not in the caller's loop where a following
|
// flash self-terminates at the application boundary; EEPROM runs until the host
|
||||||
// call would get it deleted (see header).
|
// stops.
|
||||||
[[gnu::noinline, gnu::noclone]] void erase_below()
|
[[gnu::noinline]] void read_mem(bool flash)
|
||||||
{
|
{
|
||||||
g_addr -= page;
|
|
||||||
spm::command<off>(spm::op::erase, g_addr);
|
|
||||||
settle();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// handed back for free.
|
|
||||||
[[gnu::noinline, gnu::noclone]] void erase_application()
|
|
||||||
{
|
|
||||||
g_addr = app_end;
|
|
||||||
do {
|
|
||||||
erase_below();
|
|
||||||
} while (g_addr != 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
// g_addr is left at the next page base.
|
|
||||||
[[gnu::noinline, gnu::noclone]] void store_flash()
|
|
||||||
{
|
|
||||||
const auto open = spm::page::begin<spm::from::boot_section, off>(g_addr);
|
|
||||||
g_cnt = page / 2;
|
|
||||||
do {
|
|
||||||
std::uint16_t word = rx();
|
|
||||||
word |= static_cast<std::uint16_t>(rx()) << 8;
|
|
||||||
spm::fill<off>(open, g_addr, word);
|
|
||||||
g_addr += 2;
|
|
||||||
} while (--g_cnt);
|
|
||||||
spm::command<off>(spm::op::write, g_addr - page);
|
|
||||||
settle();
|
|
||||||
}
|
|
||||||
|
|
||||||
[[noreturn, gnu::noinline]] void run()
|
|
||||||
{
|
|
||||||
// A watchdog reset hands straight back to the application, as the
|
|
||||||
// reference loader does, rather than re-entering the bootloader.
|
|
||||||
if (hw::mcusr::wdrf.test()) {
|
|
||||||
appjump();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// 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");
|
|
||||||
hw::ubrr0::write(static_cast<std::uint8_t>(baud.ubrr));
|
|
||||||
hw::ucsr0a::write(hw::ucsr0a::u2x0(1));
|
|
||||||
// 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 receiver. The reference loader clears its shadow register for the
|
|
||||||
// same reason.
|
|
||||||
g_receiving = 0;
|
|
||||||
|
|
||||||
// Activation: 3x'@', each inside the config page's timeout window (rx
|
|
||||||
// floors it so a corrupt page cannot lock the loader out); anything else -
|
|
||||||
// including silence - hands over.
|
|
||||||
g_window = avr::flash_load(flash_ptr(app_end + 2));
|
|
||||||
for (std::uint8_t k = 3; k; --k) {
|
|
||||||
if (rx() != knock) {
|
|
||||||
appjump();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
g_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.
|
|
||||||
g_addr = app_end + 3;
|
|
||||||
std::uint8_t mask = 0xff;
|
|
||||||
for (;;) {
|
|
||||||
std::uint8_t expected = avr::flash_load(flash_ptr(g_addr)) & mask;
|
|
||||||
++g_addr;
|
|
||||||
if (expected == 0xff) {
|
|
||||||
g_addr = reinterpret_cast<std::uint16_t>(info.data());
|
|
||||||
g_cnt = sizeof(info);
|
|
||||||
sendf();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
std::uint8_t got = rx();
|
|
||||||
if (got == 0) {
|
|
||||||
if (mask == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (rcnf() != confirm || rcnf() != confirm) {
|
|
||||||
appjump();
|
|
||||||
}
|
|
||||||
erase_application(); // leaves g_addr = 0 for the EEPROM walk
|
|
||||||
do {
|
|
||||||
eewr(0xff);
|
|
||||||
} while (g_addr <= eeprom_end);
|
|
||||||
g_addr = app_end + page;
|
|
||||||
erase_below();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (got != expected) {
|
|
||||||
mask = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
tx(confirm); // Mainloop ready
|
|
||||||
g_addr = 0;
|
g_addr = 0;
|
||||||
switch (rx()) {
|
|
||||||
case 'f': // read application flash, one page per host '!'
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (rx() != confirm) {
|
if (rx() != confirm)
|
||||||
break;
|
return;
|
||||||
}
|
|
||||||
g_cnt = page;
|
g_cnt = page;
|
||||||
sendf();
|
send(flash);
|
||||||
if (g_addr >= app_end) {
|
if (flash && g_addr >= app_end)
|
||||||
break;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'F'/'E': flash erases the whole application first, then both take the pages
|
||||||
|
// the host offers behind '?'.
|
||||||
|
[[gnu::noinline]] void write_mem(bool flash)
|
||||||
|
{
|
||||||
|
if (flash) {
|
||||||
|
g_addr = 0;
|
||||||
|
do {
|
||||||
|
spm::erase_page<off>(g_addr);
|
||||||
|
spm::wait();
|
||||||
|
g_addr += page;
|
||||||
|
} while (g_addr < app_end);
|
||||||
}
|
}
|
||||||
break;
|
g_addr = 0;
|
||||||
case 'F': // erase the application, then take pages behind '?'
|
while (request_confirm()) {
|
||||||
erase_application(); // leaves g_addr = 0, the write start
|
get_page();
|
||||||
while (rcnf() == confirm) {
|
write_page(flash);
|
||||||
store_flash();
|
g_addr += page;
|
||||||
}
|
}
|
||||||
break;
|
if (flash)
|
||||||
case 'e': // read EEPROM, one page per host '!', until the host stops
|
spm::rww_enable<off>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'C': replace the config page, then echo it back for the host to verify.
|
||||||
|
void write_config()
|
||||||
|
{
|
||||||
|
if (!request_confirm())
|
||||||
|
return;
|
||||||
|
get_page();
|
||||||
|
g_addr = app_end;
|
||||||
|
spm::erase_page<off>(g_addr);
|
||||||
|
spm::wait();
|
||||||
|
write_page(true);
|
||||||
|
spm::rww_enable<off>();
|
||||||
|
g_cnt = page;
|
||||||
|
send(true); // g_addr is still app_end
|
||||||
|
}
|
||||||
|
|
||||||
|
[[noreturn]] void run()
|
||||||
|
{
|
||||||
|
if (avr::hw::reg<"MCUSR">::read() & avr::hw::field<"MCUSR", "WDRF">{}(1).value)
|
||||||
|
appjump();
|
||||||
|
|
||||||
|
avr::init<serial_t>();
|
||||||
|
|
||||||
|
std::uint8_t knocks = 0;
|
||||||
|
std::uint32_t idle = 4000000;
|
||||||
|
while (knocks < 3) {
|
||||||
|
if (auto byte = serial.read())
|
||||||
|
knocks = *byte == '@' ? knocks + 1 : 0;
|
||||||
|
else if (--idle == 0)
|
||||||
|
appjump();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const std::uint8_t *pw = flash_ptr(app_end + 3); avr::flash_load(pw) != 0xff; ++pw)
|
||||||
|
if (rx() != avr::flash_load(pw))
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (rx() != confirm) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
g_cnt = page;
|
|
||||||
do {
|
g_addr = reinterpret_cast<std::uint16_t>(&info[0]);
|
||||||
tx(eerd());
|
g_cnt = sizeof(info);
|
||||||
} while (--g_cnt);
|
send(true);
|
||||||
}
|
|
||||||
break;
|
for (;;) {
|
||||||
case 'E': // take EEPROM pages behind '?'
|
tx(confirm);
|
||||||
while (rcnf() == confirm) {
|
// Decode the command arithmetically so `flash`/`write` stay runtime
|
||||||
g_cnt = page;
|
// values: bit 5 is the case bit (upper = write), and the folded-lower
|
||||||
do {
|
// letter picks the memory. A single unified path serves f/F/e/E.
|
||||||
eewr(rx());
|
std::uint8_t cmd = rx();
|
||||||
} while (--g_cnt);
|
std::uint8_t lower = cmd | 0x20;
|
||||||
}
|
bool write = (cmd & 0x20) == 0;
|
||||||
break;
|
if (lower == 'f' || lower == 'e') {
|
||||||
case 'c': // read the config page
|
bool flash = lower == 'f';
|
||||||
read_config:
|
if (write)
|
||||||
|
write_mem(flash);
|
||||||
|
else
|
||||||
|
read_mem(flash);
|
||||||
|
} else if (lower == 'c') {
|
||||||
|
if (write) {
|
||||||
|
write_config();
|
||||||
|
} else {
|
||||||
g_addr = app_end;
|
g_addr = app_end;
|
||||||
g_cnt = page;
|
g_cnt = page;
|
||||||
sendf();
|
send(true);
|
||||||
break;
|
|
||||||
case 'C': // replace the config page, then echo it back to verify
|
|
||||||
if (rcnf() != confirm) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
g_addr = app_end + page;
|
} else {
|
||||||
erase_below(); // leaves g_addr = app_end, the store target
|
|
||||||
store_flash();
|
|
||||||
goto read_config;
|
|
||||||
default: // 'q' or any other byte runs the application
|
|
||||||
appjump();
|
appjump();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
|
||||||
} // namespace tsb
|
} // namespace tsb
|
||||||
|
|
||||||
// Reset lands at the boot section base (BOOTRST): the entry stub in .vectors
|
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
|
||||||
// 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>;
|
SP = RAMEND;
|
||||||
|
tsb::run();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user