Compare commits
18 Commits
main
...
7d103ca957
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d103ca957 | |||
| eca7a41051 | |||
| 7314f7ab3b | |||
| 5b361904ab | |||
| 833e134e01 | |||
| da730b7bb5 | |||
| c351bee257 | |||
| 34e7f1be34 | |||
| 445e187722 | |||
| 250aba5cfb | |||
| 5c900720e3 | |||
| 7d6ef959b2 | |||
| 11ffbce2e2 | |||
| f32a27ff15 | |||
| 57d94cf631 | |||
| 8203a24f33 | |||
| 2906da3272 | |||
| 64c1e484b5 |
@@ -1,5 +1,6 @@
|
||||
---
|
||||
BasedOnStyle: LLVM
|
||||
Standard: Latest
|
||||
ColumnLimit: 120
|
||||
IndentWidth: 4
|
||||
TabWidth: 4
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -9,3 +9,12 @@ Debug
|
||||
*.eeprom
|
||||
*.lss
|
||||
*.map
|
||||
|
||||
# CMake / clangd
|
||||
/build/
|
||||
compile_commands.json
|
||||
.cache/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
27
.gitmodules
vendored
27
.gitmodules
vendored
@@ -1,27 +0,0 @@
|
||||
[submodule "tsb/io"]
|
||||
path = tsb/io
|
||||
url = git@git.blackmark.me:avr/io.git
|
||||
[submodule "tsb/flash"]
|
||||
path = tsb/flash
|
||||
url = git@git.blackmark.me:avr/flash.git
|
||||
[submodule "tsb/uart"]
|
||||
path = tsb/uart
|
||||
url = git@git.blackmark.me:avr/uart.git
|
||||
[submodule "tsb/type"]
|
||||
path = tsb/type
|
||||
url = git@git.blackmark.me:avr/type.git
|
||||
[submodule "stk500v2/type"]
|
||||
path = stk500v2/type
|
||||
url = git@git.blackmark.me:avr/type.git
|
||||
[submodule "stk500v2/io"]
|
||||
path = stk500v2/io
|
||||
url = git@git.blackmark.me:avr/io.git
|
||||
[submodule "stk500v2/uart"]
|
||||
path = stk500v2/uart
|
||||
url = git@git.blackmark.me:avr/uart.git
|
||||
[submodule "stk500v2/flash"]
|
||||
path = stk500v2/flash
|
||||
url = git@git.blackmark.me:avr/flash.git
|
||||
[submodule "blink/io"]
|
||||
path = blink/io
|
||||
url = git@git.blackmark.me:avr/io.git
|
||||
177
CMakeLists.txt
Normal file
177
CMakeLists.txt
Normal file
@@ -0,0 +1,177 @@
|
||||
cmake_minimum_required(VERSION 3.28)
|
||||
|
||||
project(tsb_libavr LANGUAGES CXX)
|
||||
|
||||
# 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(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()
|
||||
FetchContent_MakeAvailable(libavr)
|
||||
|
||||
if(PROJECT_IS_TOP_LEVEL)
|
||||
add_compile_options(-Werror) # warnings are errors for the port's own code
|
||||
enable_testing()
|
||||
|
||||
# The behavioral tests drive the real wire protocols over a simavr pty
|
||||
# (as the host tools do) and actually flash the device. The runners are
|
||||
# host programs built at configure time against libsimavr; if they or
|
||||
# Python are 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(PB_DEVICE ${CMAKE_BINARY_DIR}/pureboot_device)
|
||||
execute_process(
|
||||
COMMAND ${_host_cc} -O2 -I/usr/include/simavr -I/usr/include/simavr/parts
|
||||
-o ${PB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pureboot_device.c
|
||||
-lsimavr -lsimavrparts -lelf -lutil
|
||||
RESULT_VARIABLE _pbdev_res ERROR_VARIABLE _pbdev_err)
|
||||
if(NOT _pbdev_res EQUAL 0)
|
||||
message(STATUS "pureboot_device not built (${_pbdev_err}) — protocol tests skipped")
|
||||
unset(PB_DEVICE)
|
||||
endif()
|
||||
if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||
set(TSB_DEVICE ${CMAKE_BINARY_DIR}/tsb_device)
|
||||
execute_process(
|
||||
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)
|
||||
message(STATUS "tsb_device not built (${_dev_err}) — protocol tests skipped")
|
||||
unset(TSB_DEVICE)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# 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 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_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 three 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" — see dev/lessons.md.
|
||||
# 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): 510 B in the 512 B section the
|
||||
# hand-written 500 B oracle occupies. Everything else, from
|
||||
# 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, and the bring-up is the two reset-non-default
|
||||
# registers only. 526 B in the 1 KB section (BOOTSZ=10) — 14
|
||||
# over the oracle's section, from 168 over at this tier's first
|
||||
# floor.
|
||||
# tsb_pure — pure idiomatic libavr, one function per command, TU-local
|
||||
# (internal linkage), streaming (no SRAM page buffer): 836 B in
|
||||
# the 1 KB section.
|
||||
#
|
||||
# add_tsb_variant(<name> <boot-section-bytes>)
|
||||
function(add_tsb_variant name bytes)
|
||||
math(EXPR base_dec "32768 - ${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)
|
||||
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${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 ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/tsbtest.py
|
||||
${TSB_DEVICE} $<TARGET_FILE:${name}> ${base_hex})
|
||||
endif()
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# The tsb tiers reimplement the ATmega328P-only reference protocol; the other
|
||||
# chips build pureboot alone.
|
||||
if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||
add_tsb_variant(tsb_asm 512)
|
||||
add_tsb_variant(tsb_pure 1024)
|
||||
add_tsb_variant(tsb_tricks 1024)
|
||||
endif()
|
||||
|
||||
# pureboot — the pure-constraint port (see pureboot/README.md): one source,
|
||||
# no inline assembly, no global register variables, every libavr chip, 512
|
||||
# bytes each. The loader owns the top 512 bytes of flash on every chip; the
|
||||
# application entry symbol is address 0 on the mega (reset re-vectors to the
|
||||
# loader through BOOTRST, so word 0 stays the application's own vector) and
|
||||
# the trampoline word just below the loader on the tinies (host-side vector
|
||||
# surgery points it at the application). --pmem-wrap-around models AVR's
|
||||
# modulo-flash PC where the flash is big enough to need it.
|
||||
if(LIBAVR_MCU STREQUAL "attiny13a")
|
||||
set(_pb_flash 1024)
|
||||
set(_pb_wrap "")
|
||||
set(_pb_page 32)
|
||||
set(_pb_hz 9600000)
|
||||
set(_pb_baud 57600)
|
||||
set(_pb_eeprom 64)
|
||||
elseif(LIBAVR_MCU STREQUAL "attiny85")
|
||||
set(_pb_flash 8192)
|
||||
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
|
||||
set(_pb_page 64)
|
||||
set(_pb_hz 8000000)
|
||||
set(_pb_baud 57600)
|
||||
set(_pb_eeprom 512)
|
||||
else()
|
||||
set(_pb_flash 32768)
|
||||
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
|
||||
set(_pb_page 128)
|
||||
set(_pb_hz 16000000)
|
||||
set(_pb_baud 115200)
|
||||
set(_pb_eeprom 1024)
|
||||
endif()
|
||||
math(EXPR _pb_base "${_pb_flash} - 512")
|
||||
math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL)
|
||||
if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||
set(_pb_app 0)
|
||||
else()
|
||||
math(EXPR _pb_app "${_pb_base} - 2")
|
||||
endif()
|
||||
|
||||
add_executable(pureboot pureboot/pureboot.cpp)
|
||||
target_link_libraries(pureboot PRIVATE libavr)
|
||||
target_link_options(pureboot PRIVATE -nostartfiles -Wl,--section-start=.text=${_pb_base_hex}
|
||||
-Wl,--defsym=pureboot_app=${_pb_app} ${_pb_wrap})
|
||||
add_custom_command(TARGET pureboot POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:pureboot>)
|
||||
if(PROJECT_IS_TOP_LEVEL)
|
||||
add_test(NAME pureboot.size
|
||||
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot>
|
||||
-DLIMIT=512 -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
||||
|
||||
# The protocol test flashes this fixture through the loader with the real
|
||||
# host tool and expects its banner after the hand-over; a normally linked
|
||||
# application whose reset vector is what the tinies' surgery re-homes.
|
||||
if(DEFINED PB_DEVICE)
|
||||
add_executable(pbapp test/pbapp.cpp)
|
||||
target_link_libraries(pbapp PRIVATE libavr)
|
||||
add_custom_command(TARGET pbapp POST_BUILD
|
||||
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:pbapp> $<TARGET_FILE:pbapp>.bin)
|
||||
add_test(NAME pureboot.protocol
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
|
||||
${PB_DEVICE} $<TARGET_FILE:pureboot> ${LIBAVR_MCU} ${_pb_hz} ${_pb_base_hex}
|
||||
${_pb_page} ${_pb_baud} ${_pb_eeprom} $<TARGET_FILE:pbapp>.bin
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||
${CMAKE_BINARY_DIR}/pbtest-work)
|
||||
set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180)
|
||||
endif()
|
||||
endif()
|
||||
86
CMakePresets.json
Normal file
86
CMakePresets.json
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"version": 8,
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "base",
|
||||
"hidden": true,
|
||||
"generator": "Ninja",
|
||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||
"toolchainFile": "$env{LIBAVR_ROOT}/cmake/avr-toolchain.cmake",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release",
|
||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
||||
"CMAKE_COLOR_DIAGNOSTICS": "ON"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "atmega328p-generated",
|
||||
"inherits": "base",
|
||||
"cacheVariables": { "LIBAVR_MCU": "atmega328p", "LIBAVR_REFLECT": "OFF" }
|
||||
},
|
||||
{
|
||||
"name": "atmega328p-reflect",
|
||||
"inherits": "base",
|
||||
"cacheVariables": { "LIBAVR_MCU": "atmega328p", "LIBAVR_REFLECT": "ON" }
|
||||
},
|
||||
{
|
||||
"name": "attiny85-generated",
|
||||
"inherits": "base",
|
||||
"cacheVariables": { "LIBAVR_MCU": "attiny85", "LIBAVR_REFLECT": "OFF" }
|
||||
},
|
||||
{
|
||||
"name": "attiny85-reflect",
|
||||
"inherits": "base",
|
||||
"cacheVariables": { "LIBAVR_MCU": "attiny85", "LIBAVR_REFLECT": "ON" }
|
||||
},
|
||||
{
|
||||
"name": "attiny13a-generated",
|
||||
"inherits": "base",
|
||||
"cacheVariables": { "LIBAVR_MCU": "attiny13a", "LIBAVR_REFLECT": "OFF" }
|
||||
},
|
||||
{
|
||||
"name": "attiny13a-reflect",
|
||||
"inherits": "base",
|
||||
"cacheVariables": { "LIBAVR_MCU": "attiny13a", "LIBAVR_REFLECT": "ON" }
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{ "name": "atmega328p-generated", "configurePreset": "atmega328p-generated" },
|
||||
{ "name": "atmega328p-reflect", "configurePreset": "atmega328p-reflect" },
|
||||
{ "name": "attiny85-generated", "configurePreset": "attiny85-generated" },
|
||||
{ "name": "attiny85-reflect", "configurePreset": "attiny85-reflect" },
|
||||
{ "name": "attiny13a-generated", "configurePreset": "attiny13a-generated" },
|
||||
{ "name": "attiny13a-reflect", "configurePreset": "attiny13a-reflect" }
|
||||
],
|
||||
"workflowPresets": [
|
||||
{
|
||||
"name": "atmega328p-generated",
|
||||
"steps": [
|
||||
{ "type": "configure", "name": "atmega328p-generated" },
|
||||
{ "type": "build", "name": "atmega328p-generated" },
|
||||
{ "type": "test", "name": "atmega328p-generated" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "attiny85-generated",
|
||||
"steps": [
|
||||
{ "type": "configure", "name": "attiny85-generated" },
|
||||
{ "type": "build", "name": "attiny85-generated" },
|
||||
{ "type": "test", "name": "attiny85-generated" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "attiny13a-generated",
|
||||
"steps": [
|
||||
{ "type": "configure", "name": "attiny13a-generated" },
|
||||
{ "type": "build", "name": "attiny13a-generated" },
|
||||
{ "type": "test", "name": "attiny13a-generated" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"testPresets": [
|
||||
{ "name": "atmega328p-generated", "configurePreset": "atmega328p-generated", "output": { "outputOnFailure": true } },
|
||||
{ "name": "attiny85-generated", "configurePreset": "attiny85-generated", "output": { "outputOnFailure": true } },
|
||||
{ "name": "attiny13a-generated", "configurePreset": "attiny13a-generated", "output": { "outputOnFailure": true } }
|
||||
]
|
||||
}
|
||||
@@ -1,239 +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>{d887fc8e-ee68-4248-8382-92dbc9a54145}</ProjectGuid>
|
||||
<avrdevice>ATmega328P</avrdevice>
|
||||
<avrdeviceseries>none</avrdeviceseries>
|
||||
<OutputType>Executable</OutputType>
|
||||
<Language>CPP</Language>
|
||||
<OutputFileName>$(MSBuildProjectName)</OutputFileName>
|
||||
<OutputFileExtension>.elf</OutputFileExtension>
|
||||
<OutputDirectory>$(MSBuildProjectDirectory)\$(Configuration)</OutputDirectory>
|
||||
<AssemblyName>blink</AssemblyName>
|
||||
<Name>blink</Name>
|
||||
<RootNamespace>blink</RootNamespace>
|
||||
<ToolchainFlavour>avr-g++-9.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 />
|
||||
<avrtool>com.atmel.avrdbg.tool.atmelice</avrtool>
|
||||
<avrtoolserialnumber>J41800099437</avrtoolserialnumber>
|
||||
<avrdeviceexpectedsignature>0x1E950F</avrdeviceexpectedsignature>
|
||||
<com_atmel_avrdbg_tool_stk500>
|
||||
<ToolOptions>
|
||||
<InterfaceProperties>
|
||||
<IspClock>125000</IspClock>
|
||||
</InterfaceProperties>
|
||||
<InterfaceName>ISP</InterfaceName>
|
||||
</ToolOptions>
|
||||
<ToolType>com.atmel.avrdbg.tool.stk500</ToolType>
|
||||
<ToolNumber>
|
||||
</ToolNumber>
|
||||
<ToolName>STK500</ToolName>
|
||||
</com_atmel_avrdbg_tool_stk500>
|
||||
<avrtoolinterface>ISP</avrtoolinterface>
|
||||
<avrtoolinterfaceclock>125000</avrtoolinterfaceclock>
|
||||
<AsfFrameworkConfig>
|
||||
<framework-data xmlns="">
|
||||
<options />
|
||||
<configurations />
|
||||
<files />
|
||||
<documentation help="" />
|
||||
<offline-documentation help="" />
|
||||
<dependencies>
|
||||
<content-extension eid="atmel.asf" uuidref="Atmel.ASF" version="3.47.0" />
|
||||
</dependencies>
|
||||
</framework-data>
|
||||
</AsfFrameworkConfig>
|
||||
<com_atmel_avrdbg_tool_atmelice>
|
||||
<ToolOptions>
|
||||
<InterfaceProperties>
|
||||
<IspClock>125000</IspClock>
|
||||
</InterfaceProperties>
|
||||
<InterfaceName>ISP</InterfaceName>
|
||||
</ToolOptions>
|
||||
<ToolType>com.atmel.avrdbg.tool.atmelice</ToolType>
|
||||
<ToolNumber>J41800099437</ToolNumber>
|
||||
<ToolName>Atmel-ICE</ToolName>
|
||||
</com_atmel_avrdbg_tool_atmelice>
|
||||
<custom>
|
||||
<ToolOptions>
|
||||
<InterfaceProperties>
|
||||
<IspClock>125000</IspClock>
|
||||
</InterfaceProperties>
|
||||
<InterfaceName>
|
||||
</InterfaceName>
|
||||
</ToolOptions>
|
||||
<ToolType>custom</ToolType>
|
||||
<ToolNumber>
|
||||
</ToolNumber>
|
||||
<ToolName>Custom Programming Tool</ToolName>
|
||||
</custom>
|
||||
<com_atmel_avrdbg_tool_simulator>
|
||||
<ToolOptions xmlns="">
|
||||
<InterfaceProperties>
|
||||
</InterfaceProperties>
|
||||
<InterfaceName>
|
||||
</InterfaceName>
|
||||
</ToolOptions>
|
||||
<ToolType xmlns="">com.atmel.avrdbg.tool.simulator</ToolType>
|
||||
<ToolNumber xmlns="">
|
||||
</ToolNumber>
|
||||
<ToolName xmlns="">Simulator</ToolName>
|
||||
</com_atmel_avrdbg_tool_simulator>
|
||||
<AAFDebugger>
|
||||
<AAFDebugFiles>
|
||||
</AAFDebugFiles>
|
||||
</AAFDebugger>
|
||||
</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>
|
||||
<avrgcc.compiler.general.ChangeDefaultCharTypeUnsigned>True</avrgcc.compiler.general.ChangeDefaultCharTypeUnsigned>
|
||||
<avrgcc.compiler.general.ChangeDefaultBitFieldUnsigned>True</avrgcc.compiler.general.ChangeDefaultBitFieldUnsigned>
|
||||
<avrgcc.compiler.symbols.DefSymbols>
|
||||
<ListValues>
|
||||
<Value>NDEBUG</Value>
|
||||
</ListValues>
|
||||
</avrgcc.compiler.symbols.DefSymbols>
|
||||
<avrgcc.compiler.directories.IncludePaths>
|
||||
<ListValues>
|
||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
||||
</ListValues>
|
||||
</avrgcc.compiler.directories.IncludePaths>
|
||||
<avrgcc.compiler.optimization.level>Optimize for size (-Os)</avrgcc.compiler.optimization.level>
|
||||
<avrgcc.compiler.optimization.AllocateBytesNeededForEnum>True</avrgcc.compiler.optimization.AllocateBytesNeededForEnum>
|
||||
<avrgcc.compiler.warnings.AllWarnings>True</avrgcc.compiler.warnings.AllWarnings>
|
||||
<avrgcc.compiler.warnings.ExtraWarnings>True</avrgcc.compiler.warnings.ExtraWarnings>
|
||||
<avrgcc.compiler.warnings.Pedantic>True</avrgcc.compiler.warnings.Pedantic>
|
||||
<avrgcc.compiler.miscellaneous.OtherFlags>-fno-threadsafe-statics -std=c11</avrgcc.compiler.miscellaneous.OtherFlags>
|
||||
<avrgcccpp.compiler.general.ChangeDefaultCharTypeUnsigned>True</avrgcccpp.compiler.general.ChangeDefaultCharTypeUnsigned>
|
||||
<avrgcccpp.compiler.general.ChangeDefaultBitFieldUnsigned>True</avrgcccpp.compiler.general.ChangeDefaultBitFieldUnsigned>
|
||||
<avrgcccpp.compiler.symbols.DefSymbols>
|
||||
<ListValues>
|
||||
<Value>NDEBUG</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.compiler.symbols.DefSymbols>
|
||||
<avrgcccpp.compiler.directories.IncludePaths>
|
||||
<ListValues>
|
||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.compiler.directories.IncludePaths>
|
||||
<avrgcccpp.compiler.optimization.level>Optimize for size (-Os)</avrgcccpp.compiler.optimization.level>
|
||||
<avrgcccpp.compiler.optimization.AllocateBytesNeededForEnum>True</avrgcccpp.compiler.optimization.AllocateBytesNeededForEnum>
|
||||
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
|
||||
<avrgcccpp.compiler.warnings.Pedantic>True</avrgcccpp.compiler.warnings.Pedantic>
|
||||
<avrgcccpp.compiler.miscellaneous.OtherFlags>-fno-threadsafe-statics -Wextra -std=c++17</avrgcccpp.compiler.miscellaneous.OtherFlags>
|
||||
<avrgcccpp.linker.libraries.Libraries>
|
||||
<ListValues>
|
||||
<Value>libm</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.linker.libraries.Libraries>
|
||||
<avrgcccpp.assembler.general.IncludePaths>
|
||||
<ListValues>
|
||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.assembler.general.IncludePaths>
|
||||
</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>
|
||||
<avrgcc.compiler.general.ChangeDefaultCharTypeUnsigned>True</avrgcc.compiler.general.ChangeDefaultCharTypeUnsigned>
|
||||
<avrgcc.compiler.general.ChangeDefaultBitFieldUnsigned>True</avrgcc.compiler.general.ChangeDefaultBitFieldUnsigned>
|
||||
<avrgcc.compiler.symbols.DefSymbols>
|
||||
<ListValues>
|
||||
<Value>DEBUG</Value>
|
||||
</ListValues>
|
||||
</avrgcc.compiler.symbols.DefSymbols>
|
||||
<avrgcc.compiler.directories.IncludePaths>
|
||||
<ListValues>
|
||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
||||
</ListValues>
|
||||
</avrgcc.compiler.directories.IncludePaths>
|
||||
<avrgcc.compiler.optimization.level>Optimize (-O1)</avrgcc.compiler.optimization.level>
|
||||
<avrgcc.compiler.optimization.AllocateBytesNeededForEnum>True</avrgcc.compiler.optimization.AllocateBytesNeededForEnum>
|
||||
<avrgcc.compiler.optimization.DebugLevel>Maximum (-g3)</avrgcc.compiler.optimization.DebugLevel>
|
||||
<avrgcc.compiler.warnings.AllWarnings>True</avrgcc.compiler.warnings.AllWarnings>
|
||||
<avrgcc.compiler.warnings.ExtraWarnings>True</avrgcc.compiler.warnings.ExtraWarnings>
|
||||
<avrgcc.compiler.warnings.Pedantic>True</avrgcc.compiler.warnings.Pedantic>
|
||||
<avrgcc.compiler.miscellaneous.OtherFlags>-fno-threadsafe-statics -std=c11</avrgcc.compiler.miscellaneous.OtherFlags>
|
||||
<avrgcccpp.compiler.general.ChangeDefaultCharTypeUnsigned>True</avrgcccpp.compiler.general.ChangeDefaultCharTypeUnsigned>
|
||||
<avrgcccpp.compiler.general.ChangeDefaultBitFieldUnsigned>True</avrgcccpp.compiler.general.ChangeDefaultBitFieldUnsigned>
|
||||
<avrgcccpp.compiler.symbols.DefSymbols>
|
||||
<ListValues>
|
||||
<Value>DEBUG</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.compiler.symbols.DefSymbols>
|
||||
<avrgcccpp.compiler.directories.IncludePaths>
|
||||
<ListValues>
|
||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.compiler.directories.IncludePaths>
|
||||
<avrgcccpp.compiler.optimization.level>Optimize (-O1)</avrgcccpp.compiler.optimization.level>
|
||||
<avrgcccpp.compiler.optimization.AllocateBytesNeededForEnum>True</avrgcccpp.compiler.optimization.AllocateBytesNeededForEnum>
|
||||
<avrgcccpp.compiler.optimization.DebugLevel>Maximum (-g3)</avrgcccpp.compiler.optimization.DebugLevel>
|
||||
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
|
||||
<avrgcccpp.compiler.warnings.Pedantic>True</avrgcccpp.compiler.warnings.Pedantic>
|
||||
<avrgcccpp.compiler.miscellaneous.OtherFlags>-fno-threadsafe-statics -Wextra -std=c++17</avrgcccpp.compiler.miscellaneous.OtherFlags>
|
||||
<avrgcccpp.linker.libraries.Libraries>
|
||||
<ListValues>
|
||||
<Value>libm</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.linker.libraries.Libraries>
|
||||
<avrgcccpp.assembler.general.IncludePaths>
|
||||
<ListValues>
|
||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.assembler.general.IncludePaths>
|
||||
<avrgcccpp.assembler.debugging.DebugLevel>Default (-Wa,-g)</avrgcccpp.assembler.debugging.DebugLevel>
|
||||
</AvrGccCpp>
|
||||
</ToolchainSettings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="bootloader.cpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="bootloader.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="clock.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="io\io.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="main.cpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="io" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(AVRSTUDIO_EXE_PATH)\\Vs\\Compiler.targets" />
|
||||
</Project>
|
||||
@@ -1,46 +0,0 @@
|
||||
#include "bootloader.hpp"
|
||||
|
||||
#include <avr/io.h>
|
||||
#include <avr/pgmspace.h>
|
||||
#include <avr/wdt.h>
|
||||
|
||||
namespace {
|
||||
|
||||
typedef void (*jmp_fn)() __attribute__((noreturn));
|
||||
|
||||
jmp_fn boot = reinterpret_cast<jmp_fn>(0x0000);
|
||||
jmp_fn bootloader = reinterpret_cast<jmp_fn>(0x7800 / 2);
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Bootloader::handleReset()
|
||||
{
|
||||
wdt_reset();
|
||||
uint8_t mcuStatus = MCUSR;
|
||||
MCUSR &= ~(1 << WDRF);
|
||||
wdt_disable();
|
||||
return (mcuStatus & (1 << WDRF));
|
||||
}
|
||||
|
||||
void Bootloader::reset()
|
||||
{
|
||||
wdt_enable(WDTO_15MS);
|
||||
while (true)
|
||||
;
|
||||
}
|
||||
|
||||
bool Bootloader::check()
|
||||
{
|
||||
if (pgm_read_byte(reinterpret_cast<uint16_t>(bootloader) * 2) != 0xFF)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Bootloader::call()
|
||||
{
|
||||
if (check())
|
||||
bootloader();
|
||||
else
|
||||
boot();
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
class Bootloader {
|
||||
public:
|
||||
template <typename Fn>
|
||||
static inline void init(Fn callback)
|
||||
{
|
||||
if (handleReset()) {
|
||||
callback();
|
||||
call();
|
||||
}
|
||||
}
|
||||
|
||||
static inline void enter()
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
private:
|
||||
static bool handleReset();
|
||||
static void reset();
|
||||
static bool check();
|
||||
static void call();
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
//#define F_CPU 18'432'000
|
||||
#define F_CPU 16'000'000
|
||||
#include <util/delay.h>
|
||||
1
blink/io
1
blink/io
Submodule blink/io deleted from 80de36ee7e
@@ -1,30 +0,0 @@
|
||||
#include "clock.hpp"
|
||||
|
||||
#include "io/io.hpp"
|
||||
|
||||
#include "bootloader.hpp"
|
||||
|
||||
int main()
|
||||
{
|
||||
io::Pin<io::P::B5> ledPin;
|
||||
ledPin.dir(io::Dir::OUT);
|
||||
ledPin = false;
|
||||
|
||||
Bootloader::init([&ledPin]() {
|
||||
for (uint8_t i = 0; i < 10; ++i) {
|
||||
ledPin = true;
|
||||
_delay_ms(50);
|
||||
ledPin = false;
|
||||
_delay_ms(50);
|
||||
}
|
||||
});
|
||||
|
||||
for (uint8_t i = 0; i < 10; ++i) {
|
||||
ledPin.toggle();
|
||||
_delay_ms(1000);
|
||||
}
|
||||
|
||||
Bootloader::enter();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,34 +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", "tsb\tsb.cppproj", "{DCE6C7E3-EE26-4D79-826B-08594B9AD897}"
|
||||
EndProject
|
||||
Project("{E66E83B9-2572-4076-B26E-6BE79FF3018A}") = "stk500v2", "stk500v2\stk500v2.cppproj", "{19798CCE-5D96-40E9-B769-D209715DCE0C}"
|
||||
EndProject
|
||||
Project("{E66E83B9-2572-4076-B26E-6BE79FF3018A}") = "blink", "blink\blink.cppproj", "{D887FC8E-EE68-4248-8382-92DBC9A54145}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|AVR = Debug|AVR
|
||||
Release|AVR = Release|AVR
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{DCE6C7E3-EE26-4D79-826B-08594B9AD897}.Debug|AVR.ActiveCfg = Debug|AVR
|
||||
{DCE6C7E3-EE26-4D79-826B-08594B9AD897}.Debug|AVR.Build.0 = Debug|AVR
|
||||
{DCE6C7E3-EE26-4D79-826B-08594B9AD897}.Release|AVR.ActiveCfg = Release|AVR
|
||||
{DCE6C7E3-EE26-4D79-826B-08594B9AD897}.Release|AVR.Build.0 = Release|AVR
|
||||
{19798CCE-5D96-40E9-B769-D209715DCE0C}.Debug|AVR.ActiveCfg = Debug|AVR
|
||||
{19798CCE-5D96-40E9-B769-D209715DCE0C}.Debug|AVR.Build.0 = Debug|AVR
|
||||
{19798CCE-5D96-40E9-B769-D209715DCE0C}.Release|AVR.ActiveCfg = Release|AVR
|
||||
{19798CCE-5D96-40E9-B769-D209715DCE0C}.Release|AVR.Build.0 = Release|AVR
|
||||
{D887FC8E-EE68-4248-8382-92DBC9A54145}.Debug|AVR.ActiveCfg = Debug|AVR
|
||||
{D887FC8E-EE68-4248-8382-92DBC9A54145}.Debug|AVR.Build.0 = Debug|AVR
|
||||
{D887FC8E-EE68-4248-8382-92DBC9A54145}.Release|AVR.ActiveCfg = Release|AVR
|
||||
{D887FC8E-EE68-4248-8382-92DBC9A54145}.Release|AVR.Build.0 = Release|AVR
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
49
oracle/README.md
Normal file
49
oracle/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# 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 `tsb_asm` tier meets the same bar at
|
||||
510 B in the same 512 B section, written in C++ on libavr except the two
|
||||
routines whose remaining cost is the calling convention itself (the bounded rx
|
||||
and the page-store loop); `tsb_tricks` needs no assembly at all at 526 B, and
|
||||
`tsb_pure` stays fully idiomatic at 836 B, both in the 1 KB section.
|
||||
|
||||
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.
|
||||
776
oracle/tsb-fixedbaud.asm
Normal file
776
oracle/tsb-fixedbaud.asm
Normal file
@@ -0,0 +1,776 @@
|
||||
;***********************************************************************
|
||||
;***********************************************************************
|
||||
;***********************************************************************
|
||||
; 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
|
||||
;***********************************************************************
|
||||
;***********************************************************************
|
||||
;***********************************************************************
|
||||
|
||||
|
||||
123
pureboot/README.md
Normal file
123
pureboot/README.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# pureboot
|
||||
|
||||
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by
|
||||
constraint: one C++ source, no inline assembly, no global register variables
|
||||
(attributes allowed), built for every chip libavr targets, **512 bytes on
|
||||
each** — 490 B on the ATtiny13A, 510 B on the ATtiny85, 484 B on the
|
||||
ATmega328P. The device speaks primitives; every composite — verify, erase,
|
||||
reset-vector surgery, timeout configuration — lives in the host tool
|
||||
(`pureboot.py`).
|
||||
|
||||
## Link
|
||||
|
||||
| Chip | Serial | Baud | Clock assumed |
|
||||
|---|---|---|---|
|
||||
| ATmega328P | USART0, RXD/TXD = PD0/PD1 | 115200 8N1 | 16 MHz crystal |
|
||||
| ATtiny85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC |
|
||||
| ATtiny13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC |
|
||||
|
||||
The tiny RX pin has its pull-up enabled; TX idles high. All multi-byte
|
||||
quantities on the wire are little-endian.
|
||||
|
||||
## Activation
|
||||
|
||||
Reset enters the loader (BOOTRST on the mega, the patched reset vector on the
|
||||
tinies) — except a watchdog reset, which hands straight to the application
|
||||
(the application owns its watchdog; it must clear WDRF itself, which also
|
||||
releases the WDRF-forced WDE).
|
||||
|
||||
The host then has one activation window per awaited byte to knock: `p` then
|
||||
`b`. Each awaited byte gets a fresh window; any other byte is discarded and
|
||||
awaited again (line noise cannot lock the loader, only delay it). A window
|
||||
expiring with an idle line boots the application.
|
||||
|
||||
The window length in seconds is the **last EEPROM cell** (address
|
||||
`eeprom_size - 1`); `0x00` and the erased `0xff` both mean the 4 s default,
|
||||
so a full EEPROM erase resets the timeout rather than maxing it. The host
|
||||
changes it with the ordinary EEPROM-write command.
|
||||
|
||||
## Session
|
||||
|
||||
After the knock the loader stays in its command loop until `G` or a reset.
|
||||
Before reading each command it waits for any pending EEPROM write to finish
|
||||
and sends the prompt `+` (0x2b) — the prompt is therefore also the completion
|
||||
ack of the previous command. A session is: await `+`, send a command, read
|
||||
its reply, repeat.
|
||||
|
||||
| Cmd | Arguments | Reply |
|
||||
|---|---|---|
|
||||
| `b` | — | the 12-byte info block |
|
||||
| `R` | addr16, n8 | n flash bytes (n = 0 means 256) |
|
||||
| `W` | addr16, then one page of data | — (completion = next prompt) |
|
||||
| `r` | addr16, n8 | n EEPROM bytes (n = 0 means 256) |
|
||||
| `w` | addr16, n8, then n data bytes | `+` per byte, sent once its write has begun |
|
||||
| `F` | — | 4 bytes: low fuse, lock, extended fuse, high fuse |
|
||||
| `G` | — | `+`, then the application runs |
|
||||
| other | — | ignored; the loop re-prompts (send a junk byte, await `+`, to resync) |
|
||||
|
||||
`W` streams exactly one SPM page (size from the info block) into the buffer,
|
||||
then erases and programs; the address must be page-aligned. Pages inside the
|
||||
loader's own 512 bytes are drained but never programmed — a broken host
|
||||
cannot brick the chip. `w` is host-paced: send the next byte only after the
|
||||
previous byte's `+`. `F` returns the bytes in the hardware's Z order; on a chip without an
|
||||
extended fuse byte (the ATtiny13A) that slot carries no meaning. Fuse *writing* does not
|
||||
exist: SPM reaches flash (and, on the mega, lock bits) only — fuse bytes are
|
||||
external-programming territory by hardware.
|
||||
|
||||
The info block (`b`):
|
||||
|
||||
| Offset | Content |
|
||||
|---|---|
|
||||
| 0–2 | `'P'`, `'B'`, protocol version (1) |
|
||||
| 3–5 | device signature |
|
||||
| 6 | SPM page size in bytes |
|
||||
| 7–8 | loader base — application flash ends here |
|
||||
| 9–10 | EEPROM size |
|
||||
| 11 | bit 0 set: host must patch the reset vector (no hardware boot section) |
|
||||
|
||||
Composites are the host's job: verify = read back and compare, erase =
|
||||
write `0xff` (per page for flash, per byte for EEPROM), timeout = EEPROM
|
||||
write to the last cell.
|
||||
|
||||
## Deployment
|
||||
|
||||
**ATmega328P**: program the loader at 0x7e00 with an external programmer;
|
||||
fuses BOOTSZ = 11 (256 words) and BOOTRST programmed. Applications are
|
||||
flashed unmodified — reset re-vectors to the loader in hardware, word 0
|
||||
stays the application's own reset vector, and `G` jumps to 0.
|
||||
|
||||
**Tinies** (no boot section): program the loader at `flash - 512`; erased
|
||||
flash below it walks up into the loader, so a virgin chip activates. When
|
||||
flashing an application the host performs reset-vector surgery: the
|
||||
application's own `rjmp` target is re-encoded as a trampoline `rjmp` in the
|
||||
word just below the loader (`base - 2`, where `G` jumps), and word 0 is
|
||||
rewritten to `rjmp` to the loader base. Every other vector stays the
|
||||
application's. Page 0 is written last, so an interrupted flash leaves word 0
|
||||
erased and the chip still falls through to the loader on the next reset.
|
||||
|
||||
## Host tool
|
||||
|
||||
`pureboot.py` — Python 3, standard library only (termios drives any tty,
|
||||
a USB adapter as well as a simavr pty):
|
||||
|
||||
pureboot.py --port /dev/ttyUSB0 --baud 57600 \
|
||||
--info --fuses --flash app.hex --timeout 10
|
||||
|
||||
Operations run in a fixed order within one session: info, fuses, flash
|
||||
(erase / program / read / verify), EEPROM (erase / program / read / verify),
|
||||
timeout — then the loader hands over to the application; `--stay` keeps the
|
||||
session alive instead, and a later invocation reconnects into it (the knock
|
||||
converges there too). `--flash` and `--eeprom` verify by read-back unless
|
||||
`--no-verify`; images are raw binary, or Intel HEX by extension.
|
||||
|
||||
## Tests
|
||||
|
||||
Per chip preset, `ctest` runs the 512-byte size gate and the end-to-end
|
||||
protocol test: a simavr device (`test/pureboot_device.c` — the mega's USART
|
||||
as a pty; on the tinies a cycle-timed GPIO⇄pty bridge for the software UART,
|
||||
plus the SPM/NVM module simavr's tiny cores lack) driven by the real host
|
||||
tool through knock-from-reset, program + verify of both memories, timeout
|
||||
configuration, session reconnect, an external reset through the patched
|
||||
vector, and the hand-over to a fixture application whose banner proves the
|
||||
launch — cross-checked against the simulator's ground-truth memory dumps and
|
||||
an independent decode of the surgery's rjmp words.
|
||||
326
pureboot/pureboot.cpp
Normal file
326
pureboot/pureboot.cpp
Normal file
@@ -0,0 +1,326 @@
|
||||
// pureboot — a serial bootloader on libavr, pure by constraint: one C++
|
||||
// source with no inline assembly and no global register variables, built for
|
||||
// every chip libavr targets, 512 bytes on each. The device speaks primitives
|
||||
// — read/program flash, read/write EEPROM, fuse bytes, an info block, run —
|
||||
// and everything composite (verify, erase, reset-vector surgery on the
|
||||
// tinies, timeout configuration) lives in the host tool. Protocol reference:
|
||||
// README.md next to this file.
|
||||
//
|
||||
// Entry: reset lands in avr::startup::entry below (BOOTRST on the mega; the
|
||||
// patched reset vector — or erased flash walking up into the loader — on the
|
||||
// tinies). A watchdog reset hands straight to the application. Otherwise the
|
||||
// host has one activation window — EEPROM's last cell, in seconds — to knock
|
||||
// ("pb"); an idle line boots the application. A session then stays in the
|
||||
// command loop until 'G' hands over or the chip resets.
|
||||
|
||||
#include <libavr/libavr.hpp>
|
||||
|
||||
using namespace avr::literals;
|
||||
namespace spm = avr::spm;
|
||||
namespace ee = avr::eeprom;
|
||||
|
||||
namespace pureboot {
|
||||
namespace {
|
||||
|
||||
// Purely polled — interrupts stay off, every guard folds to nothing.
|
||||
constexpr auto off = avr::irq::guard_policy::unused;
|
||||
|
||||
constexpr std::uint8_t ack = '+';
|
||||
|
||||
// Per-chip personality, from the chip database: the clocks the dogfood
|
||||
// boards run (16 MHz crystal on the mega, calibrated RC on the tinies) and
|
||||
// the device signature (compile-time data — the tiny13A cannot even read its
|
||||
// signature row from code).
|
||||
consteval avr::hertz_t clock()
|
||||
{
|
||||
if (avr::hw::db.name == "ATtiny13A")
|
||||
return 9.6_MHz;
|
||||
if (avr::hw::db.name == "ATtiny85")
|
||||
return 8_MHz;
|
||||
return 16_MHz;
|
||||
}
|
||||
|
||||
consteval std::array<std::uint8_t, 3> signature()
|
||||
{
|
||||
if (avr::hw::db.name == "ATtiny13A")
|
||||
return {0x1e, 0x90, 0x07};
|
||||
if (avr::hw::db.name == "ATtiny85")
|
||||
return {0x1e, 0x93, 0x0b};
|
||||
return {0x1e, 0x95, 0x0f};
|
||||
}
|
||||
|
||||
using dev = avr::device<{.clock = clock()}>;
|
||||
|
||||
// Geometry: the loader owns the top 512 bytes of flash; the byte below it is
|
||||
// the trampoline word (the application's relocated reset vector) on chips
|
||||
// without a hardware boot section. The RWWSRE bit marks a separate boot
|
||||
// section — on classic AVR the two capabilities coincide.
|
||||
constexpr std::uint16_t boot_bytes = 512;
|
||||
constexpr std::uint16_t base = static_cast<std::uint16_t>(spm::flash_bytes - boot_bytes);
|
||||
constexpr std::uint16_t page = spm::page_bytes;
|
||||
constexpr bool boot_section = avr::hw::db.field_index("SPMCSR", "RWWSRE") >= 0;
|
||||
|
||||
// The activation timeout lives in EEPROM's last cell, in seconds; the host
|
||||
// rewrites it with the ordinary EEPROM-write command. An unprogrammed cell —
|
||||
// 0x00 or the erased 0xff — means the 4 s default: a stray value can never
|
||||
// floor the window to nothing and lock the loader out, and erasing the whole
|
||||
// EEPROM resets the timeout instead of maxing it to 255 s.
|
||||
constexpr std::uint16_t timeout_cell = avr::hw::db.mem.eeprom_size - 1;
|
||||
constexpr std::uint8_t default_seconds = 4;
|
||||
|
||||
// The 12-byte info block the host reads with the 'b' command; flash-resident
|
||||
// (there is no crt to copy a .data image).
|
||||
inline constexpr std::array<std::uint8_t, 12> info_data = {
|
||||
'P',
|
||||
'B',
|
||||
1, // magic, protocol version
|
||||
signature()[0],
|
||||
signature()[1],
|
||||
signature()[2],
|
||||
static_cast<std::uint8_t>(page),
|
||||
base & 0xff,
|
||||
base >> 8, // app flash ends here; loader base
|
||||
avr::hw::db.mem.eeprom_size & 0xff,
|
||||
avr::hw::db.mem.eeprom_size >> 8,
|
||||
boot_section ? 0 : 1, // bit 0: host must patch the reset vector (no hardware boot section)
|
||||
};
|
||||
using info = avr::flash_table<info_data>;
|
||||
|
||||
// The serial link: the hardware USART where the chip has one, the polled
|
||||
// software UART (no vector — the table belongs to the application) on PB0/PB1
|
||||
// elsewhere. Both are class templates on the clock so only the selected
|
||||
// backend is ever instantiated. pending() is the cheap line test the
|
||||
// activation window polls; rx() then picks the byte up.
|
||||
template <avr::hertz_t C>
|
||||
consteval std::int16_t rxc_field()
|
||||
{
|
||||
return avr::hw::db.field_index("UCSR0A", "RXC0");
|
||||
}
|
||||
|
||||
template <avr::hertz_t C>
|
||||
struct hardware_link {
|
||||
using uart = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>;
|
||||
|
||||
static void init()
|
||||
{
|
||||
avr::init<uart>();
|
||||
}
|
||||
|
||||
static bool pending()
|
||||
{
|
||||
return avr::hw::field_impl<rxc_field<C>()>::test();
|
||||
}
|
||||
|
||||
static std::uint8_t rx()
|
||||
{
|
||||
return uart::read_blocking();
|
||||
}
|
||||
|
||||
static void tx(std::uint8_t byte)
|
||||
{
|
||||
uart::write(byte);
|
||||
}
|
||||
};
|
||||
|
||||
template <avr::hertz_t C>
|
||||
struct software_link {
|
||||
using rx_t = avr::uart::software_rx_polled<C, avr::pb0, 57600_Bd>;
|
||||
using tx_t = avr::uart::software_tx<C, avr::pb1, 57600_Bd>;
|
||||
|
||||
static void init()
|
||||
{
|
||||
avr::init<rx_t, tx_t>();
|
||||
}
|
||||
|
||||
static bool pending()
|
||||
{
|
||||
return !avr::io::input<avr::pb0>::read(); // a start bit has begun
|
||||
}
|
||||
|
||||
static std::uint8_t rx()
|
||||
{
|
||||
return rx_t::template read_blocking<off>();
|
||||
}
|
||||
|
||||
static void tx(std::uint8_t byte)
|
||||
{
|
||||
tx_t::template write<off>(byte);
|
||||
}
|
||||
};
|
||||
|
||||
using link = std::conditional_t<avr::hw::db.has_reg("UDR0"), hardware_link<dev::clock>, software_link<dev::clock>>;
|
||||
|
||||
// The application's entry: the linker pins pureboot_app to 0x0000 on the
|
||||
// mega (reset re-vectors here through BOOTRST, so address 0 stays the
|
||||
// application's own vector) and to the trampoline word at base - 2 on the
|
||||
// tinies (--defsym in CMakeLists.txt).
|
||||
extern "C" [[noreturn]] void pureboot_app();
|
||||
|
||||
[[noreturn]] void run_app()
|
||||
{
|
||||
pureboot_app();
|
||||
}
|
||||
|
||||
// One activation tick is 65536 pending() polls — a pin (or flag) test plus a
|
||||
// 16-bit countdown, about 8 cycles. Whole-second precision is all the
|
||||
// timeout cell promises; the seconds count stays a loop bound (a runtime
|
||||
// multiply would drag libgcc's __mulhi3 into the MUL-less tinies).
|
||||
consteval std::uint16_t ticks_per_second()
|
||||
{
|
||||
return static_cast<std::uint16_t>(dev::clock.hz / (65536ull * 8u));
|
||||
}
|
||||
static_assert(ticks_per_second() >= 1);
|
||||
|
||||
bool pending_before(std::uint8_t seconds)
|
||||
{
|
||||
do {
|
||||
std::uint16_t ticks = ticks_per_second();
|
||||
do {
|
||||
std::uint16_t spins = 0; // wraps first, so 65536 polls per tick
|
||||
do {
|
||||
if (link::pending())
|
||||
return true;
|
||||
} while (--spins);
|
||||
} while (--ticks);
|
||||
} while (--seconds);
|
||||
return false;
|
||||
}
|
||||
|
||||
// A knock byte under the activation deadline: an idle line means no host is
|
||||
// there, and the application runs.
|
||||
std::uint8_t rx_deadline(std::uint8_t seconds)
|
||||
{
|
||||
if (!pending_before(seconds))
|
||||
run_app();
|
||||
return link::rx();
|
||||
}
|
||||
|
||||
std::uint16_t rx16()
|
||||
{
|
||||
std::uint8_t low = link::rx();
|
||||
return static_cast<std::uint16_t>(low | (link::rx() << 8));
|
||||
}
|
||||
|
||||
const std::uint8_t *flash_ptr(std::uint16_t address)
|
||||
{
|
||||
return reinterpret_cast<const std::uint8_t *>(address);
|
||||
}
|
||||
|
||||
// The streamers take the count in the wire's 8-bit form: 0 means 256.
|
||||
void send_flash(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
do
|
||||
link::tx(avr::flash_load(flash_ptr(address++)));
|
||||
while (--count);
|
||||
}
|
||||
|
||||
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
do
|
||||
link::tx(ee::read(address++));
|
||||
while (--count);
|
||||
}
|
||||
|
||||
// EEPROM write, host-paced: each ack goes out once the byte's write has
|
||||
// begun, so the next byte arrives while it completes and the following
|
||||
// write's own ready-wait sees an idle line. Nothing is ever missed, on
|
||||
// either serial backend, without a buffer.
|
||||
void store_eeprom(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
do {
|
||||
ee::write<off>(address++, link::rx());
|
||||
link::tx(ack);
|
||||
} while (--count);
|
||||
}
|
||||
|
||||
// One flash page: stream the bytes into the SPM buffer as little-endian
|
||||
// words, then erase and program. Addresses in the loader's own 512 bytes
|
||||
// are drained but never programmed — a broken host cannot brick the chip.
|
||||
// On the mega the RWW section is re-enabled so reads work immediately.
|
||||
void program_flash(std::uint16_t address)
|
||||
{
|
||||
for (std::uint16_t i = 0; i < page; i += 2) {
|
||||
std::uint8_t low = link::rx();
|
||||
std::uint8_t high = link::rx();
|
||||
spm::fill<off>(address + i, static_cast<std::uint16_t>(low | (high << 8)));
|
||||
}
|
||||
if (address < base) {
|
||||
spm::erase_page<off>(address);
|
||||
spm::wait();
|
||||
spm::write_page<off>(address);
|
||||
spm::wait();
|
||||
if constexpr (boot_section)
|
||||
spm::rww_enable<off>();
|
||||
}
|
||||
}
|
||||
|
||||
// The four fuse/lock bytes in the hardware's own Z order: low, lock,
|
||||
// extended, high. Writing fuses is not a thing self-programming can do on
|
||||
// AVR — SPM reaches flash (and boot lock bits) only.
|
||||
void send_fuses()
|
||||
{
|
||||
for (std::uint8_t which = 0; which < 4; ++which)
|
||||
link::tx(spm::read_fuse<off>(static_cast<spm::fuse>(which)));
|
||||
}
|
||||
|
||||
[[noreturn]] void run()
|
||||
{
|
||||
// A watchdog reset belongs to the application (whose watchdog stays
|
||||
// forced on until it clears WDRF) — no activation window in its way.
|
||||
if (avr::hw::mcusr::wdrf.test())
|
||||
run_app();
|
||||
|
||||
link::init();
|
||||
|
||||
std::uint8_t seconds = ee::read(timeout_cell);
|
||||
if (seconds == 0 || seconds == 0xff)
|
||||
seconds = default_seconds;
|
||||
|
||||
// The knock: 'p' then 'b', each under a fresh window; any other byte is
|
||||
// line noise and waits again. Falling out of a window runs the app.
|
||||
while (rx_deadline(seconds) != 'p' || rx_deadline(seconds) != 'b') {
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
// No prompt while an EEPROM write runs: a pending write blocks SPM
|
||||
// and fuse reads (§26.2.1), and the ack tells the host all is done.
|
||||
ee::wait();
|
||||
link::tx(ack);
|
||||
switch (link::rx()) {
|
||||
case 'b': // info block
|
||||
send_flash(reinterpret_cast<std::uint16_t>(info::storage.data()), info::size());
|
||||
break;
|
||||
case 'R': { // read flash: addr16, n8 (0 = 256)
|
||||
std::uint16_t address = rx16();
|
||||
send_flash(address, link::rx());
|
||||
break;
|
||||
}
|
||||
case 'W': // program one flash page: addr16, page bytes
|
||||
program_flash(rx16());
|
||||
break;
|
||||
case 'r': { // read EEPROM: addr16, n8
|
||||
std::uint16_t address = rx16();
|
||||
send_eeprom(address, link::rx());
|
||||
break;
|
||||
}
|
||||
case 'w': { // write EEPROM: addr16, n8, then n bytes each acked
|
||||
std::uint16_t address = rx16();
|
||||
store_eeprom(address, link::rx());
|
||||
break;
|
||||
}
|
||||
case 'F': // fuse and lock bytes
|
||||
send_fuses();
|
||||
break;
|
||||
case 'G': // hand over to the application
|
||||
link::tx(ack);
|
||||
run_app();
|
||||
default: // unknown bytes are ignored; the loop re-acks
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace pureboot
|
||||
|
||||
template struct avr::startup::entry<pureboot::run>;
|
||||
450
pureboot/pureboot.py
Normal file
450
pureboot/pureboot.py
Normal file
@@ -0,0 +1,450 @@
|
||||
#!/usr/bin/env python3
|
||||
"""pureboot host tool — the smart half of the pureboot protocol (README.md).
|
||||
|
||||
The device exposes primitives; this tool composes them: image loading (raw
|
||||
binary or Intel HEX), flash programming with read-back verification, erase as
|
||||
writing 0xff, EEPROM programming, fuse and info readout, activation-timeout
|
||||
configuration, and — on chips without a hardware boot section — the
|
||||
reset-vector surgery that re-homes the application's entry through the
|
||||
trampoline word below the loader, writing page 0 last so an interrupted
|
||||
flash still falls through to the loader.
|
||||
|
||||
Python standard library only; the serial port is driven with termios, so any
|
||||
tty works — a USB adapter as well as a simavr pty.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import select
|
||||
import sys
|
||||
import termios
|
||||
import time
|
||||
|
||||
PROMPT = b"+"
|
||||
PROTOCOL_VERSION = 1
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- serial ---
|
||||
|
||||
|
||||
class Port:
|
||||
"""A raw serial port with deadline-based reads."""
|
||||
|
||||
def __init__(self, path, baud):
|
||||
self.fd = os.open(path, os.O_RDWR | os.O_NOCTTY)
|
||||
attrs = termios.tcgetattr(self.fd)
|
||||
attrs[0] = 0 # iflag
|
||||
attrs[1] = 0 # oflag
|
||||
attrs[2] = termios.CREAD | termios.CLOCAL | termios.CS8 # cflag
|
||||
attrs[3] = 0 # lflag
|
||||
try:
|
||||
speed = getattr(termios, f"B{baud}")
|
||||
except AttributeError:
|
||||
raise Error(f"unsupported baud rate {baud}") from None
|
||||
attrs[4] = attrs[5] = speed
|
||||
attrs[6][termios.VMIN] = 0
|
||||
attrs[6][termios.VTIME] = 0
|
||||
termios.tcsetattr(self.fd, termios.TCSANOW, attrs)
|
||||
|
||||
def close(self):
|
||||
os.close(self.fd)
|
||||
|
||||
def write(self, data):
|
||||
os.write(self.fd, data)
|
||||
|
||||
def flush_input(self):
|
||||
termios.tcflush(self.fd, termios.TCIFLUSH)
|
||||
|
||||
def read_available(self, wait):
|
||||
"""Everything that arrives within `wait` seconds of quiet start."""
|
||||
ready, _, _ = select.select([self.fd], [], [], wait)
|
||||
return os.read(self.fd, 4096) if ready else b""
|
||||
|
||||
def read_exact(self, count, timeout):
|
||||
data = b""
|
||||
deadline = time.monotonic() + timeout
|
||||
while len(data) < count:
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise Error(f"timeout: got {len(data)} of {count} bytes")
|
||||
ready, _, _ = select.select([self.fd], [], [], remaining)
|
||||
if ready:
|
||||
data += os.read(self.fd, count - len(data))
|
||||
return data
|
||||
|
||||
|
||||
# -------------------------------------------------------------- protocol ---
|
||||
|
||||
|
||||
class Info:
|
||||
"""The 12-byte info block."""
|
||||
|
||||
def __init__(self, raw):
|
||||
if len(raw) != 12 or raw[0:2] != b"PB":
|
||||
raise Error(f"bad info block: {raw.hex()}")
|
||||
if raw[2] != PROTOCOL_VERSION:
|
||||
raise Error(f"protocol version {raw[2]}, tool speaks {PROTOCOL_VERSION}")
|
||||
self.signature = raw[3:6]
|
||||
self.page = raw[6]
|
||||
self.base = raw[7] | (raw[8] << 8)
|
||||
self.eeprom_size = raw[9] | (raw[10] << 8)
|
||||
self.patch_vector = bool(raw[11] & 1)
|
||||
self.flash_size = self.base + 512
|
||||
|
||||
def describe(self):
|
||||
sig = " ".join(f"{b:02x}" for b in self.signature)
|
||||
vector = "host-patched reset vector" if self.patch_vector else "hardware boot section"
|
||||
return (
|
||||
f"signature {sig}, page {self.page} B, "
|
||||
f"app flash {self.base} B (loader at {self.base:#06x}), "
|
||||
f"EEPROM {self.eeprom_size} B, {vector}"
|
||||
)
|
||||
|
||||
|
||||
class Loader:
|
||||
"""A pureboot session. Between commands the loader has prompted `+` and
|
||||
awaits a command byte; every method restores that invariant."""
|
||||
|
||||
def __init__(self, port):
|
||||
self.port = port
|
||||
self.info = None
|
||||
|
||||
def connect(self, wait):
|
||||
"""Knock until the activation window answers, then read the info
|
||||
block. Also converges when the loader already sits in its command
|
||||
loop: the knock bytes are ignored-or-executed there, and the drain
|
||||
absorbs whatever they produced."""
|
||||
self.port.flush_input()
|
||||
deadline = time.monotonic() + wait
|
||||
while True:
|
||||
self.port.write(b"pb")
|
||||
if PROMPT in self.port.read_available(0.4):
|
||||
break
|
||||
if time.monotonic() > deadline:
|
||||
raise Error("no answer — reset the device within its activation window")
|
||||
while self.port.read_available(0.3):
|
||||
pass
|
||||
self.port.write(b"b")
|
||||
self.info = Info(self.port.read_exact(12, 2.0))
|
||||
self._expect_prompt()
|
||||
return self.info
|
||||
|
||||
def _expect_prompt(self, timeout=2.0):
|
||||
byte = self.port.read_exact(1, timeout)
|
||||
if byte != PROMPT:
|
||||
raise Error(f"expected prompt, got {byte.hex()}")
|
||||
|
||||
def _command(self, tx, reply_len=0, timeout=2.0):
|
||||
self.port.write(tx)
|
||||
reply = self.port.read_exact(reply_len, timeout) if reply_len else b""
|
||||
self._expect_prompt(timeout)
|
||||
return reply
|
||||
|
||||
def _stream_read(self, command, address, count):
|
||||
data = b""
|
||||
while count:
|
||||
chunk = min(count, 256)
|
||||
head = bytes((ord(command), address & 0xFF, address >> 8, chunk & 0xFF))
|
||||
data += self._command(head, chunk, 5.0)
|
||||
address += chunk
|
||||
count -= chunk
|
||||
return data
|
||||
|
||||
def read_flash(self, address, count):
|
||||
return self._stream_read("R", address, count)
|
||||
|
||||
def read_eeprom(self, address, count):
|
||||
return self._stream_read("r", address, count)
|
||||
|
||||
def write_page(self, address, data):
|
||||
assert len(data) == self.info.page and address % self.info.page == 0
|
||||
head = bytes((ord("W"), address & 0xFF, address >> 8))
|
||||
self._command(head + data, 0, 2.0)
|
||||
|
||||
def write_eeprom(self, address, data):
|
||||
offset = 0
|
||||
while offset < len(data):
|
||||
chunk = data[offset : offset + 256]
|
||||
head = bytes((ord("w"), address & 0xFF, address >> 8, len(chunk) & 0xFF))
|
||||
self.port.write(head)
|
||||
for byte in chunk:
|
||||
self.port.write(bytes((byte,)))
|
||||
self._expect_prompt() # per-byte ack: the write has begun
|
||||
self._expect_prompt() # the next command prompt
|
||||
address += len(chunk)
|
||||
offset += len(chunk)
|
||||
|
||||
def read_fuses(self):
|
||||
return self._command(b"F", 4, 2.0)
|
||||
|
||||
def run_application(self):
|
||||
self.port.write(b"G")
|
||||
self._expect_prompt()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- images ---
|
||||
|
||||
|
||||
def load_image(path):
|
||||
"""Raw binary, or Intel HEX by extension (.hex/.ihx/.ihex)."""
|
||||
data = open(path, "rb").read()
|
||||
if not path.lower().endswith((".hex", ".ihx", ".ihex")):
|
||||
if not data:
|
||||
raise Error(f"{path}: empty image")
|
||||
return data
|
||||
memory = {}
|
||||
for number, line in enumerate(data.decode("ascii", "replace").splitlines(), 1):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
if not line.startswith(":"):
|
||||
raise Error(f"{path}:{number}: not an Intel HEX record")
|
||||
record = bytes.fromhex(line[1:])
|
||||
if sum(record) & 0xFF:
|
||||
raise Error(f"{path}:{number}: checksum mismatch")
|
||||
count, address, kind = record[0], (record[1] << 8) | record[2], record[3]
|
||||
payload = record[4 : 4 + count]
|
||||
if kind == 0:
|
||||
for i, byte in enumerate(payload):
|
||||
memory[address + i] = byte
|
||||
elif kind == 1:
|
||||
break
|
||||
elif kind in (2, 4) and not any(payload):
|
||||
continue # a zero base extends nothing
|
||||
elif kind in (3, 5):
|
||||
continue # start address: irrelevant, reset is the entry
|
||||
else:
|
||||
raise Error(f"{path}:{number}: record type {kind} reaches beyond the 16-bit space")
|
||||
if not memory:
|
||||
raise Error(f"{path}: empty image")
|
||||
return bytes(memory.get(i, 0xFF) for i in range(max(memory) + 1))
|
||||
|
||||
|
||||
# --------------------------------------------------------------- surgery ---
|
||||
|
||||
|
||||
def rjmp_target(word_address, opcode, flash_words):
|
||||
return (word_address + 1 + (opcode & 0x0FFF)) % flash_words
|
||||
|
||||
|
||||
def rjmp_to(word_address, destination, flash_words):
|
||||
return 0xC000 | ((destination - word_address - 1) % flash_words % 0x1000)
|
||||
|
||||
|
||||
def plan_flash(image, info):
|
||||
"""The pages to program, as {page_address: bytes}, already carrying the
|
||||
reset-vector surgery where the chip needs it. Page 0 must go last —
|
||||
callers get it separated."""
|
||||
page = info.page
|
||||
limit = info.base - (2 if info.patch_vector else 0)
|
||||
if len(image) > limit:
|
||||
raise Error(f"image is {len(image)} B, application flash ends at {limit}")
|
||||
final = bytearray(image) + bytearray([0xFF] * (-len(image) % page))
|
||||
|
||||
if info.patch_vector:
|
||||
flash_words = info.flash_size // 2
|
||||
word0 = final[0] | (final[1] << 8)
|
||||
if word0 & 0xF000 != 0xC000:
|
||||
raise Error(
|
||||
"the image's reset vector is not an rjmp — pureboot's vector "
|
||||
"surgery cannot re-home it (crt-less entry at address 0?)"
|
||||
)
|
||||
entry = rjmp_target(0, word0, flash_words)
|
||||
if entry >= info.base // 2:
|
||||
raise Error(
|
||||
"the image's reset vector already targets the loader — this "
|
||||
"is a read-back of a patched image; flash the original"
|
||||
)
|
||||
trampoline_word = (info.base - 2) // 2
|
||||
patch = rjmp_to(0, info.base // 2, flash_words)
|
||||
final[0], final[1] = patch & 0xFF, patch >> 8
|
||||
trampoline_page = info.base - page
|
||||
if len(final) < trampoline_page + page:
|
||||
final += bytearray([0xFF] * (trampoline_page + page - len(final)))
|
||||
jump = rjmp_to(trampoline_word, entry, flash_words)
|
||||
final[info.base - 2], final[info.base - 1] = jump & 0xFF, jump >> 8
|
||||
|
||||
pages = {a: bytes(final[a : a + page]) for a in range(0, len(final), page)}
|
||||
return pages
|
||||
|
||||
|
||||
def covered(pages, skip_blank):
|
||||
"""Pages in programming order: ascending, page 0 last; optionally
|
||||
dropping all-0xff pages (sound only over erased flash) — never the
|
||||
load-bearing page 0."""
|
||||
rest = [a for a in sorted(pages) if a != 0]
|
||||
if skip_blank:
|
||||
rest = [a for a in rest if pages[a].count(0xFF) != len(pages[a])]
|
||||
return rest + [0]
|
||||
|
||||
|
||||
# ------------------------------------------------------------ operations ---
|
||||
|
||||
|
||||
def op_erase_flash(loader):
|
||||
"""0xff over the whole application area. Order does not matter here —
|
||||
every target byte is the same value — and a blank word 0 still falls
|
||||
through to the loader, so an interruption is harmless."""
|
||||
blank = bytes([0xFF] * loader.info.page)
|
||||
for address in range(0, loader.info.base, loader.info.page):
|
||||
loader.write_page(address, blank)
|
||||
print(f"erase: {loader.info.base // loader.info.page} pages")
|
||||
|
||||
|
||||
def op_erase_eeprom(loader):
|
||||
loader.write_eeprom(0, bytes([0xFF] * loader.info.eeprom_size))
|
||||
print(f"erase: {loader.info.eeprom_size} B of EEPROM")
|
||||
|
||||
|
||||
def op_flash(loader, path, erase, verify):
|
||||
image = load_image(path)
|
||||
pages = plan_flash(image, loader.info)
|
||||
if erase:
|
||||
op_erase_flash(loader)
|
||||
order = covered(pages, skip_blank=erase)
|
||||
for address in order:
|
||||
loader.write_page(address, pages[address])
|
||||
print(f"flash: {path}: {len(order)} pages")
|
||||
if verify:
|
||||
verify_pages(loader, pages)
|
||||
|
||||
|
||||
def verify_pages(loader, pages):
|
||||
for address in sorted(pages):
|
||||
got = loader.read_flash(address, loader.info.page)
|
||||
if got != pages[address]:
|
||||
first = next(i for i in range(len(got)) if got[i] != pages[address][i])
|
||||
raise Error(
|
||||
f"verify failed at {address + first:#06x}: "
|
||||
f"wrote {pages[address][first]:02x}, read {got[first]:02x}"
|
||||
)
|
||||
print(f"verify: {len(pages)} pages ok")
|
||||
|
||||
|
||||
def op_verify_flash(loader, path):
|
||||
verify_pages(loader, plan_flash(load_image(path), loader.info))
|
||||
|
||||
|
||||
def op_read_flash(loader, path):
|
||||
data = loader.read_flash(0, loader.info.base)
|
||||
open(path, "wb").write(data)
|
||||
print(f"read flash: {len(data)} B -> {path}")
|
||||
|
||||
|
||||
def op_eeprom(loader, path, erase, verify):
|
||||
image = load_image(path)
|
||||
if len(image) > loader.info.eeprom_size:
|
||||
raise Error(f"EEPROM image is {len(image)} B, device has {loader.info.eeprom_size}")
|
||||
if erase:
|
||||
op_erase_eeprom(loader)
|
||||
loader.write_eeprom(0, image)
|
||||
print(f"eeprom: {path}: {len(image)} B")
|
||||
if verify:
|
||||
got = loader.read_eeprom(0, len(image))
|
||||
if got != image:
|
||||
first = next(i for i in range(len(got)) if got[i] != image[i])
|
||||
raise Error(f"verify failed at EEPROM {first:#06x}: wrote {image[first]:02x}, read {got[first]:02x}")
|
||||
print(f"verify: {len(image)} B ok")
|
||||
|
||||
|
||||
def op_verify_eeprom(loader, path):
|
||||
image = load_image(path)
|
||||
got = loader.read_eeprom(0, len(image))
|
||||
if got != image:
|
||||
first = next(i for i in range(len(got)) if got[i] != image[i])
|
||||
raise Error(f"verify failed at EEPROM {first:#06x}: expected {image[first]:02x}, read {got[first]:02x}")
|
||||
print(f"verify: {len(image)} B of EEPROM ok")
|
||||
|
||||
|
||||
def op_read_eeprom(loader, path):
|
||||
data = loader.read_eeprom(0, loader.info.eeprom_size)
|
||||
open(path, "wb").write(data)
|
||||
print(f"read EEPROM: {len(data)} B -> {path}")
|
||||
|
||||
|
||||
def op_timeout(loader, seconds):
|
||||
loader.write_eeprom(loader.info.eeprom_size - 1, bytes((seconds,)))
|
||||
label = f"{seconds} s" if seconds else "the device default"
|
||||
print(f"activation timeout: {label}")
|
||||
|
||||
|
||||
def op_fuses(loader):
|
||||
low, lock, extended, high = loader.read_fuses()
|
||||
print(f"fuses: low {low:02x} high {high:02x} extended {extended:02x} lock {lock:02x}")
|
||||
|
||||
|
||||
# -------------------------------------------------------------------- cli ---
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="pureboot host tool", epilog="operations run in the order listed above"
|
||||
)
|
||||
parser.add_argument("--port", required=True, help="serial device (or simavr pty)")
|
||||
parser.add_argument("--baud", type=int, default=115200, help="115200 mega, 57600 tinies")
|
||||
parser.add_argument("--wait", type=float, default=30.0, help="seconds to keep knocking")
|
||||
parser.add_argument("--info", action="store_true", help="print the device info block")
|
||||
parser.add_argument("--fuses", action="store_true", help="read the fuse and lock bytes")
|
||||
parser.add_argument("--erase-flash", action="store_true", help="0xff over the application flash")
|
||||
parser.add_argument("--flash", metavar="FILE", help="program an application (bin or ihex)")
|
||||
parser.add_argument("--no-verify", action="store_true", help="skip read-back after writes")
|
||||
parser.add_argument("--read-flash", metavar="FILE", help="dump the application flash")
|
||||
parser.add_argument("--verify-flash", metavar="FILE", help="compare flash against an image")
|
||||
parser.add_argument("--erase-eeprom", action="store_true", help="0xff over the EEPROM")
|
||||
parser.add_argument("--eeprom", metavar="FILE", help="program the EEPROM (bin or ihex)")
|
||||
parser.add_argument("--read-eeprom", metavar="FILE", help="dump the EEPROM")
|
||||
parser.add_argument("--verify-eeprom", metavar="FILE", help="compare EEPROM against an image")
|
||||
parser.add_argument("--timeout", type=int, metavar="S", help="activation window, 1-254 s (0: default)")
|
||||
parser.add_argument("--stay", action="store_true", help="leave the loader in its session")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.timeout is not None and not 0 <= args.timeout <= 254:
|
||||
parser.error("--timeout must be 0..254 (255 is the erased cell)")
|
||||
|
||||
port = Port(args.port, args.baud)
|
||||
try:
|
||||
loader = Loader(port)
|
||||
info = loader.connect(args.wait)
|
||||
if args.info:
|
||||
print(f"device: {info.describe()}")
|
||||
if args.fuses:
|
||||
op_fuses(loader)
|
||||
if args.flash:
|
||||
op_flash(loader, args.flash, args.erase_flash, not args.no_verify)
|
||||
elif args.erase_flash:
|
||||
op_erase_flash(loader)
|
||||
if args.read_flash:
|
||||
op_read_flash(loader, args.read_flash)
|
||||
if args.verify_flash:
|
||||
op_verify_flash(loader, args.verify_flash)
|
||||
if args.eeprom:
|
||||
op_eeprom(loader, args.eeprom, args.erase_eeprom, not args.no_verify)
|
||||
elif args.erase_eeprom:
|
||||
op_erase_eeprom(loader)
|
||||
if args.read_eeprom:
|
||||
op_read_eeprom(loader, args.read_eeprom)
|
||||
if args.verify_eeprom:
|
||||
op_verify_eeprom(loader, args.verify_eeprom)
|
||||
if args.timeout is not None:
|
||||
op_timeout(loader, args.timeout)
|
||||
if args.stay:
|
||||
print("loader stays in its session (reset to leave)")
|
||||
else:
|
||||
loader.run_application()
|
||||
print("application running")
|
||||
finally:
|
||||
port.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Error as error:
|
||||
print(f"error: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(130)
|
||||
@@ -1,5 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
//#define F_CPU 18'432'000
|
||||
#define F_CPU 16'000'000
|
||||
#include <util/delay.h>
|
||||
@@ -1,113 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// STK message constants
|
||||
|
||||
static constexpr uint8_t MESSAGE_START = 0x1B; // ASCII ESC
|
||||
static constexpr uint8_t TOKEN = 0x0E;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// STK general command constants
|
||||
|
||||
static constexpr uint8_t CMD_SIGN_ON = 0x01;
|
||||
static constexpr uint8_t CMD_SET_PARAMETER = 0x02;
|
||||
static constexpr uint8_t CMD_GET_PARAMETER = 0x03;
|
||||
static constexpr uint8_t CMD_SET_DEVICE_PARAMETERS = 0x04;
|
||||
static constexpr uint8_t CMD_OSCCAL = 0x05;
|
||||
static constexpr uint8_t CMD_LOAD_ADDRESS = 0x06;
|
||||
static constexpr uint8_t CMD_FIRMWARE_UPGRADE = 0x07;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// STK ISP command constants
|
||||
|
||||
static constexpr uint8_t CMD_ENTER_PROGMODE_ISP = 0x10;
|
||||
static constexpr uint8_t CMD_LEAVE_PROGMODE_ISP = 0x11;
|
||||
static constexpr uint8_t CMD_CHIP_ERASE_ISP = 0x12;
|
||||
static constexpr uint8_t CMD_PROGRAM_FLASH_ISP = 0x13;
|
||||
static constexpr uint8_t CMD_READ_FLASH_ISP = 0x14;
|
||||
static constexpr uint8_t CMD_PROGRAM_EEPROM_ISP = 0x15;
|
||||
static constexpr uint8_t CMD_READ_EEPROM_ISP = 0x16;
|
||||
static constexpr uint8_t CMD_PROGRAM_FUSE_ISP = 0x17;
|
||||
static constexpr uint8_t CMD_READ_FUSE_ISP = 0x18;
|
||||
static constexpr uint8_t CMD_PROGRAM_LOCK_ISP = 0x19;
|
||||
static constexpr uint8_t CMD_READ_LOCK_ISP = 0x1A;
|
||||
static constexpr uint8_t CMD_READ_SIGNATURE_ISP = 0x1B;
|
||||
static constexpr uint8_t CMD_READ_OSCCAL_ISP = 0x1C;
|
||||
static constexpr uint8_t CMD_SPI_MULTI = 0x1D;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// STK PP command constants
|
||||
|
||||
static constexpr uint8_t CMD_ENTER_PROGMODE_PP = 0x20;
|
||||
static constexpr uint8_t CMD_LEAVE_PROGMODE_PP = 0x21;
|
||||
static constexpr uint8_t CMD_CHIP_ERASE_PP = 0x22;
|
||||
static constexpr uint8_t CMD_PROGRAM_FLASH_PP = 0x23;
|
||||
static constexpr uint8_t CMD_READ_FLASH_PP = 0x24;
|
||||
static constexpr uint8_t CMD_PROGRAM_EEPROM_PP = 0x25;
|
||||
static constexpr uint8_t CMD_READ_EEPROM_PP = 0x26;
|
||||
static constexpr uint8_t CMD_PROGRAM_FUSE_PP = 0x27;
|
||||
static constexpr uint8_t CMD_READ_FUSE_PP = 0x28;
|
||||
static constexpr uint8_t CMD_PROGRAM_LOCK_PP = 0x29;
|
||||
static constexpr uint8_t CMD_READ_LOCK_PP = 0x2A;
|
||||
static constexpr uint8_t CMD_READ_SIGNATURE_PP = 0x2B;
|
||||
static constexpr uint8_t CMD_READ_OSCCAL_PP = 0x2C;
|
||||
|
||||
static constexpr uint8_t CMD_SET_CONTROL_STACK = 0x2D;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// STK HVSP command constants
|
||||
|
||||
static constexpr uint8_t CMD_ENTER_PROGMODE_HVSP = 0x30;
|
||||
static constexpr uint8_t CMD_LEAVE_PROGMODE_HVSP = 0x31;
|
||||
static constexpr uint8_t CMD_CHIP_ERASE_HVSP = 0x32;
|
||||
static constexpr uint8_t CMD_PROGRAM_FLASH_HVSP = 0x33;
|
||||
static constexpr uint8_t CMD_READ_FLASH_HVSP = 0x34;
|
||||
static constexpr uint8_t CMD_PROGRAM_EEPROM_HVSP = 0x35;
|
||||
static constexpr uint8_t CMD_READ_EEPROM_HVSP = 0x36;
|
||||
static constexpr uint8_t CMD_PROGRAM_FUSE_HVSP = 0x37;
|
||||
static constexpr uint8_t CMD_READ_FUSE_HVSP = 0x38;
|
||||
static constexpr uint8_t CMD_PROGRAM_LOCK_HVSP = 0x39;
|
||||
static constexpr uint8_t CMD_READ_LOCK_HVSP = 0x3A;
|
||||
static constexpr uint8_t CMD_READ_SIGNATURE_HVSP = 0x3B;
|
||||
static constexpr uint8_t CMD_READ_OSCCAL_HVSP = 0x3C;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// STK status constants
|
||||
|
||||
// Success
|
||||
static constexpr uint8_t STATUS_CMD_OK = 0x00;
|
||||
|
||||
// Warnings
|
||||
static constexpr uint8_t STATUS_CMD_TOUT = 0x80;
|
||||
static constexpr uint8_t STATUS_RDY_BSY_TOUT = 0x81;
|
||||
static constexpr uint8_t STATUS_SET_PARAM_MISSING = 0x82;
|
||||
|
||||
// Errors
|
||||
static constexpr uint8_t STATUS_CMD_FAILED = 0xC0;
|
||||
static constexpr uint8_t STATUS_CKSUM_ERROR = 0xC1;
|
||||
static constexpr uint8_t STATUS_CMD_UNKNOWN = 0xC9;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// STK parameter constants
|
||||
static constexpr uint8_t PARAM_BUILD_NUMBER_LOW = 0x80;
|
||||
static constexpr uint8_t PARAM_BUILD_NUMBER_HIGH = 0x81;
|
||||
static constexpr uint8_t PARAM_HW_VER = 0x90;
|
||||
static constexpr uint8_t PARAM_SW_MAJOR = 0x91;
|
||||
static constexpr uint8_t PARAM_SW_MINOR = 0x92;
|
||||
static constexpr uint8_t PARAM_VTARGET = 0x94;
|
||||
static constexpr uint8_t PARAM_VADJUST = 0x95;
|
||||
static constexpr uint8_t PARAM_OSC_PSCALE = 0x96;
|
||||
static constexpr uint8_t PARAM_OSC_CMATCH = 0x97;
|
||||
static constexpr uint8_t PARAM_SCK_DURATION = 0x98;
|
||||
static constexpr uint8_t PARAM_TOPCARD_DETECT = 0x9A;
|
||||
static constexpr uint8_t PARAM_STATUS = 0x9C;
|
||||
static constexpr uint8_t PARAM_DATA = 0x9D;
|
||||
static constexpr uint8_t PARAM_RESET_POLARITY = 0x9E;
|
||||
static constexpr uint8_t PARAM_CONTROLLER_INIT = 0x9F;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// STK answer constants
|
||||
|
||||
static constexpr uint8_t ANSWER_CKSUM_ERROR = 0xB0;
|
||||
Submodule stk500v2/flash deleted from 6edb2e5a21
Submodule stk500v2/io deleted from 80de36ee7e
@@ -1,611 +0,0 @@
|
||||
#include "clock.hpp"
|
||||
|
||||
#include "uart/uart.hpp"
|
||||
|
||||
#include <math.h>
|
||||
|
||||
#include <avr/boot.h>
|
||||
#include <avr/interrupt.h>
|
||||
#include <avr/io.h>
|
||||
#include <avr/pgmspace.h>
|
||||
|
||||
#include "command.hpp"
|
||||
|
||||
static constexpr auto TIMEOUT = 5000;
|
||||
static constexpr auto BAUD_RATE = 115200;
|
||||
|
||||
using uart_interface = uart::Hardware0<uart::Config<BAUD_RATE>, uart::Driven::BLOCKING>;
|
||||
uart::Uart<uart_interface> serial;
|
||||
|
||||
struct Message {
|
||||
uint8_t start;
|
||||
uint8_t number;
|
||||
uint16_t size;
|
||||
uint8_t token;
|
||||
uint8_t body[275];
|
||||
uint8_t checksum;
|
||||
};
|
||||
|
||||
static inline bool receiveByte(uint8_t &data, uint16_t &timeout)
|
||||
{
|
||||
constexpr auto MICROSECOND = 1000.0 * 1000;
|
||||
constexpr auto SYMBOL_SIZE = 9;
|
||||
constexpr auto BYTE_DELAY_US = (SYMBOL_SIZE * MICROSECOND) / BAUD_RATE;
|
||||
constexpr auto NUM_MS_DELAY_STEPS = static_cast<uint16_t>(round(1000 / BYTE_DELAY_US));
|
||||
uint16_t msDelay = NUM_MS_DELAY_STEPS;
|
||||
|
||||
while (timeout) {
|
||||
if (serial.rxByte(data)) {
|
||||
timeout = TIMEOUT;
|
||||
return true;
|
||||
}
|
||||
|
||||
_delay_us(BYTE_DELAY_US);
|
||||
|
||||
if (--msDelay == 0) {
|
||||
msDelay = NUM_MS_DELAY_STEPS;
|
||||
--timeout;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline uint8_t calcChecksum(const Message &msg)
|
||||
{
|
||||
uint8_t checksum = msg.start;
|
||||
|
||||
for (uint16_t i = 1; i < 5 + msg.size; ++i) {
|
||||
checksum ^= *(reinterpret_cast<const uint8_t *>(&msg) + i);
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
static inline bool receiveMessage(Message &msg, uint16_t &timeout)
|
||||
{
|
||||
if (!receiveByte(msg.start, timeout) || msg.start != MESSAGE_START)
|
||||
return false;
|
||||
if (!receiveByte(msg.number, timeout))
|
||||
return false;
|
||||
if (!receiveByte(*(reinterpret_cast<uint8_t *>(&msg.size) + 1), timeout))
|
||||
return false;
|
||||
if (!receiveByte(*reinterpret_cast<uint8_t *>(&msg.size), timeout) || msg.size > sizeof(msg.body))
|
||||
return false;
|
||||
if (!receiveByte(msg.token, timeout) || msg.token != TOKEN)
|
||||
return false;
|
||||
for (uint16_t i = 0; i < msg.size; ++i) {
|
||||
if (!receiveByte(msg.body[i], timeout))
|
||||
return false;
|
||||
}
|
||||
if (!receiveByte(msg.checksum, timeout) || msg.checksum != calcChecksum(msg))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline void transmitMessage(const Message &msg)
|
||||
{
|
||||
serial.txByte(msg.start);
|
||||
serial.txByte(msg.number);
|
||||
serial.txByte(msg.size >> 8);
|
||||
serial.txByte(msg.size & 0xFF);
|
||||
serial.txByte(msg.token);
|
||||
for (uint16_t i = 0; i < msg.size; ++i)
|
||||
serial.txByte(msg.body[i]);
|
||||
serial.txByte(msg.checksum);
|
||||
}
|
||||
|
||||
static inline bool isSignOn(const Message &msg)
|
||||
{
|
||||
if (msg.size == 1 && msg.body[0] == CMD_SIGN_ON)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline bool isGetParameter(const Message &msg)
|
||||
{
|
||||
if (msg.size == 2 && msg.body[0] == CMD_GET_PARAMETER)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline bool isSetParameter(const Message &msg)
|
||||
{
|
||||
if (msg.size == 3 && msg.body[0] == CMD_SET_PARAMETER)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline bool isEnterProgmodeIsp(const Message &msg)
|
||||
{
|
||||
if (msg.size == 12 && msg.body[0] == CMD_ENTER_PROGMODE_ISP)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline bool isReadSignatureIsp(const Message &msg)
|
||||
{
|
||||
if (msg.size == 6 && msg.body[0] == CMD_READ_SIGNATURE_ISP)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline bool isReadFuseIsp(const Message &msg)
|
||||
{
|
||||
if (msg.size == 6 && msg.body[0] == CMD_READ_FUSE_ISP)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline bool isReadLockIsp(const Message &msg)
|
||||
{
|
||||
if (msg.size == 6 && msg.body[0] == CMD_READ_LOCK_ISP)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline bool isLoadAddress(const Message &msg)
|
||||
{
|
||||
if (msg.size == 5 && msg.body[0] == CMD_LOAD_ADDRESS)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline bool isReadFlashIsp(const Message &msg)
|
||||
{
|
||||
if (msg.size == 4 && msg.body[0] == CMD_READ_FLASH_ISP)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline bool isReadEepromIsp(const Message &msg)
|
||||
{
|
||||
if (msg.size == 4 && msg.body[0] == CMD_READ_EEPROM_ISP)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline bool isChipEraseIsp(const Message &msg)
|
||||
{
|
||||
if (msg.size == 7 && msg.body[0] == CMD_CHIP_ERASE_ISP)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline bool isProgramFlashIsp(const Message &msg)
|
||||
{
|
||||
if (msg.body[0] == CMD_PROGRAM_FLASH_ISP) {
|
||||
const auto dataSize = static_cast<uint16_t>(msg.body[1]) << 8 | msg.body[2];
|
||||
if (msg.size == (dataSize + 10) && dataSize == SPM_PAGESIZE)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline bool isProgramEepromIsp(const Message &msg)
|
||||
{
|
||||
if (msg.body[0] == CMD_PROGRAM_EEPROM_ISP) {
|
||||
if (msg.size == (static_cast<uint16_t>(msg.body[1]) << 8 | msg.body[2]) + 10)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline bool isLeaveProgmodeIsp(const Message &msg)
|
||||
{
|
||||
if (msg.size == 3 && msg.body[0] == CMD_LEAVE_PROGMODE_ISP)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline void formatSignOnAnswer(Message &msg)
|
||||
{
|
||||
msg.size = 3 + 8;
|
||||
msg.body[1] = STATUS_CMD_OK;
|
||||
msg.body[2] = 8;
|
||||
msg.body[3] = 'S';
|
||||
msg.body[4] = 'T';
|
||||
msg.body[5] = 'K';
|
||||
msg.body[6] = '5';
|
||||
msg.body[7] = '0';
|
||||
msg.body[8] = '0';
|
||||
msg.body[9] = '_';
|
||||
msg.body[10] = '2';
|
||||
msg.checksum = calcChecksum(msg);
|
||||
}
|
||||
|
||||
static inline void formatGetParameterAnswer(Message &msg)
|
||||
{
|
||||
msg.size = 3;
|
||||
|
||||
if (msg.body[1] == PARAM_HW_VER) {
|
||||
msg.body[2] = 1;
|
||||
} else if (msg.body[1] == PARAM_SW_MAJOR) {
|
||||
msg.body[2] = 0x02;
|
||||
} else if (msg.body[1] == PARAM_SW_MINOR) {
|
||||
msg.body[2] = 0x0a;
|
||||
} else if (msg.body[1] == PARAM_SCK_DURATION) {
|
||||
msg.body[2] = 2;
|
||||
} else if (msg.body[1] == PARAM_VADJUST) {
|
||||
msg.body[2] = 25;
|
||||
} else if (msg.body[1] == PARAM_VTARGET) {
|
||||
msg.body[2] = 49;
|
||||
} else if (msg.body[1] == PARAM_OSC_PSCALE) {
|
||||
msg.body[2] = 2;
|
||||
} else if (msg.body[1] == PARAM_OSC_CMATCH) {
|
||||
msg.body[2] = 127;
|
||||
} else if (msg.body[1] == PARAM_TOPCARD_DETECT) {
|
||||
msg.body[2] = 0xFF;
|
||||
} else {
|
||||
msg.size = 2;
|
||||
}
|
||||
|
||||
if (msg.size == 2) {
|
||||
msg.body[1] = STATUS_CMD_FAILED;
|
||||
} else {
|
||||
msg.body[1] = STATUS_CMD_OK;
|
||||
}
|
||||
|
||||
msg.checksum = calcChecksum(msg);
|
||||
}
|
||||
|
||||
static inline void formatSetParameterAnswer(Message &msg)
|
||||
{
|
||||
msg.size = 2;
|
||||
msg.body[1] = STATUS_CMD_OK;
|
||||
|
||||
msg.checksum = calcChecksum(msg);
|
||||
}
|
||||
|
||||
static inline void formatEnterProgmodeIspAnswer(Message &msg)
|
||||
{
|
||||
msg.size = 2;
|
||||
msg.body[1] = STATUS_CMD_OK;
|
||||
|
||||
msg.checksum = calcChecksum(msg);
|
||||
}
|
||||
|
||||
static inline void formatReadSignatureIspAnswer(Message &msg)
|
||||
{
|
||||
msg.size = 4;
|
||||
msg.body[2] = boot_signature_byte_get(msg.body[4] * 2);
|
||||
msg.body[1] = STATUS_CMD_OK;
|
||||
msg.body[3] = STATUS_CMD_OK;
|
||||
|
||||
msg.checksum = calcChecksum(msg);
|
||||
}
|
||||
|
||||
static inline void formatReadFuseIspAnswer(Message &msg)
|
||||
{
|
||||
constexpr auto READ_LOW_FUSE_BITS = 0x0050;
|
||||
constexpr auto READ_HIGH_FUSE_BITS = 0x0858;
|
||||
constexpr auto READ_EXTENDED_FUSE_BITS = 0x0850;
|
||||
|
||||
msg.size = 4;
|
||||
|
||||
if (*reinterpret_cast<uint16_t *>(msg.body + 2) == READ_EXTENDED_FUSE_BITS) {
|
||||
msg.body[2] = boot_lock_fuse_bits_get(GET_EXTENDED_FUSE_BITS);
|
||||
}
|
||||
if (*reinterpret_cast<uint16_t *>(msg.body + 2) == READ_HIGH_FUSE_BITS) {
|
||||
msg.body[2] = boot_lock_fuse_bits_get(GET_HIGH_FUSE_BITS);
|
||||
}
|
||||
if (*reinterpret_cast<uint16_t *>(msg.body + 2) == READ_LOW_FUSE_BITS) {
|
||||
msg.body[2] = boot_lock_fuse_bits_get(GET_LOW_FUSE_BITS);
|
||||
}
|
||||
|
||||
msg.body[1] = STATUS_CMD_OK;
|
||||
msg.body[3] = STATUS_CMD_OK;
|
||||
|
||||
msg.checksum = calcChecksum(msg);
|
||||
}
|
||||
|
||||
static inline void formatReadLockIspAnswer(Message &msg)
|
||||
{
|
||||
msg.size = 4;
|
||||
msg.body[2] = boot_lock_fuse_bits_get(GET_LOCK_BITS);
|
||||
msg.body[1] = STATUS_CMD_OK;
|
||||
msg.body[3] = STATUS_CMD_OK;
|
||||
|
||||
msg.checksum = calcChecksum(msg);
|
||||
}
|
||||
|
||||
static inline void formatLoadAddressAnswer(Message &msg)
|
||||
{
|
||||
msg.size = 2;
|
||||
msg.body[1] = STATUS_CMD_OK;
|
||||
|
||||
msg.checksum = calcChecksum(msg);
|
||||
}
|
||||
|
||||
static inline void formatReadFlashIspAnswer(Message &msg, uint32_t &addr)
|
||||
{
|
||||
const uint16_t byteAddress = 2 * addr;
|
||||
const uint16_t numBytes = static_cast<uint16_t>(msg.body[1]) << 8 | msg.body[2];
|
||||
msg.size = 3 + numBytes;
|
||||
msg.body[1] = STATUS_CMD_OK;
|
||||
for (uint16_t i = 0; i < numBytes; ++i) {
|
||||
msg.body[i + 2] = pgm_read_byte(static_cast<uint16_t>(byteAddress + i));
|
||||
}
|
||||
const auto numWords = numBytes / 2;
|
||||
addr += numWords;
|
||||
msg.body[numBytes + 2] = STATUS_CMD_OK;
|
||||
|
||||
msg.checksum = calcChecksum(msg);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
bool isEepromReady()
|
||||
{
|
||||
return (EECR & (1 << EEPE)) ? false : true;
|
||||
}
|
||||
|
||||
void waitEepromReady()
|
||||
{
|
||||
while (!isEepromReady())
|
||||
;
|
||||
}
|
||||
|
||||
uint8_t readEepromByte(const uint8_t *addr)
|
||||
{
|
||||
EEAR = reinterpret_cast<uint16_t>(addr);
|
||||
EECR |= (1 << EERE);
|
||||
return EEDR;
|
||||
}
|
||||
|
||||
void writeEepromByte(uint8_t *addr, uint8_t value)
|
||||
{
|
||||
EECR = 0;
|
||||
EEAR = reinterpret_cast<uint16_t>(addr);
|
||||
EEDR = value;
|
||||
EECR |= (1 << EEMPE);
|
||||
EECR |= (1 << EEPE);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void writeFlashPage(uint32_t pageAddress, const uint8_t *data)
|
||||
{
|
||||
boot_page_erase(pageAddress);
|
||||
boot_spm_busy_wait();
|
||||
|
||||
for (uint16_t i = 0; i < SPM_PAGESIZE; i += 2) {
|
||||
uint16_t dataWord = *data++;
|
||||
dataWord |= (*data++) << 8;
|
||||
boot_page_fill(pageAddress + i, dataWord);
|
||||
}
|
||||
|
||||
boot_page_write(pageAddress);
|
||||
boot_spm_busy_wait();
|
||||
boot_rww_enable();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static inline uint16_t getBootloaderSize()
|
||||
{
|
||||
const auto highFuse = boot_lock_fuse_bits_get(GET_HIGH_FUSE_BITS);
|
||||
constexpr auto BOOTSZ0 = 1;
|
||||
constexpr auto BOOTSZ1 = 2;
|
||||
|
||||
if (highFuse & (1 << BOOTSZ1) && highFuse & (1 << BOOTSZ0))
|
||||
return 256 * 2;
|
||||
else if (highFuse & (1 << BOOTSZ1))
|
||||
return 512 * 2;
|
||||
else if (highFuse & (1 << BOOTSZ0))
|
||||
return 1024 * 2;
|
||||
return 2048 * 2;
|
||||
}
|
||||
|
||||
static inline uint32_t getFlashSize()
|
||||
{
|
||||
const auto bootloaderSize = getBootloaderSize();
|
||||
return (FLASHEND - bootloaderSize + 1);
|
||||
}
|
||||
|
||||
static inline void performChipErase(uint16_t flashStartAddress = 0x0000)
|
||||
{
|
||||
constexpr auto getEepromEraseFuseBit = []() -> bool {
|
||||
constexpr auto EESAVE = 3;
|
||||
return boot_lock_fuse_bits_get(GET_HIGH_FUSE_BITS) & (1 << EESAVE);
|
||||
};
|
||||
|
||||
constexpr auto eraseFlash = [](const uint16_t &flashStartAddress) {
|
||||
const auto flashSize = getFlashSize();
|
||||
const auto byteAddress = 2 * flashStartAddress;
|
||||
for (uint16_t i = byteAddress; i < flashSize; i += SPM_PAGESIZE) {
|
||||
boot_page_erase(i);
|
||||
boot_spm_busy_wait();
|
||||
}
|
||||
boot_rww_enable();
|
||||
};
|
||||
|
||||
constexpr auto eraseEeprom = [getEepromEraseFuseBit]() {
|
||||
const auto eraseEeprom = getEepromEraseFuseBit();
|
||||
if (eraseEeprom) {
|
||||
constexpr auto EEPROM_SIZE = E2END + 1;
|
||||
for (uint16_t i = 0; i < EEPROM_SIZE; ++i) {
|
||||
writeEepromByte(reinterpret_cast<uint8_t *>(i), 0xFF);
|
||||
waitEepromReady();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
eraseFlash(flashStartAddress);
|
||||
eraseEeprom();
|
||||
}
|
||||
|
||||
static inline void formatChipEraseIspAnswer(Message &msg)
|
||||
{
|
||||
msg.size = 2;
|
||||
msg.body[1] = STATUS_CMD_OK;
|
||||
|
||||
msg.checksum = calcChecksum(msg);
|
||||
}
|
||||
|
||||
static inline void formatReadEepromIspAnswer(Message &msg, uint32_t &addr)
|
||||
{
|
||||
const uint16_t numBytes = static_cast<uint16_t>(msg.body[1]) << 8 | msg.body[2];
|
||||
msg.size = 3 + numBytes;
|
||||
msg.body[1] = STATUS_CMD_OK;
|
||||
for (uint16_t i = 0; i < numBytes; ++i) {
|
||||
msg.body[i + 2] = readEepromByte(reinterpret_cast<const uint8_t *>(addr + i));
|
||||
}
|
||||
addr += numBytes;
|
||||
msg.body[numBytes + 2] = STATUS_CMD_OK;
|
||||
|
||||
msg.checksum = calcChecksum(msg);
|
||||
}
|
||||
|
||||
static inline void formatProgramFlashIspAnswer(Message &msg, uint32_t &addr)
|
||||
{
|
||||
const auto byteAddress = 2 * addr;
|
||||
if (byteAddress < getFlashSize())
|
||||
writeFlashPage(byteAddress, msg.body + 10);
|
||||
const uint16_t numBytes = static_cast<uint16_t>(msg.body[1]) << 8 | msg.body[2];
|
||||
const auto numWords = numBytes / 2;
|
||||
addr += numWords;
|
||||
|
||||
msg.size = 2;
|
||||
msg.body[1] = STATUS_CMD_OK;
|
||||
|
||||
msg.checksum = calcChecksum(msg);
|
||||
}
|
||||
|
||||
static inline void formatProgramEepromIspAnswer(Message &msg, uint32_t &addr)
|
||||
{
|
||||
const uint16_t numBytes = static_cast<uint16_t>(msg.body[1]) << 8 | msg.body[2];
|
||||
for (uint16_t i = 0; i < numBytes; ++i) {
|
||||
writeEepromByte(reinterpret_cast<uint8_t *>(addr + i), msg.body[10 + i]);
|
||||
waitEepromReady();
|
||||
}
|
||||
addr += numBytes;
|
||||
msg.size = 2;
|
||||
msg.body[1] = STATUS_CMD_OK;
|
||||
|
||||
msg.checksum = calcChecksum(msg);
|
||||
}
|
||||
|
||||
static inline void formatLeaveProgmodeIspAnswer(Message &msg)
|
||||
{
|
||||
msg.size = 2;
|
||||
msg.body[1] = STATUS_CMD_OK;
|
||||
|
||||
msg.checksum = calcChecksum(msg);
|
||||
}
|
||||
|
||||
static inline void formatErrorAnswer(Message &msg)
|
||||
{
|
||||
msg.start = MESSAGE_START;
|
||||
msg.size = 1;
|
||||
msg.token = TOKEN;
|
||||
msg.body[0] = STATUS_CMD_UNKNOWN;
|
||||
msg.checksum = calcChecksum(msg);
|
||||
}
|
||||
|
||||
enum class ChipEraseState {
|
||||
NONE = 0,
|
||||
REQUEST = (1 << 1),
|
||||
RESPONSE = (1 << 2),
|
||||
PERFORM = REQUEST | RESPONSE,
|
||||
PROGRAM = (1 << 3),
|
||||
FINISH = REQUEST | RESPONSE | PROGRAM,
|
||||
};
|
||||
|
||||
constexpr ChipEraseState operator|(const ChipEraseState &self, const ChipEraseState &other)
|
||||
{
|
||||
return static_cast<ChipEraseState>(static_cast<uint8_t>(self) | static_cast<uint8_t>(other));
|
||||
}
|
||||
|
||||
constexpr ChipEraseState &operator|=(ChipEraseState &self, const ChipEraseState &other)
|
||||
{
|
||||
self = self | other;
|
||||
return self;
|
||||
}
|
||||
|
||||
static inline void handleMessage(Message &msg, uint32_t &addr, uint16_t &finishEraseAddress,
|
||||
ChipEraseState &chipEraseFlag)
|
||||
{
|
||||
if (isSignOn(msg))
|
||||
formatSignOnAnswer(msg);
|
||||
else if (isGetParameter(msg))
|
||||
formatGetParameterAnswer(msg);
|
||||
else if (isSetParameter(msg))
|
||||
formatSetParameterAnswer(msg);
|
||||
else if (isEnterProgmodeIsp(msg))
|
||||
formatEnterProgmodeIspAnswer(msg);
|
||||
else if (isReadSignatureIsp(msg))
|
||||
formatReadSignatureIspAnswer(msg);
|
||||
else if (isReadFuseIsp(msg))
|
||||
formatReadFuseIspAnswer(msg);
|
||||
else if (isReadLockIsp(msg))
|
||||
formatReadLockIspAnswer(msg);
|
||||
else if (isLoadAddress(msg)) {
|
||||
addr = msg.body[1];
|
||||
addr = (addr << 8) | msg.body[2];
|
||||
addr = (addr << 8) | msg.body[3];
|
||||
addr = (addr << 8) | msg.body[4];
|
||||
formatLoadAddressAnswer(msg);
|
||||
} else if (isReadFlashIsp(msg))
|
||||
formatReadFlashIspAnswer(msg, addr);
|
||||
else if (isReadEepromIsp(msg))
|
||||
formatReadEepromIspAnswer(msg, addr);
|
||||
else if (isChipEraseIsp(msg)) {
|
||||
chipEraseFlag |= ChipEraseState::REQUEST;
|
||||
formatChipEraseIspAnswer(msg);
|
||||
} else if (isProgramFlashIsp(msg)) {
|
||||
chipEraseFlag |= ChipEraseState::PROGRAM;
|
||||
formatProgramFlashIspAnswer(msg, addr);
|
||||
finishEraseAddress = addr;
|
||||
} else if (isProgramEepromIsp(msg))
|
||||
formatProgramEepromIspAnswer(msg, addr);
|
||||
else if (isLeaveProgmodeIsp(msg)) {
|
||||
chipEraseFlag |= ChipEraseState::RESPONSE;
|
||||
formatLeaveProgmodeIspAnswer(msg);
|
||||
} else
|
||||
formatErrorAnswer(msg);
|
||||
|
||||
transmitMessage(msg);
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
serial.init();
|
||||
|
||||
Message msg;
|
||||
uint32_t addr = 0x0000;
|
||||
uint16_t finishEraseAddress = 0x0000;
|
||||
ChipEraseState chipEraseFlag = ChipEraseState::NONE;
|
||||
uint16_t timeout = TIMEOUT;
|
||||
|
||||
while (true) {
|
||||
if (receiveMessage(msg, timeout)) {
|
||||
handleMessage(msg, addr, finishEraseAddress, chipEraseFlag);
|
||||
}
|
||||
|
||||
if (timeout == 0) {
|
||||
if (chipEraseFlag == ChipEraseState::PERFORM) {
|
||||
performChipErase();
|
||||
chipEraseFlag = ChipEraseState::NONE;
|
||||
} else if (chipEraseFlag == ChipEraseState::FINISH) {
|
||||
performChipErase(finishEraseAddress);
|
||||
chipEraseFlag = ChipEraseState::NONE;
|
||||
}
|
||||
|
||||
asm volatile("jmp 0x0000");
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void startup() __attribute__((naked, section(".vectors")));
|
||||
void startup()
|
||||
{
|
||||
asm volatile("clr __zero_reg__");
|
||||
SP = RAMEND;
|
||||
SREG = 0;
|
||||
asm volatile("jmp main");
|
||||
}
|
||||
@@ -1,275 +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>{19798cce-5d96-40e9-b769-d209715dce0c}</ProjectGuid>
|
||||
<avrdevice>ATmega328P</avrdevice>
|
||||
<avrdeviceseries>none</avrdeviceseries>
|
||||
<OutputType>Executable</OutputType>
|
||||
<Language>CPP</Language>
|
||||
<OutputFileName>$(MSBuildProjectName)</OutputFileName>
|
||||
<OutputFileExtension>.elf</OutputFileExtension>
|
||||
<OutputDirectory>$(MSBuildProjectDirectory)\$(Configuration)</OutputDirectory>
|
||||
<AssemblyName>stk500v2</AssemblyName>
|
||||
<Name>stk500v2</Name>
|
||||
<RootNamespace>stk500v2</RootNamespace>
|
||||
<ToolchainFlavour>avr-g++-9.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 />
|
||||
<avrtool>com.atmel.avrdbg.tool.atmelice</avrtool>
|
||||
<avrtoolserialnumber>J41800099437</avrtoolserialnumber>
|
||||
<avrdeviceexpectedsignature>0x1E950F</avrdeviceexpectedsignature>
|
||||
<com_atmel_avrdbg_tool_stk500>
|
||||
<ToolOptions>
|
||||
<InterfaceProperties>
|
||||
<IspClock>125000</IspClock>
|
||||
</InterfaceProperties>
|
||||
<InterfaceName>ISP</InterfaceName>
|
||||
</ToolOptions>
|
||||
<ToolType>com.atmel.avrdbg.tool.stk500</ToolType>
|
||||
<ToolNumber>
|
||||
</ToolNumber>
|
||||
<ToolName>STK500</ToolName>
|
||||
</com_atmel_avrdbg_tool_stk500>
|
||||
<avrtoolinterface>ISP</avrtoolinterface>
|
||||
<avrtoolinterfaceclock>125000</avrtoolinterfaceclock>
|
||||
<AsfFrameworkConfig>
|
||||
<framework-data xmlns="">
|
||||
<options />
|
||||
<configurations />
|
||||
<files />
|
||||
<documentation help="" />
|
||||
<offline-documentation help="" />
|
||||
<dependencies>
|
||||
<content-extension eid="atmel.asf" uuidref="Atmel.ASF" version="3.47.0" />
|
||||
</dependencies>
|
||||
</framework-data>
|
||||
</AsfFrameworkConfig>
|
||||
<com_atmel_avrdbg_tool_atmelice>
|
||||
<ToolOptions>
|
||||
<InterfaceProperties>
|
||||
<IspClock>125000</IspClock>
|
||||
</InterfaceProperties>
|
||||
<InterfaceName>ISP</InterfaceName>
|
||||
</ToolOptions>
|
||||
<ToolType>com.atmel.avrdbg.tool.atmelice</ToolType>
|
||||
<ToolNumber>J41800099437</ToolNumber>
|
||||
<ToolName>Atmel-ICE</ToolName>
|
||||
</com_atmel_avrdbg_tool_atmelice>
|
||||
<custom>
|
||||
<ToolOptions>
|
||||
<InterfaceProperties>
|
||||
<IspClock>125000</IspClock>
|
||||
</InterfaceProperties>
|
||||
<InterfaceName>
|
||||
</InterfaceName>
|
||||
</ToolOptions>
|
||||
<ToolType>custom</ToolType>
|
||||
<ToolNumber>
|
||||
</ToolNumber>
|
||||
<ToolName>Custom Programming Tool</ToolName>
|
||||
</custom>
|
||||
<com_atmel_avrdbg_tool_simulator>
|
||||
<ToolOptions xmlns="">
|
||||
<InterfaceProperties>
|
||||
</InterfaceProperties>
|
||||
<InterfaceName>
|
||||
</InterfaceName>
|
||||
</ToolOptions>
|
||||
<ToolType xmlns="">com.atmel.avrdbg.tool.simulator</ToolType>
|
||||
<ToolNumber xmlns="">
|
||||
</ToolNumber>
|
||||
<ToolName xmlns="">Simulator</ToolName>
|
||||
</com_atmel_avrdbg_tool_simulator>
|
||||
<AAFDebugger>
|
||||
<AAFDebugFiles>
|
||||
</AAFDebugFiles>
|
||||
</AAFDebugger>
|
||||
</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>
|
||||
<avrgcc.compiler.general.ChangeDefaultCharTypeUnsigned>True</avrgcc.compiler.general.ChangeDefaultCharTypeUnsigned>
|
||||
<avrgcc.compiler.general.ChangeDefaultBitFieldUnsigned>True</avrgcc.compiler.general.ChangeDefaultBitFieldUnsigned>
|
||||
<avrgcc.compiler.symbols.DefSymbols>
|
||||
<ListValues>
|
||||
<Value>NDEBUG</Value>
|
||||
</ListValues>
|
||||
</avrgcc.compiler.symbols.DefSymbols>
|
||||
<avrgcc.compiler.directories.IncludePaths>
|
||||
<ListValues>
|
||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
||||
</ListValues>
|
||||
</avrgcc.compiler.directories.IncludePaths>
|
||||
<avrgcc.compiler.optimization.level>Optimize for size (-Os)</avrgcc.compiler.optimization.level>
|
||||
<avrgcc.compiler.optimization.AllocateBytesNeededForEnum>True</avrgcc.compiler.optimization.AllocateBytesNeededForEnum>
|
||||
<avrgcc.compiler.warnings.AllWarnings>True</avrgcc.compiler.warnings.AllWarnings>
|
||||
<avrgcc.compiler.warnings.ExtraWarnings>True</avrgcc.compiler.warnings.ExtraWarnings>
|
||||
<avrgcc.compiler.warnings.Pedantic>True</avrgcc.compiler.warnings.Pedantic>
|
||||
<avrgcc.compiler.miscellaneous.OtherFlags>-fno-threadsafe-statics -std=c11</avrgcc.compiler.miscellaneous.OtherFlags>
|
||||
<avrgcccpp.compiler.general.ChangeDefaultCharTypeUnsigned>True</avrgcccpp.compiler.general.ChangeDefaultCharTypeUnsigned>
|
||||
<avrgcccpp.compiler.general.ChangeDefaultBitFieldUnsigned>True</avrgcccpp.compiler.general.ChangeDefaultBitFieldUnsigned>
|
||||
<avrgcccpp.compiler.symbols.DefSymbols>
|
||||
<ListValues>
|
||||
<Value>NDEBUG</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.compiler.symbols.DefSymbols>
|
||||
<avrgcccpp.compiler.directories.IncludePaths>
|
||||
<ListValues>
|
||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.compiler.directories.IncludePaths>
|
||||
<avrgcccpp.compiler.optimization.level>Optimize for size (-Os)</avrgcccpp.compiler.optimization.level>
|
||||
<avrgcccpp.compiler.optimization.AllocateBytesNeededForEnum>True</avrgcccpp.compiler.optimization.AllocateBytesNeededForEnum>
|
||||
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
|
||||
<avrgcccpp.compiler.warnings.Pedantic>True</avrgcccpp.compiler.warnings.Pedantic>
|
||||
<avrgcccpp.compiler.miscellaneous.OtherFlags>-fno-threadsafe-statics -Wextra -std=c++17</avrgcccpp.compiler.miscellaneous.OtherFlags>
|
||||
<avrgcccpp.linker.general.NoStartupOrDefaultLibs>True</avrgcccpp.linker.general.NoStartupOrDefaultLibs>
|
||||
<avrgcccpp.linker.libraries.Libraries>
|
||||
<ListValues>
|
||||
<Value>libm</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.linker.libraries.Libraries>
|
||||
<avrgcccpp.linker.memorysettings.Flash>
|
||||
<ListValues>
|
||||
<Value>.text=0x3C00</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.linker.memorysettings.Flash>
|
||||
<avrgcccpp.assembler.general.IncludePaths>
|
||||
<ListValues>
|
||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.assembler.general.IncludePaths>
|
||||
</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>
|
||||
<avrgcc.compiler.general.ChangeDefaultCharTypeUnsigned>True</avrgcc.compiler.general.ChangeDefaultCharTypeUnsigned>
|
||||
<avrgcc.compiler.general.ChangeDefaultBitFieldUnsigned>True</avrgcc.compiler.general.ChangeDefaultBitFieldUnsigned>
|
||||
<avrgcc.compiler.symbols.DefSymbols>
|
||||
<ListValues>
|
||||
<Value>DEBUG</Value>
|
||||
</ListValues>
|
||||
</avrgcc.compiler.symbols.DefSymbols>
|
||||
<avrgcc.compiler.directories.IncludePaths>
|
||||
<ListValues>
|
||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
||||
</ListValues>
|
||||
</avrgcc.compiler.directories.IncludePaths>
|
||||
<avrgcc.compiler.optimization.level>Optimize (-O1)</avrgcc.compiler.optimization.level>
|
||||
<avrgcc.compiler.optimization.AllocateBytesNeededForEnum>True</avrgcc.compiler.optimization.AllocateBytesNeededForEnum>
|
||||
<avrgcc.compiler.optimization.DebugLevel>Maximum (-g3)</avrgcc.compiler.optimization.DebugLevel>
|
||||
<avrgcc.compiler.warnings.AllWarnings>True</avrgcc.compiler.warnings.AllWarnings>
|
||||
<avrgcc.compiler.warnings.ExtraWarnings>True</avrgcc.compiler.warnings.ExtraWarnings>
|
||||
<avrgcc.compiler.warnings.Pedantic>True</avrgcc.compiler.warnings.Pedantic>
|
||||
<avrgcc.compiler.miscellaneous.OtherFlags>-fno-threadsafe-statics -std=c11</avrgcc.compiler.miscellaneous.OtherFlags>
|
||||
<avrgcccpp.compiler.general.ChangeDefaultCharTypeUnsigned>True</avrgcccpp.compiler.general.ChangeDefaultCharTypeUnsigned>
|
||||
<avrgcccpp.compiler.general.ChangeDefaultBitFieldUnsigned>True</avrgcccpp.compiler.general.ChangeDefaultBitFieldUnsigned>
|
||||
<avrgcccpp.compiler.symbols.DefSymbols>
|
||||
<ListValues>
|
||||
<Value>DEBUG</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.compiler.symbols.DefSymbols>
|
||||
<avrgcccpp.compiler.directories.IncludePaths>
|
||||
<ListValues>
|
||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.compiler.directories.IncludePaths>
|
||||
<avrgcccpp.compiler.optimization.level>Optimize debugging experience (-Og)</avrgcccpp.compiler.optimization.level>
|
||||
<avrgcccpp.compiler.optimization.AllocateBytesNeededForEnum>True</avrgcccpp.compiler.optimization.AllocateBytesNeededForEnum>
|
||||
<avrgcccpp.compiler.optimization.DebugLevel>Maximum (-g3)</avrgcccpp.compiler.optimization.DebugLevel>
|
||||
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
|
||||
<avrgcccpp.compiler.warnings.Pedantic>True</avrgcccpp.compiler.warnings.Pedantic>
|
||||
<avrgcccpp.compiler.miscellaneous.OtherFlags>-fno-threadsafe-statics -Wextra -std=c++17</avrgcccpp.compiler.miscellaneous.OtherFlags>
|
||||
<avrgcccpp.linker.general.NoStartupOrDefaultLibs>True</avrgcccpp.linker.general.NoStartupOrDefaultLibs>
|
||||
<avrgcccpp.linker.libraries.Libraries>
|
||||
<ListValues>
|
||||
<Value>libm</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.linker.libraries.Libraries>
|
||||
<avrgcccpp.linker.memorysettings.Flash>
|
||||
<ListValues>
|
||||
<Value>.text=0x3800</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.linker.memorysettings.Flash>
|
||||
<avrgcccpp.assembler.general.IncludePaths>
|
||||
<ListValues>
|
||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.assembler.general.IncludePaths>
|
||||
<avrgcccpp.assembler.debugging.DebugLevel>Default (-Wa,-g)</avrgcccpp.assembler.debugging.DebugLevel>
|
||||
</AvrGccCpp>
|
||||
</ToolchainSettings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="clock.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="command.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="flash\flash.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="io\io.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="main.cpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="type\type.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="uart\config.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="uart\hardware.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="uart\hardware0.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="uart\hardware1.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="uart\software.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="uart\uart.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="flash" />
|
||||
<Folder Include="io" />
|
||||
<Folder Include="uart" />
|
||||
<Folder Include="type" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(AVRSTUDIO_EXE_PATH)\\Vs\\Compiler.targets" />
|
||||
</Project>
|
||||
Submodule stk500v2/type deleted from ce31ef017f
Submodule stk500v2/uart deleted from 8f88cdccea
11
test/check_size.cmake
Normal file
11
test/check_size.cmake
Normal file
@@ -0,0 +1,11 @@
|
||||
execute_process(COMMAND ${SIZE_TOOL} ${ELF} OUTPUT_VARIABLE _out RESULT_VARIABLE _res)
|
||||
if(NOT _res EQUAL 0)
|
||||
message(FATAL_ERROR "avr-size failed")
|
||||
endif()
|
||||
# avr-size line 2 is "<text> <data> <bss> <dec> <hex> <file>".
|
||||
string(REGEX MATCH "\n[ \t]*([0-9]+)" _m "${_out}")
|
||||
set(_text ${CMAKE_MATCH_1})
|
||||
if(_text GREATER LIMIT)
|
||||
message(FATAL_ERROR ".text is ${_text} bytes, over the ${LIMIT}-byte boot section")
|
||||
endif()
|
||||
message(STATUS ".text ${_text} <= ${LIMIT} (boot section budget)")
|
||||
109
test/device.c
Normal file
109
test/device.c
Normal file
@@ -0,0 +1,109 @@
|
||||
// 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 "avr_uart.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;
|
||||
|
||||
// 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 = getenv("TSB_CONFIG");
|
||||
if (cfg) {
|
||||
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) {
|
||||
char b[3] = {cfg[i], cfg[i + 1], 0};
|
||||
avr->flash[app_end + i / 2] = (uint8_t)strtoul(b, NULL, 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.
|
||||
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');
|
||||
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;
|
||||
}
|
||||
51
test/pbapp.cpp
Normal file
51
test/pbapp.cpp
Normal file
@@ -0,0 +1,51 @@
|
||||
// Test-fixture application for the pureboot protocol test: prints "APP" on
|
||||
// the chip's serial link (the same link the loader uses) and idles — the
|
||||
// proof that the loader's hand-over, and on the tinies the host's
|
||||
// reset-vector surgery, actually launched it. Linked normally (crt, vectors
|
||||
// at 0); on the tinies its reset vector is the rjmp the host re-homes.
|
||||
#include <libavr/libavr.hpp>
|
||||
|
||||
using namespace avr::literals;
|
||||
|
||||
namespace {
|
||||
|
||||
consteval avr::hertz_t clock()
|
||||
{
|
||||
if (avr::hw::db.name == "ATtiny13A")
|
||||
return 9.6_MHz;
|
||||
if (avr::hw::db.name == "ATtiny85")
|
||||
return 8_MHz;
|
||||
return 16_MHz;
|
||||
}
|
||||
|
||||
using dev = avr::device<{.clock = clock()}>;
|
||||
|
||||
template <avr::hertz_t C, bool Hardware = avr::hw::db.has_reg("UDR0")>
|
||||
struct link {
|
||||
using tx_t = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>;
|
||||
static void tx(char c)
|
||||
{
|
||||
tx_t::write(static_cast<std::uint8_t>(c));
|
||||
}
|
||||
};
|
||||
|
||||
template <avr::hertz_t C>
|
||||
struct link<C, false> {
|
||||
using tx_t = avr::uart::software_tx<C, avr::pb1, 57600_Bd>;
|
||||
static void tx(char c)
|
||||
{
|
||||
tx_t::write(static_cast<std::uint8_t>(c));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
avr::init<typename link<dev::clock>::tx_t>();
|
||||
link<dev::clock>::tx('A');
|
||||
link<dev::clock>::tx('P');
|
||||
link<dev::clock>::tx('P');
|
||||
while (true) {
|
||||
}
|
||||
}
|
||||
178
test/pbtest.py
Normal file
178
test/pbtest.py
Normal file
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
"""End-to-end pureboot protocol test: spawn the simavr device, then drive it
|
||||
with the real host tool (pureboot.py, as a subprocess over the device's pty)
|
||||
through flash + EEPROM + timeout + fuse + hand-over scenarios, and cross-check
|
||||
the tool's view against the simulator's ground-truth memory dumps.
|
||||
|
||||
Usage: pbtest.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||
<baud> <eeprom_size> <app_bin> <tool_py> <workdir>
|
||||
Exits 0 if every scenario passes.
|
||||
"""
|
||||
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
def fail(message):
|
||||
print(f"FAIL: {message}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def rjmp_decode(word, at, flash_words):
|
||||
"""Where an rjmp word at word-address `at` lands — deliberately written
|
||||
against the instruction-set definition (12-bit signed offset), not with
|
||||
the host tool's encoder, so an encoding bug cannot verify itself."""
|
||||
if word & 0xF000 != 0xC000:
|
||||
fail(f"word at {at * 2:#06x} is {word:#06x}, not an rjmp")
|
||||
offset = word & 0x0FFF
|
||||
if offset >= 0x800:
|
||||
offset -= 0x1000
|
||||
return (at + 1 + offset) % flash_words
|
||||
|
||||
|
||||
class Device:
|
||||
def __init__(self, binary, elf, mcu, hz, base, page, baud, dump):
|
||||
self.proc = subprocess.Popen(
|
||||
[binary, elf, mcu, hz, base, str(page), str(baud), dump],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
)
|
||||
self.dump = dump
|
||||
self.pty = None
|
||||
deadline = time.time() + 5
|
||||
while time.time() < deadline:
|
||||
line = self.proc.stdout.readline()
|
||||
if not line:
|
||||
break
|
||||
if line.startswith("PB_PTY"):
|
||||
self.pty = line.split()[1]
|
||||
break
|
||||
if not self.pty:
|
||||
self.stop()
|
||||
raise RuntimeError("device did not report a pty")
|
||||
|
||||
def stop(self):
|
||||
self.proc.terminate()
|
||||
try:
|
||||
self.proc.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.proc.kill()
|
||||
|
||||
|
||||
def run_tool(tool, pty, baud, *args):
|
||||
result = subprocess.run(
|
||||
[sys.executable, tool, "--port", pty, "--baud", str(baud), "--wait", "20", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
print(result.stdout, end="")
|
||||
if result.returncode != 0:
|
||||
fail(f"tool exited {result.returncode}: {result.stderr.strip()}")
|
||||
return result.stdout
|
||||
|
||||
|
||||
def main():
|
||||
(device_bin, elf, mcu, hz, base_hex, page, baud, eeprom_size, app_bin, tool, workdir) = sys.argv[1:]
|
||||
base, page, baud, eeprom_size = int(base_hex, 0), int(page), int(baud), int(eeprom_size)
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||
import pureboot as pb
|
||||
|
||||
os.makedirs(workdir, exist_ok=True)
|
||||
ee_image = bytes(range(0xA0, 0xB0))
|
||||
ee_path = os.path.join(workdir, "ee.bin")
|
||||
open(ee_path, "wb").write(ee_image)
|
||||
dump = os.path.join(workdir, "flash_dump.bin")
|
||||
read_flash = os.path.join(workdir, "readback_flash.bin")
|
||||
read_eeprom = os.path.join(workdir, "readback_eeprom.bin")
|
||||
|
||||
# The geometry the host will discover, for computing the expected image.
|
||||
info = pb.Info(
|
||||
bytes([ord("P"), ord("B"), 1, 0, 0, 0, page])
|
||||
+ bytes([base & 0xFF, base >> 8, eeprom_size & 0xFF, eeprom_size >> 8])
|
||||
+ bytes([0 if mcu == "atmega328p" else 1])
|
||||
)
|
||||
|
||||
device = Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump)
|
||||
try:
|
||||
# Session 1: knock from reset, identify, program everything, stay.
|
||||
out = run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin,
|
||||
"--eeprom", ee_path, "--timeout", "8", "--stay")
|
||||
for needed in ("device: signature", "fuses:", "verify:", "activation timeout: 8 s", "stays"):
|
||||
if needed not in out:
|
||||
fail(f"session 1 output lacks {needed!r}")
|
||||
|
||||
# Session 2: reconnect into the live session, verify, dump, hand over
|
||||
# is deferred — the pty must be reopened for the APP banner first.
|
||||
out = run_tool(tool, device.pty, baud, "--verify-flash", app_bin, "--verify-eeprom", ee_path,
|
||||
"--read-flash", read_flash, "--read-eeprom", read_eeprom, "--stay")
|
||||
if out.count("verify:") != 2:
|
||||
fail("session 2 did not verify both memories")
|
||||
|
||||
eeprom_back = open(read_eeprom, "rb").read()
|
||||
if eeprom_back[: len(ee_image)] != ee_image:
|
||||
fail("EEPROM read-back mismatch")
|
||||
if eeprom_back[-1] != 8:
|
||||
fail(f"timeout cell reads {eeprom_back[-1]}, expected 8")
|
||||
|
||||
# The expected post-surgery flash, straight from the tool's planner.
|
||||
pages = pb.plan_flash(open(app_bin, "rb").read(), info)
|
||||
flash_back = open(read_flash, "rb").read()
|
||||
for address, data in pages.items():
|
||||
if flash_back[address : address + page] != data:
|
||||
fail(f"flash read-back mismatch in page {address:#06x}")
|
||||
|
||||
# An external reset re-enters through the patched word 0 (tinies; the
|
||||
# runner resets them to address 0 like silicon) or BOOTRST (mega).
|
||||
# The loader must answer a fresh knock, and 'G' must land in the
|
||||
# application, which banners on the same link.
|
||||
device.proc.send_signal(signal.SIGUSR1)
|
||||
port = pb.Port(device.pty, baud)
|
||||
try:
|
||||
loader = pb.Loader(port)
|
||||
loader.connect(15)
|
||||
port.write(b"G")
|
||||
if port.read_exact(1, 5.0) != pb.PROMPT:
|
||||
fail("no ack for G")
|
||||
banner = port.read_exact(3, 5.0)
|
||||
if banner != b"APP":
|
||||
fail(f"application banner was {banner!r}")
|
||||
finally:
|
||||
port.close()
|
||||
finally:
|
||||
device.stop()
|
||||
|
||||
# Ground truth: the simulator's own memories, against the host's view.
|
||||
flash_true = open(dump, "rb").read()
|
||||
if flash_true[:base] != flash_back:
|
||||
fail("host flash read-back differs from the simulator's flash")
|
||||
if flash_true[base] == 0xFF and flash_true[base + 1] == 0xFF:
|
||||
fail("loader region looks erased in the ground-truth dump")
|
||||
|
||||
# The surgery, decoded independently: the patched vector must land on the
|
||||
# loader, the trampoline on the application's own entry.
|
||||
if mcu != "atmega328p":
|
||||
flash_words = (base + 512) // 2
|
||||
app = open(app_bin, "rb").read()
|
||||
word0 = flash_true[0] | (flash_true[1] << 8)
|
||||
if rjmp_decode(word0, 0, flash_words) != base // 2:
|
||||
fail("patched reset vector does not land on the loader base")
|
||||
trampoline = flash_true[base - 2] | (flash_true[base - 1] << 8)
|
||||
original = app[0] | (app[1] << 8)
|
||||
if rjmp_decode(trampoline, (base - 2) // 2, flash_words) != rjmp_decode(original, 0, flash_words):
|
||||
fail("trampoline does not land on the application's own entry")
|
||||
ee_true_path = dump + ".eeprom"
|
||||
if os.path.exists(ee_true_path):
|
||||
ee_true = open(ee_true_path, "rb").read()
|
||||
if ee_true[: len(ee_image)] != ee_image or ee_true[-1] != 8:
|
||||
fail("ground-truth EEPROM does not match what was programmed")
|
||||
|
||||
print("pbtest: all scenarios pass")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
323
test/pureboot_device.c
Normal file
323
test/pureboot_device.c
Normal file
@@ -0,0 +1,323 @@
|
||||
// simavr "device" for the pureboot protocol tests, all three chips. Loads
|
||||
// the boot-linked ELF at the loader base, starts execution there (BOOTRST /
|
||||
// the patched vector are not what is under test), and exposes the loader's
|
||||
// serial link as a pty for the real host tool:
|
||||
//
|
||||
// - ATmega328P: the hardware USART0 through simavr's uart_pty.
|
||||
// - Tinies: an 8N1 bridge between a pty and the GPIO software UART
|
||||
// (drives PB0, the loader's RX; decodes PB1, its TX), timed against the
|
||||
// simulated cycle counter.
|
||||
//
|
||||
// simavr's tiny cores decode the SPM opcode but attach no NVM module — SPM
|
||||
// is a silent no-op (the mega's boot section has one, avr_flash). The
|
||||
// missing module is supplied here: the SPM ioctl reads SPMCSR/Z/r1:r0 and
|
||||
// implements buffer fill, page erase, page write, and CTPB, completing
|
||||
// instantly. RFLB's LPM diversion (fuse readout) stays unmodeled, so the
|
||||
// 'F' command answers with flash bytes — the tests assert transport only.
|
||||
//
|
||||
// On exit (or SIGTERM) the flash and EEPROM are dumped to files for a
|
||||
// ground-truth cross-check against what the host read back.
|
||||
#include <fcntl.h>
|
||||
#include <pty.h>
|
||||
#include <signal.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "avr_eeprom.h"
|
||||
#include "avr_flash.h"
|
||||
#include "avr_ioport.h"
|
||||
#include "avr_uart.h"
|
||||
#include "sim_avr.h"
|
||||
#include "sim_elf.h"
|
||||
#include "sim_io.h"
|
||||
#include "uart_pty.h"
|
||||
|
||||
static avr_t *avr;
|
||||
static uart_pty_t uart_pty;
|
||||
static int use_uart_pty;
|
||||
static const char *dump_path;
|
||||
static uint32_t reset_pc;
|
||||
static volatile sig_atomic_t reset_requested;
|
||||
|
||||
static void request_reset(int sig)
|
||||
{
|
||||
(void)sig;
|
||||
reset_requested = 1;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- tiny NVM ---
|
||||
|
||||
typedef struct {
|
||||
avr_io_t io;
|
||||
uint8_t buffer[128];
|
||||
unsigned page;
|
||||
} tiny_nvm_t;
|
||||
|
||||
static tiny_nvm_t nvm;
|
||||
|
||||
static int nvm_ioctl(avr_io_t *io, uint32_t ctl, void *param)
|
||||
{
|
||||
(void)param;
|
||||
if (ctl != AVR_IOCTL_FLASH_SPM)
|
||||
return -1;
|
||||
tiny_nvm_t *n = (tiny_nvm_t *)io;
|
||||
avr_t *mcu = io->avr;
|
||||
uint8_t command = mcu->data[0x57] & 0x1f; // SPMCSR, both tinies
|
||||
uint16_t z = (uint16_t)(mcu->data[30] | (mcu->data[31] << 8));
|
||||
uint32_t page_base = (uint32_t)(z & ~(n->page - 1)) % (mcu->flashend + 1);
|
||||
if (command == 0x01) { // SPMEN alone: buffer fill from r1:r0
|
||||
unsigned offset = z & (n->page - 1) & ~1u;
|
||||
n->buffer[offset] = mcu->data[0];
|
||||
n->buffer[offset + 1] = mcu->data[1];
|
||||
} else if (command == 0x03) { // PGERS
|
||||
memset(mcu->flash + page_base, 0xff, n->page);
|
||||
} else if (command == 0x05) { // PGWRT: programming only clears bits
|
||||
for (unsigned i = 0; i < n->page; i++)
|
||||
mcu->flash[page_base + i] &= n->buffer[i];
|
||||
memset(n->buffer, 0xff, n->page);
|
||||
} else if (command == 0x11) { // CTPB
|
||||
memset(n->buffer, 0xff, n->page);
|
||||
}
|
||||
mcu->data[0x57] &= (uint8_t)~0x1f; // the operation completes instantly
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------- GPIO bridge ---
|
||||
|
||||
static int pty_master = -1;
|
||||
static avr_irq_t *rx_pin; // the loader's RX (PB0), driven from the pty
|
||||
static avr_cycle_count_t bit_cycles;
|
||||
|
||||
static int tx_level = 1, tx_active, tx_bit;
|
||||
static uint8_t tx_shift;
|
||||
|
||||
static avr_cycle_count_t tx_sample(avr_t *mcu, avr_cycle_count_t when, void *param)
|
||||
{
|
||||
(void)mcu;
|
||||
(void)param;
|
||||
tx_shift = (uint8_t)((tx_shift >> 1) | (tx_level ? 0x80 : 0));
|
||||
if (++tx_bit < 8)
|
||||
return when + bit_cycles;
|
||||
if (write(pty_master, &tx_shift, 1) != 1)
|
||||
fprintf(stderr, "device: pty write lost a byte\n");
|
||||
tx_active = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void tx_hook(avr_irq_t *irq, uint32_t value, void *param)
|
||||
{
|
||||
(void)irq;
|
||||
(void)param;
|
||||
int level = value & 1;
|
||||
if (!tx_active && tx_level == 1 && level == 0) { // start edge
|
||||
tx_active = 1;
|
||||
tx_bit = 0;
|
||||
avr_cycle_timer_register(avr, bit_cycles + bit_cycles / 2, tx_sample, NULL);
|
||||
}
|
||||
tx_level = level;
|
||||
}
|
||||
|
||||
static uint8_t rx_queue[8192];
|
||||
static unsigned rx_head, rx_tail; // ring: head = next to send
|
||||
static int rx_active, rx_bit;
|
||||
static uint8_t rx_byte;
|
||||
|
||||
static void rx_start_next(void);
|
||||
|
||||
static avr_cycle_count_t rx_step(avr_t *mcu, avr_cycle_count_t when, void *param)
|
||||
{
|
||||
(void)mcu;
|
||||
(void)param;
|
||||
if (rx_bit < 8) {
|
||||
avr_raise_irq(rx_pin, (rx_byte >> rx_bit) & 1);
|
||||
rx_bit++;
|
||||
return when + bit_cycles;
|
||||
}
|
||||
if (rx_bit == 8) { // stop bit, plus one idle bit of margin
|
||||
avr_raise_irq(rx_pin, 1);
|
||||
rx_bit++;
|
||||
return when + 2 * bit_cycles;
|
||||
}
|
||||
rx_active = 0;
|
||||
rx_start_next();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void rx_start_next(void)
|
||||
{
|
||||
if (rx_active || rx_head == rx_tail)
|
||||
return;
|
||||
rx_byte = rx_queue[rx_head];
|
||||
rx_head = (rx_head + 1) % sizeof(rx_queue);
|
||||
rx_active = 1;
|
||||
rx_bit = 0;
|
||||
avr_raise_irq(rx_pin, 0); // start bit
|
||||
avr_cycle_timer_register(avr, bit_cycles, rx_step, NULL);
|
||||
}
|
||||
|
||||
// A reset abandons whatever the bridge was mid-transfer: bytes still queued
|
||||
// for a chip that no longer has the context to receive them meaningfully,
|
||||
// and a decode in progress on a TX line the reset may have already changed.
|
||||
static void bridge_reset(void)
|
||||
{
|
||||
rx_head = rx_tail = 0;
|
||||
rx_active = 0;
|
||||
tx_active = 0;
|
||||
tx_level = 1;
|
||||
avr_raise_irq(rx_pin, 1); // idle line
|
||||
}
|
||||
|
||||
static void poll_pty(void)
|
||||
{
|
||||
uint8_t chunk[256];
|
||||
ssize_t got = read(pty_master, chunk, sizeof(chunk));
|
||||
for (ssize_t i = 0; i < got; i++) {
|
||||
unsigned next = (rx_tail + 1) % sizeof(rx_queue);
|
||||
if (next == rx_head)
|
||||
break; // full: the host will retry on timeout
|
||||
rx_queue[rx_tail] = chunk[i];
|
||||
rx_tail = next;
|
||||
}
|
||||
if (got > 0)
|
||||
rx_start_next();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ main ---
|
||||
|
||||
static void finish(int sig)
|
||||
{
|
||||
(void)sig;
|
||||
if (dump_path) {
|
||||
FILE *f = fopen(dump_path, "wb");
|
||||
if (f) {
|
||||
fwrite(avr->flash, 1, avr->flashend + 1, f);
|
||||
fclose(f);
|
||||
}
|
||||
avr_eeprom_desc_t ee = {.ee = NULL, .offset = 0, .size = 0};
|
||||
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &ee) == 0 && ee.ee && ee.size) {
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%s.eeprom", dump_path);
|
||||
f = fopen(path, "wb");
|
||||
if (f) {
|
||||
fwrite(ee.ee, 1, ee.size, f);
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (use_uart_pty)
|
||||
uart_pty_stop(&uart_pty);
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
if (argc != 8) {
|
||||
fprintf(stderr, "usage: %s <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
const char *mcu_name = argv[2];
|
||||
uint32_t base = (uint32_t)strtoul(argv[4], NULL, 0);
|
||||
unsigned page = (unsigned)atoi(argv[5]);
|
||||
unsigned baud = (unsigned)atoi(argv[6]);
|
||||
dump_path = argv[7];
|
||||
use_uart_pty = strcmp(mcu_name, "atmega328p") == 0;
|
||||
|
||||
avr = avr_make_mcu_by_name(mcu_name);
|
||||
if (!avr) {
|
||||
fprintf(stderr, "device: no %s core\n", mcu_name);
|
||||
return 1;
|
||||
}
|
||||
avr_init(avr);
|
||||
avr->frequency = (uint32_t)strtoul(argv[3], NULL, 0);
|
||||
memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased
|
||||
|
||||
elf_firmware_t fw = {0};
|
||||
if (elf_read_firmware(argv[1], &fw) != 0) {
|
||||
fprintf(stderr, "device: cannot read %s\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
memcpy(avr->flash + base, fw.flash, fw.flashsize);
|
||||
// The mega enters the loader in hardware (BOOTRST, not modeled); the
|
||||
// tinies reset to word 0 like silicon — erased flash walks up into the
|
||||
// loader, and after the host's surgery the patched vector routes there.
|
||||
reset_pc = use_uart_pty ? base : 0;
|
||||
avr->pc = reset_pc;
|
||||
avr->codeend = avr->flashend;
|
||||
|
||||
// Erased EEPROM, as hardware powers up (simavr zeroes it).
|
||||
uint8_t blank[1024];
|
||||
memset(blank, 0xff, sizeof(blank));
|
||||
avr_eeprom_desc_t seed = {.ee = blank, .offset = 0, .size = 0};
|
||||
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &seed) == 0 && seed.size <= sizeof(blank)) {
|
||||
seed.ee = blank;
|
||||
avr_ioctl(avr, AVR_IOCTL_EEPROM_SET, &seed);
|
||||
}
|
||||
|
||||
if (use_uart_pty) {
|
||||
// POLL_SLEEP paces an idle-polling loader in host real time (a
|
||||
// no-hardware CPU-saving hack); clear it so cycles run free.
|
||||
uint32_t flags = 0;
|
||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &flags);
|
||||
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &flags);
|
||||
uart_pty_init(avr, &uart_pty);
|
||||
uart_pty_connect(&uart_pty, '0');
|
||||
printf("PB_PTY %s\n", uart_pty.pty.slavename);
|
||||
} else {
|
||||
nvm.page = page;
|
||||
memset(nvm.buffer, 0xff, sizeof(nvm.buffer));
|
||||
nvm.io.kind = "tiny_nvm";
|
||||
nvm.io.ioctl = nvm_ioctl;
|
||||
avr_register_io(avr, &nvm.io);
|
||||
|
||||
bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly
|
||||
rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), 0);
|
||||
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), 1), tx_hook, NULL);
|
||||
avr_raise_irq(rx_pin, 1); // idle line
|
||||
|
||||
int slave;
|
||||
struct termios raw;
|
||||
cfmakeraw(&raw);
|
||||
if (openpty(&pty_master, &slave, NULL, &raw, NULL) != 0) {
|
||||
fprintf(stderr, "device: openpty failed\n");
|
||||
return 1;
|
||||
}
|
||||
fcntl(pty_master, F_SETFL, O_NONBLOCK);
|
||||
printf("PB_PTY %s\n", ttyname(slave));
|
||||
}
|
||||
fflush(stdout);
|
||||
|
||||
signal(SIGTERM, finish);
|
||||
signal(SIGINT, finish);
|
||||
signal(SIGUSR1, request_reset); // an external reset line, for the tests
|
||||
|
||||
long since_poll = 0;
|
||||
for (;;) {
|
||||
int state = avr_run(avr);
|
||||
if (state == cpu_Done || state == cpu_Crashed)
|
||||
break;
|
||||
if (reset_requested) {
|
||||
reset_requested = 0;
|
||||
avr_reset(avr);
|
||||
avr->pc = reset_pc;
|
||||
if (use_uart_pty) { // reset restores the pacing hack; re-clear it
|
||||
uint32_t flags = 0;
|
||||
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &flags);
|
||||
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &flags);
|
||||
} else {
|
||||
bridge_reset();
|
||||
}
|
||||
}
|
||||
if (!use_uart_pty && ++since_poll >= 2000) {
|
||||
since_poll = 0;
|
||||
poll_pty();
|
||||
}
|
||||
}
|
||||
finish(0);
|
||||
return 0;
|
||||
}
|
||||
252
test/tsbtest.py
Normal file
252
test/tsbtest.py
Normal file
@@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env python3
|
||||
"""End-to-end TSB protocol test: spawn the simavr device, speak the TinySafeBoot
|
||||
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
|
||||
|
||||
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."""
|
||||
|
||||
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
|
||||
self.proc = subprocess.Popen(
|
||||
[binary, elf, boot_base, dump],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env)
|
||||
self.dump = dump
|
||||
self.pty = None
|
||||
deadline = time.time() + 5
|
||||
while time.time() < deadline:
|
||||
line = self.proc.stdout.readline()
|
||||
if not line:
|
||||
break
|
||||
if line.startswith("TSB_PTY"):
|
||||
self.pty = line.split()[1]
|
||||
break
|
||||
if not self.pty:
|
||||
self.stop()
|
||||
raise RuntimeError("device did not report a pty")
|
||||
|
||||
def stop(self):
|
||||
self.proc.terminate()
|
||||
try:
|
||||
self.proc.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.proc.kill()
|
||||
|
||||
|
||||
class Host:
|
||||
"""A faithful TSB host, per the wire protocol."""
|
||||
|
||||
def __init__(self, pty):
|
||||
self.s = serial.Serial(pty, 115200, timeout=1.5)
|
||||
self.info = None
|
||||
|
||||
def _read(self, n):
|
||||
data = self.s.read(n)
|
||||
if len(data) != n:
|
||||
raise AssertionError(f"expected {n} bytes, got {len(data)}: {data.hex()}")
|
||||
return data
|
||||
|
||||
def activate(self):
|
||||
self.s.reset_input_buffer()
|
||||
self.s.write(b"@@@")
|
||||
reply = self._read(17)
|
||||
if reply[16] != CONFIRM:
|
||||
raise AssertionError(f"activation reply not '!'-terminated: {reply.hex()}")
|
||||
self.info = reply[:16]
|
||||
return self.info
|
||||
|
||||
# Parsed info-block fields (host math from the spec).
|
||||
@property
|
||||
def pagesize(self):
|
||||
return self.info[9] * 2
|
||||
|
||||
@property
|
||||
def appflash(self):
|
||||
return (self.info[10] | (self.info[11] << 8)) * 2
|
||||
|
||||
@property
|
||||
def eeprom_size(self):
|
||||
return (self.info[12] | (self.info[13] << 8)) + 1
|
||||
|
||||
def _expect(self, byte, what):
|
||||
r = self._read(1)
|
||||
if r[0] != byte:
|
||||
raise AssertionError(f"{what}: expected {byte:#x}, got {r.hex()}")
|
||||
|
||||
# Host-paced page read ('f'/'e'): send '!', take a page, repeat; stop with
|
||||
# anything else, then the Mainloop '!'.
|
||||
def _read_pages(self, cmd, npages):
|
||||
self.s.write(cmd.encode())
|
||||
data = b""
|
||||
for _ in range(npages):
|
||||
self.s.write(bytes([CONFIRM]))
|
||||
data += self._read(PAGE)
|
||||
self.s.write(bytes([REQUEST])) # stop
|
||||
self._expect(CONFIRM, f"{cmd} end")
|
||||
return data
|
||||
|
||||
# Device-paced page write ('F'/'E'): device offers '?', host sends '!'+page,
|
||||
# or anything else to stop.
|
||||
def _write_pages(self, cmd, data):
|
||||
if len(data) % PAGE:
|
||||
data += b"\xff" * (PAGE - len(data) % PAGE)
|
||||
self.s.write(cmd.encode())
|
||||
for off in range(0, len(data), PAGE):
|
||||
self._expect(REQUEST, f"{cmd} '?'")
|
||||
self.s.write(bytes([CONFIRM]) + data[off:off + PAGE])
|
||||
self._expect(REQUEST, f"{cmd} trailing '?'")
|
||||
self.s.write(bytes([REQUEST])) # stop
|
||||
self._expect(CONFIRM, f"{cmd} end")
|
||||
|
||||
def write_flash(self, data):
|
||||
self._write_pages("F", data)
|
||||
|
||||
def read_flash(self, npages):
|
||||
return self._read_pages("f", npages)
|
||||
|
||||
def write_eeprom(self, data):
|
||||
self._write_pages("E", data)
|
||||
|
||||
def read_eeprom(self, npages):
|
||||
return self._read_pages("e", npages)
|
||||
|
||||
def read_config(self):
|
||||
self.s.write(b"c")
|
||||
page = self._read(PAGE)
|
||||
self._expect(CONFIRM, "c end")
|
||||
return page
|
||||
|
||||
def write_config(self, data):
|
||||
assert len(data) == PAGE
|
||||
self.s.write(b"C")
|
||||
self._expect(REQUEST, "C '?'")
|
||||
self.s.write(bytes([CONFIRM]) + data)
|
||||
echo = self._read(PAGE) # device echoes what it programmed
|
||||
self._expect(CONFIRM, "C end")
|
||||
return echo
|
||||
|
||||
# Activation when the config page carries a password: 3×'@' 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")
|
||||
|
||||
|
||||
def check(cond, msg):
|
||||
if not cond:
|
||||
raise AssertionError(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 scenario_roundtrip(host):
|
||||
"""Activation + info block + flash/EEPROM/config read-write round-trips, on
|
||||
a device with a blank (erased) config page — the usual no-password case."""
|
||||
info = host.activate()
|
||||
check(info[0:3] == b"TSB", f"magic 'TSB' (got {info[0:3]!r})")
|
||||
check(info[6:9] == bytes([0x1E, 0x95, 0x0F]), f"signature 1E 95 0F (got {info[6:9].hex()})")
|
||||
check(info[14] == info[15], f"device-type bytes 14==15 (got {info[14]:#x},{info[15]:#x})")
|
||||
check(host.pagesize == PAGE, f"page size {PAGE} (got {host.pagesize})")
|
||||
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}")
|
||||
|
||||
app = bytes(range(256)) # two pages of known data
|
||||
host.write_flash(app)
|
||||
check(host.read_flash(2) == app, "flash round-trip 2 pages")
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
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 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),
|
||||
]
|
||||
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}")
|
||||
print(f" FAIL: {e}")
|
||||
finally:
|
||||
dev.stop()
|
||||
|
||||
if failures:
|
||||
print(f"FAILED ({len(failures)})")
|
||||
return 1
|
||||
print("ALL PASS")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,4 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#define F_CPU 18'432'000
|
||||
#include <util/delay.h>
|
||||
Submodule tsb/flash deleted from 6edb2e5a21
1
tsb/io
1
tsb/io
Submodule tsb/io deleted from 80de36ee7e
102
tsb/main.cpp
102
tsb/main.cpp
@@ -1,102 +0,0 @@
|
||||
#include "clock.hpp"
|
||||
|
||||
#include "uart/uart.hpp"
|
||||
|
||||
constexpr auto READ_FLASH_CMD = 'f';
|
||||
constexpr auto WRITE_FLASH_CMD = 'F';
|
||||
constexpr auto READ_EEPROM_CMD = 'e';
|
||||
constexpr auto WRITE_EEPROM_CMD = 'E';
|
||||
constexpr auto READ_USERDATA_CMD = 'c';
|
||||
constexpr auto WRITE_USERDATA_CMD = 'C';
|
||||
constexpr auto REQUEST_CMD = '?';
|
||||
constexpr auto CONFIRM_CMD = '!';
|
||||
constexpr auto AUTO_BAUDING_CMD = '@';
|
||||
|
||||
using uart_interface = uart::Hardware0<uart::Config<115200>, uart::Driven::BLOCKING>;
|
||||
|
||||
enum class State {
|
||||
WAITING,
|
||||
ACTIVE,
|
||||
};
|
||||
|
||||
struct DeviceInfo {
|
||||
char name[3] = {'T', 'S', 'B'};
|
||||
uint8_t date[2] = {0x1a, 0x1f};
|
||||
uint8_t status = 0xf0;
|
||||
uint8_t signature[3] = {0x1e, 0x95, 0x0f};
|
||||
uint8_t pageSize = 0x40;
|
||||
uint16_t flashSize = 0x3ec0;
|
||||
uint16_t eepromSize = 0x03ff;
|
||||
};
|
||||
|
||||
struct UserData {
|
||||
uint16_t jumpAddress = 0xAAAA;
|
||||
uint8_t timeout = 0x21;
|
||||
};
|
||||
|
||||
static inline void sendDeviceInfo()
|
||||
{
|
||||
uart::Uart<uart_interface> serial;
|
||||
|
||||
constexpr DeviceInfo deviceInfo;
|
||||
constexpr UserData userData;
|
||||
|
||||
for (uint8_t i = 0; i < sizeof(deviceInfo); ++i) {
|
||||
serial.txByte(*(reinterpret_cast<const uint8_t *>(&deviceInfo) + i));
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < sizeof(userData); ++i) {
|
||||
serial.txByte(*(reinterpret_cast<const uint8_t *>(&userData) + i));
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t g_lastPage[128] = {};
|
||||
|
||||
static inline void sendUserData()
|
||||
{
|
||||
uart::Uart<uart_interface> serial;
|
||||
|
||||
for (uint8_t i = 0; i < sizeof(g_lastPage); ++i) {
|
||||
serial.txByte(*(reinterpret_cast<const uint8_t *>(&g_lastPage) + i));
|
||||
}
|
||||
}
|
||||
|
||||
static inline void sendConfirm()
|
||||
{
|
||||
uart::Uart<uart_interface> serial;
|
||||
serial.txByte(CONFIRM_CMD);
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
uart::Uart<uart_interface> serial;
|
||||
serial.init();
|
||||
|
||||
State state = State::WAITING;
|
||||
|
||||
uint8_t receivedByte = 0;
|
||||
uint8_t autoBaudingCounter = 0;
|
||||
|
||||
while (true) {
|
||||
if (serial.rxByte(receivedByte)) {
|
||||
if (state == State::WAITING) {
|
||||
if (receivedByte == AUTO_BAUDING_CMD) {
|
||||
++autoBaudingCounter;
|
||||
}
|
||||
if (autoBaudingCounter == 3) {
|
||||
autoBaudingCounter = 0;
|
||||
state = State::ACTIVE;
|
||||
sendDeviceInfo();
|
||||
}
|
||||
} else if (state == State::ACTIVE) {
|
||||
if (receivedByte == READ_USERDATA_CMD) {
|
||||
sendUserData();
|
||||
sendConfirm();
|
||||
state = State::WAITING;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
263
tsb/tsb.cppproj
263
tsb/tsb.cppproj
@@ -1,263 +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>dce6c7e3-ee26-4d79-826b-08594b9ad897</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</AssemblyName>
|
||||
<Name>tsb</Name>
|
||||
<RootNamespace>tsb</RootNamespace>
|
||||
<ToolchainFlavour>avr-g++-9.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 />
|
||||
<avrtool>
|
||||
</avrtool>
|
||||
<avrtoolserialnumber>J41800099437</avrtoolserialnumber>
|
||||
<avrdeviceexpectedsignature>0x1E9705</avrdeviceexpectedsignature>
|
||||
<com_atmel_avrdbg_tool_stk500>
|
||||
<ToolOptions>
|
||||
<InterfaceProperties>
|
||||
<IspClock>125000</IspClock>
|
||||
</InterfaceProperties>
|
||||
<InterfaceName>ISP</InterfaceName>
|
||||
</ToolOptions>
|
||||
<ToolType>com.atmel.avrdbg.tool.stk500</ToolType>
|
||||
<ToolNumber>
|
||||
</ToolNumber>
|
||||
<ToolName>STK500</ToolName>
|
||||
</com_atmel_avrdbg_tool_stk500>
|
||||
<avrtoolinterface>ISP</avrtoolinterface>
|
||||
<avrtoolinterfaceclock>125000</avrtoolinterfaceclock>
|
||||
<AsfFrameworkConfig>
|
||||
<framework-data xmlns="">
|
||||
<options />
|
||||
<configurations />
|
||||
<files />
|
||||
<documentation help="" />
|
||||
<offline-documentation help="" />
|
||||
<dependencies>
|
||||
<content-extension eid="atmel.asf" uuidref="Atmel.ASF" version="3.47.0" />
|
||||
</dependencies>
|
||||
</framework-data>
|
||||
</AsfFrameworkConfig>
|
||||
<com_atmel_avrdbg_tool_atmelice>
|
||||
<ToolOptions>
|
||||
<InterfaceProperties>
|
||||
<IspClock>125000</IspClock>
|
||||
</InterfaceProperties>
|
||||
<InterfaceName>ISP</InterfaceName>
|
||||
</ToolOptions>
|
||||
<ToolType>com.atmel.avrdbg.tool.atmelice</ToolType>
|
||||
<ToolNumber>J41800099437</ToolNumber>
|
||||
<ToolName>Atmel-ICE</ToolName>
|
||||
</com_atmel_avrdbg_tool_atmelice>
|
||||
<custom>
|
||||
<ToolOptions>
|
||||
<InterfaceProperties>
|
||||
<IspClock>125000</IspClock>
|
||||
</InterfaceProperties>
|
||||
<InterfaceName>
|
||||
</InterfaceName>
|
||||
</ToolOptions>
|
||||
<ToolType>custom</ToolType>
|
||||
<ToolNumber>
|
||||
</ToolNumber>
|
||||
<ToolName>Custom Programming Tool</ToolName>
|
||||
</custom>
|
||||
<com_atmel_avrdbg_tool_simulator>
|
||||
<ToolOptions xmlns="">
|
||||
<InterfaceProperties>
|
||||
</InterfaceProperties>
|
||||
<InterfaceName>
|
||||
</InterfaceName>
|
||||
</ToolOptions>
|
||||
<ToolType xmlns="">com.atmel.avrdbg.tool.simulator</ToolType>
|
||||
<ToolNumber xmlns="">
|
||||
</ToolNumber>
|
||||
<ToolName xmlns="">Simulator</ToolName>
|
||||
</com_atmel_avrdbg_tool_simulator>
|
||||
<AAFDebugger>
|
||||
<AAFDebugFiles>
|
||||
</AAFDebugFiles>
|
||||
</AAFDebugger>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<ToolchainSettings>
|
||||
<AvrGccCpp>
|
||||
<avrgcc.common.Device>-mmcu=atmega1284p</avrgcc.common.Device>
|
||||
<avrgcc.common.optimization.RelaxBranches>True</avrgcc.common.optimization.RelaxBranches>
|
||||
<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>
|
||||
<avrgcc.compiler.general.ChangeDefaultCharTypeUnsigned>True</avrgcc.compiler.general.ChangeDefaultCharTypeUnsigned>
|
||||
<avrgcc.compiler.general.ChangeDefaultBitFieldUnsigned>True</avrgcc.compiler.general.ChangeDefaultBitFieldUnsigned>
|
||||
<avrgcc.compiler.symbols.DefSymbols>
|
||||
<ListValues>
|
||||
<Value>NDEBUG</Value>
|
||||
</ListValues>
|
||||
</avrgcc.compiler.symbols.DefSymbols>
|
||||
<avrgcc.compiler.directories.IncludePaths>
|
||||
<ListValues>
|
||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
||||
</ListValues>
|
||||
</avrgcc.compiler.directories.IncludePaths>
|
||||
<avrgcc.compiler.optimization.level>Optimize for size (-Os)</avrgcc.compiler.optimization.level>
|
||||
<avrgcc.compiler.optimization.AllocateBytesNeededForEnum>True</avrgcc.compiler.optimization.AllocateBytesNeededForEnum>
|
||||
<avrgcc.compiler.warnings.AllWarnings>True</avrgcc.compiler.warnings.AllWarnings>
|
||||
<avrgcc.compiler.warnings.ExtraWarnings>True</avrgcc.compiler.warnings.ExtraWarnings>
|
||||
<avrgcc.compiler.warnings.Pedantic>True</avrgcc.compiler.warnings.Pedantic>
|
||||
<avrgcc.compiler.miscellaneous.OtherFlags>-fno-threadsafe-statics -std=c11</avrgcc.compiler.miscellaneous.OtherFlags>
|
||||
<avrgcccpp.compiler.general.ChangeDefaultCharTypeUnsigned>True</avrgcccpp.compiler.general.ChangeDefaultCharTypeUnsigned>
|
||||
<avrgcccpp.compiler.general.ChangeDefaultBitFieldUnsigned>True</avrgcccpp.compiler.general.ChangeDefaultBitFieldUnsigned>
|
||||
<avrgcccpp.compiler.symbols.DefSymbols>
|
||||
<ListValues>
|
||||
<Value>NDEBUG</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.compiler.symbols.DefSymbols>
|
||||
<avrgcccpp.compiler.directories.IncludePaths>
|
||||
<ListValues>
|
||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.compiler.directories.IncludePaths>
|
||||
<avrgcccpp.compiler.optimization.level>Optimize for size (-Os)</avrgcccpp.compiler.optimization.level>
|
||||
<avrgcccpp.compiler.optimization.AllocateBytesNeededForEnum>True</avrgcccpp.compiler.optimization.AllocateBytesNeededForEnum>
|
||||
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
|
||||
<avrgcccpp.compiler.warnings.Pedantic>True</avrgcccpp.compiler.warnings.Pedantic>
|
||||
<avrgcccpp.compiler.miscellaneous.OtherFlags>-fno-threadsafe-statics -Wextra -std=c++17</avrgcccpp.compiler.miscellaneous.OtherFlags>
|
||||
<avrgcccpp.linker.libraries.Libraries>
|
||||
<ListValues>
|
||||
<Value>libm</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.linker.libraries.Libraries>
|
||||
<avrgcccpp.assembler.general.IncludePaths>
|
||||
<ListValues>
|
||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.assembler.general.IncludePaths>
|
||||
</AvrGccCpp>
|
||||
</ToolchainSettings>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<ToolchainSettings>
|
||||
<AvrGccCpp>
|
||||
<avrgcc.common.Device>-mmcu=atmega1284p</avrgcc.common.Device>
|
||||
<avrgcc.common.optimization.RelaxBranches>True</avrgcc.common.optimization.RelaxBranches>
|
||||
<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>
|
||||
<avrgcc.compiler.general.ChangeDefaultCharTypeUnsigned>True</avrgcc.compiler.general.ChangeDefaultCharTypeUnsigned>
|
||||
<avrgcc.compiler.general.ChangeDefaultBitFieldUnsigned>True</avrgcc.compiler.general.ChangeDefaultBitFieldUnsigned>
|
||||
<avrgcc.compiler.symbols.DefSymbols>
|
||||
<ListValues>
|
||||
<Value>DEBUG</Value>
|
||||
</ListValues>
|
||||
</avrgcc.compiler.symbols.DefSymbols>
|
||||
<avrgcc.compiler.directories.IncludePaths>
|
||||
<ListValues>
|
||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
||||
</ListValues>
|
||||
</avrgcc.compiler.directories.IncludePaths>
|
||||
<avrgcc.compiler.optimization.level>Optimize (-O1)</avrgcc.compiler.optimization.level>
|
||||
<avrgcc.compiler.optimization.AllocateBytesNeededForEnum>True</avrgcc.compiler.optimization.AllocateBytesNeededForEnum>
|
||||
<avrgcc.compiler.optimization.DebugLevel>Maximum (-g3)</avrgcc.compiler.optimization.DebugLevel>
|
||||
<avrgcc.compiler.warnings.AllWarnings>True</avrgcc.compiler.warnings.AllWarnings>
|
||||
<avrgcc.compiler.warnings.ExtraWarnings>True</avrgcc.compiler.warnings.ExtraWarnings>
|
||||
<avrgcc.compiler.warnings.Pedantic>True</avrgcc.compiler.warnings.Pedantic>
|
||||
<avrgcc.compiler.miscellaneous.OtherFlags>-fno-threadsafe-statics -std=c11</avrgcc.compiler.miscellaneous.OtherFlags>
|
||||
<avrgcccpp.compiler.general.ChangeDefaultCharTypeUnsigned>True</avrgcccpp.compiler.general.ChangeDefaultCharTypeUnsigned>
|
||||
<avrgcccpp.compiler.general.ChangeDefaultBitFieldUnsigned>True</avrgcccpp.compiler.general.ChangeDefaultBitFieldUnsigned>
|
||||
<avrgcccpp.compiler.symbols.DefSymbols>
|
||||
<ListValues>
|
||||
<Value>DEBUG</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.compiler.symbols.DefSymbols>
|
||||
<avrgcccpp.compiler.directories.IncludePaths>
|
||||
<ListValues>
|
||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.compiler.directories.IncludePaths>
|
||||
<avrgcccpp.compiler.optimization.level>Optimize (-O1)</avrgcccpp.compiler.optimization.level>
|
||||
<avrgcccpp.compiler.optimization.AllocateBytesNeededForEnum>True</avrgcccpp.compiler.optimization.AllocateBytesNeededForEnum>
|
||||
<avrgcccpp.compiler.optimization.DebugLevel>Maximum (-g3)</avrgcccpp.compiler.optimization.DebugLevel>
|
||||
<avrgcccpp.compiler.warnings.AllWarnings>True</avrgcccpp.compiler.warnings.AllWarnings>
|
||||
<avrgcccpp.compiler.warnings.Pedantic>True</avrgcccpp.compiler.warnings.Pedantic>
|
||||
<avrgcccpp.compiler.miscellaneous.OtherFlags>-fno-threadsafe-statics -Wextra -std=c++17</avrgcccpp.compiler.miscellaneous.OtherFlags>
|
||||
<avrgcccpp.linker.libraries.Libraries>
|
||||
<ListValues>
|
||||
<Value>libm</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.linker.libraries.Libraries>
|
||||
<avrgcccpp.assembler.general.IncludePaths>
|
||||
<ListValues>
|
||||
<Value>%24(PackRepoDir)\Atmel\ATmega_DFP\1.4.346\include</Value>
|
||||
</ListValues>
|
||||
</avrgcccpp.assembler.general.IncludePaths>
|
||||
<avrgcccpp.assembler.debugging.DebugLevel>Default (-Wa,-g)</avrgcccpp.assembler.debugging.DebugLevel>
|
||||
</AvrGccCpp>
|
||||
</ToolchainSettings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="clock.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="flash\flash.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="io\io.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="main.cpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="type\type.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="uart\config.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="uart\hardware.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="uart\hardware0.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="uart\hardware1.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="uart\software.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
<Compile Include="uart\uart.hpp">
|
||||
<SubType>compile</SubType>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="flash" />
|
||||
<Folder Include="io" />
|
||||
<Folder Include="uart" />
|
||||
<Folder Include="type" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(AVRSTUDIO_EXE_PATH)\\Vs\\Compiler.targets" />
|
||||
</Project>
|
||||
386
tsb/tsb_asm.cpp
Normal file
386
tsb/tsb_asm.cpp
Normal file
@@ -0,0 +1,386 @@
|
||||
// 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.
|
||||
//
|
||||
// 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 — at 510 bytes in the
|
||||
// 512-byte BOOTSZ=11 section the hand-written oracle occupies (500 B). This tier used to be one
|
||||
// monolithic inline-asm routine; it is now the tricks tier's C++ (same
|
||||
// register protocol, same structure — see tsb_tricks.cpp, including the
|
||||
// global-register miscompile rules) with exactly two routines kept in
|
||||
// assembly, the two whose remaining cost *is* the calling convention:
|
||||
//
|
||||
// rx the bounded receive: C++ must re-floor the timeout window on every
|
||||
// call (the global-register-store miscompile) and split it across
|
||||
// call-saved registers; the asm keeps the oracle's X-register nested
|
||||
// countdown.
|
||||
// store the page-store loop: C++ cannot hold the receive byte pair and the
|
||||
// walked Z pointer across the rx calls without call-saved staging
|
||||
// (push/pop + a Y→Z copy per word); the asm calls rx knowing exactly
|
||||
// which registers it touches and walks Z live across the whole page.
|
||||
//
|
||||
// Everything else — bring-up, activation, password gate, emergency erase,
|
||||
// dispatch, every SPM/EEPROM/flash primitive, every geometry/baud/info
|
||||
// constant — is C++ on libavr, and the two asm routines splice into the same
|
||||
// global-register protocol the C++ uses (g_addr in Y, g_cnt in r16, g_window
|
||||
// in r7, g_receiving in r6), so calls cross the boundary with no marshalling.
|
||||
//
|
||||
// The wire protocol is strict request/response, which is what makes the shared
|
||||
// line safe: the device drives it only between a received command and its
|
||||
// reply, and releases it (RXEN0 only) whenever it waits.
|
||||
|
||||
#include <libavr/libavr.hpp>
|
||||
|
||||
#include <avr/io.h> // SP / RAMEND for the crt-free boot entry, SFR addresses for the asm routines
|
||||
|
||||
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;
|
||||
|
||||
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).
|
||||
constexpr std::uint8_t act_min = 16;
|
||||
// Post-activation window: the host gets seconds, not milliseconds, mid-session.
|
||||
constexpr std::uint8_t comm_window = 200;
|
||||
|
||||
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
||||
|
||||
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
||||
constexpr auto baud = avr::uart::detail::solve_baud(16_MHz, 115200_Bd);
|
||||
|
||||
// The 16-byte device-info block, streamed out on activation.
|
||||
// clang-format off
|
||||
[[gnu::progmem]] constexpr std::uint8_t info[16] = {
|
||||
'T', 'S', 'B',
|
||||
build_date & 0xFF, build_date >> 8,
|
||||
0xF3, // status: native-UART fixed-baud lineage
|
||||
0x1E, 0x95, 0x0F, // ATmega328P signature
|
||||
page / 2, // page size in words
|
||||
(app_end / 2) & 0xFF, (app_end / 2) >> 8,
|
||||
eeprom_end & 0xFF, eeprom_end >> 8,
|
||||
0xAA, 0xAA,
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
register std::uint16_t g_addr asm("r28");
|
||||
register std::uint8_t g_cnt asm("r16");
|
||||
register std::uint8_t g_window asm("r7");
|
||||
register std::uint8_t g_receiving asm("r6");
|
||||
|
||||
const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
{
|
||||
return reinterpret_cast<const std::uint8_t *>(addr);
|
||||
}
|
||||
|
||||
// Bounded byte receive (asm 1 of 2): release the one-wire line on a direction
|
||||
// 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-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));
|
||||
for (std::uint8_t guard = 46; guard; --guard)
|
||||
;
|
||||
}
|
||||
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(g_addr);
|
||||
++g_addr;
|
||||
return byte;
|
||||
}
|
||||
|
||||
// One EEPROM byte → [g_addr++].
|
||||
[[gnu::noinline, gnu::noclone]] void eewr(std::uint8_t byte)
|
||||
{
|
||||
ee::write<off>(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()
|
||||
{
|
||||
spm::wait();
|
||||
spm::rww_enable<off>();
|
||||
}
|
||||
|
||||
extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --defsym=tsb_app=0
|
||||
|
||||
[[noreturn]] void appjump()
|
||||
{
|
||||
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::erase_page<off>(g_addr);
|
||||
settle();
|
||||
}
|
||||
|
||||
// Erase the whole application, top-down like the oracle: the loop bound is a
|
||||
// compare with zero, and g_addr = 0 is handed back for free.
|
||||
[[gnu::noinline, gnu::noclone]] void erase_application()
|
||||
{
|
||||
g_addr = app_end;
|
||||
do {
|
||||
erase_below();
|
||||
} while (g_addr != 0);
|
||||
}
|
||||
|
||||
// Stream one host page into the erased flash page at g_addr (asm 2 of 2): the
|
||||
// word pair stages in r0:r1 straight from rx (whose register set is known —
|
||||
// the cross-call liveness C++ cannot express), Z walks the page and PGWRT
|
||||
// programs it. g_addr is left at the next page base.
|
||||
[[gnu::noinline, gnu::noclone]] void store_flash()
|
||||
{
|
||||
asm volatile(" movw r30, r28 \n\t" // Z = page base; rx leaves Z live
|
||||
" ldi r20, %[words] \n\t"
|
||||
"1: rcall %x[rx] \n\t"
|
||||
" mov r0, r24 \n\t" // word low byte
|
||||
" rcall %x[rx] \n\t"
|
||||
" mov r1, r24 \n\t" // word high byte
|
||||
" ldi r24, 0x01 \n\t" // SPMEN: buffer the word at Z
|
||||
" out %[spmcsr], r24 \n\t"
|
||||
" spm \n\t"
|
||||
" 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();
|
||||
}
|
||||
|
||||
[[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::reg<"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: 3×'@', 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[0]);
|
||||
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;
|
||||
g_cnt = page;
|
||||
sendf();
|
||||
if (g_addr >= app_end)
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'F': // erase the application, then take pages behind '?'
|
||||
erase_application(); // leaves g_addr = 0, the write start
|
||||
while (rcnf() == confirm)
|
||||
store_flash();
|
||||
break;
|
||||
case 'e': // read EEPROM, one page per host '!', until the host stops
|
||||
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 = app_end;
|
||||
g_cnt = page;
|
||||
sendf();
|
||||
break;
|
||||
case 'C': // replace the config page, then echo it back to verify
|
||||
if (rcnf() != confirm)
|
||||
break;
|
||||
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
|
||||
appjump();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace tsb
|
||||
|
||||
// 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;
|
||||
// The one line of crt this loader needs: compiled code assumes
|
||||
// __zero_reg__ (r1) is 0, and power-on registers are undefined.
|
||||
asm volatile("clr __zero_reg__");
|
||||
tsb::run();
|
||||
}
|
||||
308
tsb/tsb_pure.cpp
Normal file
308
tsb/tsb_pure.cpp
Normal file
@@ -0,0 +1,308 @@
|
||||
// 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.
|
||||
|
||||
#include <libavr/libavr.hpp>
|
||||
|
||||
#include <avr/io.h> // SP / RAMEND for the crt-free boot entry
|
||||
|
||||
using namespace avr::literals;
|
||||
namespace spm = avr::spm;
|
||||
namespace ee = avr::eeprom;
|
||||
|
||||
using dev = avr::device<{.clock = 16_MHz}>;
|
||||
// One-wire: RX and TX share the line, exactly as the native-UART TSB expects.
|
||||
using serial_t = dev::uart0<{.baud = 115200_Bd, .max_baud_error = 3_pct, .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;
|
||||
|
||||
// The handshake bytes, identical across every TSB host.
|
||||
constexpr std::uint8_t confirm = '!';
|
||||
constexpr std::uint8_t request = '?';
|
||||
constexpr std::uint8_t knock = '@';
|
||||
|
||||
// Boot geometry for the 1 KB boot section (BOOTSZ=10). The page size and the
|
||||
// flash/EEPROM extents are the chip database's to know. app_end is the config
|
||||
// page (the LASTPAGE holding the app-jump vector, activation timeout and
|
||||
// password), one page below the boot section.
|
||||
constexpr std::uint16_t page = spm::page_bytes;
|
||||
constexpr std::uint16_t boot_bytes = 1024;
|
||||
constexpr std::uint16_t 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;
|
||||
|
||||
// The 16-byte device-info block the host reads on activation. A flash_table
|
||||
// keeps it in progmem with no .data image (there is no crt to copy one).
|
||||
// clang-format off
|
||||
inline constexpr 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)
|
||||
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>;
|
||||
|
||||
// Blocking byte read/write over the one-wire line: read() releases the line to
|
||||
// the receiver, write() takes it and holds it until the frame is out.
|
||||
std::uint8_t rx()
|
||||
{
|
||||
return serial.read_blocking();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Stream `count` bytes to the host, from flash (LPM) or from EEPROM.
|
||||
void send_flash(std::uint16_t addr, std::uint8_t count)
|
||||
{
|
||||
while (count--)
|
||||
tx(avr::flash_load(flash_ptr(addr++)));
|
||||
}
|
||||
|
||||
void send_eeprom(std::uint16_t addr, std::uint8_t count)
|
||||
{
|
||||
while (count--)
|
||||
tx(ee::read(addr++));
|
||||
}
|
||||
|
||||
// Prompt the host with '?' and report whether it answered '!'.
|
||||
bool request_confirm()
|
||||
{
|
||||
tx(request);
|
||||
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)
|
||||
{
|
||||
for (std::uint16_t i = 0; i < page; i += 2) {
|
||||
std::uint8_t lo = rx();
|
||||
std::uint8_t hi = rx();
|
||||
spm::fill<off>(addr + i, static_cast<std::uint16_t>(lo | (hi << 8)));
|
||||
}
|
||||
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)
|
||||
{
|
||||
for (std::uint16_t i = 0; i < page; ++i)
|
||||
ee::write<off>(addr + i, rx());
|
||||
}
|
||||
|
||||
// Erase one flash page and wait it out — 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<off>(addr);
|
||||
spm::wait();
|
||||
}
|
||||
|
||||
// Erase the whole application, one page at a time, top-down as the reference
|
||||
// loader does (unwritten pages stay erased and the host cannot observe the
|
||||
// order; the loop bound becomes a compare with zero).
|
||||
void erase_application()
|
||||
{
|
||||
for (std::uint16_t a = app_end; a != 0;) {
|
||||
a -= page;
|
||||
erase_page(a);
|
||||
}
|
||||
spm::rww_enable<off>();
|
||||
}
|
||||
|
||||
// The application's reset vector; the linker pins it to 0x0000 (--defsym).
|
||||
extern "C" [[noreturn]] void tsb_app();
|
||||
|
||||
// Run the application. Any non-command byte, a wrong password, or an idle
|
||||
// programmer port lands here.
|
||||
[[noreturn]] void appjump()
|
||||
{
|
||||
spm::wait(); // make sure any pending SPM finished before handing over
|
||||
tsb_app();
|
||||
}
|
||||
|
||||
// 'f': stream the application flash back, one page per host '!'. Self-terminates
|
||||
// at the application boundary; the host normally stops earlier with a non-'!'.
|
||||
void read_flash()
|
||||
{
|
||||
for (std::uint16_t a = 0; a < app_end; a += page) {
|
||||
if (rx() != confirm)
|
||||
return;
|
||||
send_flash(a, page);
|
||||
}
|
||||
}
|
||||
|
||||
// 'e': stream EEPROM back, one page per host '!', until the host stops.
|
||||
void read_eeprom()
|
||||
{
|
||||
for (std::uint16_t a = 0;; a += page) {
|
||||
if (rx() != confirm)
|
||||
return;
|
||||
send_eeprom(a, page);
|
||||
}
|
||||
}
|
||||
|
||||
// 'F': erase the whole application first, 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);
|
||||
}
|
||||
|
||||
// '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);
|
||||
}
|
||||
|
||||
// 'C': replace the config page, then echo it back for the host to verify.
|
||||
void write_config()
|
||||
{
|
||||
if (!request_confirm())
|
||||
return;
|
||||
erase_page(app_end);
|
||||
store_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>(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())
|
||||
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.
|
||||
__uint24 idle = static_cast<__uint24>(avr::flash_load(flash_ptr(app_end + 2)) | 16) << 16;
|
||||
std::uint8_t knocks = 0;
|
||||
while (knocks < 3) {
|
||||
if (auto byte = serial.read())
|
||||
knocks = *byte == knock ? knocks + 1 : 0;
|
||||
else if (--idle == 0)
|
||||
appjump();
|
||||
}
|
||||
|
||||
switch (password_gate()) {
|
||||
case gate::pass:
|
||||
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
|
||||
switch (rx()) {
|
||||
case 'f':
|
||||
read_flash();
|
||||
break;
|
||||
case 'F':
|
||||
write_flash();
|
||||
break;
|
||||
case 'e':
|
||||
read_eeprom();
|
||||
break;
|
||||
case 'E':
|
||||
write_eeprom();
|
||||
break;
|
||||
case 'c':
|
||||
send_flash(app_end, page);
|
||||
break;
|
||||
case 'C':
|
||||
write_config();
|
||||
break;
|
||||
default:
|
||||
appjump(); // 'q' or any other byte runs the application
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace tsb
|
||||
|
||||
// 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;
|
||||
// The one line of crt this loader needs: compiled code assumes
|
||||
// __zero_reg__ (r1) is 0, and power-on registers are undefined.
|
||||
asm volatile("clr __zero_reg__");
|
||||
tsb::run();
|
||||
}
|
||||
363
tsb/tsb_tricks.cpp
Normal file
363
tsb/tsb_tricks.cpp
Normal file
@@ -0,0 +1,363 @@
|
||||
// TinySafeBoot on libavr — tier 2: C++ with compiler trickery, no assembly.
|
||||
//
|
||||
// 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++, 526 bytes: 14 over the 512-byte
|
||||
// boot section the hand-written oracle fits, from 168 over at this tier's first
|
||||
// floor. 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
|
||||
// local/scratch/probes/gcc-avr-globalreg-repro.cpp, lessons.md entry). Every
|
||||
// g_* update below therefore sits where a *local* read or a jump/ret follows
|
||||
// it — the helpers advance g_addr immediately before returning, and rx()
|
||||
// re-floors the window on every call instead of storing the floored value
|
||||
// once. The layout is load-bearing; do not "simplify" it.
|
||||
//
|
||||
// The wire protocol is strict request/response, which is what makes the shared
|
||||
// line safe: the device drives it only between a received command and its
|
||||
// reply, and releases it (RXEN0 only) whenever it waits.
|
||||
|
||||
#include <libavr/libavr.hpp>
|
||||
|
||||
#include <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;
|
||||
|
||||
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).
|
||||
constexpr std::uint8_t act_min = 16;
|
||||
// Post-activation window: the host gets seconds, not milliseconds, mid-session.
|
||||
constexpr std::uint8_t comm_window = 200;
|
||||
|
||||
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
||||
|
||||
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
|
||||
constexpr auto baud = avr::uart::detail::solve_baud(16_MHz, 115200_Bd);
|
||||
|
||||
// The 16-byte device-info block, streamed out on activation.
|
||||
// clang-format off
|
||||
[[gnu::progmem]] constexpr std::uint8_t info[16] = {
|
||||
'T', 'S', 'B',
|
||||
build_date & 0xFF, build_date >> 8,
|
||||
0xF3, // status: native-UART fixed-baud lineage
|
||||
0x1E, 0x95, 0x0F, // ATmega328P signature
|
||||
page / 2, // page size in words
|
||||
(app_end / 2) & 0xFF, (app_end / 2) >> 8,
|
||||
eeprom_end & 0xFF, eeprom_end >> 8,
|
||||
0xAA, 0xAA,
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
register std::uint16_t g_addr asm("r28");
|
||||
register std::uint8_t g_cnt asm("r16");
|
||||
register std::uint8_t g_window asm("r7");
|
||||
register std::uint8_t g_receiving asm("r6");
|
||||
|
||||
const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
{
|
||||
return reinterpret_cast<const std::uint8_t *>(addr);
|
||||
}
|
||||
|
||||
// Bounded byte receive, the oracle's shape: release the one-wire line on a
|
||||
// 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-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)
|
||||
{
|
||||
if (g_receiving) {
|
||||
g_receiving = 0;
|
||||
hw::ucsr0b::write(hw::ucsr0b::txen0(1));
|
||||
for (std::uint8_t guard = 46; guard; --guard)
|
||||
;
|
||||
}
|
||||
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));
|
||||
++g_addr;
|
||||
return byte;
|
||||
}
|
||||
|
||||
// One EEPROM byte ← [g_addr++].
|
||||
[[gnu::noinline, gnu::noclone]] std::uint8_t eerd()
|
||||
{
|
||||
std::uint8_t byte = ee::read(g_addr);
|
||||
++g_addr;
|
||||
return byte;
|
||||
}
|
||||
|
||||
// One EEPROM byte → [g_addr++].
|
||||
[[gnu::noinline, gnu::noclone]] void eewr(std::uint8_t byte)
|
||||
{
|
||||
ee::write<off>(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()
|
||||
{
|
||||
spm::wait();
|
||||
spm::rww_enable<off>();
|
||||
}
|
||||
|
||||
extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --defsym=tsb_app=0
|
||||
|
||||
[[noreturn]] void appjump()
|
||||
{
|
||||
settle();
|
||||
tsb_app();
|
||||
}
|
||||
|
||||
// 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()
|
||||
{
|
||||
g_addr -= page;
|
||||
spm::erase_page<off>(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()
|
||||
{
|
||||
g_cnt = page / 2;
|
||||
do {
|
||||
std::uint16_t word = rx();
|
||||
word |= static_cast<std::uint16_t>(rx()) << 8;
|
||||
spm::fill<off>(g_addr, word);
|
||||
g_addr += 2;
|
||||
} while (--g_cnt);
|
||||
spm::write_page<off>(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::reg<"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: 3×'@', 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[0]);
|
||||
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;
|
||||
g_cnt = page;
|
||||
sendf();
|
||||
if (g_addr >= app_end)
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'F': // erase the application, then take pages behind '?'
|
||||
erase_application(); // leaves g_addr = 0, the write start
|
||||
while (rcnf() == confirm)
|
||||
store_flash();
|
||||
break;
|
||||
case 'e': // read EEPROM, one page per host '!', until the host stops
|
||||
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 = app_end;
|
||||
g_cnt = page;
|
||||
sendf();
|
||||
break;
|
||||
case 'C': // replace the config page, then echo it back to verify
|
||||
if (rcnf() != confirm)
|
||||
break;
|
||||
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
|
||||
appjump();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace tsb
|
||||
|
||||
// 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;
|
||||
// The one line of crt this loader needs: compiled code assumes
|
||||
// __zero_reg__ (r1) is 0, and power-on registers are undefined.
|
||||
asm volatile("clr __zero_reg__");
|
||||
tsb::run();
|
||||
}
|
||||
1
tsb/type
1
tsb/type
Submodule tsb/type deleted from ce31ef017f
1
tsb/uart
1
tsb/uart
Submodule tsb/uart deleted from 8f88cdccea
Reference in New Issue
Block a user