pureboot.py: reject an empty image file with a clear error instead of an IndexError deep in the vector-surgery planner; tighten the erase docstring (order is irrelevant there — every target byte is the same value, unlike a real flash where page 0 must go last). pureboot_device.c: the GPIO bridge's bit_cycles used plain truncating division where the firmware computes its own bit period with round-to-nearest (uart.hpp: (Clock.hz + Baud.bd/2)/Baud.bd) — one cycle off per bit on both tinies, harmless in practice but needless drift against a firmware built to a different constant. Matched exactly. Also clear the queued-bytes/decode-in-progress bridge state on the test-only reset signal, so a future reset-mid-transfer scenario can't feed a freshly reset chip bytes queued for its previous life. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pureboot
A serial bootloader on libavr, pure by
constraint: one C++ source, no inline assembly, no global register variables
(attributes allowed), built for every chip libavr targets, 512 bytes on
each — 490 B on the ATtiny13A, 510 B on the ATtiny85, 484 B on the
ATmega328P. The device speaks primitives; every composite — verify, erase,
reset-vector surgery, timeout configuration — lives in the host tool
(pureboot.py).
Link
| Chip | Serial | Baud | Clock assumed |
|---|---|---|---|
| ATmega328P | USART0, RXD/TXD = PD0/PD1 | 115200 8N1 | 16 MHz crystal |
| ATtiny85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC |
| ATtiny13A | 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.
Activation
Reset enters the loader (BOOTRST on the mega, the patched reset vector on the tinies) — except a watchdog reset, which hands straight to the application (the application owns its watchdog; it must clear WDRF itself, which also releases the WDRF-forced WDE).
The host then has one activation window per awaited byte to knock: p then
b. Each awaited byte gets a fresh window; any other byte is discarded and
awaited again (line noise cannot lock the loader, only delay it). A window
expiring with an idle line boots the application.
The window length in seconds is the last EEPROM cell (address
eeprom_size - 1); 0x00 and the erased 0xff both mean the 4 s default,
so a full EEPROM erase resets the timeout rather than maxing it. The host
changes it with the ordinary EEPROM-write command.
Session
After the knock the loader stays in its command loop until G or a reset.
Before reading each command it waits for any pending EEPROM write to finish
and sends the prompt + (0x2b) — the prompt is therefore also the completion
ack of the previous command. A session is: await +, send a command, read
its reply, repeat.
| Cmd | Arguments | Reply |
|---|---|---|
b |
— | the 12-byte info block |
R |
addr16, n8 | n flash bytes (n = 0 means 256) |
W |
addr16, then one page of data | — (completion = next prompt) |
r |
addr16, n8 | n EEPROM bytes (n = 0 means 256) |
w |
addr16, n8, then n data bytes | + per byte, sent once its write has begun |
F |
— | 4 bytes: low fuse, lock, extended fuse, high fuse |
G |
— | +, then the application runs |
| other | — | ignored; the loop re-prompts (send a junk byte, await +, to resync) |
W streams exactly one SPM page (size from the info block) into the buffer,
then erases and programs; the address must be page-aligned. Pages inside the
loader's own 512 bytes are drained but never programmed — a broken host
cannot brick the chip. w is host-paced: send the next byte only after the
previous byte's +. F returns the bytes in the hardware's Z order; on a chip without an
extended fuse byte (the ATtiny13A) that slot carries no meaning. Fuse writing does not
exist: SPM reaches flash (and, on the mega, lock bits) only — fuse bytes are
external-programming territory by hardware.
The info block (b):
| Offset | Content |
|---|---|
| 0–2 | 'P', 'B', protocol version (1) |
| 3–5 | device signature |
| 6 | SPM page size in bytes |
| 7–8 | loader base — application flash ends here |
| 9–10 | EEPROM size |
| 11 | bit 0 set: host must patch the reset vector (no hardware boot section) |
Composites are the host's job: verify = read back and compare, erase =
write 0xff (per page for flash, per byte for EEPROM), timeout = EEPROM
write to the last cell.
Deployment
ATmega328P: program the loader at 0x7e00 with an external programmer;
fuses BOOTSZ = 11 (256 words) and BOOTRST programmed. Applications are
flashed unmodified — reset re-vectors to the loader in hardware, word 0
stays the application's own reset vector, and G jumps to 0.
Tinies (no boot section): program the loader at flash - 512; erased
flash below it walks up into the loader, so a virgin chip activates. When
flashing an application the host performs reset-vector surgery: the
application's own rjmp target is re-encoded as a trampoline rjmp in the
word just below the loader (base - 2, where G jumps), and word 0 is
rewritten to rjmp to the loader base. Every other vector stays the
application's. Page 0 is written last, so an interrupted flash leaves word 0
erased and the chip still falls through to the loader on the next reset.
Host tool
pureboot.py — Python 3, standard library only (termios drives any tty,
a USB adapter as well as a simavr pty):
pureboot.py --port /dev/ttyUSB0 --baud 57600 \
--info --fuses --flash app.hex --timeout 10
Operations run in a fixed order within one session: info, fuses, flash
(erase / program / read / verify), EEPROM (erase / program / read / verify),
timeout — then the loader hands over to the application; --stay keeps the
session alive instead, and a later invocation reconnects into it (the knock
converges there too). --flash and --eeprom verify by read-back unless
--no-verify; images are raw binary, or Intel HEX by extension.
Tests
Per chip preset, ctest runs the 512-byte size gate and the end-to-end
protocol test: 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 knock-from-reset, program + verify of both memories, timeout
configuration, 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 simulator's ground-truth memory dumps and
an independent decode of the surgery's rjmp words.