Every plausible oscillator against every rate it reaches against every backend, on one chip per size-bearing class, under --full only. The baud ladder becomes a reachability predicate the enumeration filters on, so an unreachable point drops out instead of aborting the configure. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
313 lines
12 KiB
CMake
313 lines
12 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()
|
||
|
||
# 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 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] [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>) — what a test harness speaks to it with.
|
||
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})
|
||
# sw:<RX>,<TX> as port letter and bit, 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})
|
||
# Codegen shaping for the loader TU only, worth 14–36 B depending on the
|
||
# chip. 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 loop-invariant
|
||
# immediates and expression temporaries in registers
|
||
# (-fno-move-loop-invariants, -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.
|
||
target_compile_options(${name} PRIVATE
|
||
-fno-ivopts -fira-algorithm=priority -fno-move-loop-invariants -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()
|