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
|
||||
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot_autobaud>
|
||||
-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
|
||||
# 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})
|
||||
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
|
||||
# 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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
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