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:
@@ -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, 498–502 B on the tiny25/45/85, 466–504 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, 458–506 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 556–562 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.
|
||||
|
||||
Reference in New Issue
Block a user