# 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 fastest standard rate the clock reaches within 2.5 %, by 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
# 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_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>])
#
# 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()
