4 Commits

Author SHA1 Message Date
57d94cf631 tsb: refactor the pure tier onto libavr sugar
The showcase tier now leans on the helpers it fed back instead of reaching under
them: the info block is an avr::flash_table (no raw [[gnu::progmem]]), a page is
filled with spm::fill(addr, span) (no hand-packed lo|hi<<8 loop), and the
WDT-reset bail reads field<"MCUSR","WDRF">::test() (no read() & {}(1).value).

Zero-overhead throughout: .text stays 740 B, byte-identical across generated and
reflect modes, protocol test green. The info block streams through the existing
address-based send_flash rather than a range-for over the flash_table — the
range-for is a distinct loop that cannot share the loader's one flash streamer,
so it would add 14 B for no functional gain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 13:33:53 +02:00
8203a24f33 tsb: slim the port branch to the libavr reimplementation
main carried the whole pre-libavr tree beside the port: the other-bootloader
directories (blink, stk500v2), the Atmel Studio solution/project, and — dead in
the tsb dir itself — four submodule links to the superseded io/flash/uart/type
libraries the libavr sources never include. None are build inputs; CMake drives
the three variants through FetchContent. master keeps the full legacy tree
untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 13:12:56 +02:00
2906da3272 tsb: drop the local -O3 strip, now handled by the libavr toolchain
The -O3 leak is fixed upstream (cmake/release-os.cmake via CMAKE_PROJECT_INCLUDE),
so the port no longer needs its own string(REPLACE); a Release build is -Os
through the toolchain file. Verified: all three variants build at their sizes
(508/658/740) and pass the size + protocol ctest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 10:52:13 +02:00
64c1e484b5 tsb: reimplement TinySafeBoot on libavr in three size tiers
The native-UART fixed-baud TinySafeBoot protocol, ported onto libavr as a
crt-free boot-section loader, in three variants that trade clarity for size:

  tsb_pure   740 B  idiomatic C++: SRAM page buffer, separate flash/EEPROM
                    leaves, shared framing; the polled `unused` guard posture.
  tsb_tricks 658 B  unified runtime-flag paths (noinline/noclone), call-saved
                    global-register page walk — attributes only, no asm.
  tsb_asm    508 B  streaming store + hand-rolled UART/SPM/EEPROM/erase loops;
                    fits the 512 B boot section (BOOTSZ=11). Trims the optional
                    password gate and WDT-reset bail — unreachable in C++ with
                    both (hand-asm is ~15 % denser). Tiers 1-2 keep them and
                    live in the 1 KB section they fit.

All three are .text byte-identical across libavr's generated and reflect modes.
The CMake build strips the leaked -O3 (a Release build is silently -O3, not the
-Os this loader is measured against) and gates each variant's size against its
section. A simavr harness (test/device.c + test/tsbtest.py) drives the real wire
protocol over a pty and flashes the device; the size and protocol tests run in
ctest. Verified byte-for-byte against the reference tsbloader_adv (C#/mono):
activate, read info, flash write + verify.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 05:00:51 +02:00
25 changed files with 719 additions and 2902 deletions

View File

@@ -7,9 +7,8 @@ TabWidth: 4
UseTab: ForIndentation
AlignEscapedNewlines: DontAlign
AllowShortFunctionsOnASingleLine: Empty
BreakTemplateDeclarations: Yes
AlwaysBreakTemplateDeclarations: true
BreakBeforeBraces: Custom
BraceWrapping:
AfterFunction: true
InsertBraces: true
...

38
.clangd
View File

@@ -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
View File

@@ -1,11 +1,8 @@
# Line endings are the repository's, not the editing machine's: this checkout
# is reached from two hosts, and a file rewritten by a Windows tool comes back
# with every line changed unless something says otherwise. Naming the source
# extensions left Markdown, Python, shell and CMake to whatever the writing
# tool defaulted to, which is CRLF on one of the two.
* text=auto eol=lf
# Atmel Studio writes these and expects them back.
*.h eol=lf
*.hpp eol=lf
*.c eol=lf
*.cpp eol=lf
.git* eol=lf
*.vcxproj* eol=crlf
*.cppproj eol=crlf
*.sln eol=crlf

5
.gitignore vendored
View File

@@ -12,10 +12,5 @@ Debug
# CMake / clangd
/build/
/local/
compile_commands.json
.cache/
# Python
__pycache__/
*.pyc

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "libavr"]
path = libavr
url = ../libavr.git

View File

@@ -1,6 +0,0 @@
{
"recommendations": [
"llvm-vs-code-extensions.vscode-clangd",
"ms-vscode.cmake-tools"
]
}

36
.vscode/settings.json vendored
View File

@@ -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"
}
}

View File

@@ -2,119 +2,52 @@ cmake_minimum_required(VERSION 3.28)
project(tsb_libavr LANGUAGES CXX)
# libavr rides as the pinned submodule; LIBAVR_ROOT (cache or environment)
# overrides it for tandem development against a working tree. The toolchain
# file comes from the submodule via CMakePresets.json either way.
# libavr from a local checkout (LIBAVR_ROOT) or the forge; the toolchain file
# comes from the same checkout via CMakePresets.json.
include(FetchContent)
if(NOT LIBAVR_ROOT AND DEFINED ENV{LIBAVR_ROOT})
set(LIBAVR_ROOT $ENV{LIBAVR_ROOT})
endif()
if(NOT LIBAVR_ROOT)
set(LIBAVR_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/libavr)
if(LIBAVR_ROOT)
FetchContent_Declare(libavr SOURCE_DIR ${LIBAVR_ROOT})
else()
FetchContent_Declare(libavr GIT_REPOSITORY git@git.blackmark.me:avr/libavr.git GIT_TAG main)
endif()
if(NOT EXISTS ${LIBAVR_ROOT}/CMakeLists.txt)
message(FATAL_ERROR "libavr not found at ${LIBAVR_ROOT} - run: git submodule update --init libavr")
endif()
add_subdirectory(${LIBAVR_ROOT} libavr-build)
include(${LIBAVR_ROOT}/cmake/checks.cmake)
FetchContent_MakeAvailable(libavr)
if(PROJECT_IS_TOP_LEVEL)
add_compile_options(-Werror) # warnings are errors for the port's own code
enable_testing()
# Rules 11 and 33 over this repo's own sources. The oracle's assembly needs
# no exclusion: it is neither formatted nor ASCII-checked, being in neither
# glob, which is the right answer for a vendored reference whose text is
# the artifact.
libavr_format_test()
# The behavioral tests drive the real wire protocols over a simavr pty
# (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.
# The behavioral test drives the real TinySafeBoot wire protocol over a
# simavr pty (as the host tools do) and actually flashes the device. The
# runner is a host program built at configure time against libsimavr; if it
# or Python is missing, only the size tests run.
find_program(_host_cc NAMES cc gcc)
find_package(Python3 COMPONENTS Interpreter)
if(_host_cc AND Python3_FOUND)
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(
COMMAND ${_host_cxx} -std=c++23 -Wall -Wextra -O2
-I/usr/include/simavr -I/usr/include/simavr/parts
-o ${TSB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/device.cpp
COMMAND ${_host_cc} -O2 -I/usr/include/simavr -I/usr/include/simavr/parts
-o ${TSB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/device.c
-lsimavr -lsimavrparts -lelf
RESULT_VARIABLE _dev_res ERROR_VARIABLE _dev_err)
if(NOT _dev_res EQUAL 0)
# One bounded line of it: this becomes a single argument on a
# command line, and the reading has to say what stopped the build
# 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)
message(STATUS "tsb_device not built (${_dev_err}) — protocol tests skipped")
unset(TSB_DEVICE)
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()
# The ELF is only a container (symbols, section headers) and is never flashed -
# and the host tool's load_image() dispatches on extension, so handing it one
# would silently program the header bytes. Every loader image therefore gets
# both flashable forms beside it at link time: .hex for avrdude, and .bin for
# the host tool's raw path (which is what the reloc and update tests convert to
# on the fly). .eeprom is dropped - EEPROM content is its own update.
function(add_image_outputs name)
add_custom_command(TARGET ${name} POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.hex
COMMAND ${CMAKE_OBJCOPY} -O binary -R .eeprom
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
endfunction()
# The TinySafeBoot protocol reimplemented on libavr in variants that trade
# The TinySafeBoot protocol reimplemented on libavr in three variants that trade
# 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
# loader has no use for the crt or the vector table. The entry sits in
# .vectors, laid first, and runs - avr::startup::entry on the policy tier,
# the experiment tiers' own naked stubs elsewhere, each documented in its
# source. The boot base is FLASHEND+1 minus the section size; the linker
# section-start and the source's boot_bytes agree. tsb_app is
# the application's reset vector, pinned to 0 here so the loaders jump to a
# 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.
# its size; BOOTRST vectors a reset to its base) with -nostartfiles a polled
# loader has no use for the crt or the vector table. The naked entry sits in
# .vectors, laid first, and runs. The boot base is FLASHEND+1 minus the section
# size; the linker section-start and the source's boot_bytes agree.
# tsb_asm — inline-asm variant, the headline: ≤512 B, the 512 B section.
# tsb_pure / tsb_tricks — pure-C++ and compiler-trickery variants, larger,
# shown in the 1 KB section (BOOTSZ=10) they fit.
#
# add_tsb_variant(<name> <boot-section-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)
add_executable(${name} tsb/${name}.cpp)
target_link_libraries(${name} PRIVATE libavr)
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${base_hex}
-Wl,--defsym=tsb_app=0 -Wl,--pmem-wrap-around=32k)
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${base_hex})
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
add_image_outputs(${name})
if(PROJECT_IS_TOP_LEVEL)
add_test(NAME ${name}.size
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}>
-DLIMIT=${bytes} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
if(DEFINED TSB_DEVICE)
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})
endif()
endif()
endfunction()
# The tiers reimplement the ATmega328P-only reference protocol, so the guard is
# the whole of what this repo builds.
if(LIBAVR_MCU STREQUAL "atmega328p")
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()
add_tsb_variant(tsb_asm 512)
add_tsb_variant(tsb_pure 1024)
add_tsb_variant(tsb_tricks 1024)

View File

@@ -6,7 +6,7 @@
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"toolchainFile": "${sourceDir}/libavr/cmake/avr-toolchain.cmake",
"toolchainFile": "$env{LIBAVR_ROOT}/cmake/avr-toolchain.cmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
@@ -16,69 +16,29 @@
{
"name": "atmega328p-generated",
"inherits": "base",
"cacheVariables": {
"LIBAVR_MCU": "atmega328p",
"LIBAVR_REFLECT": "OFF"
}
"cacheVariables": { "LIBAVR_MCU": "atmega328p", "LIBAVR_REFLECT": "OFF" }
},
{
"name": "atmega328p-reflect",
"inherits": "base",
"cacheVariables": {
"LIBAVR_MCU": "atmega328p",
"LIBAVR_REFLECT": "ON"
}
"cacheVariables": { "LIBAVR_MCU": "atmega328p", "LIBAVR_REFLECT": "ON" }
}
],
"buildPresets": [
{
"name": "atmega328p-generated",
"configurePreset": "atmega328p-generated"
},
{
"name": "atmega328p-reflect",
"configurePreset": "atmega328p-reflect"
}
],
"testPresets": [
{
"name": "atmega328p-generated",
"configurePreset": "atmega328p-generated",
"output": {
"outputOnFailure": true
}
}
{ "name": "atmega328p-generated", "configurePreset": "atmega328p-generated" },
{ "name": "atmega328p-reflect", "configurePreset": "atmega328p-reflect" }
],
"workflowPresets": [
{
"name": "atmega328p-generated",
"steps": [
{
"type": "configure",
"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"
}
{ "type": "configure", "name": "atmega328p-generated" },
{ "type": "build", "name": "atmega328p-generated" },
{ "type": "test", "name": "atmega328p-generated" }
]
}
],
"testPresets": [
{ "name": "atmega328p-generated", "configurePreset": "atmega328p-generated", "output": { "outputOnFailure": true } }
]
}

View File

@@ -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`.

View File

@@ -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

View File

@@ -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

Submodule libavr deleted from 93d8b0e491

View File

@@ -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.

View File

@@ -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
;***********************************************************************
;***********************************************************************
;***********************************************************************

View File

@@ -4,9 +4,6 @@ if(NOT _res EQUAL 0)
endif()
# avr-size line 2 is "<text> <data> <bss> <dec> <hex> <file>".
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})
if(_text GREATER LIMIT)
message(FATAL_ERROR ".text is ${_text} bytes, over the ${LIMIT}-byte boot section")

86
test/device.c Normal file
View 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;
}

View File

@@ -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);
}

View File

@@ -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>
Exits 0 if every scenario passes.
"""
import os
import subprocess
import sys
import time
@@ -14,22 +13,16 @@ import serial
CONFIRM = 0x21 # '!'
REQUEST = 0x3F # '?'
KNOCK = 0x40 # '@'
PAGE = 128 # ATmega328P: 64 words
class Device:
"""The simavr runner, exposing UART0 as a pty. `config` seeds the config
page (via the device's TSB_CONFIG hook) so the password gate and emergency
erase are exercisable."""
"""The simavr runner, exposing UART0 as a pty."""
def __init__(self, binary, elf, boot_base, dump="/tmp/tsb_dump.bin", config=None):
env = dict(os.environ)
if config is not None:
env["TSB_CONFIG"] = config
def __init__(self, binary, elf, boot_base, dump="/tmp/tsb_dump.bin"):
self.proc = subprocess.Popen(
[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.pty = None
deadline = time.time() + 5
@@ -144,42 +137,6 @@ class Host:
self._expect(CONFIRM, "C end")
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):
if not cond:
@@ -187,15 +144,14 @@ def check(cond, msg):
print(f" ok: {msg}")
# A config page carrying a password "PW": appjump 0, timeout 0x40, password
# 0x50 0x57 terminated by 0xff.
PW_CONFIG = "0000405057ff"
PW_BYTES = bytes([0x50, 0x57])
def main():
binary, elf, boot_base = sys.argv[1], sys.argv[2], sys.argv[3]
dev = Device(binary, elf, boot_base)
failures = []
try:
host = Host(dev.pty)
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."""
# --- activation ---
info = host.activate()
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()})")
@@ -204,63 +160,26 @@ def scenario_roundtrip(host):
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}")
# --- flash write / read round-trip (actual self-programming) ---
app = bytes(range(256)) # two pages of known data
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))
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
check(host.write_config(cfg) == cfg, "config write echoes the programmed page")
# --- config page write / read round-trip ---
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")
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:
failures.append(f"{name}: {e}")
failures.append(str(e))
print(f" FAIL: {e}")
finally:
dev.stop()

View File

@@ -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"

View File

@@ -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())

View File

@@ -1,401 +1,297 @@
// TinySafeBoot on libavr - tier 3: full feature parity in the 512-byte boot
// section, in C++ except where the C ABI itself is the cost.
// TinySafeBoot on libavr tier 3: C++ with minimal inline assembly.
//
// The complete TinySafeBoot feature set - watchdog-reset bail, one-wire
// half-duplex UART, a config-page activation timeout, the password gate,
// emergency erase, and config/flash/EEPROM read-write - inside the 512-byte
// BOOTSZ=11 section the hand-written oracle occupies (oracle/README.md holds
// what each tier measures, in one table rather than four). The
// 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.
// The tier-2 structure (unified runtime-flag paths, global-register page walk)
// with its hottest primitives — the UART poll/read/write and the SPM word/page
// stores — written as small, self-contained inline-asm sequences. Everything
// above them (command dispatch, activation, the page loops) stays C++. This is
// the ≤512-byte boot-section deliverable.
#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;
namespace spm = avr::spm;
namespace ee = avr::eeprom;
namespace hw = avr::hw;
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;
constexpr std::uint8_t confirm = '!';
constexpr std::uint8_t request = '?';
constexpr std::uint8_t knock = '@';
// Boot geometry for the 512 B boot section (BOOTSZ=11); the page size and the
// flash/EEPROM extents are the chip database's to know. app_end is the config
// page (TSB's LASTPAGE), one page below the boot section.
constexpr std::uint16_t page = spm::page_bytes;
constexpr std::uint16_t boot_bytes = 512;
constexpr std::uint16_t 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>((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 + 19;
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd, 8, avr::uart::parity::none);
// One bit time on the wire: the turn-around a shared-line peer needs to stop
// driving before this one starts. Derived from the solved rate, so it follows
// the link rather than a count measured against one.
constexpr auto guard_cycles = static_cast<std::uint32_t>((16_MHz).hz / baud.actual);
// The 16-byte device-info block, streamed out on activation.
// clang-format off
[[gnu::progmem]] constexpr auto info = std::to_array<std::uint8_t>({
[[gnu::progmem]] constexpr std::uint8_t info[16] = {
'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
0xF3,
0x1E, 0x95, 0x0F,
page / 2,
(app_end / 2) & 0xFF, (app_end / 2) >> 8,
eeprom_end & 0xFF, eeprom_end >> 8,
0xAA, 0xAA,
});
};
// clang-format on
register std::uint16_t g_addr asm("r28");
register std::uint8_t g_cnt asm("r16");
register std::uint8_t g_window asm("r7");
register std::uint8_t g_receiving asm("r6");
// The hot page walk lives in call-saved global registers, TSB-style: g_addr is
// 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 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)
{
return reinterpret_cast<const std::uint8_t *>(addr);
}
// Bounded byte receive (asm 1 of 2): release the one-wire line on a direction
// change, poll RXC0 under the oracle's nested X-register countdown seeded from
// g_window (floored against lockout), byte or 0-on-silence in r24. Z survives
// - the property the store's word loop rides on.
[[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 page transfer, memory selected at run time. noinline + noclone keep it a
// single shared body: the `flash` flag arrives from the command byte, so the
// optimiser cannot split it back into a flash copy and an EEPROM copy. All of
// these walk g_addr / g_cnt, set by the caller.
// One-wire transmit: take the line (TXEN0 alone) on a direction change with a
// turn-around guard, put the byte out, hold the line until the whole frame is
// out (TXC0, not UDRE0), W1C TXC0 by storing the sampled status back (keeps
// U2X0). Plain C++ - it compiles *smaller* than the oracle's routine.
[[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 {
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()
// Stream g_cnt bytes to the host from flash (LPM) or EEPROM, advancing g_addr so
// a caller can send consecutive pages without re-seeding it. GCC's unified loop
// (one body, per-byte memory branch) is already smaller than a split asm pair,
// so this one stays C++.
[[gnu::noinline, gnu::noclone]] void send(bool flash)
{
do {
tx(sflash());
tx(flash ? avr::flash_load(flash_ptr(g_addr)) : ee::read(g_addr));
++g_addr;
} while (--g_cnt);
}
// 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, gnu::noclone]] void settle()
[[gnu::noinline]] bool request_confirm()
{
spm::wait();
spm::rww_enable<off>();
tx(request);
return rx() == confirm;
}
extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --defsym=tsb_app=0
[[noreturn]] void appjump()
// 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,
// 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();
tsb_app();
}
// Step g_addr one page down and erase that page (the decrement lives here -
// the global-register rule).
[[gnu::noinline, gnu::noclone]] void erase_below()
{
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 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"
if (flash) {
std::uint8_t words = page / 2;
asm volatile(" movw r30, %[base] \n\t"
"%=: rcall tsb_rx \n\t"
" mov r0, r24 \n\t"
" rcall tsb_rx \n\t"
" mov r1, r24 \n\t"
" ldi r25, %[fill] \n\t"
" out %[spmcsr], r25 \n\t"
" spm \n\t"
" clr r1 \n\t"
" adiw r30, 2 \n\t"
" dec r20 \n\t"
" brne 1b \n\t"
" movw %[base], r30 \n\t" // g_addr = the next page base
" subi r30, %[pagelo] \n\t" // Z back to this page's base
" sbci r31, %[pagehi] \n\t"
" ldi r24, 0x05 \n\t" // PGWRT | SPMEN: program the page
" out %[spmcsr], r24 \n\t"
" spm \n\t"
: [base] "+r"(g_addr)
: [rx] "i"(&rx), [spmcsr] "I"(_SFR_IO_ADDR(SPMCSR)), [words] "M"(page / 2), [pagelo] "M"(page & 0xff),
[pagehi] "M"(page >> 8)
: "r0", "r1", "r20", "r24", "r26", "r27", "r30", "r31", "cc", "memory");
settle();
" dec %[words] \n\t"
" brne %=b \n\t"
: [words] "+d"(words)
: [base] "r"(g_addr), [fill] "M"(_BV(__SPM_ENABLE)), [spmcsr] "I"(_SFR_IO_ADDR(SPMCSR))
: "r24", "r25", "r30", "r31", "memory");
spm::write_page<off>(g_addr);
spm::wait();
} else {
// EEPROM: rx each byte straight into the cell array, X the running
// address. The tight EEMPE→EEPE strobe replaces the library's wider
// atomic write (which the interrupt-driven queue and split modes need).
std::uint8_t cnt = page;
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
// reference loader does, rather than re-entering the bootloader.
if (hw::mcusr::wdrf.test()) {
appjump();
}
spm::wait();
asm volatile("jmp 0"); // hand over to the application reset vector at 0x0000
__builtin_unreachable();
}
// 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
// 'f'/'e': stream memory back one page per host '!'. send advances g_addr, so
// flash self-terminates at the application boundary; EEPROM runs until the host
// stops.
[[gnu::noinline]] void read_mem(bool flash)
{
g_addr = 0;
switch (rx()) {
case 'f': // read application flash, one page per host '!'
for (;;) {
if (rx() != confirm) {
break;
}
if (rx() != confirm)
return;
g_cnt = page;
sendf();
if (g_addr >= app_end) {
break;
send(flash);
if (flash && g_addr >= app_end)
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;
case 'F': // erase the application, then take pages behind '?'
erase_application(); // leaves g_addr = 0, the write start
while (rcnf() == confirm) {
store_flash();
g_addr = 0;
while (request_confirm()) {
store_page(flash);
g_addr += page;
}
break;
case 'e': // read EEPROM, one page per host '!', until the host stops
if (flash)
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 (;;) {
if (rx() != confirm) {
break;
}
g_cnt = page;
do {
tx(eerd());
} while (--g_cnt);
}
break;
case 'E': // take EEPROM pages behind '?'
while (rcnf() == confirm) {
g_cnt = page;
do {
eewr(rx());
} while (--g_cnt);
}
break;
case 'c': // read the config page
read_config:
tx(confirm);
// Decode the command arithmetically so `flash`/`write` stay runtime
// values: bit 5 is the case bit (upper = write), and the folded-lower
// letter picks the memory. A single unified path serves f/F/e/E.
std::uint8_t cmd = rx();
std::uint8_t lower = cmd | 0x20;
bool write = (cmd & 0x20) == 0;
if (lower == 'f' || lower == 'e') {
bool flash = lower == 'f';
if (write)
write_mem(flash);
else
read_mem(flash);
} else if (lower == 'c') {
if (write) {
write_config();
} else {
g_addr = app_end;
g_cnt = page;
sendf();
break;
case 'C': // replace the config page, then echo it back to verify
if (rcnf() != confirm) {
break;
send(true);
}
g_addr = app_end + page;
erase_below(); // leaves g_addr = app_end, the store target
store_flash();
goto read_config;
default: // 'q' or any other byte runs the application
} else {
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>;
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
{
SP = RAMEND;
tsb::run();
}

View File

@@ -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>;

View File

@@ -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
// TinySafeBoot native-UART fixed-baud protocol on libavr with the full feature
// set of the hand-written oracle: a watchdog-reset bail, one-wire half-duplex,
// a config-page activation timeout, the password gate, emergency erase, and
// config/flash/EEPROM read-write. This variant is written for clarity -
// well-factored functions, no compiler-specific size hacks, no inline assembly.
// The one-wire wiring, the flash-resident info block and every SPM/EEPROM lock
// are libavr's to handle; the only attribute is the naked reset entry that
// stands in for the absent C runtime.
// A ≤512-byte serial flash bootloader for the ATmega328P boot section,
// reimplementing the TinySafeBoot wire protocol (native-UART fixed-baud
// lineage) on libavr. This variant is written for clarity: well-factored
// functions, no compiler-specific size hacks, no inline assembly. The only
// attribute is the one the task inherently needs — the naked reset entry that
// stands in for the absent C runtime; the flash-resident info block is a libavr
// flash_table.
//
// 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>
@@ -19,92 +22,61 @@ 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,
}>;
using serial_t = dev::uart0<{.baud = 115200_Bd, .max_baud_error = 3_pct}>;
inline constexpr serial_t serial{};
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.
constexpr auto off = avr::irq::guard_policy::unused;
// Strict request/response: every SPM operation is waited out before the next
// byte moves, so no flash operation is ever in flight at an EEPROM access -
// the write procedure's step 2 has nothing to guard, the omission the
// datasheet grants (DS40002061B section 8.6.3).
constexpr auto no_spm = ee::spm_interlock::omitted;
// The handshake bytes, identical across every TSB host.
// The two handshake bytes, identical across every TSB host.
constexpr std::uint8_t confirm = '!';
constexpr std::uint8_t request = '?';
constexpr std::uint8_t knock = '@';
// Boot geometry for the 1 KB boot section (BOOTSZ=10). The page size and the
// flash/EEPROM extents are the chip database's to know. app_end is the config
// page (the LASTPAGE holding the app-jump vector, activation timeout and
// password), one page below the boot section.
// Boot geometry for the ATmega328P 512-byte boot section (BOOTSZ=11). The page
// size, flash and EEPROM extents are the chip database's to know. app_end is
// both the first byte the loader protects and the config page (the LASTPAGE
// 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 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;
// 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
// 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
inline constexpr auto info_data = std::to_array<std::uint8_t>({
inline constexpr std::array<std::uint8_t, 16> info_data = {
'T', 'S', 'B',
build_date & 0xFF, build_date >> 8,
0xF3, // status byte (native-UART fixed-baud lineage)
avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2],
0x1E, 0x95, 0x0F, // ATmega328P signature
page / 2, // page size in words
(app_end / 2) & 0xFF, (app_end / 2) >> 8, // app-flash boundary, words
eeprom_end & 0xFF, eeprom_end >> 8,
0xAA, 0xAA, // ATmega processor-type marker (bytes 14 == 15)
});
};
// clang-format on
using info = avr::flash_table<info_data>;
// The lockout-proof floor for the receive window: the oracle's F_CPU/1MHz, so
// it follows the clock rather than restating it.
constexpr auto act_min = static_cast<std::uint8_t>(dev::clock.hz / 1'000'000);
// One page staged in SRAM. Scratch that is always filled before it is read, so
// it lives in .noinit — no startup clear (there is no crt to run one) and no
// 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::uint16_t outer = static_cast<std::uint16_t>(window) << 8;
do {
std::uint8_t fine = 0;
do {
if (auto byte = serial.read()) {
for (;;)
if (auto byte = serial.read())
return *byte;
}
} while (--fine);
} while (--outer);
return 0;
}
// write() takes the line and holds it until the frame is out.
void tx(std::uint8_t byte)
{
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.
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++)));
}
}
void send_eeprom(std::uint16_t addr, std::uint8_t count)
void send_eeprom(std::uint16_t addr, std::uint16_t count)
{
while (count--) {
tx(ee::read<no_spm>(addr++));
}
while (count--)
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 '!'.
@@ -137,57 +114,29 @@ bool request_confirm()
return rx() == confirm;
}
// Stream one page from the host straight into the already-erased flash page at
// `addr`, filling the SPM word buffer low byte then high - no SRAM staging, so
// receiving and programming are the same loop.
void store_flash_page(std::uint16_t addr)
// Program the SRAM buffer into one already-erased flash page (low byte then
// high, as the SPM word buffer wants).
void write_flash_page(std::uint16_t addr)
{
const auto open = spm::page::begin<spm::from::boot_section, off>(addr);
for (std::uint16_t i = 0; i < page; i += 2) {
std::uint8_t lo = rx();
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
spm::fill<off>(addr, std::span<const std::uint8_t>{buffer, page});
spm::write_page<off>(addr);
spm::wait();
}
// Stream one page from the host straight into EEPROM, byte by byte.
void store_eeprom_page(std::uint16_t addr)
// Write the SRAM buffer into EEPROM byte by byte.
void write_eeprom_page(std::uint16_t addr)
{
for (std::uint16_t i = 0; i < page; ++i) {
ee::write<off, no_spm>(addr + i, rx());
}
for (std::uint16_t i = 0; i < page; ++i)
ee::write<off>(addr + i, buffer[i]);
}
// Erase one flash page, waited out by the blocking spelling - the erase step
// shared by the whole-app erase, the config-page rewrite and the emergency
// 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.
// Run the application: reset vector at 0x0000. Any non-command byte, a wrong
// password, or an idle programmer port lands here.
[[noreturn]] void appjump()
{
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
@@ -195,9 +144,8 @@ extern "C" [[noreturn]] void tsb_app();
void read_flash()
{
for (std::uint16_t a = 0; a < app_end; a += page) {
if (rx() != confirm) {
if (rx() != confirm)
return;
}
send_flash(a, page);
}
}
@@ -206,117 +154,79 @@ void read_flash()
void read_eeprom()
{
for (std::uint16_t a = 0;; a += page) {
if (rx() != confirm) {
if (rx() != confirm)
return;
}
send_eeprom(a, page);
}
}
// 'F': erase the whole application first, then take pages the host offers
// behind '?'.
// 'F': erase the whole application first (unwritten pages stay erased), then
// take pages the host offers behind '?'.
void write_flash()
{
erase_application();
for (std::uint16_t a = 0; request_confirm(); a += page) {
store_flash_page(a);
for (std::uint16_t a = 0; a < app_end; a += page) {
spm::erase_page<off>(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.
void write_eeprom()
{
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.
void write_config()
{
if (!request_confirm()) {
if (!request_confirm())
return;
}
erase_page(app_end);
store_flash_page(app_end);
get_page();
spm::erase_page<off>(app_end);
spm::wait();
write_flash_page(app_end);
spm::rww_enable<off>();
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()
{
// 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()) {
// A bootloader may be entered by a watchdog reset; the reference loader
// hands straight back to the application in that case rather than run.
if (avr::hw::field<"MCUSR", "WDRF">::test())
appjump();
}
avr::init<serial_t>();
// Activation: the host knocks three '@' inside a window whose length is the
// config page's timeout byte, floored so a corrupt page can never lock the
// loader out. An idle port times out and boots the application; the same
// 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;
// Activation: the host knocks three '@'. With no programmer attached the
// port stays idle, so a bounded wait boots the application instead of
// hanging forever.
std::uint8_t knocks = 0;
std::uint32_t idle = 4000000;
while (knocks < 3) {
if (auto byte = serial.read()) {
knocks = *byte == knock ? knocks + 1 : 0;
} else if (--idle == 0) {
if (auto byte = serial.read())
knocks = *byte == '@' ? knocks + 1 : 0;
else if (--idle == 0)
appjump();
}
}
switch (password_gate()) {
case gate::pass:
// Password gate: the config page holds it at app_end+3, terminated by 0xff.
// 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());
break;
case gate::emergency:
if (!request_confirm() || !request_confirm()) {
appjump();
}
emergency_erase();
break;
}
for (;;) {
tx(confirm); // Mainloop ready
@@ -345,9 +255,13 @@ gate password_gate()
}
}
} // 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>;
// Reset lands here: BOOTRST vectors to the boot section base and .vectors is
// laid first, so this is the first instruction executed. No crt ran, so set the
// stack pointer before anything is called.
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
{
SP = RAMEND;
tsb::run();
}

View File

@@ -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,
// config-page activation timeout, password gate, emergency erase, and
// config/flash/EEPROM read-write - in pure C++, a little over the 512-byte boot
// section the hand-written oracle fits (oracle/README.md holds what each tier
// measures). The structure mirrors the oracle's: a handful of tiny noinline
// primitives sharing one whole-loader register allocation, expressed as global
// register variables so no helper ever saves, spills, or reloads any of it.
//
// The register protocol (all call-saved, so calls preserve them by ABI):
// 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.
// Same protocol and libavr surface as the pure variant (tsb_pure.cpp), but the
// readable one-handler-per-command shape is traded for size: flash and EEPROM
// share a single code path selected by a *runtime* flag decoded from the
// command byte, so the compiler cannot constant-propagate it into two clones.
// Attributes pin that sharing down (noinline/noclone) and the hot page pointer
// and byte counter are pinned to call-saved registers to erase the prologue
// push/pop that C++ function decomposition otherwise pays. No inline assembly.
#include <libavr/libavr.hpp>
@@ -36,344 +15,231 @@
using namespace avr::literals;
namespace spm = avr::spm;
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 {
// The loader is purely polled - it never enables interrupts - so every SPM and
// EEPROM lock folds to nothing under this posture.
constexpr auto off = avr::irq::guard_policy::unused;
// Strict request/response: every SPM operation is waited out before the next
// byte moves, so no flash operation is ever in flight at an EEPROM access -
// the write procedure's step 2 has nothing to guard, the omission the
// datasheet grants (DS40002061B section 8.6.3).
constexpr auto no_spm = ee::spm_interlock::omitted;
constexpr std::uint8_t confirm = '!';
constexpr std::uint8_t request = '?';
constexpr std::uint8_t knock = '@';
// 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>((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 + 19;
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd, 8, avr::uart::parity::none);
// One bit time on the wire: the turn-around a shared-line peer needs to stop
// driving before this one starts. Derived from the solved rate, so it follows
// the link rather than a count measured against one.
constexpr auto guard_cycles = static_cast<std::uint32_t>((16_MHz).hz / baud.actual);
// The 16-byte device-info block, streamed out on activation.
// clang-format off
[[gnu::progmem]] constexpr auto info = std::to_array<std::uint8_t>({
[[gnu::progmem]] constexpr std::uint8_t info[16] = {
'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
0xF3,
0x1E, 0x95, 0x0F,
page / 2,
(app_end / 2) & 0xFF, (app_end / 2) >> 8,
eeprom_end & 0xFF, eeprom_end >> 8,
0xAA, 0xAA,
});
};
// clang-format on
register std::uint16_t g_addr asm("r28");
register std::uint8_t g_cnt asm("r16");
register std::uint8_t g_window asm("r7");
register std::uint8_t g_receiving asm("r6");
[[gnu::section(".noinit")]] std::uint8_t buffer[page];
// The hot page walk lives in call-saved global registers, TSB-style: g_addr is
// 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)
{
return reinterpret_cast<const std::uint8_t *>(addr);
}
// Bounded byte receive, the oracle's shape: release the one-wire line on a
// direction change, poll RXC0 under nested countdowns, 0 on silence. The 0
// 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.
[[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 page transfer, memory selected at run time. noinline + noclone keep it a
// single shared body: the `flash` flag arrives from the command byte, so the
// optimiser cannot split it back into a flash copy and an EEPROM copy. All of
// these walk g_addr / g_cnt, set by the caller.
// One-wire transmit: take the line (TXEN0 alone - the receiver must be off
// while driving) on a direction change, with a turn-around guard so a shorted
// peer can switch first; then hold the line until the whole frame is out
// (TXC0, not UDRE0 - the stop bit must be on the wire before a caller may
// 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)
// Stream g_cnt bytes to the host from flash (LPM) or EEPROM, advancing g_addr
// so a caller can send consecutive pages without re-seeding it.
[[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 - see header).
[[gnu::noinline, gnu::noclone]] std::uint8_t sflash()
{
std::uint8_t byte = avr::flash_load(flash_ptr(g_addr));
tx(flash ? avr::flash_load(flash_ptr(g_addr)) : ee::read(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);
}
// 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, gnu::noclone]] void settle()
// Take one page from the host into the SRAM buffer.
[[gnu::noinline]] void get_page()
{
spm::wait();
spm::rww_enable<off>();
g_cnt = 0;
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()
{
settle();
tsb_app();
spm::wait();
reinterpret_cast<void (*)()>(0)();
__builtin_unreachable();
}
// Step g_addr one page down and erase that page. The decrement lives in here,
// before the erase's own use of it, not in the caller's loop where a following
// call would get it deleted (see header).
[[gnu::noinline, gnu::noclone]] void erase_below()
// 'f'/'e': stream memory back one page per host '!'. send advances g_addr, so
// flash self-terminates at the application boundary; EEPROM runs until the host
// stops.
[[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;
switch (rx()) {
case 'f': // read application flash, one page per host '!'
for (;;) {
if (rx() != confirm) {
break;
}
if (rx() != confirm)
return;
g_cnt = page;
sendf();
if (g_addr >= app_end) {
break;
send(flash);
if (flash && g_addr >= app_end)
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;
case 'F': // erase the application, then take pages behind '?'
erase_application(); // leaves g_addr = 0, the write start
while (rcnf() == confirm) {
store_flash();
g_addr = 0;
while (request_confirm()) {
get_page();
write_page(flash);
g_addr += page;
}
break;
case 'e': // read EEPROM, one page per host '!', until the host stops
if (flash)
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 (;;) {
if (rx() != confirm) {
break;
}
g_cnt = page;
do {
tx(eerd());
} while (--g_cnt);
}
break;
case 'E': // take EEPROM pages behind '?'
while (rcnf() == confirm) {
g_cnt = page;
do {
eewr(rx());
} while (--g_cnt);
}
break;
case 'c': // read the config page
read_config:
g_addr = reinterpret_cast<std::uint16_t>(&info[0]);
g_cnt = sizeof(info);
send(true);
for (;;) {
tx(confirm);
// Decode the command arithmetically so `flash`/`write` stay runtime
// values: bit 5 is the case bit (upper = write), and the folded-lower
// letter picks the memory. A single unified path serves f/F/e/E.
std::uint8_t cmd = rx();
std::uint8_t lower = cmd | 0x20;
bool write = (cmd & 0x20) == 0;
if (lower == 'f' || lower == 'e') {
bool flash = lower == 'f';
if (write)
write_mem(flash);
else
read_mem(flash);
} else if (lower == 'c') {
if (write) {
write_config();
} else {
g_addr = app_end;
g_cnt = page;
sendf();
break;
case 'C': // replace the config page, then echo it back to verify
if (rcnf() != confirm) {
break;
send(true);
}
g_addr = app_end + page;
erase_below(); // leaves g_addr = app_end, the store target
store_flash();
goto read_config;
default: // 'q' or any other byte runs the application
} else {
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>;
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
{
SP = RAMEND;
tsb::run();
}