# 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( [CLOCK ] [BAUD ] # [SERIAL auto|hardware|software] [USART ] # [RX ] [TX ] [TIMEOUT ]) # # Creates the loader target plus its flashable images (.hex for a # programmer, .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:, — 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:, # 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} $) # 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 $ $.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()