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:
2026-07-28 00:53:14 +02:00
parent fe0d9f8790
commit 69f089e53a
6 changed files with 159 additions and 8 deletions

View File

@@ -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())