Pins move the image for exactly one reason — a bit-banged link on a USART's own
pins has to release that USART — and the matrix said outright that they were no
axis, so the tightest configuration in the space was one nothing built. Not
subtly, either: the 1284's slot ends at flash end, so that build does not merely
exceed the size test's limit, it fails to link. pureboot_{sw,autobaud}_on_usart
{0,1} are gate points in both matrix modes now, and the exhaustive sweep carries
the pins across its whole cross product. The hand-measured table is the gate's
output: 506 B of 512 for the 1284 autobaud on USART0's pins, 504 on USART1's.
pureboot.mute drives the defect itself — an application hands over with USART0
still enabled and the loader on those pins must still answer. Reaching that
needed the runner to know an enabled USART owns its TxD, which simavr does not
model at all: it wires a USART through IRQs and never takes the pin from the
port. It also brings UCSRnB up with TXEN already set where silicon clears the
register, so the runner restores the reset value for the USART it models — the
mute must come from the application, not from power-on. The fixture stays
silent, since nothing is listening on the USART it brings up.
test_handshake.py, written where no gate could run it, is pureboot.handshake.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
358 lines
14 KiB
CMake
358 lines
14 KiB
CMake
# 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.
|
||
# A downstream project brings its usual libavr setup (the `libavr` target and
|
||
# the LIBAVR_MCU toolchain preset), adds this directory, and states its
|
||
# deployment; every argument is optional (README.md):
|
||
#
|
||
# add_subdirectory(bootloader/pureboot)
|
||
# pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
|
||
|
||
# Per-family geometry, deployment defaults, and the linker wrap the PC modulo
|
||
# needs. The slot is 512 bytes on every chip. 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, so plain LPM still reaches
|
||
# everything and the wire stays byte-addressed. 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 addresses are words, reads go through ELPM, and the PC's
|
||
# modulo wrap exceeds what --pmem-wrap-around models.
|
||
set(_pb_flash 131072)
|
||
set(_pb_wrap "")
|
||
set(_pb_page 256)
|
||
set(_pb_hz 16000000)
|
||
set(_pb_eeprom 4096)
|
||
set(_pb_has_usart1 1)
|
||
else()
|
||
message(FATAL_ERROR "pureboot: no geometry for ${LIBAVR_MCU}")
|
||
endif()
|
||
set(_pb_slot 512)
|
||
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)
|
||
set(_pb_limit ${_pb_slot})
|
||
else()
|
||
math(EXPR _pb_app "${_pb_base} - 2")
|
||
math(EXPR _pb_limit "${_pb_slot} - 2")
|
||
endif()
|
||
|
||
# The pins each USART owns. A bit-banged link deployed on them has to release
|
||
# that USART before it can drive the line, and those instructions are the one
|
||
# way the choice of pins moves the image — so a size matrix needs them as an
|
||
# axis even though pins are otherwise immediate operands. Uniform across every
|
||
# mega libavr covers: USART0 (the classics' un-numbered USART included) on
|
||
# PD0/PD1, USART1 on PD2/PD3.
|
||
set(_pb_usart0_rx pd0)
|
||
set(_pb_usart0_tx pd1)
|
||
set(_pb_usart1_rx pd2)
|
||
set(_pb_usart1_tx pd3)
|
||
|
||
# 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})
|
||
set_property(GLOBAL PROPERTY PUREBOOT_USART0_TX ${_pb_usart0_tx})
|
||
set_property(GLOBAL PROPERTY PUREBOOT_USART1_TX ${_pb_usart1_tx})
|
||
|
||
# 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)
|
||
set(PUREBOOT_USART0_RX ${_pb_usart0_rx} PARENT_SCOPE)
|
||
set(PUREBOOT_USART0_TX ${_pb_usart0_tx} PARENT_SCOPE)
|
||
set(PUREBOOT_USART1_RX ${_pb_usart1_rx} PARENT_SCOPE)
|
||
set(PUREBOOT_USART1_TX ${_pb_usart1_tx} PARENT_SCOPE)
|
||
|
||
# The rates a default may pick, fastest first.
|
||
set_property(GLOBAL PROPERTY PUREBOOT_BAUD_LADDER 115200 57600 38400 19200 9600)
|
||
|
||
# Whether <baud> is reachable from <clock> within 2.5 %, by the same
|
||
# best-of-U2X-and-plain divisor search libavr's solve_baud runs, so a build
|
||
# never trips the compile-time error it is checked against. A software build
|
||
# also needs the polled receiver's 100-cycles-a-bit floor: at low clocks the
|
||
# U2X divisor reaches rates the bit-banged sampler cannot.
|
||
function(pureboot_baud_feasible clock baud software outvar)
|
||
set(${outvar} 0 PARENT_SCOPE)
|
||
math(EXPR _cycles "${clock} / ${baud}")
|
||
if(software AND _cycles LESS 100)
|
||
return()
|
||
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} 1 PARENT_SCOPE)
|
||
return()
|
||
endif()
|
||
endforeach()
|
||
endfunction()
|
||
|
||
# The fastest ladder rate the clock reaches.
|
||
function(pureboot_default_baud clock software outvar)
|
||
get_property(_ladder GLOBAL PROPERTY PUREBOOT_BAUD_LADDER)
|
||
foreach(baud ${_ladder})
|
||
pureboot_baud_feasible(${clock} ${baud} ${software} _ok)
|
||
if(_ok)
|
||
set(${outvar} ${baud} PARENT_SCOPE)
|
||
return()
|
||
endif()
|
||
endforeach()
|
||
message(FATAL_ERROR "pureboot: no standard baud rate fits a ${clock} Hz clock within 2.5 % "
|
||
"— pass BAUD <rate> to deploy a non-standard one")
|
||
endfunction()
|
||
|
||
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
||
# [SERIAL auto|hardware|software|autobaud] [USART <n>]
|
||
# [RX <pin>] [TX <pin>] [TIMEOUT <s>])
|
||
#
|
||
# The loader target plus its flashable images (<name>.hex for a programmer,
|
||
# <name>.bin for --update-loader). The resolved deployment is stamped on the
|
||
# target as PUREBOOT_HZ / PUREBOOT_BAUD / PUREBOOT_LINK (the link spelled
|
||
# usart0, usart1, or sw:<RX>,<TX> with a trailing @<n> where those pins are a
|
||
# USART's own) — what a test harness speaks to it with.
|
||
#
|
||
# SERIAL autobaud measures the host's bit timing at run time, so the image
|
||
# carries no clock and no baud: CLOCK and BAUD are not build parameters there,
|
||
# and one binary per chip serves every F_CPU and every rate. The stamped
|
||
# PUREBOOT_HZ/PUREBOOT_BAUD then record what a harness should *drive* it at,
|
||
# not what it was built for.
|
||
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 NOT PB_SERIAL MATCHES "^(auto|hardware)$")
|
||
message(FATAL_ERROR "pureboot_add_loader(${name}): USART ${PB_USART} contradicts SERIAL ${PB_SERIAL}")
|
||
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 MATCHES "^(software|autobaud)$")
|
||
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()
|
||
if(PB_SERIAL STREQUAL "autobaud")
|
||
set(_serial_defines PUREBOOT_AUTOBAUD PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
|
||
else()
|
||
set(_serial_defines PUREBOOT_SOFT_SERIAL PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
|
||
endif()
|
||
# sw:<RX>,<TX> as port letter and bit, upcased — with @<n> where
|
||
# the TX pin is a USART's own TXD, since a harness driving that
|
||
# link has to know the USART owns the pin until the loader
|
||
# releases it.
|
||
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})
|
||
get_property(_tx0 GLOBAL PROPERTY PUREBOOT_USART0_TX)
|
||
get_property(_tx1 GLOBAL PROPERTY PUREBOOT_USART1_TX)
|
||
if(_usart AND PB_TX STREQUAL _tx0)
|
||
set(_link "${_link}@0")
|
||
elseif(_usart1 AND PB_TX STREQUAL _tx1)
|
||
set(_link "${_link}@1")
|
||
endif()
|
||
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()
|
||
|
||
if(PB_SERIAL STREQUAL "autobaud")
|
||
# No clock and no baud reach the image; the window is a poll budget.
|
||
set(_defines ${_serial_defines})
|
||
else()
|
||
set(_defines PUREBOOT_CLOCK_HZ=${PB_CLOCK} PUREBOOT_BAUD=${PB_BAUD} PUREBOOT_TIMEOUT=${PB_TIMEOUT}
|
||
${_serial_defines})
|
||
endif()
|
||
|
||
add_executable(${name} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/pureboot.cpp)
|
||
target_link_libraries(${name} PRIVATE libavr)
|
||
target_compile_definitions(${name} PRIVATE ${_defines})
|
||
# Codegen shaping for the loader TU only. At -Os GCC otherwise rewrites the
|
||
# byte-stream loops' counters into end-pointer forms that cost registers
|
||
# (-fno-ivopts, -fno-split-wide-types), leaves register pressure on the
|
||
# table with the default allocator (-fira-algorithm=priority), and keeps
|
||
# expression temporaries in registers (-fno-tree-ter) — but every loop body
|
||
# here contains a call, so a register held across it costs more than the
|
||
# load-immediate it saves. The set is fitted to the loader's body and has to
|
||
# be re-measured when that body changes: -fno-move-loop-invariants belonged
|
||
# here while the command loop carried four transfer bodies and costs bytes
|
||
# now that it carries one.
|
||
target_compile_options(${name} PRIVATE
|
||
-fno-ivopts -fira-algorithm=priority -fno-tree-ter -fno-split-wide-types)
|
||
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, never flashed: .hex for a programmer, .bin (the
|
||
# slot's bare bytes) for --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()
|
||
|