pureboot: a 512-byte slot on every chip, the 1284s included
The word-addressed 1284s were the one family deploying in a 1 KiB slot, because the far-flash machinery (ELPM reads, RAMPZ page commands, a word-addressed wire) did not fit 512 B. It does now: 478 B stock, 494 B in the heaviest configuration the build can produce. They take the 644s' geometry, where the smallest boot section holds the resident slot and its staging slot together. Most of the saving is one restructure. The info block and a flash read are the same act, so giving all four streamed commands one address-and-count path leaves exactly one call site for the flash streamer: it inlines into the never-returning command loop and its 24-bit cursor stops being saved and restored around every transmit. Around it, the ack byte moved out of line, the wire's byte pair is bit_cast into the word it already is, the fuse loop ends on its count, the info block's in-slot offset is taken as the one-byte relocation it is, and -fno-expensive-optimizations gives way to -fno-move-loop-invariants -fno-tree-ter. Every chip shrank 14-18 B. The size matrix grew the axes it was missing: the USART1 instance across the whole clock ladder, and the shape a slow baud gives a software UART — past 255 delay iterations libavr takes the 16-bit delay loop, which the ladder default never selects and which was 4 B over the 1284's slot the first time it was built. The protocol fixture stopped deriving the loader entry from the flash size; on the 1284s it had been jumping a slot low and reaching the loader only because erased flash walked it up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,26 +3,19 @@
|
||||
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 and compiler flags allowed), built for **every chip libavr
|
||||
targets — all 37 — in 512 bytes each**: 434 B on the tiny13s, 438–442 B on
|
||||
the tiny25/45/85, 412–452 B across the megas, and 506 B on the
|
||||
targets — all 37 — in 512 bytes each**: 416 B on the tiny13s, 420–424 B on
|
||||
the tiny25/45/85, 396–438 B across the megas, and 478 B on the
|
||||
ATmega1284/1284P, whose far-flash machinery (ELPM reads, RAMPZ page commands,
|
||||
word-addressed wire) is the heaviest. Those are the stock deployments;
|
||||
choosing the software UART where the chip has a USART costs 8–46 B more (a
|
||||
bit-bang against a peripheral), which every chip still absorbs inside its
|
||||
slot — on the 1284s that means their 1 KiB boot sector, where the
|
||||
software-serial image lands at 546 B. Bringing the 1284's default build
|
||||
under 512 at all is what the loop-placement attributes on the byte streamers
|
||||
(`pureboot.cpp`) and the codegen flags on the loader TU (`CMakeLists.txt`)
|
||||
are for; measured against each chip's own budget the tightest is the
|
||||
ATmega328P, 50 B spare. 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 1284s still *deploy* in a 1 KiB slot, their smallest boot sector being
|
||||
512 words; at 506 B the image would also fit the 644's
|
||||
two-512-byte-slots-per-boot-sector geometry.
|
||||
word-addressed wire) is the heaviest. Those are the stock deployments; the
|
||||
rest of the configuration space costs a little more, and the dearest point
|
||||
is a software UART at a slow baud — 494 B on the 1284s, the tightest image
|
||||
in the matrix at 18 B spare. Keeping the far-flash build inside 512 is what
|
||||
the single call site the streamed commands share (`pureboot.cpp`) and the
|
||||
codegen flags on the loader TU (`CMakeLists.txt`) are for. 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
|
||||
@@ -31,9 +24,8 @@ addressed from that same anchor, and the application jump is an indirect
|
||||
call to an absolute entry. The identical binary therefore runs from any
|
||||
slot with every command intact — which makes pureboot **its own staging
|
||||
loader**: the host installs the same binary one slot below the resident,
|
||||
jumps into it, and lets it rewrite the resident. The slot is 512 bytes
|
||||
(1 KiB on the word-addressed large chips, matching their boot-sector
|
||||
minimum); on the tinies the budget is 510, not 512: a slot's last word
|
||||
jumps into it, and lets it rewrite the resident. The slot is 512 bytes on
|
||||
every chip; on the tinies the budget is 510, not 512: a slot's last word
|
||||
belongs to the host-managed trampoline (below).
|
||||
|
||||
## Configuration
|
||||
@@ -215,23 +207,20 @@ a newer one). `pureboot.rehome` is the acceptance test for both
|
||||
positions. Flashing the application afterwards overwrites the stale copy,
|
||||
vector surgery included.
|
||||
|
||||
**Boot-sectioned megas**: program the loader at `flash − slot` with an
|
||||
**Boot-sectioned megas**: program the loader at `flash − 512` with an
|
||||
external programmer. Every such mega has a BOOTSZ step whose boot section
|
||||
is exactly the loader slot — 512 B, the second-smallest step on the 8 KiB
|
||||
and 16 KiB chips (m8, m88, m16, m168, m164), the smallest on the 32 KiB
|
||||
ones (m32, m328, m324); on the 1284s that step is the smallest, 512 words,
|
||||
which is why their slot is 1 KiB — so the ATmega328P profiles below apply
|
||||
to every one of them with its own addresses and slot size; the per-chip
|
||||
BOOTSZ ladders live in the host tool (`BOOT_FUSE`). The 1284s' numbers:
|
||||
standalone = BOOTSZ 512 words (reset at the loader base 0x1fc00);
|
||||
self-update = 1024 words, covering both 1 KiB slots, the loader-first
|
||||
reset landing at 0x1f800 — the staging slot, walked across when erased.
|
||||
is exactly the 512-byte slot — the second-smallest step on the 8 KiB and
|
||||
16 KiB chips (m8, m88, m16, m168, m164), the smallest on the 32 KiB ones
|
||||
(m32, m328, m324) — so the ATmega328P profiles below apply to every one of
|
||||
them with its own addresses; the per-chip BOOTSZ ladders live in the host
|
||||
tool (`BOOT_FUSE`).
|
||||
|
||||
The **644s** are the geometry's sweet spot: their smallest boot section
|
||||
(512 words = 1 KiB) is exactly *two* 512-byte slots, so the resident and
|
||||
its staging slot both live inside the minimum section — self-update needs
|
||||
no fuse step up, and the standalone profile does not exist (reset lands at
|
||||
0xfc00, one erased slot below the loader: the loader-first walk built in).
|
||||
The **644s and 1284s** are the geometry's sweet spot: their smallest boot
|
||||
section (512 words = 1 KiB) is exactly *two* 512-byte slots, so the
|
||||
resident and its staging slot both live inside the minimum section —
|
||||
self-update needs no fuse step up, and the standalone profile does not
|
||||
exist (reset lands one erased slot below the loader — 0xfc00 on the 644s,
|
||||
0x1fc00 on the 1284s: the loader-first walk built in).
|
||||
|
||||
ATmega328P profiles (addresses for its 32 KiB):
|
||||
|
||||
@@ -344,11 +333,16 @@ 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_*.size` — the size matrix: every configuration axis that could
|
||||
move the image, each variant against the same slot budget. The serial
|
||||
backends × the clock ladder (1/8/16 MHz; the t13s' own RC menu), the
|
||||
USART1 instance across that same ladder on the x4 chips, and
|
||||
`pureboot_sw_wide` — the slowest ladder rate at the fastest clock, where
|
||||
a software UART's per-bit spin outgrows its one-register delay loop and
|
||||
takes the 16-bit one, the largest image the configuration space produces
|
||||
and a shape the ladder default (always the *fastest* rate a clock
|
||||
reaches) never picks. Pins are immediate operands and the timeout is a
|
||||
constant: neither is an axis;
|
||||
- `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
|
||||
|
||||
Reference in New Issue
Block a user