# 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 is reachable from 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 to deploy a non-standard one") endfunction() # pureboot_add_loader( [CLOCK ] [BAUD ] # [SERIAL auto|hardware|software|autobaud] [USART ] # [RX ] [TX ] [TIMEOUT ]) # # The loader target plus its flashable images (.hex for a programmer, # .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:, with a trailing @ 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:, as port letter and bit, upcased — with @ 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} $) # 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 $ $.hex COMMAND ${CMAKE_OBJCOPY} -O binary -R .eeprom $ $.bin) set_target_properties(${name} PROPERTIES PUREBOOT_HZ ${PB_CLOCK} PUREBOOT_BAUD ${PB_BAUD} PUREBOOT_LINK ${_link}) endfunction()