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:
@@ -136,6 +136,19 @@ elseif(LIBAVR_MCU STREQUAL "atmega644pa")
|
||||
set(_pb_sim_mcu atmega644p)
|
||||
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
|
||||
# scopes as global properties.
|
||||
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_EEPROM ${_pb_eeprom} 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_USART1 ${_pb_has_usart1} PARENT_SCOPE)
|
||||
set(PUREBOOT_SIM_MCU ${_pb_sim_mcu} PARENT_SCOPE)
|
||||
@@ -212,7 +226,7 @@ endfunction()
|
||||
|
||||
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
||||
# [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,
|
||||
# <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
|
||||
# PUREBOOT_HZ/PUREBOOT_BAUD then record what a harness should *drive* it at,
|
||||
# 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)
|
||||
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)
|
||||
message(FATAL_ERROR "pureboot_add_loader(${name}): unknown arguments ${PB_UNPARSED_ARGUMENTS}")
|
||||
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}
|
||||
${_serial_defines})
|
||||
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)
|
||||
target_link_libraries(${name} PRIVATE libavr)
|
||||
|
||||
@@ -72,9 +72,17 @@ constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
||||
#endif
|
||||
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 —
|
||||
// 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
|
||||
// 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()
|
||||
{
|
||||
#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
|
||||
// on until it clears WDRF — no activation window in its way.
|
||||
if (avr::hw::field_impl<wdrf_field()>::test())
|
||||
|
||||
@@ -24,13 +24,15 @@ else:
|
||||
import termios
|
||||
|
||||
PROMPT = b"+"
|
||||
VERSION = 5 # this tool's own version — free to drift from a loader's
|
||||
# The loader versions this tool speaks. A pureboot version implies its wire
|
||||
VERSION = 6 # this tool's own version — free to drift from a loader's
|
||||
# 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
|
||||
# map lives: every version so far speaks the same protocol, and one that
|
||||
# changes it becomes the new floor here.
|
||||
# map lives: the tool keeps a decoder for every generation in it (1–4 speak
|
||||
# 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
|
||||
NEWEST_LOADER = 5
|
||||
NEWEST_LOADER = 6
|
||||
SLOT = 512 # the loader slot, on every chip
|
||||
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
||||
|
||||
|
||||
Reference in New Issue
Block a user