pureboot: every deployment axis is a build parameter
Clock, baud, serial backend (hardware USART 0/1 or the software UART on any pins) and the activation window all resolve through one CMake function, pureboot_add_loader() in pureboot/CMakeLists.txt — the unit a downstream project consumes. The default baud is the fastest standard rate within 2.5 % (the same best-divisor search libavr's solver runs), gated on software builds by the polled receiver's 100-cycles-a-bit floor; every explicit pick is re-checked by the compile's static asserts. The size matrix builds each axis that can move the image — backend x clock ladder x USART instance, per chip — against the slot budget, and two nondefault deployments run the whole protocol suite live: the 328P on its shipped 1 MHz fuses over software serial on TX=PB1/RX=PB5 (pureboot.custom), and the 644A over USART1 (pureboot.usart1). The sim runner takes -l to bridge any link, paces a fully quiet bridge toward real time (a free-running 8 M-cycle window loses the reset-race knock), and the fixture application speaks the deployment it is built for. The loader itself shed bytes on the way: the return-address high byte spelled through byteswap (the double swap folds to the one-byte pick), the info-block address composed instead of bit_cast, and libavr's new polled-UART helpers replacing the port's uart::detail reaches. Every combination fits: 458-506 B across the megas' whole matrix, 470-484 B on the tinies, 556-562 B in the 1284s' 1 KiB slot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
310
pureboot/CMakeLists.txt
Normal file
310
pureboot/CMakeLists.txt
Normal file
@@ -0,0 +1,310 @@
|
||||
# pureboot as a consumable CMake unit: the per-chip geometry, the default
|
||||
# baud ladder, and pureboot_add_loader() — the one way a loader target is
|
||||
# created, both by this port's own build and by a downstream project. A
|
||||
# downstream project brings its usual libavr setup (the `libavr` target and
|
||||
# the LIBAVR_MCU toolchain preset), adds this directory, and states its
|
||||
# deployment:
|
||||
#
|
||||
# add_subdirectory(bootloader/pureboot)
|
||||
# pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
|
||||
#
|
||||
# Every argument is optional — CLOCK defaults to the family assumption
|
||||
# below, BAUD to the fastest standard rate the clock reaches within 2.5 %
|
||||
# (the ladder), SERIAL to the chip's hardware USART where it has one
|
||||
# (`hardware`/`software` force a backend, USART 1 picks the second
|
||||
# instance), RX/TX to pb0/pb1 for the software UART, TIMEOUT to 8 s.
|
||||
# Infeasible picks fail the build by name: libavr's baud-error and
|
||||
# software-UART cycle-floor static asserts re-check whatever is passed.
|
||||
|
||||
# Per-family geometry: flash/page/EEPROM sizes and the linker wrap the PC
|
||||
# modulo needs, the loader slot (each chip's smallest boot sector — 1 KiB on
|
||||
# the word-addressed 1284s), and the deployment defaults (crystal assumption
|
||||
# on the megas, calibrated RC on the tinies). The USART flags mirror the
|
||||
# hardware inventory the loader's own static asserts check (the plain 644 is
|
||||
# the x4 family's one single-USART die, Atmel-2593).
|
||||
set(_pb_has_usart 1)
|
||||
set(_pb_has_usart1 0)
|
||||
if(LIBAVR_MCU MATCHES "^attiny13a?$")
|
||||
set(_pb_flash 1024)
|
||||
set(_pb_wrap "")
|
||||
set(_pb_page 32)
|
||||
set(_pb_hz 9600000)
|
||||
set(_pb_eeprom 64)
|
||||
set(_pb_has_usart 0)
|
||||
elseif(LIBAVR_MCU STREQUAL "attiny25")
|
||||
set(_pb_flash 2048)
|
||||
set(_pb_wrap "")
|
||||
set(_pb_page 32)
|
||||
set(_pb_hz 8000000)
|
||||
set(_pb_eeprom 128)
|
||||
set(_pb_has_usart 0)
|
||||
elseif(LIBAVR_MCU STREQUAL "attiny45")
|
||||
set(_pb_flash 4096)
|
||||
set(_pb_wrap "")
|
||||
set(_pb_page 64)
|
||||
set(_pb_hz 8000000)
|
||||
set(_pb_eeprom 256)
|
||||
set(_pb_has_usart 0)
|
||||
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_eeprom 512)
|
||||
set(_pb_has_usart 0)
|
||||
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_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_eeprom 512)
|
||||
elseif(LIBAVR_MCU MATCHES "^atmega16a?$" OR LIBAVR_MCU MATCHES "^atmega168(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_eeprom 512)
|
||||
elseif(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_eeprom 512)
|
||||
set(_pb_has_usart1 1)
|
||||
elseif(LIBAVR_MCU MATCHES "^atmega32a?$" OR LIBAVR_MCU MATCHES "^atmega328p?$")
|
||||
set(_pb_flash 32768)
|
||||
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
|
||||
set(_pb_page 128)
|
||||
set(_pb_hz 16000000)
|
||||
set(_pb_eeprom 1024)
|
||||
elseif(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_eeprom 1024)
|
||||
set(_pb_has_usart1 1)
|
||||
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 README.md). The plain 644 is the family's one
|
||||
# single-USART die.
|
||||
set(_pb_flash 65536)
|
||||
set(_pb_wrap -Wl,--pmem-wrap-around=64k)
|
||||
set(_pb_page 256)
|
||||
set(_pb_hz 16000000)
|
||||
set(_pb_eeprom 2048)
|
||||
if(NOT LIBAVR_MCU STREQUAL "atmega644")
|
||||
set(_pb_has_usart1 1)
|
||||
endif()
|
||||
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 README.md).
|
||||
set(_pb_flash 131072)
|
||||
set(_pb_wrap "")
|
||||
set(_pb_page 256)
|
||||
set(_pb_hz 16000000)
|
||||
set(_pb_eeprom 4096)
|
||||
set(_pb_slot 1024)
|
||||
set(_pb_limit 1024)
|
||||
set(_pb_has_usart1 1)
|
||||
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()
|
||||
|
||||
# The function runs in its caller's scope, so everything it needs crosses
|
||||
# scopes as global properties.
|
||||
set_property(GLOBAL PROPERTY PUREBOOT_BASE_HEX ${_pb_base_hex})
|
||||
set_property(GLOBAL PROPERTY PUREBOOT_APP ${_pb_app})
|
||||
set_property(GLOBAL PROPERTY PUREBOOT_WRAP "${_pb_wrap}")
|
||||
set_property(GLOBAL PROPERTY PUREBOOT_DEFAULT_HZ ${_pb_hz})
|
||||
set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART ${_pb_has_usart})
|
||||
set_property(GLOBAL PROPERTY PUREBOOT_HAS_USART1 ${_pb_has_usart1})
|
||||
|
||||
# The port's own build (tests, the size matrix) reads the geometry from the
|
||||
# parent scope; a downstream consumer gets the same variables for free.
|
||||
set(PUREBOOT_BASE_HEX ${_pb_base_hex} PARENT_SCOPE)
|
||||
set(PUREBOOT_PAGE ${_pb_page} PARENT_SCOPE)
|
||||
set(PUREBOOT_SLOT ${_pb_slot} PARENT_SCOPE)
|
||||
set(PUREBOOT_LIMIT ${_pb_limit} PARENT_SCOPE)
|
||||
set(PUREBOOT_EEPROM ${_pb_eeprom} PARENT_SCOPE)
|
||||
set(PUREBOOT_DEFAULT_HZ ${_pb_hz} PARENT_SCOPE)
|
||||
set(PUREBOOT_HAS_USART ${_pb_has_usart} PARENT_SCOPE)
|
||||
set(PUREBOOT_HAS_USART1 ${_pb_has_usart1} PARENT_SCOPE)
|
||||
set(PUREBOOT_SIM_MCU ${_pb_sim_mcu} PARENT_SCOPE)
|
||||
|
||||
# The fastest standard rate the clock reaches within 2.5 % — the same
|
||||
# best-of-U2X-and-plain divisor search libavr's solve_baud runs, so a
|
||||
# default never trips the compile-time error it is checked against. A
|
||||
# software build additionally requires the polled receiver's 100-cycles-a-bit
|
||||
# floor (its own static assert): at low clocks the U2X divisor still reaches
|
||||
# rates the bit-banged sampler cannot, so the backend gates the ladder.
|
||||
function(pureboot_default_baud clock software outvar)
|
||||
foreach(baud 115200 57600 38400 19200 9600)
|
||||
math(EXPR _cycles "${clock} / ${baud}")
|
||||
if(software AND _cycles LESS 100)
|
||||
continue()
|
||||
endif()
|
||||
foreach(divisor 8 16)
|
||||
math(EXPR _step "${divisor} * ${baud}")
|
||||
math(EXPR _n "(${clock} + ${_step} / 2) / ${_step}")
|
||||
if(_n LESS 1 OR _n GREATER 4096)
|
||||
continue()
|
||||
endif()
|
||||
math(EXPR _actual "${clock} / (${divisor} * ${_n})")
|
||||
math(EXPR _delta "${_actual} - ${baud}")
|
||||
if(_delta LESS 0)
|
||||
math(EXPR _delta "-(${_delta})")
|
||||
endif()
|
||||
math(EXPR _error_bp "${_delta} * 10000 / ${baud}")
|
||||
if(_error_bp LESS_EQUAL 250)
|
||||
set(${outvar} ${baud} PARENT_SCOPE)
|
||||
return()
|
||||
endif()
|
||||
endforeach()
|
||||
endforeach()
|
||||
message(FATAL_ERROR "pureboot: no standard baud rate fits a ${clock} Hz clock within 2.5 %")
|
||||
endfunction()
|
||||
|
||||
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
||||
# [SERIAL auto|hardware|software] [USART <n>]
|
||||
# [RX <pin>] [TX <pin>] [TIMEOUT <s>])
|
||||
#
|
||||
# Creates the loader target plus its flashable images (<name>.hex for a
|
||||
# programmer, <name>.bin for --update-loader) and stamps the resolved
|
||||
# deployment on the target: the PUREBOOT_HZ, PUREBOOT_BAUD and PUREBOOT_LINK
|
||||
# properties (the link as usart0/usart1/sw:<RX>,<TX> — what a test harness
|
||||
# needs to speak to the build).
|
||||
function(pureboot_add_loader name)
|
||||
cmake_parse_arguments(PB "" "CLOCK;BAUD;SERIAL;USART;RX;TX;TIMEOUT" "" ${ARGN})
|
||||
if(PB_UNPARSED_ARGUMENTS)
|
||||
message(FATAL_ERROR "pureboot_add_loader(${name}): unknown arguments ${PB_UNPARSED_ARGUMENTS}")
|
||||
endif()
|
||||
get_property(_hz GLOBAL PROPERTY PUREBOOT_DEFAULT_HZ)
|
||||
get_property(_base_hex GLOBAL PROPERTY PUREBOOT_BASE_HEX)
|
||||
get_property(_app GLOBAL PROPERTY PUREBOOT_APP)
|
||||
get_property(_wrap GLOBAL PROPERTY PUREBOOT_WRAP)
|
||||
get_property(_usart GLOBAL PROPERTY PUREBOOT_HAS_USART)
|
||||
get_property(_usart1 GLOBAL PROPERTY PUREBOOT_HAS_USART1)
|
||||
|
||||
if(NOT PB_CLOCK)
|
||||
set(PB_CLOCK ${_hz})
|
||||
endif()
|
||||
if(NOT PB_TIMEOUT)
|
||||
set(PB_TIMEOUT 8)
|
||||
endif()
|
||||
if(NOT PB_SERIAL)
|
||||
set(PB_SERIAL auto)
|
||||
endif()
|
||||
if(DEFINED PB_USART AND PB_SERIAL STREQUAL "software")
|
||||
message(FATAL_ERROR "pureboot_add_loader(${name}): USART ${PB_USART} contradicts SERIAL software")
|
||||
endif()
|
||||
if(DEFINED PB_USART)
|
||||
set(PB_SERIAL hardware)
|
||||
elseif(PB_SERIAL STREQUAL "hardware")
|
||||
set(PB_USART 0)
|
||||
endif()
|
||||
|
||||
set(_serial_defines "")
|
||||
if(PB_SERIAL STREQUAL "hardware")
|
||||
if(PB_USART EQUAL 1 AND NOT _usart1)
|
||||
message(FATAL_ERROR "pureboot_add_loader(${name}): ${LIBAVR_MCU} has no USART1")
|
||||
elseif(NOT _usart)
|
||||
message(FATAL_ERROR "pureboot_add_loader(${name}): ${LIBAVR_MCU} has no hardware USART")
|
||||
endif()
|
||||
set(_serial_defines PUREBOOT_USART=${PB_USART})
|
||||
set(_link usart${PB_USART})
|
||||
else()
|
||||
if(PB_SERIAL STREQUAL "auto")
|
||||
if(_usart AND (PB_RX OR PB_TX))
|
||||
message(WARNING "pureboot_add_loader(${name}): RX/TX apply to the software UART, "
|
||||
"which auto does not pick on ${LIBAVR_MCU} — SERIAL software to force it")
|
||||
endif()
|
||||
if(_usart)
|
||||
set(_link usart0)
|
||||
else()
|
||||
set(PB_SERIAL software)
|
||||
endif()
|
||||
endif()
|
||||
if(PB_SERIAL STREQUAL "software")
|
||||
if(NOT PB_RX)
|
||||
set(PB_RX pb0)
|
||||
endif()
|
||||
if(NOT PB_TX)
|
||||
set(PB_TX pb1)
|
||||
endif()
|
||||
foreach(_pin ${PB_RX} ${PB_TX})
|
||||
if(NOT _pin MATCHES "^p[a-h][0-7]$")
|
||||
message(FATAL_ERROR "pureboot_add_loader(${name}): pin '${_pin}' is not of the form pb1")
|
||||
endif()
|
||||
endforeach()
|
||||
set(_serial_defines PUREBOOT_SOFT_SERIAL PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
|
||||
# The link spec a test harness drives a GPIO bridge with: sw:<RX>,<TX>
|
||||
# as the port letter and bit, the loader's own pin naming upcased.
|
||||
string(SUBSTRING ${PB_RX} 1 2 _rx_pin)
|
||||
string(SUBSTRING ${PB_TX} 1 2 _tx_pin)
|
||||
string(TOUPPER "sw:${_rx_pin},${_tx_pin}" _link)
|
||||
string(REPLACE "SW" "sw" _link ${_link})
|
||||
endif()
|
||||
endif()
|
||||
if(NOT PB_BAUD)
|
||||
if(PB_SERIAL STREQUAL "software")
|
||||
pureboot_default_baud(${PB_CLOCK} 1 PB_BAUD)
|
||||
else()
|
||||
pureboot_default_baud(${PB_CLOCK} 0 PB_BAUD)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(_defines PUREBOOT_CLOCK_HZ=${PB_CLOCK} PUREBOOT_BAUD=${PB_BAUD} PUREBOOT_TIMEOUT=${PB_TIMEOUT}
|
||||
${_serial_defines})
|
||||
|
||||
add_executable(${name} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/pureboot.cpp)
|
||||
target_link_libraries(${name} PRIVATE libavr)
|
||||
target_compile_definitions(${name} PRIVATE ${_defines})
|
||||
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${_base_hex}
|
||||
-Wl,--defsym=pureboot_app=${_app} ${_wrap})
|
||||
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
|
||||
# The ELF is a container (symbols, section headers), never flashed; the
|
||||
# flashable forms sit beside it: .hex for a programmer, .bin (the slot's
|
||||
# bare bytes) for the host tool's raw path and --update-loader.
|
||||
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)
|
||||
set_target_properties(${name} PROPERTIES PUREBOOT_HZ ${PB_CLOCK} PUREBOOT_BAUD ${PB_BAUD}
|
||||
PUREBOOT_LINK ${_link})
|
||||
endfunction()
|
||||
@@ -3,16 +3,18 @@
|
||||
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`).
|
||||
fitting each chip's smallest boot sector**: 512 bytes everywhere — 470 B on
|
||||
the tiny13s, ~484 B on the tiny25/45/85, 458–506 B across the megas and
|
||||
every configured variant of them (the m8's software-serial build is the
|
||||
fattest) — 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 556–562 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. Clock, baud, serial
|
||||
backend and pins are per-build configuration (below); the size matrix in
|
||||
the test suite holds every combination inside its slot. 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
|
||||
@@ -26,16 +28,63 @@ jumps into it, and lets it rewrite the resident. The slot is 512 bytes
|
||||
minimum); on the tinies the budget is 510, not 512: a slot's last word
|
||||
belongs to the host-managed trampoline (below).
|
||||
|
||||
## Configuration
|
||||
|
||||
Every deployment axis is a build parameter, resolved by the CMake function
|
||||
`pureboot_add_loader()` (in `pureboot/CMakeLists.txt`) — the one way a
|
||||
loader target is created, by this repo's own build and by a downstream
|
||||
project alike:
|
||||
|
||||
| Argument | Meaning | Default |
|
||||
|---|---|---|
|
||||
| `CLOCK <hz>` | the clock the board runs | 16 MHz megas, 8 MHz t25/45/85, 9.6 MHz t13s |
|
||||
| `BAUD <bd>` | the wire rate | the ladder below |
|
||||
| `SERIAL auto\|hardware\|software` | the link backend | `auto`: the hardware USART where the chip has one |
|
||||
| `USART <n>` | the USART instance (x4 megas carry two) | 0 |
|
||||
| `RX <pin>`, `TX <pin>` | software-UART pins | `pb0`, `pb1` |
|
||||
| `TIMEOUT <s>` | the activation window | 8 |
|
||||
|
||||
The default baud is the fastest of 115200/57600/38400/19200/9600 the clock
|
||||
reaches within 2.5 % — the same U2X-included divisor search libavr's baud
|
||||
solver runs — and on a software build additionally within the polled
|
||||
receiver's 100-cycles-a-bit floor. 16 MHz lands 115200, 8 MHz 57600,
|
||||
1 MHz 9600. Whatever is picked or overridden is re-checked in the compile:
|
||||
an infeasible clock/baud/backend combination, or a USART the chip does not
|
||||
have, fails with a named static assert.
|
||||
|
||||
A downstream project brings its usual libavr setup (the `libavr` target,
|
||||
the chip via the `LIBAVR_MCU` toolchain preset), consumes this directory,
|
||||
and states its deployment — for example an ATmega328P on its shipped
|
||||
1 MHz fuses with the software UART on hand-picked pins:
|
||||
|
||||
```cmake
|
||||
FetchContent_Declare(bootloader GIT_REPOSITORY git@git.blackmark.me:avr/bootloader.git GIT_TAG main)
|
||||
FetchContent_MakeAvailable(bootloader)
|
||||
add_subdirectory(${bootloader_SOURCE_DIR}/pureboot pureboot)
|
||||
|
||||
pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
|
||||
```
|
||||
|
||||
The function emits the ELF plus `myboot.hex` (the programmer artifact) and
|
||||
`myboot.bin` (the self-update image), prints the size, and stamps the
|
||||
resolved deployment on the target as the `PUREBOOT_HZ`, `PUREBOOT_BAUD`
|
||||
and `PUREBOOT_LINK` properties — what a flashing script or test harness
|
||||
needs to speak to the build. This exact example deployment runs the full
|
||||
protocol suite in CI (`pureboot.custom`).
|
||||
|
||||
## Link
|
||||
|
||||
The stock builds assume the family's natural deployment; any axis moves
|
||||
per build (above).
|
||||
|
||||
| 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.
|
||||
The software-UART RX pin has its pull-up enabled; TX idles high. All
|
||||
multi-byte quantities on the wire are little-endian.
|
||||
|
||||
## Activation
|
||||
|
||||
@@ -50,9 +99,10 @@ The host then has one activation window per awaited byte to knock: `p` then
|
||||
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
|
||||
The window length is a compile-time constant — 8 s by default, another
|
||||
value via `pureboot_add_loader(... TIMEOUT <s>)` (the stock target keeps
|
||||
the `PUREBOOT_TIMEOUT` 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
|
||||
@@ -237,11 +287,35 @@ 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).
|
||||
|
||||
Readouts come one fact per line: `--info` prints the decoded info block
|
||||
field by field, `--fuses` each fuse byte on its own line — plus, on a
|
||||
boot-sectioned mega, the decoded meaning (where the BOOTSZ section starts,
|
||||
what BOOTRST does to reset). Transfers that take wire time — programming,
|
||||
reading, erasing, verifying, the update phases — draw a transient progress
|
||||
bar on stderr when it is a tty; logs and pipes see only the summary lines.
|
||||
`-v`/`--verbose` adds the decisions as they happen: knock counts, the
|
||||
programming plan (vector-surgery targets, skipped blank pages), update
|
||||
state handling and per-phase page counts.
|
||||
|
||||
## Tests
|
||||
|
||||
Per chip preset, `ctest` runs:
|
||||
`tools/check.sh` runs every chip's workflow (`tools/check.sh --full` adds
|
||||
the reflect-mode builds of libavr's spot set; `tools/make_presets.py`
|
||||
regenerates the presets). Per chip preset, `ctest` runs:
|
||||
|
||||
- `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget;
|
||||
- `pureboot_*.size` — the size matrix: the serial backends × the clock
|
||||
ladder (1/8/16 MHz; the t13s' own RC menu), plus the USART1 build on the
|
||||
x4 chips — every configuration axis that could move the image, each
|
||||
variant against the same slot budget (pins are immediate operands and the
|
||||
timeout is a constant: size-neutral);
|
||||
- `pureboot.custom` (328P) — the configured-deployment acceptance test: the
|
||||
1 MHz software-serial TX=PB1/RX=PB5 build from the configuration example
|
||||
drives the full protocol suite through the runner's GPIO bridge, fixture
|
||||
application included;
|
||||
- `pureboot.usart1` (644A) — the same protocol suite over the second
|
||||
hardware USART: instance selection is compile-checked everywhere, but
|
||||
only a live session proves the loader polls the USART it claims;
|
||||
- `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
|
||||
@@ -249,9 +323,10 @@ Per chip preset, `ctest` runs:
|
||||
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
|
||||
(`test/pureboot_device.c` — a hardware USART as a pty, or a cycle-timed
|
||||
GPIO⇄pty bridge for a software-UART build, selected with `-l` to match
|
||||
the loader's link; 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
|
||||
@@ -265,6 +340,6 @@ Per chip preset, `ctest` runs:
|
||||
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
|
||||
`size`, `pi`, and `planner` are host logic and run anywhere; the
|
||||
simulator-driven targets need simavr and a pty, so they are POSIX-only —
|
||||
on Windows the tool is exercised against real hardware.
|
||||
|
||||
@@ -38,22 +38,19 @@ 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;
|
||||
}
|
||||
// Per-deployment personality, passed in by the build — pureboot_add_loader()
|
||||
// (the CMake function next to this file) resolves the defaults: the clock the
|
||||
// board actually runs, the wire baud, the serial backend and its pins. The
|
||||
// device signature needs no configuring — it comes from the chip database
|
||||
// (avr::hw::db.signature), the only universal source, since the tiny13A
|
||||
// cannot even read its signature row from code.
|
||||
#if !defined(PUREBOOT_CLOCK_HZ) || !defined(PUREBOOT_BAUD)
|
||||
#error \
|
||||
"PUREBOOT_CLOCK_HZ and PUREBOOT_BAUD select this build's clock and baud — create loader targets with pureboot_add_loader() (README.md)"
|
||||
#endif
|
||||
|
||||
using dev = avr::device<{.clock = clock()}>;
|
||||
using dev = avr::device<{.clock = avr::hertz_t{PUREBOOT_CLOCK_HZ}}>;
|
||||
constexpr avr::baud_t wire_baud{PUREBOOT_BAUD};
|
||||
|
||||
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
|
||||
consteval std::int16_t wdrf_field()
|
||||
@@ -118,34 +115,43 @@ constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
||||
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#">();
|
||||
}
|
||||
// The serial link. PUREBOOT_USART forces a hardware USART instance,
|
||||
// PUREBOOT_SOFT_SERIAL the polled software UART (no vector — the table
|
||||
// belongs to the application) on PUREBOOT_RX/PUREBOOT_TX; with neither, the
|
||||
// chip's first USART where it has one and the software UART 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).
|
||||
#if defined(PUREBOOT_SOFT_SERIAL) && defined(PUREBOOT_USART)
|
||||
#error "PUREBOOT_SOFT_SERIAL and PUREBOOT_USART select opposing serial backends"
|
||||
#endif
|
||||
#if !defined(PUREBOOT_RX)
|
||||
#define PUREBOOT_RX pb0
|
||||
#endif
|
||||
#if !defined(PUREBOOT_TX)
|
||||
#define PUREBOOT_TX pb1
|
||||
#endif
|
||||
#if defined(PUREBOOT_USART)
|
||||
constexpr char usart_digit = '0' + PUREBOOT_USART;
|
||||
#else
|
||||
constexpr char usart_digit = '0';
|
||||
#endif
|
||||
|
||||
template <avr::hertz_t C>
|
||||
consteval std::int16_t txc_field()
|
||||
// Whether the chip carries the selected USART: the suffixed instance name,
|
||||
// or — for instance 0 — the classic megas' un-numbered block.
|
||||
consteval bool usart_exists()
|
||||
{
|
||||
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">();
|
||||
const char name[] = {'U', 'S', 'A', 'R', 'T', usart_digit};
|
||||
if (avr::hw::db.has_instance(std::string_view{name, sizeof(name)}))
|
||||
return true;
|
||||
return usart_digit == '0' && avr::hw::db.has_instance("USART");
|
||||
}
|
||||
|
||||
template <avr::hertz_t C>
|
||||
struct hardware_link {
|
||||
using uart = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>;
|
||||
using uart = avr::uart::usart<usart_digit, C, {.baud = wire_baud, .max_baud_error = 2.5_pct}>;
|
||||
|
||||
// The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2),
|
||||
// sbiw + sbci + sbci + brne (6).
|
||||
@@ -158,7 +164,7 @@ struct hardware_link {
|
||||
|
||||
static bool pending()
|
||||
{
|
||||
return avr::hw::field_impl<rxc_field<C>()>::test();
|
||||
return uart::rx_ready();
|
||||
}
|
||||
|
||||
static std::uint8_t rx()
|
||||
@@ -173,22 +179,14 @@ struct hardware_link {
|
||||
|
||||
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()) {
|
||||
}
|
||||
uart::drain();
|
||||
}
|
||||
};
|
||||
|
||||
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>;
|
||||
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, wire_baud>;
|
||||
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, wire_baud>;
|
||||
|
||||
// The compiled idle poll: sbis skipping the exit (2), sbiw + sbci +
|
||||
// sbci + brne (6).
|
||||
@@ -201,7 +199,7 @@ struct software_link {
|
||||
|
||||
static bool pending()
|
||||
{
|
||||
return !avr::io::input<avr::pb0>::read(); // a start bit has begun
|
||||
return rx_t::start_pending();
|
||||
}
|
||||
|
||||
static std::uint8_t rx()
|
||||
@@ -220,8 +218,14 @@ struct software_link {
|
||||
}
|
||||
};
|
||||
|
||||
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>>;
|
||||
#if defined(PUREBOOT_USART)
|
||||
static_assert(usart_exists(), "PUREBOOT_USART selects a hardware USART this chip does not have");
|
||||
using link = hardware_link<dev::clock>;
|
||||
#elif defined(PUREBOOT_SOFT_SERIAL)
|
||||
using link = software_link<dev::clock>;
|
||||
#else
|
||||
using link = std::conditional_t<usart_exists(), hardware_link<dev::clock>, software_link<dev::clock>>;
|
||||
#endif
|
||||
|
||||
// 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
|
||||
@@ -417,10 +421,13 @@ void send_fuses()
|
||||
// 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.
|
||||
// from it, so both follow wherever the code was flashed. The high byte is
|
||||
// spelled as byteswap's low byte: the builtin's value is itself built by
|
||||
// swapping the two stacked bytes, and the double swap folds to the single
|
||||
// byte pick a hand assembler writes — `>> 8` leaves the swap materialized.
|
||||
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);
|
||||
const std::uint8_t ra_high = static_cast<std::uint8_t>(std::byteswap(ra_words));
|
||||
const std::uint8_t slot_high = word_flash ? ra_high & 0xfe : static_cast<std::uint8_t>(ra_high << 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.
|
||||
@@ -439,13 +446,13 @@ void send_fuses()
|
||||
// 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.
|
||||
// runtime address is the running slot's. Composed from the two
|
||||
// bytes — the high half is runtime data, 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()));
|
||||
send_flash(static_cast<std::uint16_t>(low | (slot_high << 8)), static_cast<std::uint8_t>(info_data.size()));
|
||||
break;
|
||||
}
|
||||
case 'J': { // jump to a wire word address: hand-over and staging transfer
|
||||
|
||||
Reference in New Issue
Block a user