pureboot 6: a build-time OSCCAL trim, applied ahead of every reset path
The RC-oscillator answer's device half (dev/tasks.md in libavr): OSCCAL joins pureboot_add_loader() as one optional byte, written at the top of run() before the WDRF bail so the watchdog hand-over inherits the corrected clock too. Orthogonal to the backend — an autobaud build may carry it purely for the application. No value, no code: the stock image differs from v5 in exactly the version's two bytes (the stamp and the 'b' immediate). Measured: +6 B where OSCCAL takes sts (328P, 404→410), +4 B in low I/O (t85, 402→406); the tightest image in the space (1284 autobaud on USART pins, 504) carries the sts form at 510 of 512. New gates: the OSCCAL size points on every chip, the wire-observed trim byte on both addressing classes (test/pbosccal.py, red-green), and the autobaud unit pinned to ram_start (test/check_unit.cmake, red-green) — the address --info's measured-clock read is about to rely on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -270,6 +270,12 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
add_test(NAME pureboot_autobaud.size
|
add_test(NAME pureboot_autobaud.size
|
||||||
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot_autobaud>
|
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot_autobaud>
|
||||||
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
||||||
|
# The measured unit is the loader's only RAM object and sits at the very
|
||||||
|
# start of SRAM — where the host reads the bit period from (--info's
|
||||||
|
# measured clock), so the address is wire contract, not layout accident.
|
||||||
|
add_test(NAME pureboot_autobaud.unit
|
||||||
|
COMMAND ${CMAKE_COMMAND} -DOBJDUMP=${CMAKE_OBJDUMP} -DELF=$<TARGET_FILE:pureboot_autobaud>
|
||||||
|
-DRAM_START=${PUREBOOT_RAM_START} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_unit.cmake)
|
||||||
|
|
||||||
# One point of the exhaustive matrix, named from its resolved parameters
|
# One point of the exhaustive matrix, named from its resolved parameters
|
||||||
# so the enumeration cannot collide with itself. `pins` is empty for the
|
# so the enumeration cannot collide with itself. `pins` is empty for the
|
||||||
@@ -390,6 +396,38 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
RX ${PUREBOOT_USART1_RX} TX ${PUREBOOT_USART1_TX})
|
RX ${PUREBOOT_USART1_RX} TX ${PUREBOOT_USART1_TX})
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# The OSCCAL axis at its fixed points: the stock shape, and the tightest
|
||||||
|
# image in the space with the trim on top — the axis adds one register
|
||||||
|
# write, and these points hold both of its addressing encodings to every
|
||||||
|
# chip's budget.
|
||||||
|
pureboot_size_variant(pureboot_osccal OSCCAL 0x9c)
|
||||||
|
pureboot_size_variant(pureboot_autobaud_osccal SERIAL autobaud OSCCAL 0x9c)
|
||||||
|
if(PUREBOOT_HAS_USART)
|
||||||
|
pureboot_size_variant(pureboot_autobaud_osccal_on_usart0 SERIAL autobaud OSCCAL 0x9c
|
||||||
|
RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_TX})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# The trim byte, observed through the wire from the first prompt — one
|
||||||
|
# chip per OSCCAL addressing class: extended I/O on the 328P (data 0x66,
|
||||||
|
# an sts — DS40002061B §36), plain I/O on the 85 (data 0x51, an out —
|
||||||
|
# Atmel-2586 §21).
|
||||||
|
if(LIBAVR_MCU MATCHES "^(atmega328p|attiny85)$" AND DEFINED PB_DEVICE)
|
||||||
|
if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||||
|
set(_osccal_addr 0x66)
|
||||||
|
else()
|
||||||
|
set(_osccal_addr 0x51)
|
||||||
|
endif()
|
||||||
|
get_target_property(_osccal_hz pureboot_osccal PUREBOOT_HZ)
|
||||||
|
get_target_property(_osccal_baud pureboot_osccal PUREBOOT_BAUD)
|
||||||
|
add_test(NAME pureboot.osccal
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbosccal.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot_osccal> ${PUREBOOT_SIM_MCU}
|
||||||
|
${_osccal_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_osccal_baud}
|
||||||
|
${_osccal_addr} 0x9c ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbosccal-work)
|
||||||
|
set_tests_properties(pureboot.osccal PROPERTIES TIMEOUT 120)
|
||||||
|
endif()
|
||||||
|
|
||||||
# One configured deployment end to end — a real board's shape rather
|
# One configured deployment end to end — a real board's shape rather
|
||||||
# than the stock assumption: the ATmega328P on its shipped 1 MHz fuses,
|
# than the stock assumption: the ATmega328P on its shipped 1 MHz fuses,
|
||||||
# the software UART on hand-picked pins (TX = PB1, RX = PB5), the ladder
|
# the software UART on hand-picked pins (TX = PB1, RX = PB5), the ladder
|
||||||
|
|||||||
@@ -136,6 +136,19 @@ elseif(LIBAVR_MCU STREQUAL "atmega644pa")
|
|||||||
set(_pb_sim_mcu atmega644p)
|
set(_pb_sim_mcu atmega644p)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Where SRAM begins: the classic megas keep it right after the plain I/O
|
||||||
|
# registers, the x8/x4 generations push it past their extended I/O file, and
|
||||||
|
# the tinies match the classics. An autobaud loader's measured unit lives at
|
||||||
|
# exactly this address (the host reads it there — pureboot.py), and the
|
||||||
|
# unit-position test holds the layout to it.
|
||||||
|
if(LIBAVR_MCU MATCHES "^atmega(8|16|32)a?$")
|
||||||
|
set(_pb_ram 0x60)
|
||||||
|
elseif(LIBAVR_MCU MATCHES "^atmega")
|
||||||
|
set(_pb_ram 0x100)
|
||||||
|
else()
|
||||||
|
set(_pb_ram 0x60)
|
||||||
|
endif()
|
||||||
|
|
||||||
# The function runs in its caller's scope, so everything it needs crosses
|
# The function runs in its caller's scope, so everything it needs crosses
|
||||||
# scopes as global properties.
|
# scopes as global properties.
|
||||||
set_property(GLOBAL PROPERTY PUREBOOT_BASE_HEX ${_pb_base_hex})
|
set_property(GLOBAL PROPERTY PUREBOOT_BASE_HEX ${_pb_base_hex})
|
||||||
@@ -155,6 +168,7 @@ set(PUREBOOT_SLOT ${_pb_slot} PARENT_SCOPE)
|
|||||||
set(PUREBOOT_LIMIT ${_pb_limit} PARENT_SCOPE)
|
set(PUREBOOT_LIMIT ${_pb_limit} PARENT_SCOPE)
|
||||||
set(PUREBOOT_EEPROM ${_pb_eeprom} PARENT_SCOPE)
|
set(PUREBOOT_EEPROM ${_pb_eeprom} PARENT_SCOPE)
|
||||||
set(PUREBOOT_DEFAULT_HZ ${_pb_hz} PARENT_SCOPE)
|
set(PUREBOOT_DEFAULT_HZ ${_pb_hz} PARENT_SCOPE)
|
||||||
|
set(PUREBOOT_RAM_START ${_pb_ram} PARENT_SCOPE)
|
||||||
set(PUREBOOT_HAS_USART ${_pb_has_usart} PARENT_SCOPE)
|
set(PUREBOOT_HAS_USART ${_pb_has_usart} PARENT_SCOPE)
|
||||||
set(PUREBOOT_HAS_USART1 ${_pb_has_usart1} PARENT_SCOPE)
|
set(PUREBOOT_HAS_USART1 ${_pb_has_usart1} PARENT_SCOPE)
|
||||||
set(PUREBOOT_SIM_MCU ${_pb_sim_mcu} PARENT_SCOPE)
|
set(PUREBOOT_SIM_MCU ${_pb_sim_mcu} PARENT_SCOPE)
|
||||||
@@ -212,7 +226,7 @@ endfunction()
|
|||||||
|
|
||||||
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
||||||
# [SERIAL auto|hardware|software|autobaud] [USART <n>]
|
# [SERIAL auto|hardware|software|autobaud] [USART <n>]
|
||||||
# [RX <pin>] [TX <pin>] [TIMEOUT <s>])
|
# [RX <pin>] [TX <pin>] [TIMEOUT <s>] [OSCCAL <byte>])
|
||||||
#
|
#
|
||||||
# The loader target plus its flashable images (<name>.hex for a programmer,
|
# The loader target plus its flashable images (<name>.hex for a programmer,
|
||||||
# <name>.bin for --update-loader). The resolved deployment is stamped on the
|
# <name>.bin for --update-loader). The resolved deployment is stamped on the
|
||||||
@@ -225,8 +239,15 @@ endfunction()
|
|||||||
# and one binary per chip serves every F_CPU and every rate. The stamped
|
# 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,
|
# PUREBOOT_HZ/PUREBOOT_BAUD then record what a harness should *drive* it at,
|
||||||
# not what it was built for.
|
# not what it was built for.
|
||||||
|
#
|
||||||
|
# OSCCAL bakes a measured oscillator trim into the loader (README.md: the
|
||||||
|
# RC-oscillator deployment answer): the byte is written at the top of run(),
|
||||||
|
# so every reset path — the watchdog hand-over included — runs on the
|
||||||
|
# corrected clock. Orthogonal to the backend: an autobaud build may carry it
|
||||||
|
# purely for the application's benefit, its own link being clock-free. No
|
||||||
|
# value, no code.
|
||||||
function(pureboot_add_loader name)
|
function(pureboot_add_loader name)
|
||||||
cmake_parse_arguments(PB "" "CLOCK;BAUD;SERIAL;USART;RX;TX;TIMEOUT" "" ${ARGN})
|
cmake_parse_arguments(PB "" "CLOCK;BAUD;SERIAL;USART;RX;TX;TIMEOUT;OSCCAL" "" ${ARGN})
|
||||||
if(PB_UNPARSED_ARGUMENTS)
|
if(PB_UNPARSED_ARGUMENTS)
|
||||||
message(FATAL_ERROR "pureboot_add_loader(${name}): unknown arguments ${PB_UNPARSED_ARGUMENTS}")
|
message(FATAL_ERROR "pureboot_add_loader(${name}): unknown arguments ${PB_UNPARSED_ARGUMENTS}")
|
||||||
endif()
|
endif()
|
||||||
@@ -325,6 +346,13 @@ function(pureboot_add_loader name)
|
|||||||
set(_defines PUREBOOT_CLOCK_HZ=${PB_CLOCK} PUREBOOT_BAUD=${PB_BAUD} PUREBOOT_TIMEOUT=${PB_TIMEOUT}
|
set(_defines PUREBOOT_CLOCK_HZ=${PB_CLOCK} PUREBOOT_BAUD=${PB_BAUD} PUREBOOT_TIMEOUT=${PB_TIMEOUT}
|
||||||
${_serial_defines})
|
${_serial_defines})
|
||||||
endif()
|
endif()
|
||||||
|
if(DEFINED PB_OSCCAL)
|
||||||
|
math(EXPR _osccal "${PB_OSCCAL}" OUTPUT_FORMAT DECIMAL)
|
||||||
|
if(_osccal LESS 0 OR _osccal GREATER 255)
|
||||||
|
message(FATAL_ERROR "pureboot_add_loader(${name}): OSCCAL ${PB_OSCCAL} is not one byte")
|
||||||
|
endif()
|
||||||
|
list(APPEND _defines PUREBOOT_OSCCAL=${_osccal})
|
||||||
|
endif()
|
||||||
|
|
||||||
add_executable(${name} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/pureboot.cpp)
|
add_executable(${name} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/pureboot.cpp)
|
||||||
target_link_libraries(${name} PRIVATE libavr)
|
target_link_libraries(${name} PRIVATE libavr)
|
||||||
|
|||||||
@@ -72,9 +72,17 @@ constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
|||||||
#endif
|
#endif
|
||||||
constexpr avr::uint24_t autobaud_budget = PUREBOOT_AUTOBAUD_POLLS;
|
constexpr avr::uint24_t autobaud_budget = PUREBOOT_AUTOBAUD_POLLS;
|
||||||
|
|
||||||
|
// A build may bake a measured oscillator trim (README.md: the RC-oscillator
|
||||||
|
// deployment answer); the byte is applied at the top of run(). Orthogonal to
|
||||||
|
// the serial backend — an autobaud build may carry it for the application's
|
||||||
|
// benefit alone.
|
||||||
|
#if defined(PUREBOOT_OSCCAL)
|
||||||
|
static_assert(PUREBOOT_OSCCAL >= 0 && PUREBOOT_OSCCAL <= 0xff, "PUREBOOT_OSCCAL is one OSCCAL byte");
|
||||||
|
#endif
|
||||||
|
|
||||||
// The loader's one identity number. The protocol carries none of its own —
|
// The loader's one identity number. The protocol carries none of its own —
|
||||||
// a version implies it, and the host tool holds that map (README.md).
|
// a version implies it, and the host tool holds that map (README.md).
|
||||||
constexpr std::uint8_t version = 5;
|
constexpr std::uint8_t version = 6;
|
||||||
|
|
||||||
// The image's identity stamp, for the host tool rather than for the wire: an
|
// The image's identity stamp, for the host tool rather than for the wire: an
|
||||||
// update image is a bare 512-byte slot, and without this nothing in it says
|
// update image is a bare 512-byte slot, and without this nothing in it says
|
||||||
@@ -477,6 +485,12 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
|
|||||||
|
|
||||||
[[noreturn]] void run()
|
[[noreturn]] void run()
|
||||||
{
|
{
|
||||||
|
#if defined(PUREBOOT_OSCCAL)
|
||||||
|
// The build's oscillator trim, ahead of everything — the WDRF bail
|
||||||
|
// included — so every path out of reset, the watchdog hand-over to the
|
||||||
|
// application first among them, runs on the corrected clock.
|
||||||
|
avr::clock::calibrate(PUREBOOT_OSCCAL);
|
||||||
|
#endif
|
||||||
// A watchdog reset belongs to the application, whose watchdog stays forced
|
// A watchdog reset belongs to the application, whose watchdog stays forced
|
||||||
// on until it clears WDRF — no activation window in its way.
|
// on until it clears WDRF — no activation window in its way.
|
||||||
if (avr::hw::field_impl<wdrf_field()>::test())
|
if (avr::hw::field_impl<wdrf_field()>::test())
|
||||||
|
|||||||
@@ -24,13 +24,15 @@ else:
|
|||||||
import termios
|
import termios
|
||||||
|
|
||||||
PROMPT = b"+"
|
PROMPT = b"+"
|
||||||
VERSION = 5 # this tool's own version — free to drift from a loader's
|
VERSION = 6 # this tool's own version — free to drift from a loader's
|
||||||
# The loader versions this tool speaks. A pureboot version implies its wire
|
# The loader versions this tool can drive. A pureboot version implies its wire
|
||||||
# protocol, which carries no number of its own, so this window is where that
|
# protocol, which carries no number of its own, so this window is where that
|
||||||
# map lives: every version so far speaks the same protocol, and one that
|
# map lives: the tool keeps a decoder for every generation in it (1–4 speak
|
||||||
# changes it becomes the new floor here.
|
# the per-memory commands, 5 the unified pair; 6 marks the OSCCAL-carrying
|
||||||
|
# builds and changes nothing on the wire), and a version it has no decoder
|
||||||
|
# for moves the floor.
|
||||||
OLDEST_LOADER = 1
|
OLDEST_LOADER = 1
|
||||||
NEWEST_LOADER = 5
|
NEWEST_LOADER = 6
|
||||||
SLOT = 512 # the loader slot, on every chip
|
SLOT = 512 # the loader slot, on every chip
|
||||||
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
||||||
|
|
||||||
|
|||||||
23
test/check_unit.cmake
Normal file
23
test/check_unit.cmake
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Asserts the autobaud loader's measured unit is the first RAM object: the
|
||||||
|
# host tool reads the bit period from ram_start (--info's measured clock), so
|
||||||
|
# the unit's address is wire contract. Run as
|
||||||
|
# cmake -DOBJDUMP=... -DELF=... -DRAM_START=<data address> -P check_unit.cmake
|
||||||
|
|
||||||
|
execute_process(COMMAND ${OBJDUMP} -t ${ELF} OUTPUT_VARIABLE _syms RESULT_VARIABLE _res)
|
||||||
|
if(NOT _res EQUAL 0)
|
||||||
|
message(FATAL_ERROR "${OBJDUMP} -t ${ELF} failed")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# The symbol line: "00800100 l O .noinit 00000002 <mangled>unit_E".
|
||||||
|
string(REGEX MATCH "\n0*([0-9a-f]+)[^\n]+[ \t][^ \t\n]*unit_[^ \t\n]*\n" _line "${_syms}")
|
||||||
|
if(NOT _line)
|
||||||
|
message(FATAL_ERROR "no unit_ symbol in ${ELF} — is this the autobaud loader?")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# AVR data-space symbols carry the 0x800000 VMA offset.
|
||||||
|
math(EXPR _want "0x800000 + ${RAM_START}" OUTPUT_FORMAT HEXADECIMAL)
|
||||||
|
math(EXPR _have "0x${CMAKE_MATCH_1}" OUTPUT_FORMAT HEXADECIMAL)
|
||||||
|
if(NOT _have STREQUAL _want)
|
||||||
|
message(FATAL_ERROR "unit_ sits at ${_have}, ram_start is ${_want} — the host peeks ram_start")
|
||||||
|
endif()
|
||||||
|
message(STATUS "unit_ at ${_have} == ram_start")
|
||||||
46
test/pbosccal.py
Normal file
46
test/pbosccal.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""The build-time OSCCAL trim, observed through the wire: a loader built with
|
||||||
|
the OSCCAL axis holds the trim register at the built byte from its first
|
||||||
|
prompt on — the write sits at the top of run(), ahead of the WDRF bail, so
|
||||||
|
every path out of reset runs on the corrected clock. simavr's clock does not
|
||||||
|
follow OSCCAL, which is what makes the value assertable at all: the register
|
||||||
|
is plain state there, and the peek must return exactly what the build
|
||||||
|
declared rather than whatever the oscillator needed.
|
||||||
|
|
||||||
|
Usage: pbosccal.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||||
|
<baud> <osccal_addr> <osccal_value> <tool_py> <workdir>
|
||||||
|
[link]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
print(f"FAIL: {message}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = sys.argv[1:]
|
||||||
|
link = args.pop() if len(args) == 12 else None
|
||||||
|
(device_bin, elf, mcu, hz, base_hex, page, baud, addr, value, tool, workdir) = args
|
||||||
|
addr, value, baud = int(addr, 0), int(value, 0), int(baud)
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import pbsim
|
||||||
|
|
||||||
|
os.makedirs(workdir, exist_ok=True)
|
||||||
|
dump = os.path.join(workdir, "flash_dump.bin")
|
||||||
|
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, link=link)
|
||||||
|
try:
|
||||||
|
out = pbsim.run_tool(tool, device.pty, baud, "--peek", f"{addr:#x}:1")
|
||||||
|
want = f"{addr:#06x} {value:02x}"
|
||||||
|
if want not in out:
|
||||||
|
fail(f"OSCCAL at {addr:#x} did not read back {value:#04x}:\n{out}")
|
||||||
|
finally:
|
||||||
|
device.stop()
|
||||||
|
print("OK")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user