Compare commits
31 Commits
v2
...
9fc28aa89a
| Author | SHA1 | Date | |
|---|---|---|---|
| 9fc28aa89a | |||
| 05ed8f560b | |||
| b3d4101d8e | |||
| dacd21bd78 | |||
| 90a29d872f | |||
| cbddbc4457 | |||
| 93f4ac784d | |||
|
|
e85840fa39 | ||
|
|
045372a2bc | ||
| 19662d2386 | |||
| f6598b0511 | |||
| 0604d3a0ad | |||
| 62a548cc09 | |||
| 7d103ca957 | |||
| eca7a41051 | |||
| 7314f7ab3b | |||
| 5b361904ab | |||
| 833e134e01 | |||
| da730b7bb5 | |||
| c351bee257 | |||
| 34e7f1be34 | |||
| 445e187722 | |||
| 250aba5cfb | |||
| 5c900720e3 | |||
| 7d6ef959b2 | |||
| 11ffbce2e2 | |||
| f32a27ff15 | |||
| 57d94cf631 | |||
| 8203a24f33 | |||
| 2906da3272 | |||
| 64c1e484b5 |
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
BasedOnStyle: LLVM
|
BasedOnStyle: LLVM
|
||||||
|
Standard: Latest
|
||||||
ColumnLimit: 120
|
ColumnLimit: 120
|
||||||
IndentWidth: 4
|
IndentWidth: 4
|
||||||
TabWidth: 4
|
TabWidth: 4
|
||||||
|
|||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -9,3 +9,12 @@ Debug
|
|||||||
*.eeprom
|
*.eeprom
|
||||||
*.lss
|
*.lss
|
||||||
*.map
|
*.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
|
|
||||||
341
CMakeLists.txt
Normal file
341
CMakeLists.txt
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
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 ELF is only a container (symbols, section headers) and is never flashed —
|
||||||
|
# and the host tool's load_image() dispatches on extension, so handing it one
|
||||||
|
# would silently program the header bytes. Every loader image therefore gets
|
||||||
|
# both flashable forms beside it at link time: .hex for avrdude, and .bin for
|
||||||
|
# the host tool's raw path (which is what the reloc and update tests convert to
|
||||||
|
# on the fly). .eeprom is dropped — EEPROM content is its own update.
|
||||||
|
function(add_image_outputs name)
|
||||||
|
add_custom_command(TARGET ${name} POST_BUILD
|
||||||
|
COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom
|
||||||
|
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.hex
|
||||||
|
COMMAND ${CMAKE_OBJCOPY} -O binary -R .eeprom
|
||||||
|
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# The TinySafeBoot protocol reimplemented on libavr in 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}>)
|
||||||
|
add_image_outputs(${name})
|
||||||
|
if(PROJECT_IS_TOP_LEVEL)
|
||||||
|
add_test(NAME ${name}.size
|
||||||
|
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}>
|
||||||
|
-DLIMIT=${bytes} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
||||||
|
if(DEFINED TSB_DEVICE)
|
||||||
|
add_test(NAME ${name}.protocol
|
||||||
|
COMMAND ${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.
|
||||||
|
#
|
||||||
|
# The image is position-independent (check_pi.py asserts the two link-time
|
||||||
|
# facts that make it so), and on the tinies its budget is 510, not 512: the
|
||||||
|
# slot's last word is the trampoline the host composes — the resident slot's
|
||||||
|
# holds the application entry, and a staging copy's holds the jump through
|
||||||
|
# which it reaches the loader it installed. The activation window is a
|
||||||
|
# compile-time constant; a different PUREBOOT_TIMEOUT builds the re-timed
|
||||||
|
# binary a self-update then installs.
|
||||||
|
set(PUREBOOT_TIMEOUT 8 CACHE STRING "pureboot activation window, seconds")
|
||||||
|
# Per-family geometry. The boot-sectioned megas run the loader from the
|
||||||
|
# hardware boot section and boot the application at word 0; the tinies and
|
||||||
|
# the boot-section-less m48s get the trampoline surgery. All megas assume a
|
||||||
|
# 16 MHz crystal at 115200 Bd; the tinies their internal RC at 57600 Bd over
|
||||||
|
# the software UART. --pmem-wrap-around models AVR's modulo-flash PC where
|
||||||
|
# the flash is big enough to need it (an rjmp reaches all of 4 KiB by
|
||||||
|
# itself).
|
||||||
|
if(LIBAVR_MCU MATCHES "^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 "attiny25")
|
||||||
|
set(_pb_flash 2048)
|
||||||
|
set(_pb_wrap "")
|
||||||
|
set(_pb_page 32)
|
||||||
|
set(_pb_hz 8000000)
|
||||||
|
set(_pb_baud 57600)
|
||||||
|
set(_pb_eeprom 128)
|
||||||
|
elseif(LIBAVR_MCU STREQUAL "attiny45")
|
||||||
|
set(_pb_flash 4096)
|
||||||
|
set(_pb_wrap "")
|
||||||
|
set(_pb_page 64)
|
||||||
|
set(_pb_hz 8000000)
|
||||||
|
set(_pb_baud 57600)
|
||||||
|
set(_pb_eeprom 256)
|
||||||
|
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)
|
||||||
|
elseif(LIBAVR_MCU MATCHES "^atmega48(a|p|pa)?$")
|
||||||
|
set(_pb_flash 4096)
|
||||||
|
set(_pb_wrap "")
|
||||||
|
set(_pb_page 64)
|
||||||
|
set(_pb_hz 16000000)
|
||||||
|
set(_pb_baud 115200)
|
||||||
|
set(_pb_eeprom 256)
|
||||||
|
elseif(LIBAVR_MCU MATCHES "^atmega8a?$" OR LIBAVR_MCU MATCHES "^atmega88(a|p|pa)?$")
|
||||||
|
set(_pb_flash 8192)
|
||||||
|
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
|
||||||
|
set(_pb_page 64)
|
||||||
|
set(_pb_hz 16000000)
|
||||||
|
set(_pb_baud 115200)
|
||||||
|
set(_pb_eeprom 512)
|
||||||
|
elseif(LIBAVR_MCU MATCHES "^atmega16a?$" OR LIBAVR_MCU MATCHES "^atmega168(a|p|pa)?$" OR
|
||||||
|
LIBAVR_MCU MATCHES "^atmega164(a|p|pa)$")
|
||||||
|
set(_pb_flash 16384)
|
||||||
|
set(_pb_wrap -Wl,--pmem-wrap-around=16k)
|
||||||
|
set(_pb_page 128)
|
||||||
|
set(_pb_hz 16000000)
|
||||||
|
set(_pb_baud 115200)
|
||||||
|
set(_pb_eeprom 512)
|
||||||
|
elseif(LIBAVR_MCU MATCHES "^atmega32a?$" OR LIBAVR_MCU MATCHES "^atmega328p?$" OR
|
||||||
|
LIBAVR_MCU MATCHES "^atmega324(a|p|pa)$")
|
||||||
|
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)
|
||||||
|
elseif(LIBAVR_MCU MATCHES "^atmega644(a|p|pa)?$")
|
||||||
|
# 64 KiB is exactly the 16-bit byte space: plain LPM reaches everything,
|
||||||
|
# and the smallest boot section (1 KiB) holds the loader and its staging
|
||||||
|
# slot together (see pureboot/README.md).
|
||||||
|
set(_pb_flash 65536)
|
||||||
|
set(_pb_wrap -Wl,--pmem-wrap-around=64k)
|
||||||
|
set(_pb_page 256)
|
||||||
|
set(_pb_hz 16000000)
|
||||||
|
set(_pb_baud 115200)
|
||||||
|
set(_pb_eeprom 2048)
|
||||||
|
elseif(LIBAVR_MCU MATCHES "^atmega1284p?$")
|
||||||
|
# 128 KiB: wire flash addresses are word addresses, reads go through
|
||||||
|
# ELPM, and the PC's modulo wrap exceeds what --pmem-wrap-around models.
|
||||||
|
# The slot is 1 KiB — this chip's own smallest boot sector; the far
|
||||||
|
# machinery cannot fit 512 B (see pureboot/README.md).
|
||||||
|
set(_pb_flash 131072)
|
||||||
|
set(_pb_wrap "")
|
||||||
|
set(_pb_page 256)
|
||||||
|
set(_pb_hz 16000000)
|
||||||
|
set(_pb_baud 115200)
|
||||||
|
set(_pb_eeprom 4096)
|
||||||
|
set(_pb_slot 1024)
|
||||||
|
set(_pb_limit 1024)
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "pureboot: no geometry for ${LIBAVR_MCU}")
|
||||||
|
endif()
|
||||||
|
if(NOT DEFINED _pb_slot)
|
||||||
|
set(_pb_slot 512)
|
||||||
|
endif()
|
||||||
|
math(EXPR _pb_base "${_pb_flash} - ${_pb_slot}")
|
||||||
|
math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL)
|
||||||
|
# Patched-vector chips hand over through the trampoline word below the slot,
|
||||||
|
# which is also the slot's own last word — their budget is slot − 2.
|
||||||
|
if(LIBAVR_MCU MATCHES "^atmega" AND NOT LIBAVR_MCU MATCHES "^atmega48")
|
||||||
|
set(_pb_app 0)
|
||||||
|
if(NOT DEFINED _pb_limit)
|
||||||
|
set(_pb_limit ${_pb_slot})
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
math(EXPR _pb_app "${_pb_base} - 2")
|
||||||
|
math(EXPR _pb_limit "${_pb_slot} - 2")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# simavr names its cores after the base dies; the A revisions run on them
|
||||||
|
# (the 644PA on the 644P core).
|
||||||
|
set(_pb_sim_mcu ${LIBAVR_MCU})
|
||||||
|
if(LIBAVR_MCU MATCHES "^atmega(8|16|32|48|88|164|168|644)a$")
|
||||||
|
string(REGEX REPLACE "a$" "" _pb_sim_mcu ${LIBAVR_MCU})
|
||||||
|
elseif(LIBAVR_MCU STREQUAL "atmega644pa")
|
||||||
|
set(_pb_sim_mcu atmega644p)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_executable(pureboot pureboot/pureboot.cpp)
|
||||||
|
target_link_libraries(pureboot PRIVATE libavr)
|
||||||
|
target_compile_definitions(pureboot PRIVATE PUREBOOT_TIMEOUT=${PUREBOOT_TIMEOUT})
|
||||||
|
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>)
|
||||||
|
add_image_outputs(pureboot)
|
||||||
|
if(PROJECT_IS_TOP_LEVEL)
|
||||||
|
add_test(NAME pureboot.size
|
||||||
|
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot>
|
||||||
|
-DLIMIT=${_pb_limit} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
||||||
|
if(Python3_FOUND)
|
||||||
|
add_test(NAME pureboot.pi
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/check_pi.py
|
||||||
|
${CMAKE_OBJDUMP} ${CMAKE_NM} $<TARGET_FILE:pureboot> ${_pb_base_hex})
|
||||||
|
add_test(NAME pureboot.planner
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_planner.py
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# 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> ${_pb_sim_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)
|
||||||
|
|
||||||
|
# The position-independence acceptance test: the identical image,
|
||||||
|
# installed one slot lower, must serve the full command set.
|
||||||
|
add_test(NAME pureboot.reloc
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbreloc.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot> ${_pb_sim_mcu} ${_pb_hz} ${_pb_base_hex}
|
||||||
|
${_pb_page} ${_pb_baud} ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbreloc-work)
|
||||||
|
set_tests_properties(pureboot.reloc PROPERTIES TIMEOUT 180
|
||||||
|
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
||||||
|
|
||||||
|
# Re-homing: a loader mistakenly programmed at address 0 (a raw .bin
|
||||||
|
# handed to a programmer) must heal into the canonical slot through
|
||||||
|
# the ordinary --update-loader flow. Patched-vector behavior, so one
|
||||||
|
# representative chip carries it.
|
||||||
|
if(LIBAVR_MCU STREQUAL "attiny85")
|
||||||
|
add_test(NAME pureboot.rehome
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbrehome.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9>.bin ${_pb_sim_mcu}
|
||||||
|
${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_baud} $<TARGET_FILE:pbapp>.bin
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbrehome-work)
|
||||||
|
set_tests_properties(pureboot.rehome PROPERTIES TIMEOUT 180)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# The self-update end-to-end: the re-timed build (same source, only
|
||||||
|
# PUREBOOT_TIMEOUT differs — a byte-different image) replaces the
|
||||||
|
# resident through --update-loader, with every power-fail phase
|
||||||
|
# rehearsed from the runner's flash dumps.
|
||||||
|
add_executable(pureboot9 pureboot/pureboot.cpp)
|
||||||
|
target_link_libraries(pureboot9 PRIVATE libavr)
|
||||||
|
target_compile_definitions(pureboot9 PRIVATE PUREBOOT_TIMEOUT=9)
|
||||||
|
target_link_options(pureboot9 PRIVATE -nostartfiles -Wl,--section-start=.text=${_pb_base_hex}
|
||||||
|
-Wl,--defsym=pureboot_app=${_pb_app} ${_pb_wrap})
|
||||||
|
add_image_outputs(pureboot9)
|
||||||
|
add_test(NAME pureboot.update
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbupdate.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9> ${_pb_sim_mcu}
|
||||||
|
${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_baud} $<TARGET_FILE:pbapp>.bin
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbupdate-work)
|
||||||
|
set_tests_properties(pureboot.update PROPERTIES TIMEOUT 600
|
||||||
|
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
1813
CMakePresets.json
Normal file
1813
CMakePresets.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
;***********************************************************************
|
||||||
|
;***********************************************************************
|
||||||
|
;***********************************************************************
|
||||||
|
|
||||||
|
|
||||||
267
pureboot/README.md
Normal file
267
pureboot/README.md
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
# 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 — all 37 —
|
||||||
|
fitting each chip's smallest boot sector**: 512 bytes everywhere — 488 B on
|
||||||
|
the tiny13s, 498–502 B on the tiny25/45/85, 466–504 B across the megas
|
||||||
|
(474 B on the boot-section-less m48s, 498 B on the 644s) — except the
|
||||||
|
ATmega1284/1284P, whose smallest boot sector is 1 KiB and whose far-flash
|
||||||
|
machinery (ELPM reads, RAMPZ page commands, word-addressed wire) lands at
|
||||||
|
558 B in a 1 KiB slot: the 512-byte figure is a hardware boundary those
|
||||||
|
chips simply do not have, and no implementation of this feature set fits it
|
||||||
|
there. The device speaks primitives; every composite — verify, erase,
|
||||||
|
reset-vector surgery, updating the loader itself — lives in the host tool
|
||||||
|
(`pureboot.py`).
|
||||||
|
|
||||||
|
The image is **position-independent**: control flow is PC-relative, the
|
||||||
|
read/write paths take wire addresses, the write guard protects the slot the
|
||||||
|
code is *running* in (from the runtime return address), the info block is
|
||||||
|
addressed from that same anchor, and the application jump is an indirect
|
||||||
|
call to an absolute entry. The identical binary therefore runs from any
|
||||||
|
slot with every command intact — which makes pureboot **its own staging
|
||||||
|
loader**: the host installs the same binary one slot below the resident,
|
||||||
|
jumps into it, and lets it rewrite the resident. The slot is 512 bytes
|
||||||
|
(1 KiB on the word-addressed large chips, matching their boot-sector
|
||||||
|
minimum); on the tinies the budget is 510, not 512: a slot's last word
|
||||||
|
belongs to the host-managed trampoline (below).
|
||||||
|
|
||||||
|
## Link
|
||||||
|
|
||||||
|
| Chip | Serial | Baud | Clock assumed |
|
||||||
|
|---|---|---|---|
|
||||||
|
| every ATmega | the hardware USART (USART0), RXD/TXD per pinout | 115200 8N1 | 16 MHz crystal |
|
||||||
|
| ATtiny25/45/85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC |
|
||||||
|
| ATtiny13/13A | 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 boot-sectioned megas; the patched
|
||||||
|
reset vector on the tinies and the boot-section-less m48s) — 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 is a compile-time constant — 8 s by default, another value
|
||||||
|
via the `PUREBOOT_TIMEOUT` CMake cache variable — so the whole EEPROM belongs
|
||||||
|
to the application; pureboot never uses it for its own state. Re-timing a
|
||||||
|
deployed loader is a self-update with a re-timed build (below).
|
||||||
|
|
||||||
|
## Session
|
||||||
|
|
||||||
|
After the knock the loader stays in its command loop until `J` jumps away or
|
||||||
|
the chip resets. 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.
|
||||||
|
|
||||||
|
On chips whose flash exceeds 64 KiB (the 1284s — info-block flag bit 1) the
|
||||||
|
`R`/`W` flash addresses are **word** addresses; everywhere else they are byte
|
||||||
|
addresses (the 644s' 64 KiB is exactly the 16-bit byte space and stays
|
||||||
|
byte-addressed). EEPROM addresses are always bytes, counts always bytes.
|
||||||
|
|
||||||
|
| 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 |
|
||||||
|
| `J` | word address (16-bit) | `+`, then execution continues there |
|
||||||
|
| 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
|
||||||
|
512-byte slot the loader is *running* in are drained but never programmed — a
|
||||||
|
broken host cannot brick the running copy, and a staged copy may rewrite the
|
||||||
|
resident slot. `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.
|
||||||
|
|
||||||
|
`J` is the one control-transfer primitive: the host uses it to run the
|
||||||
|
application (word 0 on the mega, the trampoline word on the tinies — both
|
||||||
|
known from the info block) and to move between loader copies during a
|
||||||
|
self-update. A jump to a loader slot's base re-enters that copy's own
|
||||||
|
startup; it must then be knocked afresh.
|
||||||
|
|
||||||
|
The info block (`b`):
|
||||||
|
|
||||||
|
| Offset | Content |
|
||||||
|
|---|---|
|
||||||
|
| 0–2 | `'P'`, `'B'`, protocol version (1) |
|
||||||
|
| 3–5 | device signature |
|
||||||
|
| 6 | SPM page size in bytes (0 means 256) |
|
||||||
|
| 7–8 | loader base — application flash ends here (a word address when bit 1 is set) |
|
||||||
|
| 9–10 | EEPROM size |
|
||||||
|
| 11 | bit 0: host must patch the reset vector (no hardware boot section); bit 1: flash wire addresses are word addresses |
|
||||||
|
|
||||||
|
Composites are the host's job: verify = read back and compare, erase =
|
||||||
|
write `0xff` (per page for flash, per byte for EEPROM).
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
The build leaves three artifacts per chip. The ELF is a container for the
|
||||||
|
tests and objcopy — never flashed. The **.hex is the programmer artifact**:
|
||||||
|
it carries its own addresses and lands the loader in its top slot,
|
||||||
|
touching nothing else. The **.bin is the self-update image** — the slot's
|
||||||
|
bare bytes with no addressing, which a programmer would put at address 0.
|
||||||
|
On a boot-sectioned mega a copy at 0 is dead weight (SPM only executes
|
||||||
|
from the boot section, so it cannot even heal itself — reflash the .hex);
|
||||||
|
on the patched-vector chips it *runs* (the image is position-independent
|
||||||
|
and reset enters word 0), reports its canonical geometry, and the ordinary
|
||||||
|
`--update-loader` flow re-homes a build into the top slot — the staging
|
||||||
|
install and the word-0 redirect both execute from copies outside page 0's
|
||||||
|
slot, so the running-slot guard never blocks the flow (`pureboot.rehome`
|
||||||
|
is the acceptance test). Flashing the application afterwards overwrites
|
||||||
|
the stale copy, vector surgery included. The one position that cannot
|
||||||
|
re-home itself is the staging slot: installing the staging copy would hit
|
||||||
|
the running copy's own guard and the update stops at its verify — flash
|
||||||
|
the .hex instead.
|
||||||
|
|
||||||
|
**Boot-sectioned megas**: program the loader at `flash − slot` with an
|
||||||
|
external programmer. Every such mega has a BOOTSZ step whose boot section
|
||||||
|
is exactly the loader slot — 512 B, the second-smallest step on the 8 KiB
|
||||||
|
and 16 KiB chips (m8, m88, m16, m168, m164), the smallest on the 32 KiB
|
||||||
|
ones (m32, m328, m324); on the 1284s that step is the smallest, 512 words,
|
||||||
|
which is why their slot is 1 KiB — so the ATmega328P profiles below apply
|
||||||
|
to every one of them with its own addresses and slot size; the per-chip
|
||||||
|
BOOTSZ ladders live in the host tool (`BOOT_FUSE`). The 1284s' numbers:
|
||||||
|
standalone = BOOTSZ 512 words (reset at the loader base 0x1fc00);
|
||||||
|
self-update = 1024 words, covering both 1 KiB slots, the loader-first
|
||||||
|
reset landing at 0x1f800 — the staging slot, walked across when erased.
|
||||||
|
|
||||||
|
The **644s** are the geometry's sweet spot: their smallest boot section
|
||||||
|
(512 words = 1 KiB) is exactly *two* 512-byte slots, so the resident and
|
||||||
|
its staging slot both live inside the minimum section — self-update needs
|
||||||
|
no fuse step up, and the standalone profile does not exist (reset lands at
|
||||||
|
0xfc00, one erased slot below the loader: the loader-first walk built in).
|
||||||
|
|
||||||
|
ATmega328P profiles (addresses for its 32 KiB):
|
||||||
|
|
||||||
|
| BOOTSZ | BOOTRST | Behavior |
|
||||||
|
|---|---|---|
|
||||||
|
| 256 words (512 B) | programmed | *Standalone*: reset always enters the loader; **self-update impossible** (the staging slot lies outside the boot section, where SPM is disabled). |
|
||||||
|
| 512 words (1 KB) | unprogrammed | *Self-update, app-first*: reset always boots the application, which owns all 31.5 KB and must offer its own jump to 0x7e00 to reach the loader (a virgin chip reaches it by reset across erased flash). Updates are power-fail-safe except mid-rewrite of the resident slot itself (no reset path leads to the staging copy then). |
|
||||||
|
| 512 words (1 KB) | programmed | *Self-update, loader-first*: reset lands at 0x7c00 — the staging slot, normally erased, so execution walks up into the loader; during an update it is the staging copy itself, so a mid-rewrite power loss recovers by reset. The loss windows move to the staging install/retire page writes instead (page-write scale). The host keeps `[0x7c00, 0x7e00)` clear of application data (`--force` overrides). |
|
||||||
|
|
||||||
|
Applications are flashed unmodified — word 0 stays the application's own
|
||||||
|
reset vector, and the hand-over jumps to 0.
|
||||||
|
|
||||||
|
**Patched-vector chips — the tinies and the m48s** (no boot section; the
|
||||||
|
m48s' SPM runs from the entire flash, Atmel-8271 §26): 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: word 0 is rewritten to `rjmp` to the loader base, and
|
||||||
|
the application's own entry is re-encoded as a trampoline `rjmp` in the
|
||||||
|
word just below the loader (`base − 2`, where the hand-over jumps). Every
|
||||||
|
other vector stays the application's. The patched page 0 and the trampoline
|
||||||
|
page are written *first*, so from the first write on an interrupted flash
|
||||||
|
still resets into the loader; an erase runs top-down for the same reason.
|
||||||
|
The m48s speak this profile over their hardware USART — no fuse preflight,
|
||||||
|
BOOTRST does not exist there.
|
||||||
|
|
||||||
|
## Updating the loader
|
||||||
|
|
||||||
|
`pureboot.py --update-loader new_pureboot.bin` replaces the resident loader
|
||||||
|
with any pureboot build — a re-timed window, a newer protocol — using the
|
||||||
|
loader itself as its own staging loader. The image is the loader's own 512
|
||||||
|
bytes as a raw binary, or the Intel HEX the build emits beside it, which
|
||||||
|
links the loader at its base inside an otherwise blank flash image:
|
||||||
|
|
||||||
|
The preflight refuses an image built for another chip: the info block
|
||||||
|
embedded in every pureboot binary (signature, page size, loader base,
|
||||||
|
EEPROM size, flags) must match the device's own, and the error names both.
|
||||||
|
Die revisions share their base signature and geometry, so their images are
|
||||||
|
interchangeable — as the silicon is. `loader_image()` also accepts a
|
||||||
|
padded image (a raw .bin padded from 0, or a whole-flash read-back with
|
||||||
|
the loader resident) and peels it to the slot content by the embedded base.
|
||||||
|
|
||||||
|
1. The staging slot `[base−slot, base)` is saved to a host-side state file
|
||||||
|
(on the 1 KB tiny13s that is the whole application, vectors included).
|
||||||
|
2. The resident installs the identical update image there. On the
|
||||||
|
patched-vector chips the host composes the slot's last word — the same
|
||||||
|
address as the resident's trampoline — as a jump to the resident base,
|
||||||
|
so even an abandoned staging copy times out into a loader, never into
|
||||||
|
garbage.
|
||||||
|
3. `J` enters the staging copy, which rewrites the resident slot. On the
|
||||||
|
patched-vector chips whose staging slot sits away from page 0 the host
|
||||||
|
first re-aims word 0 at the staging copy, so a power loss mid-rewrite
|
||||||
|
still resets into a loader; on the tiny13s the staging slot carries the
|
||||||
|
reset vector itself.
|
||||||
|
4. `J` enters the new resident, which restores the staging slot's saved
|
||||||
|
content (word 0 and the trampoline with it) and the state file is
|
||||||
|
discarded.
|
||||||
|
|
||||||
|
Every phase is idempotent and keyed off the actual flash state: re-running
|
||||||
|
the same command after any interruption resumes and completes. The state
|
||||||
|
file carries the only bytes not recoverable from the device; if it is lost
|
||||||
|
mid-update the update still completes, and the staging region is restored by
|
||||||
|
reflashing the application. A boot-sectioned mega needs its fuses for the
|
||||||
|
preflight (BOOTSZ gate, profile notes) — read from the device, or supplied
|
||||||
|
with `--assume-fuses` where reading is impossible (simulators); the
|
||||||
|
patched-vector chips need none.
|
||||||
|
|
||||||
|
## Host tool
|
||||||
|
|
||||||
|
`pureboot.py` — Python 3, standard library only. The port layer is the one
|
||||||
|
platform-specific part: termios drives any tty on POSIX (a USB adapter as
|
||||||
|
well as a simavr pty), the Win32 serial API through `ctypes` drives a COM
|
||||||
|
port on Windows (`--port COM6`; the `\\.\` form for two-digit ports is
|
||||||
|
supplied by the tool). Opening the port asserts DTR and RTS on both, so a
|
||||||
|
board that wires DTR to reset gets its reset pulse and opens the activation
|
||||||
|
window by itself.
|
||||||
|
|
||||||
|
pureboot.py --port /dev/ttyUSB0 --baud 57600 \
|
||||||
|
--info --fuses --flash app.hex
|
||||||
|
|
||||||
|
Operations run in a fixed order within one session: info, fuses, loader
|
||||||
|
update, flash (erase / program / read / verify), EEPROM (erase / program /
|
||||||
|
read / verify) — 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. `--force` overrides the refusable safety checks (today: flashing
|
||||||
|
application data into a mega's reset walk region).
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Per chip preset, `ctest` runs:
|
||||||
|
|
||||||
|
- `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget;
|
||||||
|
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`
|
||||||
|
in the image, the info block within its first 256 bytes;
|
||||||
|
- `pureboot.planner` — the host tool's pure logic: programming orders and
|
||||||
|
their recovery properties, the surgery, the staging composition, the
|
||||||
|
boot-fuse decode, and the update preflight's error/warning matrix over
|
||||||
|
synthetic fuse bytes;
|
||||||
|
- `pureboot.protocol` — end to end against 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, 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;
|
||||||
|
- `pureboot.reloc` — the identical image installed one slot below the
|
||||||
|
resident serves the complete command set from there (the
|
||||||
|
position-independence acceptance test);
|
||||||
|
- `pureboot.update` — the full `--update-loader` flow to a re-timed build,
|
||||||
|
then every power-fail phase: the device is killed mid-write, restarted
|
||||||
|
from its flash dump, and a re-run must complete the update with the
|
||||||
|
application intact throughout.
|
||||||
|
|
||||||
|
`size`, `pi`, and `planner` are host logic and run anywhere; the three
|
||||||
|
simulator-driven targets need simavr and a pty, so they are POSIX-only —
|
||||||
|
on Windows the tool is exercised against real hardware.
|
||||||
485
pureboot/pureboot.cpp
Normal file
485
pureboot/pureboot.cpp
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
// 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, a jump
|
||||||
|
// — and everything composite (verify, erase, reset-vector surgery, updating
|
||||||
|
// the loader itself) lives in the host tool. Protocol reference: README.md
|
||||||
|
// next to this file.
|
||||||
|
//
|
||||||
|
// The image is position-independent: control flow is PC-relative, the write
|
||||||
|
// and read paths take wire addresses, the write guard refuses the 512-byte
|
||||||
|
// slot the code is *running* in (taken from the runtime return address), the
|
||||||
|
// info block is read relative to that same anchor, and the application jump
|
||||||
|
// is an indirect call to an absolute entry. The identical binary therefore
|
||||||
|
// runs from any 512-byte slot with every command intact: flashed one slot
|
||||||
|
// below the resident loader it becomes the staging loader that rewrites the
|
||||||
|
// resident — how pureboot updates itself, host-driven, with no other
|
||||||
|
// firmware involved.
|
||||||
|
//
|
||||||
|
// Entry: reset lands in avr::startup::entry below (BOOTRST on the
|
||||||
|
// boot-sectioned megas; the patched reset vector — or erased flash walking
|
||||||
|
// up into the loader — on the tinies and the boot-section-less m48s). A
|
||||||
|
// watchdog reset hands straight to the application. Otherwise the
|
||||||
|
// host has one activation window per awaited knock byte ("pb"); an idle line
|
||||||
|
// boots the application. A session then stays in the command loop until 'J'
|
||||||
|
// jumps away 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: the clocks the dogfood boards run (16 MHz crystal on
|
||||||
|
// the mega, calibrated RC on the tinies). The device signature comes straight
|
||||||
|
// from the chip database (avr::hw::db.signature) — compile-time data is the
|
||||||
|
// only universal source, since the tiny13A cannot even read its signature row
|
||||||
|
// from code.
|
||||||
|
consteval avr::hertz_t clock()
|
||||||
|
{
|
||||||
|
auto name = std::string_view{avr::hw::db.name};
|
||||||
|
if (name.starts_with("ATtiny13"))
|
||||||
|
return 9.6_MHz;
|
||||||
|
if (name.starts_with("ATtiny"))
|
||||||
|
return 8_MHz;
|
||||||
|
return 16_MHz;
|
||||||
|
}
|
||||||
|
|
||||||
|
using dev = avr::device<{.clock = clock()}>;
|
||||||
|
|
||||||
|
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
|
||||||
|
consteval std::int16_t wdrf_field()
|
||||||
|
{
|
||||||
|
auto reg = std::string_view{avr::hw::db.regs[static_cast<std::size_t>(avr::power::detail::reset_reg())].name};
|
||||||
|
return avr::hw::db.field_index(reg, "WDRF");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Geometry: the resident loader owns the top slot of flash — 512 bytes,
|
||||||
|
// except on the >64 KiB chips whose own smallest boot sector is 1 KiB (the
|
||||||
|
// 1284s): there the slot is 1 KiB, matching the hardware boundary the
|
||||||
|
// 512-byte figure comes from everywhere else. The word below the slot is
|
||||||
|
// the trampoline (the application's relocated reset vector) on chips
|
||||||
|
// without a hardware boot section — the tinies and the m48s, whose SPM
|
||||||
|
// runs from anywhere (Atmel-8271 §26). A boot section also means the CPU
|
||||||
|
// runs on while the RWW section programs; everywhere else it halts through
|
||||||
|
// the operation. The m48s still carry RWWSRE as their temporary-buffer
|
||||||
|
// discard (§26.2), so the discard picks by that bit, not by the section.
|
||||||
|
constexpr std::uint16_t slot_bytes = spm::flash_bytes > 65536 ? 1024 : 512;
|
||||||
|
constexpr std::uint32_t base = spm::flash_bytes - slot_bytes;
|
||||||
|
constexpr std::uint16_t page = spm::page_bytes;
|
||||||
|
constexpr bool boot_section = avr::hw::curated::has_boot_section();
|
||||||
|
constexpr bool rww_discard = spm::detail::has_rww();
|
||||||
|
|
||||||
|
// Past 64 KiB a byte address no longer fits the wire's 16 bits, so on the
|
||||||
|
// large chips every flash address on the wire — and all slot arithmetic —
|
||||||
|
// is a word address instead ('J' always was one). A slot spans the same
|
||||||
|
// wire-high-byte pair in either unit (512 B = 2 x 256 bytes, 1 KiB =
|
||||||
|
// 2 x 256 words), so the slot index is the high byte with its low bit
|
||||||
|
// dropped everywhere.
|
||||||
|
constexpr bool word_flash = spm::flash_bytes > 65536;
|
||||||
|
constexpr std::uint16_t wire_base =
|
||||||
|
word_flash ? static_cast<std::uint16_t>(base / 2) : static_cast<std::uint16_t>(base);
|
||||||
|
constexpr std::uint16_t wire_page_mask = word_flash ? (page / 2 - 1) : (page - 1);
|
||||||
|
|
||||||
|
// The activation window, in seconds, is a compile-time constant (the build
|
||||||
|
// may override it): the whole EEPROM belongs to the application, and
|
||||||
|
// re-timing the loader is a bootloader self-update with a re-timed binary.
|
||||||
|
#if !defined(PUREBOOT_TIMEOUT)
|
||||||
|
#define PUREBOOT_TIMEOUT 8
|
||||||
|
#endif
|
||||||
|
constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
||||||
|
|
||||||
|
// The 12-byte info block the host reads with the 'b' command; flash-resident
|
||||||
|
// (there is no crt to copy a .data image), word-aligned so its wire (word)
|
||||||
|
// address is exact on the large chips. The page byte is the wire count
|
||||||
|
// convention: 0 means 256.
|
||||||
|
[[gnu::progmem]] alignas(2) inline constexpr std::array<std::uint8_t, 12> info_data = {
|
||||||
|
'P',
|
||||||
|
'B',
|
||||||
|
1, // magic, protocol version
|
||||||
|
avr::hw::db.signature[0],
|
||||||
|
avr::hw::db.signature[1],
|
||||||
|
avr::hw::db.signature[2],
|
||||||
|
static_cast<std::uint8_t>(page),
|
||||||
|
wire_base & 0xff,
|
||||||
|
wire_base >> 8, // app flash ends here; resident loader base (a word address on large chips)
|
||||||
|
avr::hw::db.mem.eeprom_size & 0xff,
|
||||||
|
avr::hw::db.mem.eeprom_size >> 8,
|
||||||
|
// bit 0: host must patch the reset vector (no hardware boot section);
|
||||||
|
// bit 1: flash wire addresses are word addresses
|
||||||
|
static_cast<std::uint8_t>((boot_section ? 0 : 1) | (word_flash ? 2 : 0)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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; drain() holds until
|
||||||
|
// the last transmitted frame is fully on the wire (the jump hand-over must
|
||||||
|
// not let the target's re-init clip the ack).
|
||||||
|
template <avr::hertz_t C>
|
||||||
|
consteval std::int16_t rxc_field()
|
||||||
|
{
|
||||||
|
return avr::uart::detail::ufield<'0', "UCSR#A", "RXC#">();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <avr::hertz_t C>
|
||||||
|
consteval std::int16_t txc_field()
|
||||||
|
{
|
||||||
|
return avr::uart::detail::ufield<'0', "UCSR#A", "TXC#">();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <avr::hertz_t C>
|
||||||
|
consteval std::int16_t status_reg()
|
||||||
|
{
|
||||||
|
return avr::uart::detail::ureg<'0', "UCSR#A">();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <avr::hertz_t C>
|
||||||
|
struct hardware_link {
|
||||||
|
using uart = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>;
|
||||||
|
|
||||||
|
// The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2),
|
||||||
|
// sbiw + sbci + sbci + brne (6).
|
||||||
|
static constexpr std::uint8_t poll_cycles = 10;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drain()
|
||||||
|
{
|
||||||
|
// write() leaves the byte draining behind it. Clear a stale TXC0
|
||||||
|
// first (W1C by writing the sampled status back — the store a hand
|
||||||
|
// assembler writes, keeping U2X0), then wait for the fresh
|
||||||
|
// completion; with a byte still ahead in the shifter TXC0 cannot
|
||||||
|
// re-set until the last pending byte has fully left.
|
||||||
|
using status = avr::hw::reg_impl<status_reg<C>()>;
|
||||||
|
status::write(status::read());
|
||||||
|
while (!avr::hw::field_impl<txc_field<C>()>::test()) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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>;
|
||||||
|
|
||||||
|
// The compiled idle poll: sbis skipping the exit (2), sbiw + sbci +
|
||||||
|
// sbci + brne (6).
|
||||||
|
static constexpr std::uint8_t poll_cycles = 8;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drain()
|
||||||
|
{
|
||||||
|
// The software transmitter returns only after the stop bit.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using link = std::conditional_t<avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART"),
|
||||||
|
hardware_link<dev::clock>, software_link<dev::clock>>;
|
||||||
|
|
||||||
|
// The application's entry, an absolute address the linker pins (--defsym in
|
||||||
|
// CMakeLists.txt): 0x0000 on the mega (word 0 stays the application's own
|
||||||
|
// vector — BOOTRST re-vectors a reset into the loader in hardware) and the
|
||||||
|
// trampoline word at base - 2 on the tinies. Reaching it must not depend on
|
||||||
|
// where this copy runs, so the jump goes through a pointer: [[gnu::noipa]]
|
||||||
|
// keeps the constant from folding back into a PC-relative call.
|
||||||
|
extern "C" [[noreturn]] void pureboot_app();
|
||||||
|
|
||||||
|
[[gnu::noipa, noreturn]] void jump(void (*target)())
|
||||||
|
{
|
||||||
|
target();
|
||||||
|
__builtin_unreachable();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[gnu::noinline, noreturn]] void run_app()
|
||||||
|
{
|
||||||
|
jump(pureboot_app);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One activation window is a single 32-bit poll countdown. The divisor is
|
||||||
|
// the backend's counted poll-loop cycles (its own comment reads them off the
|
||||||
|
// compiled loop); whole-second precision is all the window promises, so the
|
||||||
|
// nearest cycle count is plenty.
|
||||||
|
consteval std::uint32_t window_polls()
|
||||||
|
{
|
||||||
|
return timeout_seconds * static_cast<std::uint32_t>(dev::clock.hz / link::poll_cycles);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pending_before_deadline()
|
||||||
|
{
|
||||||
|
std::uint32_t polls = window_polls();
|
||||||
|
do {
|
||||||
|
if (link::pending())
|
||||||
|
return true;
|
||||||
|
} while (--polls);
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
if (!pending_before_deadline())
|
||||||
|
run_app();
|
||||||
|
return link::rx();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint16_t rx16()
|
||||||
|
{
|
||||||
|
std::uint16_t low = link::rx();
|
||||||
|
return static_cast<std::uint16_t>(low | (link::rx() << 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The streamers take the count in the wire's 8-bit form: 0 means 256.
|
||||||
|
// send_flash stays out of line: its two callers ('b' and 'R') otherwise each
|
||||||
|
// inline a private copy of the loop. On the large chips the address is a
|
||||||
|
// word address and the read goes through ELPM (flash_load_far).
|
||||||
|
[[gnu::noinline]] void send_flash(std::uint16_t address, std::uint8_t count)
|
||||||
|
{
|
||||||
|
if constexpr (word_flash) {
|
||||||
|
// The 24-bit cursor as the machine holds it: the RAMPZ byte and a
|
||||||
|
// 16-bit Z, carried explicitly (the reassembled 32-bit address
|
||||||
|
// folds away inside the inlined far load). A single read never
|
||||||
|
// crosses a 64 KiB boundary — the protocol forbids it and the host
|
||||||
|
// splits its chunks there — so RAMPZ holds for the whole run.
|
||||||
|
std::uint8_t rampz = static_cast<std::uint8_t>(address >> 15);
|
||||||
|
std::uint16_t z = static_cast<std::uint16_t>(address << 1);
|
||||||
|
do {
|
||||||
|
link::tx(avr::flash_load_far<std::uint8_t>((static_cast<std::uint32_t>(rampz) << 16) | z));
|
||||||
|
if (++z == 0)
|
||||||
|
++rampz; // robustness for a host that reads across 64 KiB
|
||||||
|
} while (--count);
|
||||||
|
} else {
|
||||||
|
do
|
||||||
|
link::tx(avr::flash_load(reinterpret_cast<const std::uint8_t *>(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 — except the 512-byte slot this code runs
|
||||||
|
// in, which is drained but never programmed, so a copy can never erase
|
||||||
|
// itself. `slot_high` is the high byte of that running slot's base (run()
|
||||||
|
// derives it); a broken host thus cannot brick the running loader, and a
|
||||||
|
// copy flashed one slot lower may rewrite the slot above it — how pureboot
|
||||||
|
// updates itself. On the mega the RWW section is re-enabled so reads work
|
||||||
|
// immediately.
|
||||||
|
void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
|
||||||
|
{
|
||||||
|
// A buffer word cannot be loaded twice without an erase (§26.2.1), so a
|
||||||
|
// refused page's drained data must not linger for the next write:
|
||||||
|
// discard the buffer up front — CTPB on the tinies; on the megas
|
||||||
|
// writing RWWSRE aborts a pending load (§26.2.2 — on the m48s that
|
||||||
|
// flush is the bit's whole documented job).
|
||||||
|
if constexpr (rww_discard)
|
||||||
|
spm::rww_enable<off>();
|
||||||
|
else
|
||||||
|
spm::clear_buffer<off>();
|
||||||
|
// One induction either way. On the byte-addressed chips the wire address
|
||||||
|
// itself walks the page (aligned, so the offset bits wrap to zero); on
|
||||||
|
// the word-addressed large chips the wire word address becomes a 32-bit
|
||||||
|
// byte cursor once, and their 256-byte page makes its low byte the whole
|
||||||
|
// in-page offset. The slot index is one high byte of the wire address —
|
||||||
|
// two values on byte-addressed chips (the & ~1), bits 16:9 re-packed on
|
||||||
|
// the large ones.
|
||||||
|
spm::flash_address_t address;
|
||||||
|
std::uint8_t page_high;
|
||||||
|
if constexpr (word_flash) {
|
||||||
|
// Pages are aligned, so one page never crosses a 64 KiB boundary:
|
||||||
|
// RAMPZ is a per-page constant and the fill cursor is a 16-bit Z
|
||||||
|
// whose low byte is the whole in-page offset (256-byte pages). The
|
||||||
|
// slot index is simply the wire word address's high byte.
|
||||||
|
const std::uint8_t rampz = static_cast<std::uint8_t>(wire_address >> 15);
|
||||||
|
const std::uint16_t z0 = static_cast<std::uint16_t>(wire_address << 1);
|
||||||
|
std::uint16_t z = z0;
|
||||||
|
do {
|
||||||
|
std::uint8_t low = link::rx();
|
||||||
|
std::uint8_t high = link::rx();
|
||||||
|
spm::fill<off>((static_cast<spm::flash_address_t>(rampz) << 16) | z,
|
||||||
|
static_cast<std::uint16_t>(low | (high << 8)));
|
||||||
|
z += 2;
|
||||||
|
} while (static_cast<std::uint8_t>(z));
|
||||||
|
address = (static_cast<spm::flash_address_t>(rampz) << 16) | z0;
|
||||||
|
page_high = static_cast<std::uint8_t>(wire_address >> 8) & 0xfe;
|
||||||
|
} else {
|
||||||
|
address = static_cast<spm::flash_address_t>(wire_address);
|
||||||
|
do {
|
||||||
|
std::uint8_t low = link::rx();
|
||||||
|
std::uint8_t high = link::rx();
|
||||||
|
spm::fill<off>(address, static_cast<std::uint16_t>(low | (high << 8)));
|
||||||
|
address += 2;
|
||||||
|
} while (static_cast<std::uint8_t>(address) & (page - 1));
|
||||||
|
address -= 2; // back inside the page — erase and write ignore the word bits
|
||||||
|
page_high = static_cast<std::uint8_t>(address >> 8) & 0xfe;
|
||||||
|
}
|
||||||
|
if (page_high != slot_high) {
|
||||||
|
// The tinies and the m48s halt the CPU through the erase and the
|
||||||
|
// write, so only the boot-sectioned megas — running on while their
|
||||||
|
// RWW section programs — wait.
|
||||||
|
spm::erase_page<off>(address);
|
||||||
|
if constexpr (boot_section)
|
||||||
|
spm::wait();
|
||||||
|
spm::write_page<off>(address);
|
||||||
|
if constexpr (boot_section) {
|
||||||
|
spm::wait();
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
std::uint8_t which = 0;
|
||||||
|
do
|
||||||
|
link::tx(spm::read_fuse<off>(static_cast<spm::fuse>(which)));
|
||||||
|
while (++which & 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[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.
|
||||||
|
// The flag register is MCUSR, or the classic megas' MCUCSR.
|
||||||
|
if (avr::hw::field_impl<wdrf_field()>::test())
|
||||||
|
run_app();
|
||||||
|
|
||||||
|
link::init();
|
||||||
|
|
||||||
|
// The high byte of the 512-byte-aligned base this copy runs at: the
|
||||||
|
// return address is a word address, whose high byte is the 256-word slot
|
||||||
|
// index — on byte-addressed chips doubled back into byte terms.
|
||||||
|
// program_flash refuses this one slot and the info block is addressed
|
||||||
|
// from it, so both follow wherever the code was flashed.
|
||||||
|
const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
|
||||||
|
const std::uint8_t slot_high =
|
||||||
|
word_flash ? static_cast<std::uint8_t>(ra_words >> 8) & 0xfe : static_cast<std::uint8_t>((ra_words >> 8) << 1);
|
||||||
|
|
||||||
|
// 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() != 'p' || rx_deadline() != '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);
|
||||||
|
const std::uint8_t command = link::rx();
|
||||||
|
switch (command) {
|
||||||
|
case 'b': { // info block, read relative to the running slot
|
||||||
|
// The block sits in the image's first 256 bytes (the build lint
|
||||||
|
// asserts it), and slots are 512-aligned — so the low byte of its
|
||||||
|
// link address (in wire units: bytes, or words on the large
|
||||||
|
// chips) is its offset in any slot, and the high byte of its
|
||||||
|
// runtime address is the running slot's. Built as a byte pair so
|
||||||
|
// no absolute address is ever materialized.
|
||||||
|
const auto link_low = reinterpret_cast<std::uint16_t>(info_data.data());
|
||||||
|
const std::uint8_t low =
|
||||||
|
word_flash ? static_cast<std::uint8_t>(link_low >> 1) : static_cast<std::uint8_t>(link_low);
|
||||||
|
send_flash(std::bit_cast<std::uint16_t>(std::array{low, slot_high}),
|
||||||
|
static_cast<std::uint8_t>(info_data.size()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'J': { // jump to a wire word address: hand-over and staging transfer
|
||||||
|
auto target = reinterpret_cast<void (*)()>(rx16());
|
||||||
|
link::tx(ack);
|
||||||
|
link::drain();
|
||||||
|
jump(target);
|
||||||
|
}
|
||||||
|
case 'R': // read flash: addr16, n8 (0 = 256)
|
||||||
|
case 'r': // read EEPROM: addr16, n8
|
||||||
|
case 'w': { // write EEPROM: addr16, n8, then n bytes each acked
|
||||||
|
std::uint16_t address = rx16();
|
||||||
|
std::uint8_t count = link::rx();
|
||||||
|
if (command == 'R')
|
||||||
|
send_flash(address, count);
|
||||||
|
else if (command == 'r')
|
||||||
|
send_eeprom(address, count);
|
||||||
|
else
|
||||||
|
store_eeprom(address, count);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'W': // program one flash page: addr16, page bytes
|
||||||
|
program_flash(rx16(), slot_high);
|
||||||
|
break;
|
||||||
|
case 'F': // fuse and lock bytes
|
||||||
|
send_fuses();
|
||||||
|
break;
|
||||||
|
default: // unknown bytes are ignored; the loop re-acks
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace pureboot
|
||||||
|
|
||||||
|
template struct avr::startup::entry<pureboot::run>;
|
||||||
961
pureboot/pureboot.py
Normal file
961
pureboot/pureboot.py
Normal file
@@ -0,0 +1,961 @@
|
|||||||
|
#!/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, the hand-over jump,
|
||||||
|
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. Page 0 and the trampoline are written first, so every interruption
|
||||||
|
point of a flash leaves the chip reset-recoverable into the loader.
|
||||||
|
|
||||||
|
It also updates the loader itself (--update-loader): pureboot's image is
|
||||||
|
position-independent, so the tool installs the identical binary one 512-byte
|
||||||
|
slot below the resident loader, jumps into that staging copy, lets it rewrite
|
||||||
|
the resident slot, and restores what the staging slot held — resumable at
|
||||||
|
every phase from the flash state plus a host-side state file carrying the
|
||||||
|
saved bytes.
|
||||||
|
|
||||||
|
Python standard library only; the serial port is driven with termios on POSIX
|
||||||
|
and the Win32 serial API (through ctypes) on Windows, so any tty or COM port
|
||||||
|
works — a USB adapter as well as a simavr pty.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
if os.name == "nt":
|
||||||
|
import ctypes
|
||||||
|
from ctypes import wintypes
|
||||||
|
else:
|
||||||
|
import select
|
||||||
|
import termios
|
||||||
|
|
||||||
|
PROMPT = b"+"
|
||||||
|
PROTOCOL_VERSION = 1
|
||||||
|
SLOT = 512 # the loader slot on byte-addressed chips; word-addressed ones (>64 KiB) use 1 KiB — their own smallest boot sector
|
||||||
|
|
||||||
|
|
||||||
|
class Error(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- serial ---
|
||||||
|
|
||||||
|
|
||||||
|
class PosixPort:
|
||||||
|
"""A raw serial port with deadline-based reads, over termios."""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
if os.name == "nt":
|
||||||
|
# The same port, over the Win32 serial API — kernel32 through ctypes, so
|
||||||
|
# the tool stays standard-library only. Timeouts live in the driver
|
||||||
|
# (COMMTIMEOUTS) rather than in a readiness call: Windows has no select()
|
||||||
|
# for a COM handle, so each read asks the driver for its own deadline.
|
||||||
|
|
||||||
|
_GENERIC_READ, _GENERIC_WRITE = 0x80000000, 0x40000000
|
||||||
|
_OPEN_EXISTING, _PURGE_RXCLEAR = 3, 0x0008
|
||||||
|
_INVALID_HANDLE = wintypes.HANDLE(-1).value
|
||||||
|
# A gap this long ends a read_available(): longer than the coalescing a
|
||||||
|
# USB-serial adapter's latency timer imposes (16 ms on FTDI parts), so a
|
||||||
|
# burst is not split, short enough to stay responsive.
|
||||||
|
_GAP_MS = 30
|
||||||
|
|
||||||
|
class _DCB(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("DCBlength", wintypes.DWORD),
|
||||||
|
("BaudRate", wintypes.DWORD),
|
||||||
|
("fBits", wintypes.DWORD), # the packed flag bitfield, set below
|
||||||
|
("wReserved", wintypes.WORD),
|
||||||
|
("XonLim", wintypes.WORD),
|
||||||
|
("XoffLim", wintypes.WORD),
|
||||||
|
("ByteSize", wintypes.BYTE),
|
||||||
|
("Parity", wintypes.BYTE),
|
||||||
|
("StopBits", wintypes.BYTE),
|
||||||
|
("XonChar", ctypes.c_char),
|
||||||
|
("XoffChar", ctypes.c_char),
|
||||||
|
("ErrorChar", ctypes.c_char),
|
||||||
|
("EofChar", ctypes.c_char),
|
||||||
|
("EvtChar", ctypes.c_char),
|
||||||
|
("wReserved1", wintypes.WORD),
|
||||||
|
]
|
||||||
|
|
||||||
|
class _COMMTIMEOUTS(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("ReadIntervalTimeout", wintypes.DWORD),
|
||||||
|
("ReadTotalTimeoutMultiplier", wintypes.DWORD),
|
||||||
|
("ReadTotalTimeoutConstant", wintypes.DWORD),
|
||||||
|
("WriteTotalTimeoutMultiplier", wintypes.DWORD),
|
||||||
|
("WriteTotalTimeoutConstant", wintypes.DWORD),
|
||||||
|
]
|
||||||
|
|
||||||
|
_k32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||||
|
_LPDWORD = ctypes.POINTER(wintypes.DWORD)
|
||||||
|
# Declared, not inferred: a HANDLE is a pointer, and a defaulted int
|
||||||
|
# return would truncate it on 64-bit.
|
||||||
|
_k32.CreateFileW.restype = wintypes.HANDLE
|
||||||
|
_k32.CreateFileW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD,
|
||||||
|
wintypes.LPVOID, wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE]
|
||||||
|
_k32.ReadFile.argtypes = [wintypes.HANDLE, wintypes.LPVOID, wintypes.DWORD, _LPDWORD, wintypes.LPVOID]
|
||||||
|
_k32.WriteFile.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, wintypes.DWORD, _LPDWORD, wintypes.LPVOID]
|
||||||
|
_k32.GetCommState.argtypes = [wintypes.HANDLE, ctypes.POINTER(_DCB)]
|
||||||
|
_k32.SetCommState.argtypes = [wintypes.HANDLE, ctypes.POINTER(_DCB)]
|
||||||
|
_k32.SetCommTimeouts.argtypes = [wintypes.HANDLE, ctypes.POINTER(_COMMTIMEOUTS)]
|
||||||
|
_k32.PurgeComm.argtypes = [wintypes.HANDLE, wintypes.DWORD]
|
||||||
|
_k32.CloseHandle.argtypes = [wintypes.HANDLE]
|
||||||
|
|
||||||
|
def _fail(what):
|
||||||
|
code = ctypes.get_last_error()
|
||||||
|
raise Error(f"{what}: {ctypes.FormatError(code).strip()} (Windows error {code})")
|
||||||
|
|
||||||
|
class WindowsPort:
|
||||||
|
"""A raw serial port with deadline-based reads, over Win32."""
|
||||||
|
|
||||||
|
def __init__(self, path, baud):
|
||||||
|
# Win32 takes the rate as a plain integer, so unlike termios any
|
||||||
|
# rate the hardware can divide down to is available — but a driver
|
||||||
|
# may also accept one it cannot produce (an FT232R takes a baud of
|
||||||
|
# 3, reports it back, and goes on using the previous divisor).
|
||||||
|
# Only obvious nonsense is refusable; the rest is the driver's word.
|
||||||
|
if baud < 50:
|
||||||
|
raise Error(f"unsupported baud rate {baud}")
|
||||||
|
# \\.\COM6: the device-namespace form. A bare COMn resolves only
|
||||||
|
# for n < 10, and double-digit ports are routine on Windows.
|
||||||
|
if path.lower().startswith("com") and path[3:].isdigit():
|
||||||
|
path = rf"\\.\{path}"
|
||||||
|
self.handle = _k32.CreateFileW(
|
||||||
|
path, _GENERIC_READ | _GENERIC_WRITE, 0, None, _OPEN_EXISTING, 0, None
|
||||||
|
)
|
||||||
|
if self.handle == _INVALID_HANDLE:
|
||||||
|
_fail(f"cannot open {path}")
|
||||||
|
self.timeouts = None
|
||||||
|
try:
|
||||||
|
dcb = _DCB()
|
||||||
|
dcb.DCBlength = ctypes.sizeof(_DCB)
|
||||||
|
if not _k32.GetCommState(self.handle, ctypes.byref(dcb)):
|
||||||
|
_fail(f"cannot read the state of {path}")
|
||||||
|
dcb.BaudRate, dcb.ByteSize, dcb.Parity, dcb.StopBits = baud, 8, 0, 0 # 8N1
|
||||||
|
# fBinary, and DTR/RTS asserted (fDtrControl and fRtsControl,
|
||||||
|
# two bits each, = _ENABLE); every other flag clear, so no
|
||||||
|
# parity and no flow control. Raising both matches what opening
|
||||||
|
# a POSIX tty does — including the reset pulse on the boards
|
||||||
|
# that wire DTR to it.
|
||||||
|
dcb.fBits = 0x1 | (1 << 4) | (1 << 12)
|
||||||
|
if not _k32.SetCommState(self.handle, ctypes.byref(dcb)):
|
||||||
|
_fail(f"cannot configure {path} for {baud} baud 8N1")
|
||||||
|
# Arm them once here too: reads re-arm per call, but the write
|
||||||
|
# timeout would otherwise stay at the driver's default — which
|
||||||
|
# may be "wait forever" — until the first read.
|
||||||
|
self._deadline(_GAP_MS, 1000)
|
||||||
|
except Error:
|
||||||
|
# An open port outlives the exception otherwise, and a COM
|
||||||
|
# handle is exclusive: the next attempt would meet its own
|
||||||
|
# leftover as "Access is denied".
|
||||||
|
self.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
_k32.CloseHandle(self.handle)
|
||||||
|
|
||||||
|
def _deadline(self, interval, total):
|
||||||
|
"""Arm the driver's read timeouts: `interval` ms of quiet ends a
|
||||||
|
read once bytes have arrived, `total` ms ends it regardless."""
|
||||||
|
if self.timeouts == (interval, total):
|
||||||
|
return
|
||||||
|
spec = _COMMTIMEOUTS()
|
||||||
|
spec.ReadIntervalTimeout = interval
|
||||||
|
spec.ReadTotalTimeoutConstant = total
|
||||||
|
spec.WriteTotalTimeoutConstant = 5000
|
||||||
|
if not _k32.SetCommTimeouts(self.handle, ctypes.byref(spec)):
|
||||||
|
_fail("cannot set the port timeouts")
|
||||||
|
self.timeouts = (interval, total)
|
||||||
|
|
||||||
|
def _read(self, count):
|
||||||
|
buffer = ctypes.create_string_buffer(count)
|
||||||
|
got = wintypes.DWORD()
|
||||||
|
if not _k32.ReadFile(self.handle, buffer, count, ctypes.byref(got), None):
|
||||||
|
_fail("read failed")
|
||||||
|
return buffer.raw[: got.value]
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
written = wintypes.DWORD()
|
||||||
|
if not _k32.WriteFile(self.handle, data, len(data), ctypes.byref(written), None):
|
||||||
|
_fail("write failed")
|
||||||
|
if written.value != len(data):
|
||||||
|
raise Error(f"short write: {written.value} of {len(data)} bytes")
|
||||||
|
|
||||||
|
def flush_input(self):
|
||||||
|
if not _k32.PurgeComm(self.handle, _PURGE_RXCLEAR):
|
||||||
|
_fail("cannot flush the input buffer")
|
||||||
|
|
||||||
|
def read_available(self, wait):
|
||||||
|
"""Everything that arrives within `wait` seconds of quiet start."""
|
||||||
|
# A zero total means *no* timeout to the driver, so never round
|
||||||
|
# down to it — the same trap on the deadline below.
|
||||||
|
self._deadline(_GAP_MS, max(1, round(wait * 1000)))
|
||||||
|
return self._read(4096)
|
||||||
|
|
||||||
|
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")
|
||||||
|
# No interval timeout here: only the count or the deadline
|
||||||
|
# ends the read, so a gap mid-reply is simply waited out.
|
||||||
|
self._deadline(0, max(1, round(remaining * 1000)))
|
||||||
|
data += self._read(count - len(data))
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
Port = WindowsPort if os.name == "nt" else PosixPort
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------- 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.raw = bytes(raw)
|
||||||
|
self.signature = raw[3:6]
|
||||||
|
self.page = raw[6] or 256 # the wire count convention: 0 means 256
|
||||||
|
self.patch_vector = bool(raw[11] & 1)
|
||||||
|
# Large chips speak word addresses for flash (bit 1); the host keeps
|
||||||
|
# every address in bytes and converts at the wire.
|
||||||
|
self.word_flash = bool(raw[11] & 2)
|
||||||
|
scale = 2 if self.word_flash else 1
|
||||||
|
self.base = (raw[7] | (raw[8] << 8)) * scale
|
||||||
|
self.eeprom_size = raw[9] | (raw[10] << 8)
|
||||||
|
self.slot = 1024 if self.word_flash else SLOT
|
||||||
|
self.flash_size = self.base + self.slot
|
||||||
|
self.stage = self.base - self.slot # where a staging copy of the loader goes
|
||||||
|
# The hand-over target, as the word address 'J' takes: the trampoline
|
||||||
|
# below the loader (tinies), or word 0 (mega — the application's own
|
||||||
|
# reset vector; BOOTRST re-vectors a reset into the loader instead).
|
||||||
|
self.app_entry_word = (self.base - 2) // 2 if self.patch_vector else 0
|
||||||
|
|
||||||
|
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 — except
|
||||||
|
jump(), after which the target must be knocked afresh."""
|
||||||
|
|
||||||
|
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, address_scale=1):
|
||||||
|
data = b""
|
||||||
|
while count:
|
||||||
|
chunk = min(count, 256)
|
||||||
|
wire = address // address_scale
|
||||||
|
head = bytes((ord(command), wire & 0xFF, wire >> 8, chunk & 0xFF))
|
||||||
|
data += self._command(head, chunk, 5.0)
|
||||||
|
address += chunk
|
||||||
|
count -= chunk
|
||||||
|
return data
|
||||||
|
|
||||||
|
def read_flash(self, address, count):
|
||||||
|
if not self.info.word_flash:
|
||||||
|
return self._stream_read("R", address, count)
|
||||||
|
# Word-addressed wire: widen to even bounds and never let one read
|
||||||
|
# cross a 64 KiB boundary (the device holds RAMPZ for a whole run).
|
||||||
|
start = address & ~1
|
||||||
|
span = (address + count + 1 & ~1) - start
|
||||||
|
data = b""
|
||||||
|
at = start
|
||||||
|
remaining = span
|
||||||
|
while remaining:
|
||||||
|
chunk = min(remaining, 0x10000 - (at & 0xFFFF))
|
||||||
|
data += self._stream_read("R", at, chunk, address_scale=2)
|
||||||
|
at += chunk
|
||||||
|
remaining -= chunk
|
||||||
|
return data[address - start : address - start + 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
|
||||||
|
wire = address // (2 if self.info.word_flash else 1)
|
||||||
|
head = bytes((ord("W"), wire & 0xFF, wire >> 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 jump(self, word_address):
|
||||||
|
"""'J': the device acks, then execution continues at the word
|
||||||
|
address — a loader slot's base (whose copy must then be knocked
|
||||||
|
afresh) or the application entry."""
|
||||||
|
self.port.write(bytes((ord("J"), word_address & 0xFF, word_address >> 8)))
|
||||||
|
self._expect_prompt()
|
||||||
|
|
||||||
|
def enter_copy(self, byte_address, wait):
|
||||||
|
"""Jump into the loader copy at `byte_address` and knock it. Ending
|
||||||
|
up in the copy addressed is guaranteed by construction: a jump to a
|
||||||
|
slot base lands in that slot's entry stub."""
|
||||||
|
self.jump(byte_address // 2)
|
||||||
|
return self.connect(wait)
|
||||||
|
|
||||||
|
def run_application(self):
|
||||||
|
self.jump(self.info.app_entry_word)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 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 = 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, info, skip_blank):
|
||||||
|
"""Pages in programming order; optionally dropping all-0xff pages (sound
|
||||||
|
only over erased flash) — never a load-bearing one.
|
||||||
|
|
||||||
|
With a patched vector (tinies), the patched page 0 goes first and the
|
||||||
|
trampoline page second: from the first write on, a reset lands in the
|
||||||
|
loader and the loader's own fall-through lands on the application entry,
|
||||||
|
so every interruption point of the flash is recoverable. With a hardware
|
||||||
|
boot section a reset re-vectors to the loader regardless; ascending
|
||||||
|
order, page 0 last, maximizes what an interrupted image retains."""
|
||||||
|
trampoline_page = info.base - info.page if info.patch_vector else None
|
||||||
|
first = [0, trampoline_page] if info.patch_vector else []
|
||||||
|
rest = [a for a in sorted(pages) if a not in first]
|
||||||
|
if skip_blank:
|
||||||
|
rest = [a for a in rest if pages[a].count(0xFF) != len(pages[a])]
|
||||||
|
order = [a for a in first if a in pages] + rest
|
||||||
|
if not info.patch_vector:
|
||||||
|
order = [a for a in order if a != 0] + ([0] if 0 in pages else [])
|
||||||
|
return order
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------- fuses ---
|
||||||
|
|
||||||
|
|
||||||
|
# Per-chip boot fuse geometry, keyed by the signature's family/part bytes:
|
||||||
|
# which byte of the 'F' reply (low, lock, extended, high) carries BOOTSZ/
|
||||||
|
# BOOTRST, and the BOOTSZ->words ladder. A die revision shares its base
|
||||||
|
# signature, so one row covers it. The m48s have no boot section and no
|
||||||
|
# row — their info block says patch-vector and this table is never
|
||||||
|
# consulted. Sources: Atmel-2486/2466/2503 (HIGH fuse), Atmel-2545/8271/
|
||||||
|
# DS40002065 (x8: EXTENDED, except the m328s' HIGH), Atmel-8272/8011/2593/
|
||||||
|
# 42719 (x4: HIGH).
|
||||||
|
_LADDER_128 = {0b11: 128, 0b10: 256, 0b01: 512, 0b00: 1024}
|
||||||
|
_LADDER_256 = {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}
|
||||||
|
_LADDER_512 = {0b11: 512, 0b10: 1024, 0b01: 2048, 0b00: 4096}
|
||||||
|
BOOT_FUSE = {
|
||||||
|
bytes((0x93, 0x07)): (3, _LADDER_128), # m8/8A
|
||||||
|
bytes((0x94, 0x03)): (3, _LADDER_128), # m16/16A
|
||||||
|
bytes((0x95, 0x02)): (3, _LADDER_256), # m32/32A
|
||||||
|
bytes((0x93, 0x0A)): (2, _LADDER_128), # m88/88A
|
||||||
|
bytes((0x93, 0x0F)): (2, _LADDER_128), # m88P/88PA
|
||||||
|
bytes((0x94, 0x06)): (2, _LADDER_128), # m168/168A
|
||||||
|
bytes((0x94, 0x0B)): (2, _LADDER_128), # m168P/168PA
|
||||||
|
bytes((0x95, 0x14)): (3, _LADDER_256), # m328
|
||||||
|
bytes((0x95, 0x0F)): (3, _LADDER_256), # m328P
|
||||||
|
bytes((0x94, 0x0F)): (3, _LADDER_128), # m164A
|
||||||
|
bytes((0x94, 0x0A)): (3, _LADDER_128), # m164P/164PA
|
||||||
|
bytes((0x95, 0x15)): (3, _LADDER_256), # m324A
|
||||||
|
bytes((0x95, 0x08)): (3, _LADDER_256), # m324P
|
||||||
|
bytes((0x95, 0x11)): (3, _LADDER_256), # m324PA
|
||||||
|
bytes((0x96, 0x09)): (3, _LADDER_512), # m644/644A
|
||||||
|
bytes((0x96, 0x0A)): (3, _LADDER_512), # m644P/644PA
|
||||||
|
bytes((0x97, 0x06)): (3, _LADDER_512), # m1284
|
||||||
|
bytes((0x97, 0x05)): (3, _LADDER_512), # m1284P
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def mega_boot(info, fuse_bytes):
|
||||||
|
"""Decode a mega's boot configuration from its fuses (the byte and the
|
||||||
|
BOOTSZ ladder are per chip): BOOTSZ1:0 in bits 2:1 select the
|
||||||
|
boot-section words, BOOTRST in bit 0 (programmed = 0) re-vectors reset
|
||||||
|
to its start. Returns (bootrst_programmed, boot_section_start_byte)."""
|
||||||
|
entry = BOOT_FUSE.get(bytes(info.signature[1:3]))
|
||||||
|
if entry is None:
|
||||||
|
raise Error(f"unknown mega signature {info.signature.hex()} — no boot fuse map")
|
||||||
|
which, ladder = entry
|
||||||
|
fuse = fuse_bytes[which]
|
||||||
|
words = ladder[(fuse >> 1) & 0x03]
|
||||||
|
return (fuse & 1) == 0, info.flash_size - words * 2
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- loader update ---
|
||||||
|
|
||||||
|
|
||||||
|
def image_info(image):
|
||||||
|
"""The info block embedded in a pureboot binary, or None."""
|
||||||
|
at = image.find(b"PB" + bytes((PROTOCOL_VERSION,)))
|
||||||
|
return Info(image[at : at + 12]) if 0 <= at <= len(image) - 12 else None
|
||||||
|
|
||||||
|
|
||||||
|
def loader_image(path):
|
||||||
|
"""A loader update image, as the slot's own content. A raw binary is that
|
||||||
|
already; an Intel HEX links the loader at its base inside an otherwise
|
||||||
|
blank flash image, and load_image() anchors every image at zero, so the
|
||||||
|
blank below the base is dropped here. The base comes from the image's own
|
||||||
|
info block rather than the device's, so an image built for somewhere else
|
||||||
|
survives intact and the preflight can say so."""
|
||||||
|
image = load_image(path)
|
||||||
|
embedded = image_info(image)
|
||||||
|
if embedded and len(image) > embedded.base:
|
||||||
|
image = image[embedded.base :]
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
def staging_content(image, info):
|
||||||
|
"""The 512-byte staging-slot content: the image, padding, and — on
|
||||||
|
chips whose hand-over jumps through the word below the resident loader —
|
||||||
|
that word, which for a staging copy is the slot's own last word: an rjmp
|
||||||
|
to the resident base. The staging copy's fall-through and 'J'-free exit
|
||||||
|
both land in a loader instead of garbage."""
|
||||||
|
slot = info.slot
|
||||||
|
if len(image) > (slot - 2 if info.patch_vector else slot):
|
||||||
|
raise Error(f"loader image is {len(image)} B, the slot holds {slot - 2 if info.patch_vector else slot}")
|
||||||
|
content = bytearray(image) + bytearray([0xFF] * (slot - len(image)))
|
||||||
|
if info.patch_vector:
|
||||||
|
through = rjmp_to((info.base - 2) // 2, info.base // 2, info.flash_size // 2)
|
||||||
|
content[slot - 2], content[slot - 1] = through & 0xFF, through >> 8
|
||||||
|
return bytes(content)
|
||||||
|
|
||||||
|
|
||||||
|
def update_preflight(image, info, fuse_bytes):
|
||||||
|
"""Errors and warnings before any flash is touched. Returns warnings."""
|
||||||
|
embedded = image_info(image)
|
||||||
|
if embedded is None:
|
||||||
|
raise Error("no pureboot info block in the update image — not a pureboot binary?")
|
||||||
|
if embedded.raw[3:] != info.raw[3:]:
|
||||||
|
raise Error(
|
||||||
|
f"update image is for another target: it declares "
|
||||||
|
f"[{embedded.describe()}], the device says [{info.describe()}]"
|
||||||
|
)
|
||||||
|
warnings = []
|
||||||
|
if not info.patch_vector:
|
||||||
|
if fuse_bytes is None:
|
||||||
|
raise Error("a loader update on this chip needs its fuses — unreadable? pass --assume-fuses")
|
||||||
|
bootrst, bls_start = mega_boot(info, fuse_bytes)
|
||||||
|
if info.stage < bls_start:
|
||||||
|
raise Error(
|
||||||
|
f"cannot self-update: the staging slot {info.stage:#06x} lies below the "
|
||||||
|
f"boot section ({bls_start:#06x}) where SPM is disabled "
|
||||||
|
f"— a boot section of at least two slots ({2 * info.slot} B, BOOTSZ) is "
|
||||||
|
f"required, and only an external programmer can change fuses"
|
||||||
|
)
|
||||||
|
if not bootrst:
|
||||||
|
warnings.append(
|
||||||
|
"BOOTRST unprogrammed: reset boots the application throughout the update; "
|
||||||
|
"an interruption is recovered by re-running this update"
|
||||||
|
)
|
||||||
|
elif bls_start == info.stage:
|
||||||
|
warnings.append(
|
||||||
|
"BOOTRST targets the staging slot: brief unrecoverable windows exist while "
|
||||||
|
"the staging copy itself is being installed or retired (page-write scale)"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
warnings.append(
|
||||||
|
f"BOOTRST targets {bls_start:#06x}, inside application flash: reset reaches a "
|
||||||
|
f"loader only across erased flash from there"
|
||||||
|
)
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateState:
|
||||||
|
"""The host-side memory of an update in flight: what the staging slot
|
||||||
|
held (and page 0, where the update repoints it). Losing this file after
|
||||||
|
the staging slot was overwritten loses those saved bytes — the update
|
||||||
|
still completes, but the staging region can then only be restored by
|
||||||
|
reflashing the application."""
|
||||||
|
|
||||||
|
def __init__(self, path):
|
||||||
|
self.path = path
|
||||||
|
self.data = None
|
||||||
|
|
||||||
|
def load_or_save(self, loader):
|
||||||
|
info = loader.info
|
||||||
|
if os.path.exists(self.path):
|
||||||
|
self.data = json.load(open(self.path))
|
||||||
|
if bytes.fromhex(self.data["signature"]) != info.signature or self.data["base"] != info.base:
|
||||||
|
raise Error(f"{self.path} belongs to a different device — remove it to start over")
|
||||||
|
return
|
||||||
|
self.data = {
|
||||||
|
"signature": info.signature.hex(),
|
||||||
|
"base": info.base,
|
||||||
|
"staging": loader.read_flash(info.stage, info.slot).hex(),
|
||||||
|
"page0": loader.read_flash(0, info.page).hex() if info.patch_vector else "",
|
||||||
|
}
|
||||||
|
with open(self.path, "w") as f:
|
||||||
|
json.dump(self.data, f)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def staging(self):
|
||||||
|
return bytes.fromhex(self.data["staging"])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def page0(self):
|
||||||
|
return bytes.fromhex(self.data["page0"])
|
||||||
|
|
||||||
|
def discard(self):
|
||||||
|
os.unlink(self.path)
|
||||||
|
|
||||||
|
|
||||||
|
def write_differing(loader, base, content, order=None):
|
||||||
|
"""Program the pages of `content` at `base` that differ from flash —
|
||||||
|
idempotent, so a resumed phase redoes only what an interruption left."""
|
||||||
|
page = loader.info.page
|
||||||
|
offsets = order if order is not None else range(0, len(content), page)
|
||||||
|
written = 0
|
||||||
|
for offset in offsets:
|
||||||
|
want = content[offset : offset + page]
|
||||||
|
if loader.read_flash(base + offset, page) != want:
|
||||||
|
loader.write_page(base + offset, want)
|
||||||
|
written += 1
|
||||||
|
for at in range(0, len(content), 256):
|
||||||
|
if loader.read_flash(base + at, min(256, len(content) - at)) != content[at : at + 256]:
|
||||||
|
raise Error(f"verify failed at {base + at:#06x} after programming")
|
||||||
|
return written
|
||||||
|
|
||||||
|
|
||||||
|
def patch_word0(loader, page0, target_base):
|
||||||
|
"""Rewrite page 0 with its word 0 re-aimed at `target_base` — the
|
||||||
|
resume insurance around rewriting a loader slot the reset path uses."""
|
||||||
|
info = loader.info
|
||||||
|
patched = bytearray(page0)
|
||||||
|
word = rjmp_to(0, target_base // 2, info.flash_size // 2)
|
||||||
|
patched[0], patched[1] = word & 0xFF, word >> 8
|
||||||
|
write_differing(loader, 0, bytes(patched))
|
||||||
|
return bytes(patched)
|
||||||
|
|
||||||
|
|
||||||
|
def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
||||||
|
"""Replace the resident loader with `path`, using the loader itself as
|
||||||
|
its own staging loader. Every phase is idempotent and keyed off the
|
||||||
|
actual flash state, so a re-run after any interruption resumes; the
|
||||||
|
state file carries the bytes the staging slot held."""
|
||||||
|
info = loader.info
|
||||||
|
image = loader_image(path)
|
||||||
|
for warning in update_preflight(image, info, fuse_bytes):
|
||||||
|
print(f"note: {warning}")
|
||||||
|
staged = staging_content(image, info)
|
||||||
|
resident = bytes(image) + bytes([0xFF] * (info.slot - len(image)))
|
||||||
|
page = info.page
|
||||||
|
|
||||||
|
state = UpdateState(state_path)
|
||||||
|
state.load_or_save(loader)
|
||||||
|
|
||||||
|
# Install the staging copy. On a chip whose staging slot starts at
|
||||||
|
# address 0 (the 1 KB tiny13A), its first page carries the reset vector:
|
||||||
|
# written last, so any earlier interruption still resets into the old
|
||||||
|
# resident, and from then on resets enter the staging copy.
|
||||||
|
order = list(range(0, info.slot, page))
|
||||||
|
if info.stage == 0:
|
||||||
|
order = order[1:] + [0]
|
||||||
|
if write_differing(loader, info.stage, staged, order):
|
||||||
|
print(f"staging copy installed at {info.stage:#06x}")
|
||||||
|
|
||||||
|
# Enter it and let it rewrite the resident slot. Where a patched reset
|
||||||
|
# vector routes through the resident (a tiny with the staging slot away
|
||||||
|
# from page 0), word 0 is re-aimed at the staging copy around the
|
||||||
|
# rewrite, so a power failure mid-rewrite still resets into a loader.
|
||||||
|
loader.enter_copy(info.stage, wait)
|
||||||
|
redirect = info.patch_vector and info.stage != 0
|
||||||
|
if redirect:
|
||||||
|
patch_word0(loader, state.page0, info.stage)
|
||||||
|
if write_differing(loader, info.base, resident):
|
||||||
|
print(f"resident loader rewritten at {info.base:#06x}")
|
||||||
|
|
||||||
|
# Enter the new resident and put the staging region back: page 0 first
|
||||||
|
# where it lives in that region (word 0 then points at the new resident
|
||||||
|
# for the rest of the restore), the saved trampoline with the rest.
|
||||||
|
loader.enter_copy(info.base, wait)
|
||||||
|
if redirect:
|
||||||
|
write_differing(loader, 0, state.page0)
|
||||||
|
order = list(range(0, info.slot, page))
|
||||||
|
if info.stage == 0:
|
||||||
|
order = [0] + order[1:]
|
||||||
|
write_differing(loader, info.stage, state.staging, order)
|
||||||
|
|
||||||
|
state.discard()
|
||||||
|
print(f"loader updated: {len(image)} B at {info.base:#06x}, staging region restored")
|
||||||
|
|
||||||
|
|
||||||
|
def check_walk_region(pages, info, fuse_bytes, force):
|
||||||
|
"""With BOOTRST programmed but targeting below the loader, reset reaches
|
||||||
|
the loader only by walking across erased flash from the boot-section
|
||||||
|
start; application data in that span would divert reset into itself.
|
||||||
|
Only checkable when the fuses are known (--fuses or --assume-fuses)."""
|
||||||
|
if info.patch_vector or fuse_bytes is None:
|
||||||
|
return
|
||||||
|
bootrst, bls_start = mega_boot(info, fuse_bytes)
|
||||||
|
if not bootrst or bls_start >= info.base:
|
||||||
|
return
|
||||||
|
overlap = [a for a in sorted(pages) if a >= bls_start and pages[a].count(0xFF) != len(pages[a])]
|
||||||
|
if overlap and not force:
|
||||||
|
raise Error(
|
||||||
|
f"the image writes {overlap[0]:#06x}.. inside the reset walk region "
|
||||||
|
f"[{bls_start:#06x}, {info.base:#06x}) (BOOTRST programmed): reset could no "
|
||||||
|
f"longer reach the loader — --force to flash it anyway"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ operations ---
|
||||||
|
|
||||||
|
|
||||||
|
def op_erase_flash(loader):
|
||||||
|
"""0xff over the whole application area. Descending on a patched-vector
|
||||||
|
chip: page 0 — the patched reset vector — goes last, so an interrupted
|
||||||
|
erase still resets into the loader, and once it is gone the whole area
|
||||||
|
is erased and the reset walk reaches the loader anyway."""
|
||||||
|
blank = bytes([0xFF] * loader.info.page)
|
||||||
|
addresses = range(0, loader.info.base, loader.info.page)
|
||||||
|
for address in reversed(addresses) if loader.info.patch_vector else addresses:
|
||||||
|
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, fuse_bytes=None, force=False):
|
||||||
|
image = load_image(path)
|
||||||
|
pages = plan_flash(image, loader.info)
|
||||||
|
check_walk_region(pages, loader.info, fuse_bytes, force)
|
||||||
|
if erase:
|
||||||
|
op_erase_flash(loader)
|
||||||
|
order = covered(pages, loader.info, 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_fuses(loader):
|
||||||
|
low, lock, extended, high = loader.read_fuses()
|
||||||
|
print(f"fuses: low {low:02x} high {high:02x} extended {extended:02x} lock {lock:02x}")
|
||||||
|
return bytes((low, lock, extended, high))
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------- 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: COM6, /dev/ttyUSB0, or a 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("--update-loader", metavar="FILE", help="replace the loader with this pureboot binary")
|
||||||
|
parser.add_argument("--state", metavar="FILE", help="update state file (default: FILE.pbstate)")
|
||||||
|
parser.add_argument("--assume-fuses", metavar="HEX8", help="fuse bytes low,lock,ext,high as 8 hex digits "
|
||||||
|
"(overrides reading them — e.g. under a simulator that cannot)")
|
||||||
|
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("--force", action="store_true", help="override refusable safety checks")
|
||||||
|
parser.add_argument("--stay", action="store_true", help="leave the loader in its session")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.update_loader and (args.flash or args.erase_flash):
|
||||||
|
parser.error("--update-loader does not combine with application flash operations")
|
||||||
|
fuse_override = None
|
||||||
|
if args.assume_fuses:
|
||||||
|
try:
|
||||||
|
fuse_override = bytes.fromhex(args.assume_fuses)
|
||||||
|
assert len(fuse_override) == 4
|
||||||
|
except (ValueError, AssertionError):
|
||||||
|
parser.error("--assume-fuses takes 8 hex digits: low,lock,extended,high")
|
||||||
|
|
||||||
|
port = Port(args.port, args.baud)
|
||||||
|
try:
|
||||||
|
loader = Loader(port)
|
||||||
|
info = loader.connect(args.wait)
|
||||||
|
if args.info:
|
||||||
|
print(f"device: {info.describe()}")
|
||||||
|
fuse_bytes = fuse_override
|
||||||
|
if args.fuses or (args.update_loader and not info.patch_vector and fuse_bytes is None):
|
||||||
|
read = op_fuses(loader)
|
||||||
|
if fuse_bytes is None:
|
||||||
|
fuse_bytes = read
|
||||||
|
if args.update_loader:
|
||||||
|
state = args.state or args.update_loader + ".pbstate"
|
||||||
|
op_update_loader(loader, args.wait, args.update_loader, state, fuse_bytes)
|
||||||
|
if args.flash:
|
||||||
|
op_flash(loader, args.flash, args.erase_flash, not args.no_verify, fuse_bytes, args.force)
|
||||||
|
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.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
53
test/check_pi.py
Normal file
53
test/check_pi.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Position-independence lint for the pureboot image.
|
||||||
|
|
||||||
|
The self-staging design lets the identical binary run from any 512-byte
|
||||||
|
slot, which holds only if nothing in the image addresses itself absolutely.
|
||||||
|
Two link-time facts guarantee it, both asserted here from the built ELF:
|
||||||
|
|
||||||
|
1. No absolute jmp/call opcodes — all control flow is PC-relative
|
||||||
|
(rjmp/rcall/ijmp/icall). -mrelax normally guarantees this; a code
|
||||||
|
change that grows a branch out of relaxation range would break it
|
||||||
|
silently.
|
||||||
|
2. The info block sits within the image's first 256 bytes: the 'b'
|
||||||
|
command rebuilds its address as (running slot high byte : low byte of
|
||||||
|
the link address), which needs the offset to fit that low byte.
|
||||||
|
|
||||||
|
Usage: check_pi.py <objdump> <nm> <elf> <text_start_hex>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
objdump, nm, elf, text_start = sys.argv[1:]
|
||||||
|
text_start = int(text_start, 0)
|
||||||
|
|
||||||
|
listing = subprocess.run([objdump, "-d", elf], capture_output=True, text=True, check=True).stdout
|
||||||
|
absolute = [
|
||||||
|
line
|
||||||
|
for line in listing.splitlines()
|
||||||
|
if re.search(r"\t(jmp|call)\t", line)
|
||||||
|
]
|
||||||
|
if absolute:
|
||||||
|
print("FAIL: absolute control flow in the image:")
|
||||||
|
print("\n".join(absolute))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
symbols = subprocess.run([nm, "-C", elf], capture_output=True, text=True, check=True).stdout
|
||||||
|
info = [line for line in symbols.splitlines() if "info_data" in line]
|
||||||
|
if len(info) != 1:
|
||||||
|
print(f"FAIL: expected one info-block storage symbol, found {len(info)}")
|
||||||
|
sys.exit(1)
|
||||||
|
offset = int(info[0].split()[0], 16) - text_start
|
||||||
|
if not 0 <= offset < 256:
|
||||||
|
print(f"FAIL: info block at image offset {offset:#x}, must sit in the first 256 bytes")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"PI lint: control flow PC-relative, info block at offset {offset:#x}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
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;
|
||||||
|
}
|
||||||
71
test/pbapp.cpp
Normal file
71
test/pbapp.cpp
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// Test-fixture application for the pureboot protocol tests: prints "APP" on
|
||||||
|
// the chip's serial link (the same link the loader uses) — 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.
|
||||||
|
//
|
||||||
|
// On the mega it then listens, and an 'L' makes it jump into the resident
|
||||||
|
// loader — the application-owned loader entry a BOOTRST-unprogrammed mega
|
||||||
|
// relies on (reset always boots the application there), exercised by the
|
||||||
|
// self-update tests. The tinies idle: reset reaches their loader through
|
||||||
|
// the patched vector, so the application owes it nothing.
|
||||||
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
|
using namespace avr::literals;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
consteval avr::hertz_t clock()
|
||||||
|
{
|
||||||
|
auto name = std::string_view{avr::hw::db.name};
|
||||||
|
if (name.starts_with("ATtiny13"))
|
||||||
|
return 9.6_MHz;
|
||||||
|
if (name.starts_with("ATtiny"))
|
||||||
|
return 8_MHz;
|
||||||
|
return 16_MHz;
|
||||||
|
}
|
||||||
|
|
||||||
|
using dev = avr::device<{.clock = clock()}>;
|
||||||
|
|
||||||
|
template <avr::hertz_t C, bool Hardware = avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART")>
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
[[noreturn]] static void idle()
|
||||||
|
{
|
||||||
|
// 'L' hands back to the loader at the top slot — 512 bytes, or the
|
||||||
|
// 1 KiB the >64 KiB chips use.
|
||||||
|
constexpr std::uint32_t slot = avr::hw::db.mem.flash_size > 65536 ? 1024 : 512;
|
||||||
|
for (;;)
|
||||||
|
if (tx_t::read_blocking() == 'L')
|
||||||
|
reinterpret_cast<void (*)()>(static_cast<std::uint16_t>((avr::hw::db.mem.flash_size - slot) / 2))();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
[[noreturn]] static void idle()
|
||||||
|
{
|
||||||
|
while (true) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // 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');
|
||||||
|
link<dev::clock>::idle();
|
||||||
|
}
|
||||||
78
test/pbrehome.py
Normal file
78
test/pbrehome.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Re-homing acceptance test: a pureboot image mistakenly programmed at
|
||||||
|
address 0 of a patched-vector chip (a raw .bin handed to a programmer, which
|
||||||
|
defaults to offset 0) must still be a working loader — position-independent,
|
||||||
|
guarding its accidental slot — and the ordinary --update-loader flow must
|
||||||
|
re-home a build into the canonical top slot: the staging install and the
|
||||||
|
word-0 redirect both run from copies whose slots are not page 0's, so the
|
||||||
|
running-slot guard never blocks the flow. Flashing an application through
|
||||||
|
the healed resident then overwrites the stale copy, vector surgery included,
|
||||||
|
and the banner proves the launch.
|
||||||
|
|
||||||
|
Usage: pbrehome.py <device_bin> <pureboot_elf> <update_bin> <mcu> <hz>
|
||||||
|
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
print(f"FAIL: {message}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
(device_bin, elf, update_bin, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:]
|
||||||
|
base, baud = int(base_hex, 0), int(baud)
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import pbsim
|
||||||
|
import pureboot as pb
|
||||||
|
|
||||||
|
os.makedirs(workdir, exist_ok=True)
|
||||||
|
dump = os.path.join(workdir, "dump.bin")
|
||||||
|
state = os.path.join(workdir, "rehome.pbstate")
|
||||||
|
if os.path.exists(state):
|
||||||
|
os.unlink(state)
|
||||||
|
|
||||||
|
# The runner's base argument places the firmware: 0 is the misplaced
|
||||||
|
# flash, and the patched-vector reset at word 0 lands in it directly.
|
||||||
|
device = pbsim.Device(device_bin, elf, mcu, hz, "0x0", int(page), baud, dump, reset_hex="0")
|
||||||
|
try:
|
||||||
|
port = pb.Port(device.pty, baud)
|
||||||
|
loader = pb.Loader(port)
|
||||||
|
info = loader.connect(25)
|
||||||
|
if info.base != base:
|
||||||
|
fail(f"the misplaced copy reports base {info.base:#06x} — the info block must stay canonical")
|
||||||
|
|
||||||
|
# The accidental slot still guards itself; the canonical base slot
|
||||||
|
# must be writable from it (that is what re-homing rides on).
|
||||||
|
before = loader.read_flash(0, info.page)
|
||||||
|
loader.write_page(0, bytes(info.page))
|
||||||
|
if loader.read_flash(0, info.page) != before:
|
||||||
|
fail("the misplaced copy's guard let its own slot change")
|
||||||
|
|
||||||
|
# The ordinary update flow re-homes the build into the top slot.
|
||||||
|
pb.op_update_loader(loader, 25, update_bin, state, None)
|
||||||
|
update = open(update_bin, "rb").read()
|
||||||
|
if loader.read_flash(base, len(update)) != update:
|
||||||
|
fail("the canonical slot does not hold the update image")
|
||||||
|
|
||||||
|
# An application flashed through the healed resident overwrites the
|
||||||
|
# stale copy (surgery included) and launches.
|
||||||
|
pages = pb.plan_flash(open(app_bin, "rb").read(), loader.info)
|
||||||
|
for address in pb.covered(pages, loader.info, skip_blank=False):
|
||||||
|
loader.write_page(address, pages[address])
|
||||||
|
pb.verify_pages(loader, pages)
|
||||||
|
loader.run_application()
|
||||||
|
if port.read_exact(3, 5.0) != b"APP":
|
||||||
|
fail("application does not banner after the re-home")
|
||||||
|
port.close()
|
||||||
|
finally:
|
||||||
|
device.stop()
|
||||||
|
print("pbrehome: a misplaced loader re-homes through the ordinary update flow")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
94
test/pbreloc.py
Normal file
94
test/pbreloc.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Position-independence acceptance test: the identical pureboot binary,
|
||||||
|
flashed one slot below the resident loader, must serve the complete command
|
||||||
|
set from there. The resident installs it (through-word composed by the host
|
||||||
|
layer), 'J' transfers control, and every command is exercised against the
|
||||||
|
staged copy — the info block must come back byte-identical, the write guard
|
||||||
|
must protect the staged copy's own slot and permit the resident's, and the
|
||||||
|
staged copy must be able to rewrite the resident slot verbatim.
|
||||||
|
|
||||||
|
Usage: pbreloc.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||||
|
<baud> <tool_py> <workdir>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
print(f"FAIL: {message}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
device_bin, elf, mcu, hz, base_hex, page, baud, tool, workdir = sys.argv[1:]
|
||||||
|
base, page, baud = int(base_hex, 0), int(page), int(baud)
|
||||||
|
stage = None # derived from the device's own info (slot-sized) below
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import pbsim
|
||||||
|
import pureboot as pb
|
||||||
|
|
||||||
|
os.makedirs(workdir, exist_ok=True)
|
||||||
|
objcopy = os.environ.get("PB_OBJCOPY", "avr-objcopy")
|
||||||
|
image_path = os.path.join(workdir, "pureboot.bin")
|
||||||
|
subprocess.run([objcopy, "-O", "binary", elf, image_path], check=True)
|
||||||
|
image = open(image_path, "rb").read()
|
||||||
|
|
||||||
|
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, os.path.join(workdir, "dump.bin"))
|
||||||
|
try:
|
||||||
|
port = pb.Port(device.pty, baud)
|
||||||
|
loader = pb.Loader(port)
|
||||||
|
info = loader.connect(25)
|
||||||
|
if info.base != base:
|
||||||
|
fail(f"info reports base {info.base:#06x}")
|
||||||
|
resident_info = info.raw
|
||||||
|
|
||||||
|
# Install the staging copy exactly as the update flow would.
|
||||||
|
stage = info.stage
|
||||||
|
staged = pb.staging_content(image, info)
|
||||||
|
pb.write_differing(loader, stage, staged)
|
||||||
|
|
||||||
|
# Enter it; from here on, every command runs in the relocated copy.
|
||||||
|
staged_info = loader.enter_copy(stage, 25)
|
||||||
|
if staged_info.raw != resident_info:
|
||||||
|
fail(f"staged info {staged_info.raw.hex()} != resident info {resident_info.hex()}")
|
||||||
|
|
||||||
|
# 'R' from the staged copy already proved itself in the install
|
||||||
|
# verify; 'F' must answer 4 bytes (values are unmodeled in simavr).
|
||||||
|
if len(loader.read_fuses()) != 4:
|
||||||
|
fail("fuse read from the staged copy")
|
||||||
|
|
||||||
|
# EEPROM round-trip through the staged copy.
|
||||||
|
pattern = bytes(range(0x50, 0x60))
|
||||||
|
loader.write_eeprom(0, pattern)
|
||||||
|
if loader.read_eeprom(0, len(pattern)) != pattern:
|
||||||
|
fail("EEPROM round-trip through the staged copy")
|
||||||
|
|
||||||
|
# The guard, both ways: its own slot refused (drained, unchanged),
|
||||||
|
# the resident slot writable.
|
||||||
|
before = loader.read_flash(stage, page)
|
||||||
|
loader.write_page(stage, bytes(page))
|
||||||
|
if loader.read_flash(stage, page) != before:
|
||||||
|
fail("the staged copy's guard let its own slot change")
|
||||||
|
marker = bytes((i * 3) & 0xFF for i in range(page))
|
||||||
|
loader.write_page(base, marker)
|
||||||
|
if loader.read_flash(base, page) != marker:
|
||||||
|
fail("the staged copy could not write the resident slot")
|
||||||
|
|
||||||
|
# Restore the resident image through the staged copy, then 'J' back
|
||||||
|
# into it and prove it lives.
|
||||||
|
resident = image + b"\xff" * (info.slot - len(image))
|
||||||
|
pb.write_differing(loader, base, resident)
|
||||||
|
back_info = loader.enter_copy(base, 25)
|
||||||
|
if back_info.raw != resident_info:
|
||||||
|
fail("the restored resident does not serve its info block")
|
||||||
|
port.close()
|
||||||
|
finally:
|
||||||
|
device.stop()
|
||||||
|
print("pbreloc: the relocated copy serves the full command set")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
65
test/pbsim.py
Normal file
65
test/pbsim.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
"""Shared simavr harness for the pureboot tests: spawn the device runner,
|
||||||
|
hand out its pty, restart it from a flash dump (the power-fail path), and
|
||||||
|
keep its chatter out of undrained pipes."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
class Device:
|
||||||
|
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None):
|
||||||
|
cmd = [binary, elf, mcu, hz, base_hex, str(page), str(baud), dump]
|
||||||
|
if reset_hex is not None or resume is not None:
|
||||||
|
# Chips without a hardware boot section — the tinies and the
|
||||||
|
# m48s — reset to address 0 like silicon; the boot-sectioned
|
||||||
|
# megas re-vector to the loader base (BOOTRST).
|
||||||
|
patch = not mcu.startswith("atmega") or mcu.startswith("atmega48")
|
||||||
|
cmd.append(reset_hex if reset_hex is not None else ("0" if patch else base_hex))
|
||||||
|
if resume is not None:
|
||||||
|
cmd.append(resume)
|
||||||
|
self.log = open(dump + ".log", "a")
|
||||||
|
self.proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=self.log, text=True)
|
||||||
|
self.dump = dump
|
||||||
|
self.pty = None
|
||||||
|
for _ in range(50):
|
||||||
|
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 reset(self):
|
||||||
|
"""The external reset line: SIGUSR1 re-enters at the reset vector."""
|
||||||
|
self.proc.send_signal(signal.SIGUSR1)
|
||||||
|
|
||||||
|
def power_fail(self):
|
||||||
|
"""SIGTERM: the runner dumps its flash and exits — the image a
|
||||||
|
restart resumes from."""
|
||||||
|
self.stop()
|
||||||
|
return self.dump
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.proc.terminate()
|
||||||
|
try:
|
||||||
|
self.proc.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
self.proc.kill()
|
||||||
|
self.log.close()
|
||||||
|
|
||||||
|
|
||||||
|
def run_tool(tool, pty, baud, *args, timeout=180):
|
||||||
|
result = subprocess.run(
|
||||||
|
[os.environ.get("PYTHON", "python3"), tool, "--port", pty, "--baud", str(baud), "--wait", "25", *args],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
print(result.stdout, end="")
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"tool exited {result.returncode}: {result.stderr.strip()}")
|
||||||
|
return result.stdout
|
||||||
183
test/pbtest.py
Normal file
183
test/pbtest.py
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
#!/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 + 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:
|
||||||
|
# the boot-sectioned megas need no vector surgery (the tinies and the
|
||||||
|
# boot-section-less m48s do), the large chips speak word addresses, and
|
||||||
|
# the page byte is the wire's 0-means-256.
|
||||||
|
mega = mcu.startswith("atmega")
|
||||||
|
patch = not mega or mcu.startswith("atmega48")
|
||||||
|
word_flash = base + 512 > 0x10000
|
||||||
|
wire_base = base // 2 if word_flash else base
|
||||||
|
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||||
|
info = pb.Info(
|
||||||
|
bytes([ord("P"), ord("B"), 1, 0, 0, 0, page & 0xFF])
|
||||||
|
+ bytes([wire_base & 0xFF, wire_base >> 8, eeprom_size & 0xFF, eeprom_size >> 8])
|
||||||
|
+ bytes([flags])
|
||||||
|
)
|
||||||
|
|
||||||
|
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, "--stay")
|
||||||
|
for needed in ("device: signature", "fuses:", "verify:", "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")
|
||||||
|
|
||||||
|
# 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 the 'J' hand-over 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)
|
||||||
|
loader.run_application()
|
||||||
|
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 (patched-vector
|
||||||
|
# chips only — a boot-sectioned mega's word 0 stays the application's).
|
||||||
|
if patch:
|
||||||
|
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:
|
||||||
|
fail("ground-truth EEPROM does not match what was programmed")
|
||||||
|
|
||||||
|
print("pbtest: all scenarios pass")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
230
test/pbupdate.py
Normal file
230
test/pbupdate.py
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Self-update end-to-end: an application is flashed, then the loader
|
||||||
|
replaces itself with a re-timed build through the host tool's
|
||||||
|
--update-loader — and the power-fail phases of that update are rehearsed by
|
||||||
|
killing the simulated device mid-write, restarting it from its flash dump,
|
||||||
|
and letting a re-run complete the update.
|
||||||
|
|
||||||
|
The boot-sectioned megas run the BOOTRST-unprogrammed profile (reset boots
|
||||||
|
the application; the fixture application's 'L' jump is the application-owned
|
||||||
|
loader entry), with --assume-fuses standing in for the fuse read simavr
|
||||||
|
cannot model. The patched-vector chips — the tinies and the m48s — reset
|
||||||
|
into a loader at every phase by construction: the t13a because its staging
|
||||||
|
slot carries the reset vector itself, the others through the word-0 redirect
|
||||||
|
the tool plants around the resident rewrite.
|
||||||
|
|
||||||
|
Usage: pbupdate.py <device_bin> <pureboot_elf> <update_elf> <mcu> <hz>
|
||||||
|
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
print(f"FAIL: {message}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def rjmp_decode(word, at, flash_words):
|
||||||
|
"""Written against the instruction-set definition, not with the 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 PowerFail(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def assumed_fuses(pb, image):
|
||||||
|
"""Synthetic 'F' bytes for --assume-fuses: the smallest boot section
|
||||||
|
covering both the resident and the staging slot (two slots — what a
|
||||||
|
self-update needs), BOOTRST unprogrammed — the per-chip BOOTSZ ladder
|
||||||
|
and fuse byte come from the tool's own table, keyed by the update
|
||||||
|
image's embedded signature."""
|
||||||
|
info = pb.image_info(image)
|
||||||
|
which, ladder = pb.BOOT_FUSE[bytes(info.signature[1:3])]
|
||||||
|
bits = min((b for b in ladder if ladder[b] * 2 >= 2 * info.slot), key=lambda b: ladder[b])
|
||||||
|
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
|
||||||
|
fuses[which] = 0xF8 | (bits << 1) | 1
|
||||||
|
return bytes(fuses)
|
||||||
|
|
||||||
|
|
||||||
|
def make_fault_loader(pb, base, slot, kill_region, kill_hits, device):
|
||||||
|
"""A Loader whose write_page kills the device (or, with device=None,
|
||||||
|
just the host) at the Nth write into a region; the sequence
|
||||||
|
stage->resident->stage distinguishes the install from the restore."""
|
||||||
|
|
||||||
|
class FaultLoader(pb.Loader):
|
||||||
|
def __init__(self, port):
|
||||||
|
super().__init__(port)
|
||||||
|
self.seen_resident = False
|
||||||
|
self.hits = 0
|
||||||
|
|
||||||
|
def write_page(self, address, data):
|
||||||
|
if address >= base:
|
||||||
|
phase = "resident"
|
||||||
|
self.seen_resident = True
|
||||||
|
elif address >= base - slot:
|
||||||
|
phase = "stage_restore" if self.seen_resident else "stage"
|
||||||
|
else:
|
||||||
|
phase = "app"
|
||||||
|
if phase == kill_region:
|
||||||
|
self.hits += 1
|
||||||
|
if self.hits == kill_hits:
|
||||||
|
if device is not None:
|
||||||
|
device.power_fail()
|
||||||
|
raise PowerFail(f"{kill_region} write {kill_hits}")
|
||||||
|
super().write_page(address, data)
|
||||||
|
|
||||||
|
return FaultLoader
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
(device_bin, elf, update_elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:]
|
||||||
|
base, page, baud = int(base_hex, 0), int(page), int(baud)
|
||||||
|
mega = mcu.startswith("atmega")
|
||||||
|
# The m48s are megas without a boot section: patched vector, no fuse
|
||||||
|
# preflight, and the same reset-to-0 the tinies get.
|
||||||
|
patch = not mega or mcu.startswith("atmega48")
|
||||||
|
# Word-addressed (>64 KiB) chips use the 1 KiB slot; their loader base
|
||||||
|
# itself sits beyond the 16-bit byte space — the 644's base + slot only
|
||||||
|
# touches the 64 KiB boundary and stays byte-addressed.
|
||||||
|
slot = 1024 if base >= 0x10000 and mega else 512
|
||||||
|
reset_hex = "0" if mega else None # the boot-sectioned mega runs BOOTRST-unprogrammed here
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import pbsim
|
||||||
|
import pureboot as pb
|
||||||
|
|
||||||
|
os.makedirs(workdir, exist_ok=True)
|
||||||
|
objcopy = os.environ.get("PB_OBJCOPY", "avr-objcopy")
|
||||||
|
images = {}
|
||||||
|
for name, source in (("v0", elf), ("v9", update_elf)):
|
||||||
|
path = os.path.join(workdir, name + ".bin")
|
||||||
|
subprocess.run([objcopy, "-O", "binary", source, path], check=True)
|
||||||
|
images[name] = open(path, "rb").read()
|
||||||
|
if images["v0"] == images["v9"]:
|
||||||
|
fail("the update image is byte-identical to the resident build")
|
||||||
|
dump = os.path.join(workdir, "dump.bin")
|
||||||
|
state = os.path.join(workdir, "update.pbstate")
|
||||||
|
fuses = assumed_fuses(pb, images["v0"]) if mega and not patch else None
|
||||||
|
|
||||||
|
def connect(device):
|
||||||
|
port = pb.Port(device.pty, baud)
|
||||||
|
if mega:
|
||||||
|
# Reset boots the application here; its 'L' is the loader entry.
|
||||||
|
# To a live loader the same byte is an ignored command.
|
||||||
|
port.read_available(0.5)
|
||||||
|
port.write(b"L")
|
||||||
|
loader = pb.Loader(port)
|
||||||
|
loader.connect(25)
|
||||||
|
return port, loader
|
||||||
|
|
||||||
|
def padded(image):
|
||||||
|
return image + b"\xff" * (slot - len(image))
|
||||||
|
|
||||||
|
def resident_bytes(loader):
|
||||||
|
return loader.read_flash(base, slot)
|
||||||
|
|
||||||
|
def assert_state(loader, image, app_pages):
|
||||||
|
if resident_bytes(loader) != padded(image):
|
||||||
|
fail("resident loader does not match the update image")
|
||||||
|
stage = base - slot
|
||||||
|
got = loader.read_flash(stage, slot)
|
||||||
|
for address, data in app_pages.items():
|
||||||
|
if stage <= address < base:
|
||||||
|
if got[address - stage : address - stage + page] != data:
|
||||||
|
fail(f"staging region page {address:#06x} not restored")
|
||||||
|
|
||||||
|
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=reset_hex)
|
||||||
|
final = "v0"
|
||||||
|
try:
|
||||||
|
# The application first — its planner output is the restore truth.
|
||||||
|
pbsim.run_tool(tool, device.pty, baud, "--flash", app_bin, "--stay")
|
||||||
|
port, loader = connect(device)
|
||||||
|
app_pages = pb.plan_flash(open(app_bin, "rb").read(), loader.info)
|
||||||
|
port.close()
|
||||||
|
|
||||||
|
# A clean CLI update, resident -> v9.
|
||||||
|
args = ["--update-loader", os.path.join(workdir, "v9.bin"), "--state", state, "--stay"]
|
||||||
|
if fuses:
|
||||||
|
args += ["--assume-fuses", fuses.hex()]
|
||||||
|
out = pbsim.run_tool(tool, device.pty, baud, *args)
|
||||||
|
if "loader updated" not in out:
|
||||||
|
fail("update did not report success")
|
||||||
|
if os.path.exists(state):
|
||||||
|
fail("state file survived a completed update")
|
||||||
|
port, loader = connect(device)
|
||||||
|
assert_state(loader, images["v9"], app_pages)
|
||||||
|
loader.run_application()
|
||||||
|
if port.read_exact(3, 5.0) != b"APP":
|
||||||
|
fail("application does not banner after the update")
|
||||||
|
port.close()
|
||||||
|
final = "v9"
|
||||||
|
print("clean update: resident replaced, staging restored, application intact")
|
||||||
|
|
||||||
|
# Power-fail rehearsal: kill mid-phase, restart from the dump,
|
||||||
|
# re-run, and the update must still complete. Each round flips the
|
||||||
|
# direction so the flash is never already at its target. The mega's
|
||||||
|
# mid-resident-rewrite loss is exercised as a host crash instead:
|
||||||
|
# with BOOTRST unprogrammed and the resident mid-erase, a power loss
|
||||||
|
# there has no reset path into the staging copy — the documented
|
||||||
|
# cost of that profile (README).
|
||||||
|
for kill_region, kill_hits, kill_device in (
|
||||||
|
("stage", 2, True),
|
||||||
|
("resident", 1, patch),
|
||||||
|
("stage_restore", 2, True),
|
||||||
|
):
|
||||||
|
device.reset() # the previous round left the application running
|
||||||
|
port, loader = connect(device)
|
||||||
|
target = "v9" if resident_bytes(loader) == padded(images["v0"]) else "v0"
|
||||||
|
image_path = os.path.join(workdir, target + ".bin")
|
||||||
|
injected = make_fault_loader(pb, base, slot, kill_region, kill_hits, device if kill_device else None)(port)
|
||||||
|
injected.info = loader.info
|
||||||
|
try:
|
||||||
|
pb.op_update_loader(injected, 25, image_path, state, fuses)
|
||||||
|
fail(f"{kill_region}: fault never triggered")
|
||||||
|
except PowerFail as event:
|
||||||
|
print(f"power fail injected: {event}")
|
||||||
|
port.close()
|
||||||
|
if kill_device:
|
||||||
|
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump,
|
||||||
|
reset_hex=reset_hex, resume=dump)
|
||||||
|
port, loader = connect(device)
|
||||||
|
pb.op_update_loader(loader, 25, image_path, state, fuses)
|
||||||
|
assert_state(loader, images[target], app_pages)
|
||||||
|
loader.run_application()
|
||||||
|
if port.read_exact(3, 5.0) != b"APP":
|
||||||
|
fail(f"{kill_region}: application lost after the resumed update")
|
||||||
|
port.close()
|
||||||
|
final = target
|
||||||
|
print(f"resumed after {kill_region} loss: update completed, application intact")
|
||||||
|
finally:
|
||||||
|
device.stop()
|
||||||
|
|
||||||
|
# Ground truth: the simulator's own flash against the final state, and
|
||||||
|
# on the patched-vector chips an independent decode of the reset routing.
|
||||||
|
flash = open(dump, "rb").read()
|
||||||
|
if flash[base : base + slot] != padded(images[final]):
|
||||||
|
fail("ground-truth resident region does not match the final image")
|
||||||
|
if patch:
|
||||||
|
flash_words = (base + slot) // 2
|
||||||
|
word0 = flash[0] | (flash[1] << 8)
|
||||||
|
if rjmp_decode(word0, 0, flash_words) != base // 2:
|
||||||
|
fail("ground-truth reset vector does not land on the loader")
|
||||||
|
app = open(app_bin, "rb").read()
|
||||||
|
trampoline = flash[base - 2] | (flash[base - 1] << 8)
|
||||||
|
if rjmp_decode(trampoline, (base - 2) // 2, flash_words) != rjmp_decode(app[0] | (app[1] << 8), 0, flash_words):
|
||||||
|
fail("ground-truth trampoline does not land on the application entry")
|
||||||
|
print("pbupdate: clean update + all power-fail phases recovered")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
408
test/pureboot_device.c
Normal file
408
test/pureboot_device.c
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
// 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:
|
||||||
|
//
|
||||||
|
// - Megas: the hardware USART 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;
|
||||||
|
|
||||||
|
// simavr 1.6's avr_flash PGERS handler erases spm_pagesize bytes starting at
|
||||||
|
// Z & ~1 instead of the page containing Z (its PGWRT path masks correctly) —
|
||||||
|
// hardware ignores the in-page bits (§26.8.1), so an erase issued with Z
|
||||||
|
// anywhere inside the page wipes half the neighbouring page in simulation
|
||||||
|
// only. Wrap the mega's registered flash ioctl and re-dispatch page erases
|
||||||
|
// with Z forced to the page boundary; everything else passes through.
|
||||||
|
//
|
||||||
|
// A second gap on the boot-section-less m48s: their RWWSRE bit is the
|
||||||
|
// temporary-buffer discard (Atmel-8271 §26.2/§26.3.1), but the stock model
|
||||||
|
// gates its RWWSRE branch on AVR_SELFPROG_HAVE_RWW — absent on the m48
|
||||||
|
// core — so the discard store falls through into the buffer-fill branch and
|
||||||
|
// plants whatever Z/R1:R0 happen to hold. Perform the silicon's discard
|
||||||
|
// here instead.
|
||||||
|
static avr_flash_t *mega_flash;
|
||||||
|
static int (*mega_flash_ioctl)(avr_io_t *io, uint32_t ctl, void *param);
|
||||||
|
|
||||||
|
static int fixed_flash_ioctl(avr_io_t *io, uint32_t ctl, void *param)
|
||||||
|
{
|
||||||
|
if (ctl == AVR_IOCTL_FLASH_SPM && avr_regbit_get(io->avr, mega_flash->pgers)) {
|
||||||
|
uint16_t z = (uint16_t)(io->avr->data[30] | (io->avr->data[31] << 8));
|
||||||
|
uint16_t masked = (uint16_t)(z & ~(mega_flash->spm_pagesize - 1));
|
||||||
|
io->avr->data[30] = (uint8_t)masked;
|
||||||
|
io->avr->data[31] = (uint8_t)(masked >> 8);
|
||||||
|
int result = mega_flash_ioctl(io, ctl, param);
|
||||||
|
io->avr->data[30] = (uint8_t)z;
|
||||||
|
io->avr->data[31] = (uint8_t)(z >> 8);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (ctl == AVR_IOCTL_FLASH_SPM && !(mega_flash->flags & AVR_SELFPROG_HAVE_RWW) &&
|
||||||
|
(io->avr->data[mega_flash->r_spm] & 0x11) == 0x11) { // RWWSRE|SELFPRGEN: the m48 buffer discard
|
||||||
|
for (int i = 0; i < mega_flash->spm_pagesize / 2; i++) {
|
||||||
|
mega_flash->tmppage[i] = 0xffff;
|
||||||
|
mega_flash->tmppage_used[i] = 0;
|
||||||
|
}
|
||||||
|
avr_regbit_clear(io->avr, mega_flash->selfprgen);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return mega_flash_ioctl(io, ctl, param);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fix_mega_flash_erase(void)
|
||||||
|
{
|
||||||
|
for (avr_io_t *io = avr->io_port; io; io = io->next) {
|
||||||
|
if (io->kind && strcmp(io->kind, "flash") == 0) {
|
||||||
|
mega_flash = (avr_flash_t *)io;
|
||||||
|
mega_flash_ioctl = io->ioctl;
|
||||||
|
io->ioctl = fixed_flash_ioctl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fprintf(stderr, "device: no flash module to fix — SPM page erases may misalign\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void request_reset(int sig)
|
||||||
|
{
|
||||||
|
(void)sig;
|
||||||
|
reset_requested = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------- tiny NVM ---
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
avr_io_t io;
|
||||||
|
uint8_t buffer[128];
|
||||||
|
uint8_t used[128]; // a buffer word loads once until erased — like silicon
|
||||||
|
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;
|
||||||
|
if (!n->used[offset]) { // first write wins until the buffer clears
|
||||||
|
n->buffer[offset] = mcu->data[0];
|
||||||
|
n->buffer[offset + 1] = mcu->data[1];
|
||||||
|
n->used[offset] = 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);
|
||||||
|
memset(n->used, 0, n->page);
|
||||||
|
} else if (command == 0x11) { // CTPB
|
||||||
|
memset(n->buffer, 0xff, n->page);
|
||||||
|
memset(n->used, 0, 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.
|
||||||
|
// The pending cycle timers must go with the state: avr_reset drops the TX
|
||||||
|
// output latch, whose falling edge starts a spurious decode before this
|
||||||
|
// runs, and a stale tx_sample would then interleave with the loader's first
|
||||||
|
// real answer through the shared shift state, corrupting it.
|
||||||
|
static void bridge_reset(void)
|
||||||
|
{
|
||||||
|
avr_cycle_timer_cancel(avr, tx_sample, NULL);
|
||||||
|
avr_cycle_timer_cancel(avr, rx_step, NULL);
|
||||||
|
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 || argc > 10) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"usage: %s <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
|
||||||
|
" [reset_hex] [resume_flash]\n"
|
||||||
|
" reset_hex: reset vector (default: base on the mega, 0 on the tinies)\n"
|
||||||
|
" resume_flash: raw full-flash image loaded instead of the ELF — a prior\n"
|
||||||
|
" run's dump, for power-fail resume tests\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 = strncmp(mcu_name, "atmega", 6) == 0; // every mega links over its hardware USART
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if (argc > 9) {
|
||||||
|
// Resume: the full flash image of an interrupted prior run.
|
||||||
|
FILE *f = fopen(argv[9], "rb");
|
||||||
|
if (!f || fread(avr->flash, 1, avr->flashend + 1, f) == 0) {
|
||||||
|
fprintf(stderr, "device: cannot read %s\n", argv[9]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
} else {
|
||||||
|
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 boot-sectioned megas enter the loader in hardware (BOOTRST, not
|
||||||
|
// modeled — the argument picks the modeled fuse's target); the tinies
|
||||||
|
// and the boot-section-less m48s reset to word 0 like silicon — erased
|
||||||
|
// flash walks up into the loader, and after the host's surgery the
|
||||||
|
// patched vector routes there.
|
||||||
|
int boot_section = use_uart_pty && strncmp(mcu_name, "atmega48", 8) != 0;
|
||||||
|
reset_pc = argc > 8 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? 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) {
|
||||||
|
fix_mega_flash_erase();
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
211
test/test_planner.py
Normal file
211
test/test_planner.py
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Host-tool unit tests — the pure planning and policy logic, no simulator:
|
||||||
|
the flash-programming orders and their recovery properties, the reset-vector
|
||||||
|
surgery, the staging-slot composition, the mega boot-fuse decode, and the
|
||||||
|
update preflight's error/warning matrix (fuse combinations simavr cannot
|
||||||
|
model reach it here as synthetic bytes).
|
||||||
|
|
||||||
|
Usage: test_planner.py <tool_py>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
print(f"FAIL: {message}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def expect_error(what, fn, *needles):
|
||||||
|
try:
|
||||||
|
fn()
|
||||||
|
except Exception as error:
|
||||||
|
for needle in needles:
|
||||||
|
if needle not in str(error):
|
||||||
|
fail(f"{what}: error lacks {needle!r}: {error}")
|
||||||
|
return
|
||||||
|
fail(f"{what}: no error raised")
|
||||||
|
|
||||||
|
|
||||||
|
def info_of(pb, base, page, patch, flash, signature=(0x1E, 0x93, 0x0B), word_flash=False):
|
||||||
|
scale = 2 if word_flash else 1
|
||||||
|
wire_base = base // scale
|
||||||
|
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||||
|
raw = bytes((0x50, 0x42, 1, *signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8,
|
||||||
|
0, 2, flags))
|
||||||
|
info = pb.Info(raw)
|
||||||
|
assert info.flash_size == flash
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def rjmp_decode(word, at, flash_words):
|
||||||
|
if word & 0xF000 != 0xC000:
|
||||||
|
fail(f"not an rjmp: {word:#06x}")
|
||||||
|
offset = word & 0x0FFF
|
||||||
|
if offset >= 0x800:
|
||||||
|
offset -= 0x1000
|
||||||
|
return (at + 1 + offset) % flash_words
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(sys.argv[1])))
|
||||||
|
import pureboot as pb
|
||||||
|
|
||||||
|
tiny = info_of(pb, 0x1E00, 64, True, 0x2000)
|
||||||
|
mega = info_of(pb, 0x7E00, 128, False, 0x8000, signature=(0x1E, 0x95, 0x0F))
|
||||||
|
|
||||||
|
# mega_boot: BOOTSZ words and the BOOTRST sense per chip — the fuse byte
|
||||||
|
# index (EXTENDED on the x8 line except the m328s' HIGH, HIGH elsewhere)
|
||||||
|
# and the per-family ladders (Atmel-2486/2466/2503/2545/8271/DS40002065/
|
||||||
|
# 8272/8011/2593/42719). Synthetic 'F' replies: only the boot byte
|
||||||
|
# carries meaning.
|
||||||
|
cases = (
|
||||||
|
((0x1E, 0x93, 0x07), 0x2000, 3, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m8
|
||||||
|
((0x1E, 0x94, 0x03), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m16
|
||||||
|
((0x1E, 0x95, 0x02), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m32
|
||||||
|
((0x1E, 0x93, 0x0A), 0x2000, 2, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m88
|
||||||
|
((0x1E, 0x93, 0x0F), 0x2000, 2, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m88P
|
||||||
|
((0x1E, 0x94, 0x06), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168/168A
|
||||||
|
((0x1E, 0x94, 0x0B), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168P
|
||||||
|
((0x1E, 0x95, 0x14), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m328
|
||||||
|
((0x1E, 0x95, 0x0F), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m328P
|
||||||
|
((0x1E, 0x94, 0x0F), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m164A
|
||||||
|
((0x1E, 0x94, 0x0A), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m164P
|
||||||
|
((0x1E, 0x95, 0x15), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m324A
|
||||||
|
((0x1E, 0x96, 0x09), 0x10000, 3, {0b11: 0xFC00, 0b10: 0xF800, 0b01: 0xF000, 0b00: 0xE000}), # m644
|
||||||
|
((0x1E, 0x96, 0x0A), 0x10000, 3, {0b11: 0xFC00, 0b10: 0xF800, 0b01: 0xF000, 0b00: 0xE000}), # m644P
|
||||||
|
((0x1E, 0x97, 0x06), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284
|
||||||
|
((0x1E, 0x97, 0x05), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284P
|
||||||
|
)
|
||||||
|
for signature, flash, which, ladder in cases:
|
||||||
|
# Word-addressed chips carry the 1 KiB slot (their smallest boot sector).
|
||||||
|
slot = 1024 if flash > 0x10000 else 512
|
||||||
|
chip = info_of(pb, flash - slot, 128 if flash < 0x20000 else 0, False, flash,
|
||||||
|
signature=signature, word_flash=flash > 0x10000)
|
||||||
|
for bits, start in ladder.items():
|
||||||
|
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
|
||||||
|
fuses[which] = (0xF8 | (bits << 1)) & ~1
|
||||||
|
prog, at = pb.mega_boot(chip, bytes(fuses))
|
||||||
|
if not prog or at != start:
|
||||||
|
fail(f"mega_boot {signature[1]:02x}{signature[2]:02x} BOOTSZ={bits:02b} programmed: {prog} {at:#07x}")
|
||||||
|
fuses[which] |= 1
|
||||||
|
prog, at = pb.mega_boot(chip, bytes(fuses))
|
||||||
|
if prog or at != start:
|
||||||
|
fail(f"mega_boot {signature[1]:02x}{signature[2]:02b} unprogrammed: {prog} {at:#07x}")
|
||||||
|
|
||||||
|
# Word-addressed info decode: the 1284P's base/page ride the wire scaled,
|
||||||
|
# and its slot is 1 KiB.
|
||||||
|
big = info_of(pb, 0x1FC00, 0, False, 0x20000, signature=(0x1E, 0x97, 0x05), word_flash=True)
|
||||||
|
if big.page != 256 or big.base != 0x1FC00 or big.stage != 0x1F800 or big.slot != 1024:
|
||||||
|
fail(f"word-addressed info decode: page {big.page}, base {big.base:#x}, stage {big.stage:#x}")
|
||||||
|
|
||||||
|
# Surgery: word 0 lands on the loader, the trampoline on the original
|
||||||
|
# entry — checked with an independent decoder.
|
||||||
|
app = bytes((0xC0 | 0x00, 0xC0)) + bytes((0x12,)) * 300 # rjmp .+0x00C0... entry word 0xC0C0
|
||||||
|
entry = rjmp_decode(app[0] | (app[1] << 8), 0, tiny.flash_size // 2)
|
||||||
|
pages = pb.plan_flash(app, tiny)
|
||||||
|
word0 = pages[0][0] | (pages[0][1] << 8)
|
||||||
|
if rjmp_decode(word0, 0, tiny.flash_size // 2) != tiny.base // 2:
|
||||||
|
fail("surgery: patched word 0 misses the loader")
|
||||||
|
tp = pages[tiny.base - 64]
|
||||||
|
tramp = tp[62] | (tp[63] << 8)
|
||||||
|
if rjmp_decode(tramp, (tiny.base - 2) // 2, tiny.flash_size // 2) != entry:
|
||||||
|
fail("surgery: trampoline misses the original entry")
|
||||||
|
expect_error("non-rjmp vector", lambda: pb.plan_flash(bytes((0x0C, 0x94)) + app[2:], tiny), "not an rjmp")
|
||||||
|
looped = bytearray(app)
|
||||||
|
word = pb.rjmp_to(0, tiny.base // 2, tiny.flash_size // 2)
|
||||||
|
looped[0], looped[1] = word & 0xFF, word >> 8
|
||||||
|
expect_error("read-back image", lambda: pb.plan_flash(bytes(looped), tiny), "read-back")
|
||||||
|
expect_error("oversize image", lambda: pb.plan_flash(bytes(0x1DFF), tiny), "application flash ends")
|
||||||
|
|
||||||
|
# Ordering: patched vector puts page 0 first and the trampoline second;
|
||||||
|
# a boot section puts page 0 last. Blank pages drop only when erased.
|
||||||
|
order = pb.covered(pages, tiny, skip_blank=False)
|
||||||
|
if order[0] != 0 or order[1] != tiny.base - 64:
|
||||||
|
fail(f"tiny order starts {order[:2]}, want page 0 then trampoline page")
|
||||||
|
if sorted(order[2:]) != order[2:]:
|
||||||
|
fail("tiny order tail not ascending")
|
||||||
|
mega_pages = pb.plan_flash(bytes((0xFF,)) * 600, mega)
|
||||||
|
morder = pb.covered(mega_pages, mega, skip_blank=False)
|
||||||
|
if morder[-1] != 0 or sorted(morder[:-1]) != morder[:-1]:
|
||||||
|
fail(f"mega order {morder}, want ascending with page 0 last")
|
||||||
|
blanky = {0: pages[0], 64: bytes((0xFF,)) * 64, 128: pages[128], tiny.base - 64: tp}
|
||||||
|
slim = pb.covered(blanky, tiny, skip_blank=True)
|
||||||
|
if 64 in slim or 0 not in slim or tiny.base - 64 not in slim:
|
||||||
|
fail(f"skip_blank order wrong: {slim}")
|
||||||
|
|
||||||
|
# Staging content: the identical image plus the through-word on a
|
||||||
|
# patched-vector chip; hard size clamps either way.
|
||||||
|
image = bytes(range(256)) * 2 # 512 B — too big for a tiny slot
|
||||||
|
expect_error("tiny staging size", lambda: pb.staging_content(image, tiny), "510")
|
||||||
|
staged = pb.staging_content(image[:508], tiny)
|
||||||
|
through = staged[510] | (staged[511] << 8)
|
||||||
|
if rjmp_decode(through, (tiny.base - 2) // 2, tiny.flash_size // 2) != tiny.base // 2:
|
||||||
|
fail("through-word misses the resident base")
|
||||||
|
if pb.staging_content(image, mega) != image:
|
||||||
|
fail("mega staging content should be the bare image")
|
||||||
|
expect_error("mega staging size", lambda: pb.staging_content(image + b"!", mega), "512")
|
||||||
|
|
||||||
|
# The embedded info block: found in a synthetic binary, absent in noise.
|
||||||
|
binary = bytes((0xAA,)) * 10 + tiny.raw + bytes((0xBB,)) * 10
|
||||||
|
found = pb.image_info(binary)
|
||||||
|
if found is None or found.raw != tiny.raw:
|
||||||
|
fail("image_info misses the embedded block")
|
||||||
|
if pb.image_info(bytes((0xAA,)) * 40) is not None:
|
||||||
|
fail("image_info invents a block")
|
||||||
|
|
||||||
|
# loader_image must peel a padded image down to the slot content: a raw
|
||||||
|
# .bin padded from address 0 (or a whole-flash read-back with the loader
|
||||||
|
# resident at base) yields the same bytes as the bare slot image.
|
||||||
|
import tempfile
|
||||||
|
slot_image = bytes((0xAA,)) * 10 + tiny.raw + bytes((0xCC,)) * 40
|
||||||
|
padded = bytes((0xFF,)) * tiny.base + slot_image
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
|
||||||
|
f.write(padded)
|
||||||
|
padded_path = f.name
|
||||||
|
try:
|
||||||
|
if pb.loader_image(padded_path) != slot_image:
|
||||||
|
fail("loader_image does not peel a padded image to the slot content")
|
||||||
|
finally:
|
||||||
|
os.unlink(padded_path)
|
||||||
|
|
||||||
|
# Update preflight: the full fuse matrix, plus target mismatch.
|
||||||
|
other = info_of(pb, 0x1E00, 32, True, 0x2000)
|
||||||
|
expect_error("wrong-target image", lambda: pb.update_preflight(binary, other, None), "another target")
|
||||||
|
expect_error("mega needs fuses", lambda: pb.update_preflight(bytes((0xAA,)) * 8 + mega.raw, mega, None),
|
||||||
|
"--assume-fuses")
|
||||||
|
mega_image = bytes((0xAA,)) * 8 + mega.raw
|
||||||
|
|
||||||
|
def fuses(high):
|
||||||
|
return bytes((0xFF, 0xFF, 0xFF, high))
|
||||||
|
|
||||||
|
expect_error("BOOTSZ 512 B", lambda: pb.update_preflight(mega_image, mega, fuses(0xFE)),
|
||||||
|
"cannot self-update", "BOOTSZ")
|
||||||
|
notes = pb.update_preflight(mega_image, mega, fuses(0xFD)) # 1 KB, BOOTRST unprogrammed
|
||||||
|
if not any("BOOTRST unprogrammed" in n for n in notes):
|
||||||
|
fail(f"1K/unprogrammed notes: {notes}")
|
||||||
|
notes = pb.update_preflight(mega_image, mega, fuses(0xFC)) # 1 KB, BOOTRST programmed
|
||||||
|
if not any("staging slot" in n for n in notes):
|
||||||
|
fail(f"1K/programmed notes: {notes}")
|
||||||
|
notes = pb.update_preflight(mega_image, mega, fuses(0xFA)) # 2 KB, BOOTRST programmed
|
||||||
|
if not any("application flash" in n for n in notes):
|
||||||
|
fail(f"2K/programmed notes: {notes}")
|
||||||
|
if pb.update_preflight(bytes((0xAA,)) * 8 + tiny.raw, tiny, None) != []:
|
||||||
|
fail("tiny preflight should pass without fuses")
|
||||||
|
|
||||||
|
# The walk-region refusal: BOOTRST aimed below the loader plus app data
|
||||||
|
# in the walk span errors without --force; erased spans and unprogrammed
|
||||||
|
# BOOTRST pass.
|
||||||
|
deep = {0x7800: bytes((1,)) * 128}
|
||||||
|
expect_error("walk region", lambda: pb.check_walk_region(deep, mega, fuses(0xFA), False), "--force")
|
||||||
|
pb.check_walk_region(deep, mega, fuses(0xFA), True)
|
||||||
|
pb.check_walk_region(deep, mega, fuses(0xFB), False) # BOOTRST unprogrammed
|
||||||
|
pb.check_walk_region({0x7800: bytes((0xFF,)) * 128}, mega, fuses(0xFA), False)
|
||||||
|
pb.check_walk_region(deep, mega, None, False) # fuses unknown: no check
|
||||||
|
|
||||||
|
print("test_planner: all planner and policy checks pass")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
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