pureboot: every deployment axis is a build parameter

Clock, baud, serial backend (hardware USART 0/1 or the software UART on
any pins) and the activation window all resolve through one CMake
function, pureboot_add_loader() in pureboot/CMakeLists.txt — the unit a
downstream project consumes. The default baud is the fastest standard
rate within 2.5 % (the same best-divisor search libavr's solver runs),
gated on software builds by the polled receiver's 100-cycles-a-bit
floor; every explicit pick is re-checked by the compile's static asserts.

The size matrix builds each axis that can move the image — backend x
clock ladder x USART instance, per chip — against the slot budget, and
two nondefault deployments run the whole protocol suite live: the 328P
on its shipped 1 MHz fuses over software serial on TX=PB1/RX=PB5
(pureboot.custom), and the 644A over USART1 (pureboot.usart1). The sim
runner takes -l to bridge any link, paces a fully quiet bridge toward
real time (a free-running 8 M-cycle window loses the reset-race knock),
and the fixture application speaks the deployment it is built for.

The loader itself shed bytes on the way: the return-address high byte
spelled through byteswap (the double swap folds to the one-byte pick),
the info-block address composed instead of bit_cast, and libavr's new
polled-UART helpers replacing the port's uart::detail reaches. Every
combination fits: 458-506 B across the megas' whole matrix, 470-484 B
on the tinies, 556-562 B in the 1284s' 1 KiB slot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 23:42:57 +02:00
parent 76f4576fe0
commit a977e507f3
8 changed files with 743 additions and 338 deletions

View File

@@ -3,16 +3,18 @@
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by
constraint: one C++ source, no inline assembly, no global register variables
(attributes allowed), built for **every chip libavr targets — all 37 —
fitting each chip's smallest boot sector**: 512 bytes everywhere — 488 B on
the tiny13s, 498502 B on the tiny25/45/85, 466504 B across the megas
(474 B on the boot-section-less m48s, 498 B on the 644s) — except the
ATmega1284/1284P, whose smallest boot sector is 1 KiB and whose far-flash
machinery (ELPM reads, RAMPZ page commands, word-addressed wire) lands at
558 B in a 1 KiB slot: the 512-byte figure is a hardware boundary those
chips simply do not have, and no implementation of this feature set fits it
there. The device speaks primitives; every composite — verify, erase,
reset-vector surgery, updating the loader itself — lives in the host tool
(`pureboot.py`).
fitting each chip's smallest boot sector**: 512 bytes everywhere — 470 B on
the tiny13s, ~484 B on the tiny25/45/85, 458506 B across the megas and
every configured variant of them (the m8's software-serial build is the
fattest) — except the ATmega1284/1284P, whose smallest boot sector is 1 KiB
and whose far-flash machinery (ELPM reads, RAMPZ page commands,
word-addressed wire) lands at 556562 B in a 1 KiB slot: the 512-byte
figure is a hardware boundary those chips simply do not have, and no
implementation of this feature set fits it there. Clock, baud, serial
backend and pins are per-build configuration (below); the size matrix in
the test suite holds every combination inside its slot. The device speaks
primitives; every composite — verify, erase, reset-vector surgery, updating
the loader itself — lives in the host tool (`pureboot.py`).
The image is **position-independent**: control flow is PC-relative, the
read/write paths take wire addresses, the write guard protects the slot the
@@ -26,16 +28,63 @@ jumps into it, and lets it rewrite the resident. The slot is 512 bytes
minimum); on the tinies the budget is 510, not 512: a slot's last word
belongs to the host-managed trampoline (below).
## Configuration
Every deployment axis is a build parameter, resolved by the CMake function
`pureboot_add_loader()` (in `pureboot/CMakeLists.txt`) — the one way a
loader target is created, by this repo's own build and by a downstream
project alike:
| Argument | Meaning | Default |
|---|---|---|
| `CLOCK <hz>` | the clock the board runs | 16 MHz megas, 8 MHz t25/45/85, 9.6 MHz t13s |
| `BAUD <bd>` | the wire rate | the ladder below |
| `SERIAL auto\|hardware\|software` | the link backend | `auto`: the hardware USART where the chip has one |
| `USART <n>` | the USART instance (x4 megas carry two) | 0 |
| `RX <pin>`, `TX <pin>` | software-UART pins | `pb0`, `pb1` |
| `TIMEOUT <s>` | the activation window | 8 |
The default baud is the fastest of 115200/57600/38400/19200/9600 the clock
reaches within 2.5 % — the same U2X-included divisor search libavr's baud
solver runs — and on a software build additionally within the polled
receiver's 100-cycles-a-bit floor. 16 MHz lands 115200, 8 MHz 57600,
1 MHz 9600. Whatever is picked or overridden is re-checked in the compile:
an infeasible clock/baud/backend combination, or a USART the chip does not
have, fails with a named static assert.
A downstream project brings its usual libavr setup (the `libavr` target,
the chip via the `LIBAVR_MCU` toolchain preset), consumes this directory,
and states its deployment — for example an ATmega328P on its shipped
1 MHz fuses with the software UART on hand-picked pins:
```cmake
FetchContent_Declare(bootloader GIT_REPOSITORY git@git.blackmark.me:avr/bootloader.git GIT_TAG main)
FetchContent_MakeAvailable(bootloader)
add_subdirectory(${bootloader_SOURCE_DIR}/pureboot pureboot)
pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
```
The function emits the ELF plus `myboot.hex` (the programmer artifact) and
`myboot.bin` (the self-update image), prints the size, and stamps the
resolved deployment on the target as the `PUREBOOT_HZ`, `PUREBOOT_BAUD`
and `PUREBOOT_LINK` properties — what a flashing script or test harness
needs to speak to the build. This exact example deployment runs the full
protocol suite in CI (`pureboot.custom`).
## Link
The stock builds assume the family's natural deployment; any axis moves
per build (above).
| Chip | Serial | Baud | Clock assumed |
|---|---|---|---|
| every ATmega | the hardware USART (USART0), RXD/TXD per pinout | 115200 8N1 | 16 MHz crystal |
| ATtiny25/45/85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC |
| ATtiny13/13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC |
The tiny RX pin has its pull-up enabled; TX idles high. All multi-byte
quantities on the wire are little-endian.
The software-UART RX pin has its pull-up enabled; TX idles high. All
multi-byte quantities on the wire are little-endian.
## Activation
@@ -50,9 +99,10 @@ The host then has one activation window per awaited byte to knock: `p` then
awaited again (line noise cannot lock the loader, only delay it). A window
expiring with an idle line boots the application.
The window length is a compile-time constant — 8 s by default, another value
via the `PUREBOOT_TIMEOUT` CMake cache variable — so the whole EEPROM belongs
to the application; pureboot never uses it for its own state. Re-timing a
The window length is a compile-time constant — 8 s by default, another
value via `pureboot_add_loader(... TIMEOUT <s>)` (the stock target keeps
the `PUREBOOT_TIMEOUT` cache variable) — so the whole EEPROM belongs to
the application; pureboot never uses it for its own state. Re-timing a
deployed loader is a self-update with a re-timed build (below).
## Session
@@ -237,11 +287,35 @@ read-back unless `--no-verify`; images are raw binary, or Intel HEX by
extension. `--force` overrides the refusable safety checks (today: flashing
application data into a mega's reset walk region).
Readouts come one fact per line: `--info` prints the decoded info block
field by field, `--fuses` each fuse byte on its own line — plus, on a
boot-sectioned mega, the decoded meaning (where the BOOTSZ section starts,
what BOOTRST does to reset). Transfers that take wire time — programming,
reading, erasing, verifying, the update phases — draw a transient progress
bar on stderr when it is a tty; logs and pipes see only the summary lines.
`-v`/`--verbose` adds the decisions as they happen: knock counts, the
programming plan (vector-surgery targets, skipped blank pages), update
state handling and per-phase page counts.
## Tests
Per chip preset, `ctest` runs:
`tools/check.sh` runs every chip's workflow (`tools/check.sh --full` adds
the reflect-mode builds of libavr's spot set; `tools/make_presets.py`
regenerates the presets). Per chip preset, `ctest` runs:
- `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget;
- `pureboot_*.size` — the size matrix: the serial backends × the clock
ladder (1/8/16 MHz; the t13s' own RC menu), plus the USART1 build on the
x4 chips — every configuration axis that could move the image, each
variant against the same slot budget (pins are immediate operands and the
timeout is a constant: size-neutral);
- `pureboot.custom` (328P) — the configured-deployment acceptance test: the
1 MHz software-serial TX=PB1/RX=PB5 build from the configuration example
drives the full protocol suite through the runner's GPIO bridge, fixture
application included;
- `pureboot.usart1` (644A) — the same protocol suite over the second
hardware USART: instance selection is compile-checked everywhere, but
only a live session proves the loader polls the USART it claims;
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`
in the image, the info block within its first 256 bytes;
- `pureboot.planner` — the host tool's pure logic: programming orders and
@@ -249,9 +323,10 @@ Per chip preset, `ctest` runs:
boot-fuse decode, and the update preflight's error/warning matrix over
synthetic fuse bytes;
- `pureboot.protocol` — end to end against a simavr device
(`test/pureboot_device.c`the mega's USART as a pty; on the tinies a
cycle-timed GPIO⇄pty bridge for the software UART, plus the SPM/NVM module
simavr's tiny cores lack) driven by the real host tool through
(`test/pureboot_device.c`a hardware USART as a pty, or a cycle-timed
GPIO⇄pty bridge for a software-UART build, selected with `-l` to match
the loader's link; plus the SPM/NVM module simavr's tiny cores lack)
driven by the real host tool through
knock-from-reset, program + verify of both memories, session reconnect, an
external reset through the patched vector, and the hand-over to a fixture
application whose banner proves the launch — cross-checked against the
@@ -265,6 +340,6 @@ Per chip preset, `ctest` runs:
from its flash dump, and a re-run must complete the update with the
application intact throughout.
`size`, `pi`, and `planner` are host logic and run anywhere; the three
`size`, `pi`, and `planner` are host logic and run anywhere; the
simulator-driven targets need simavr and a pty, so they are POSIX-only —
on Windows the tool is exercised against real hardware.