pureboot.py (Python stdlib only): images as raw binary or Intel HEX, flash and EEPROM programming with read-back verify, erase composites, fuse and info readout, activation-timeout configuration, and the tinies' reset-vector surgery — the trampoline word below the loader, page 0 written last. The test spawns a simavr device (pureboot_device.c) — the mega's USART as a pty; on the tinies a cycle-timed GPIO<->pty bridge for the polled software UART plus the NVM module simavr's tiny cores lack (their SPM opcode ioctls into a void and silently does nothing) — and drives it with the real tool: knock from reset (erased-flash walk on the tinies), program and verify both memories, timeout write, session reconnect, an external reset through the patched vector, hand-over, and the fixture application's banner. Results are cross-checked against ground-truth memory dumps and an independent decode of the surgery's rjmp words, red-verified against a sabotaged encoder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
124 lines
5.8 KiB
Markdown
124 lines
5.8 KiB
Markdown
# pureboot
|
||
|
||
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, **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.
|