2 Commits

Author SHA1 Message Date
dacd21bd78 pureboot: the 1284P rides a 1 KiB slot — its own boot-sector minimum
The far machinery (ELPM reads, RAMPZ page commands, wire-word math)
costs ~46 B over the m328P's 504, and the tsb-calibrated C++-to-asm
gap says no implementation of this feature set reaches 512 on this
chip — a boundary its hardware does not have anyway: the 1284P's
smallest boot sector is 1 KiB. The slot therefore becomes
per-geometry (512 B, or 1 KiB past 64 KiB), which the host derives
from the word-addressing flag; slot arithmetic unifies (the index is
the wire high byte with its low bit dropped in either unit), the
update preflight demands a two-slot boot section in the chip's own
terms, and pbapp's hand-back jumps to the real slot base. libavr's
far primitives split their RAMPZ/Z asm operands (a page never
crosses 64 KiB, so callers keep a byte and a 16-bit cursor — the
32-bit address folds away; flash_load_far's byte form becomes the
out-RAMPZ+elpm pair avr-libc's pgm_read_byte_far rebuilds per call),
and the host splits reads at 64 KiB boundaries. All ten chips pass
the full suite — the 1284P at 558 B including protocol, relocation,
and the power-fail self-update — with pureboot byte-identical across
generated and reflect modes everywhere, and the original three
chips' images unchanged to the byte (488/502/504).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 13:54:40 +02:00
90a29d872f pureboot: the classic megas and the word-addressed 1284P groundwork
Device: boot-section detection probes SPMCR beside SPMCSR, the link
picks any hardware USART through the instance-aware lookups (URSEL
chips included), WDRF reads MCUSR-or-MCUCSR, and the >64 KiB shape
lands — word-addressed wire flash (info flag bit 1, page byte 0 means
256, base as a word address), far reads through flash_load_far, a
single 32-bit byte-cursor page walk (the 256-byte page wraps its low
byte exactly), and slot arithmetic in words (the return address
already is one). Host: addresses stay bytes internally and scale at
the wire, the boot-fuse decode becomes a per-signature table (byte
index + BOOTSZ ladder — the m168A's lives in EXTENDED), and the
planner tests pin every chip's ladder plus the word-addressed info
decode. Tests: the device runner serves every mega over the USART pty,
pbapp banners over the right link, the update rehearsal synthesizes
its assumed fuses from the tool's own table, and the PI lint tracks
the renamed info symbol. All six classic-mega/168A targets pass the
full suite (size, PI, planner, protocol, reloc, self-update) at
466–504 B; the 1284P builds await a libavr far-path slimming to make
its 512.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 12:00:19 +02:00
13 changed files with 848 additions and 169 deletions

View File

@@ -142,6 +142,10 @@ endif()
# compile-time constant; a different PUREBOOT_TIMEOUT builds the re-timed
# binary a self-update then installs.
set(PUREBOOT_TIMEOUT 8 CACHE STRING "pureboot activation window, seconds")
# Every mega runs the loader from its hardware boot section and boots the
# application at word 0; the tinies get the trampoline surgery. All megas
# assume a 16 MHz crystal at 115200 Bd; the tinies their internal RC at
# 57600 Bd over the software UART.
if(LIBAVR_MCU STREQUAL "attiny13a")
set(_pb_flash 1024)
set(_pb_wrap "")
@@ -158,6 +162,51 @@ elseif(LIBAVR_MCU STREQUAL "attiny85")
set(_pb_baud 57600)
set(_pb_eeprom 512)
set(_pb_limit 510)
elseif(LIBAVR_MCU MATCHES "^atmega8a?$")
set(_pb_flash 8192)
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
set(_pb_page 64)
set(_pb_hz 16000000)
set(_pb_baud 115200)
set(_pb_eeprom 512)
set(_pb_limit 512)
elseif(LIBAVR_MCU STREQUAL "atmega16")
set(_pb_flash 16384)
set(_pb_wrap -Wl,--pmem-wrap-around=16k)
set(_pb_page 128)
set(_pb_hz 16000000)
set(_pb_baud 115200)
set(_pb_eeprom 512)
set(_pb_limit 512)
elseif(LIBAVR_MCU MATCHES "^atmega32a?$")
set(_pb_flash 32768)
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
set(_pb_page 128)
set(_pb_hz 16000000)
set(_pb_baud 115200)
set(_pb_eeprom 1024)
set(_pb_limit 512)
elseif(LIBAVR_MCU STREQUAL "atmega168a")
set(_pb_flash 16384)
set(_pb_wrap -Wl,--pmem-wrap-around=16k)
set(_pb_page 128)
set(_pb_hz 16000000)
set(_pb_baud 115200)
set(_pb_eeprom 512)
set(_pb_limit 512)
elseif(LIBAVR_MCU STREQUAL "atmega1284p")
# 128 KiB: wire flash addresses are word addresses, reads go through
# ELPM, and the PC's modulo wrap exceeds what --pmem-wrap-around models.
# The slot is 1 KiB — this chip's own smallest boot sector; the far
# machinery cannot fit 512 B (see pureboot/README.md).
set(_pb_flash 131072)
set(_pb_wrap "")
set(_pb_page 256)
set(_pb_hz 16000000)
set(_pb_baud 115200)
set(_pb_eeprom 4096)
set(_pb_limit 1024)
set(_pb_slot 1024)
else()
set(_pb_flash 32768)
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
@@ -167,14 +216,27 @@ else()
set(_pb_eeprom 1024)
set(_pb_limit 512)
endif()
math(EXPR _pb_base "${_pb_flash} - 512")
if(NOT DEFINED _pb_slot)
set(_pb_slot 512)
endif()
math(EXPR _pb_base "${_pb_flash} - ${_pb_slot}")
math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL)
if(LIBAVR_MCU STREQUAL "atmega328p")
if(LIBAVR_MCU MATCHES "^atmega")
set(_pb_app 0)
else()
math(EXPR _pb_app "${_pb_base} - 2")
endif()
# simavr names its cores after the base dies; the A revisions run on them.
set(_pb_sim_mcu ${LIBAVR_MCU})
if(LIBAVR_MCU STREQUAL "atmega8a")
set(_pb_sim_mcu atmega8)
elseif(LIBAVR_MCU STREQUAL "atmega32a")
set(_pb_sim_mcu atmega32)
elseif(LIBAVR_MCU STREQUAL "atmega168a")
set(_pb_sim_mcu atmega168)
endif()
add_executable(pureboot pureboot/pureboot.cpp)
target_link_libraries(pureboot PRIVATE libavr)
target_compile_definitions(pureboot PRIVATE PUREBOOT_TIMEOUT=${PUREBOOT_TIMEOUT})
@@ -205,7 +267,7 @@ if(PROJECT_IS_TOP_LEVEL)
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:pbapp> $<TARGET_FILE:pbapp>.bin)
add_test(NAME pureboot.protocol
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py
${PB_DEVICE} $<TARGET_FILE:pureboot> ${LIBAVR_MCU} ${_pb_hz} ${_pb_base_hex}
${PB_DEVICE} $<TARGET_FILE:pureboot> ${_pb_sim_mcu} ${_pb_hz} ${_pb_base_hex}
${_pb_page} ${_pb_baud} ${_pb_eeprom} $<TARGET_FILE:pbapp>.bin
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
${CMAKE_BINARY_DIR}/pbtest-work)
@@ -215,7 +277,7 @@ if(PROJECT_IS_TOP_LEVEL)
# installed one slot lower, must serve the full command set.
add_test(NAME pureboot.reloc
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbreloc.py
${PB_DEVICE} $<TARGET_FILE:pureboot> ${LIBAVR_MCU} ${_pb_hz} ${_pb_base_hex}
${PB_DEVICE} $<TARGET_FILE:pureboot> ${_pb_sim_mcu} ${_pb_hz} ${_pb_base_hex}
${_pb_page} ${_pb_baud} ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
${CMAKE_BINARY_DIR}/pbreloc-work)
set_tests_properties(pureboot.reloc PROPERTIES TIMEOUT 180
@@ -233,7 +295,7 @@ if(PROJECT_IS_TOP_LEVEL)
add_image_outputs(pureboot9)
add_test(NAME pureboot.update
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbupdate.py
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9> ${LIBAVR_MCU}
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9> ${_pb_sim_mcu}
${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_baud} $<TARGET_FILE:pbapp>.bin
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
${CMAKE_BINARY_DIR}/pbupdate-work)

View File

@@ -16,71 +16,502 @@
{
"name": "atmega328p-generated",
"inherits": "base",
"cacheVariables": { "LIBAVR_MCU": "atmega328p", "LIBAVR_REFLECT": "OFF" }
"cacheVariables": {
"LIBAVR_MCU": "atmega328p",
"LIBAVR_REFLECT": "OFF"
}
},
{
"name": "atmega328p-reflect",
"inherits": "base",
"cacheVariables": { "LIBAVR_MCU": "atmega328p", "LIBAVR_REFLECT": "ON" }
"cacheVariables": {
"LIBAVR_MCU": "atmega328p",
"LIBAVR_REFLECT": "ON"
}
},
{
"name": "attiny85-generated",
"inherits": "base",
"cacheVariables": { "LIBAVR_MCU": "attiny85", "LIBAVR_REFLECT": "OFF" }
"cacheVariables": {
"LIBAVR_MCU": "attiny85",
"LIBAVR_REFLECT": "OFF"
}
},
{
"name": "attiny85-reflect",
"inherits": "base",
"cacheVariables": { "LIBAVR_MCU": "attiny85", "LIBAVR_REFLECT": "ON" }
"cacheVariables": {
"LIBAVR_MCU": "attiny85",
"LIBAVR_REFLECT": "ON"
}
},
{
"name": "attiny13a-generated",
"inherits": "base",
"cacheVariables": { "LIBAVR_MCU": "attiny13a", "LIBAVR_REFLECT": "OFF" }
"cacheVariables": {
"LIBAVR_MCU": "attiny13a",
"LIBAVR_REFLECT": "OFF"
}
},
{
"name": "attiny13a-reflect",
"inherits": "base",
"cacheVariables": { "LIBAVR_MCU": "attiny13a", "LIBAVR_REFLECT": "ON" }
"cacheVariables": {
"LIBAVR_MCU": "attiny13a",
"LIBAVR_REFLECT": "ON"
}
},
{
"name": "atmega8-generated",
"inherits": "base",
"cacheVariables": {
"LIBAVR_MCU": "atmega8",
"LIBAVR_REFLECT": "OFF"
}
},
{
"name": "atmega8-reflect",
"inherits": "base",
"cacheVariables": {
"LIBAVR_MCU": "atmega8",
"LIBAVR_REFLECT": "ON"
}
},
{
"name": "atmega8a-generated",
"inherits": "base",
"cacheVariables": {
"LIBAVR_MCU": "atmega8a",
"LIBAVR_REFLECT": "OFF"
}
},
{
"name": "atmega8a-reflect",
"inherits": "base",
"cacheVariables": {
"LIBAVR_MCU": "atmega8a",
"LIBAVR_REFLECT": "ON"
}
},
{
"name": "atmega16-generated",
"inherits": "base",
"cacheVariables": {
"LIBAVR_MCU": "atmega16",
"LIBAVR_REFLECT": "OFF"
}
},
{
"name": "atmega16-reflect",
"inherits": "base",
"cacheVariables": {
"LIBAVR_MCU": "atmega16",
"LIBAVR_REFLECT": "ON"
}
},
{
"name": "atmega32-generated",
"inherits": "base",
"cacheVariables": {
"LIBAVR_MCU": "atmega32",
"LIBAVR_REFLECT": "OFF"
}
},
{
"name": "atmega32-reflect",
"inherits": "base",
"cacheVariables": {
"LIBAVR_MCU": "atmega32",
"LIBAVR_REFLECT": "ON"
}
},
{
"name": "atmega32a-generated",
"inherits": "base",
"cacheVariables": {
"LIBAVR_MCU": "atmega32a",
"LIBAVR_REFLECT": "OFF"
}
},
{
"name": "atmega32a-reflect",
"inherits": "base",
"cacheVariables": {
"LIBAVR_MCU": "atmega32a",
"LIBAVR_REFLECT": "ON"
}
},
{
"name": "atmega168a-generated",
"inherits": "base",
"cacheVariables": {
"LIBAVR_MCU": "atmega168a",
"LIBAVR_REFLECT": "OFF"
}
},
{
"name": "atmega168a-reflect",
"inherits": "base",
"cacheVariables": {
"LIBAVR_MCU": "atmega168a",
"LIBAVR_REFLECT": "ON"
}
},
{
"name": "atmega1284p-generated",
"inherits": "base",
"cacheVariables": {
"LIBAVR_MCU": "atmega1284p",
"LIBAVR_REFLECT": "OFF"
}
},
{
"name": "atmega1284p-reflect",
"inherits": "base",
"cacheVariables": {
"LIBAVR_MCU": "atmega1284p",
"LIBAVR_REFLECT": "ON"
}
}
],
"buildPresets": [
{ "name": "atmega328p-generated", "configurePreset": "atmega328p-generated" },
{ "name": "atmega328p-reflect", "configurePreset": "atmega328p-reflect" },
{ "name": "attiny85-generated", "configurePreset": "attiny85-generated" },
{ "name": "attiny85-reflect", "configurePreset": "attiny85-reflect" },
{ "name": "attiny13a-generated", "configurePreset": "attiny13a-generated" },
{ "name": "attiny13a-reflect", "configurePreset": "attiny13a-reflect" }
{
"name": "atmega328p-generated",
"configurePreset": "atmega328p-generated"
},
{
"name": "atmega328p-reflect",
"configurePreset": "atmega328p-reflect"
},
{
"name": "attiny85-generated",
"configurePreset": "attiny85-generated"
},
{
"name": "attiny85-reflect",
"configurePreset": "attiny85-reflect"
},
{
"name": "attiny13a-generated",
"configurePreset": "attiny13a-generated"
},
{
"name": "attiny13a-reflect",
"configurePreset": "attiny13a-reflect"
},
{
"name": "atmega8-generated",
"configurePreset": "atmega8-generated"
},
{
"name": "atmega8-reflect",
"configurePreset": "atmega8-reflect"
},
{
"name": "atmega8a-generated",
"configurePreset": "atmega8a-generated"
},
{
"name": "atmega8a-reflect",
"configurePreset": "atmega8a-reflect"
},
{
"name": "atmega16-generated",
"configurePreset": "atmega16-generated"
},
{
"name": "atmega16-reflect",
"configurePreset": "atmega16-reflect"
},
{
"name": "atmega32-generated",
"configurePreset": "atmega32-generated"
},
{
"name": "atmega32-reflect",
"configurePreset": "atmega32-reflect"
},
{
"name": "atmega32a-generated",
"configurePreset": "atmega32a-generated"
},
{
"name": "atmega32a-reflect",
"configurePreset": "atmega32a-reflect"
},
{
"name": "atmega168a-generated",
"configurePreset": "atmega168a-generated"
},
{
"name": "atmega168a-reflect",
"configurePreset": "atmega168a-reflect"
},
{
"name": "atmega1284p-generated",
"configurePreset": "atmega1284p-generated"
},
{
"name": "atmega1284p-reflect",
"configurePreset": "atmega1284p-reflect"
}
],
"workflowPresets": [
{
"name": "atmega328p-generated",
"steps": [
{ "type": "configure", "name": "atmega328p-generated" },
{ "type": "build", "name": "atmega328p-generated" },
{ "type": "test", "name": "atmega328p-generated" }
{
"type": "configure",
"name": "atmega328p-generated"
},
{
"type": "build",
"name": "atmega328p-generated"
},
{
"type": "test",
"name": "atmega328p-generated"
}
]
},
{
"name": "attiny85-generated",
"steps": [
{ "type": "configure", "name": "attiny85-generated" },
{ "type": "build", "name": "attiny85-generated" },
{ "type": "test", "name": "attiny85-generated" }
{
"type": "configure",
"name": "attiny85-generated"
},
{
"type": "build",
"name": "attiny85-generated"
},
{
"type": "test",
"name": "attiny85-generated"
}
]
},
{
"name": "attiny13a-generated",
"steps": [
{ "type": "configure", "name": "attiny13a-generated" },
{ "type": "build", "name": "attiny13a-generated" },
{ "type": "test", "name": "attiny13a-generated" }
{
"type": "configure",
"name": "attiny13a-generated"
},
{
"type": "build",
"name": "attiny13a-generated"
},
{
"type": "test",
"name": "attiny13a-generated"
}
]
},
{
"name": "atmega8-generated",
"steps": [
{
"type": "configure",
"name": "atmega8-generated"
},
{
"type": "build",
"name": "atmega8-generated"
}
]
},
{
"name": "atmega8-reflect",
"steps": [
{
"type": "configure",
"name": "atmega8-reflect"
},
{
"type": "build",
"name": "atmega8-reflect"
}
]
},
{
"name": "atmega8a-generated",
"steps": [
{
"type": "configure",
"name": "atmega8a-generated"
},
{
"type": "build",
"name": "atmega8a-generated"
}
]
},
{
"name": "atmega8a-reflect",
"steps": [
{
"type": "configure",
"name": "atmega8a-reflect"
},
{
"type": "build",
"name": "atmega8a-reflect"
}
]
},
{
"name": "atmega16-generated",
"steps": [
{
"type": "configure",
"name": "atmega16-generated"
},
{
"type": "build",
"name": "atmega16-generated"
}
]
},
{
"name": "atmega16-reflect",
"steps": [
{
"type": "configure",
"name": "atmega16-reflect"
},
{
"type": "build",
"name": "atmega16-reflect"
}
]
},
{
"name": "atmega32-generated",
"steps": [
{
"type": "configure",
"name": "atmega32-generated"
},
{
"type": "build",
"name": "atmega32-generated"
}
]
},
{
"name": "atmega32-reflect",
"steps": [
{
"type": "configure",
"name": "atmega32-reflect"
},
{
"type": "build",
"name": "atmega32-reflect"
}
]
},
{
"name": "atmega32a-generated",
"steps": [
{
"type": "configure",
"name": "atmega32a-generated"
},
{
"type": "build",
"name": "atmega32a-generated"
}
]
},
{
"name": "atmega32a-reflect",
"steps": [
{
"type": "configure",
"name": "atmega32a-reflect"
},
{
"type": "build",
"name": "atmega32a-reflect"
}
]
},
{
"name": "atmega168a-generated",
"steps": [
{
"type": "configure",
"name": "atmega168a-generated"
},
{
"type": "build",
"name": "atmega168a-generated"
}
]
},
{
"name": "atmega168a-reflect",
"steps": [
{
"type": "configure",
"name": "atmega168a-reflect"
},
{
"type": "build",
"name": "atmega168a-reflect"
}
]
},
{
"name": "atmega1284p-generated",
"steps": [
{
"type": "configure",
"name": "atmega1284p-generated"
},
{
"type": "build",
"name": "atmega1284p-generated"
}
]
},
{
"name": "atmega1284p-reflect",
"steps": [
{
"type": "configure",
"name": "atmega1284p-reflect"
},
{
"type": "build",
"name": "atmega1284p-reflect"
}
]
}
],
"testPresets": [
{ "name": "atmega328p-generated", "configurePreset": "atmega328p-generated", "output": { "outputOnFailure": true } },
{ "name": "attiny85-generated", "configurePreset": "attiny85-generated", "output": { "outputOnFailure": true } },
{ "name": "attiny13a-generated", "configurePreset": "attiny13a-generated", "output": { "outputOnFailure": true } }
{
"name": "atmega328p-generated",
"configurePreset": "atmega328p-generated",
"output": {
"outputOnFailure": true
}
},
{
"name": "attiny85-generated",
"configurePreset": "attiny85-generated",
"output": {
"outputOnFailure": true
}
},
{
"name": "attiny13a-generated",
"configurePreset": "attiny13a-generated",
"output": {
"outputOnFailure": true
}
}
]
}

View File

@@ -2,28 +2,34 @@
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** — 488 B on the ATtiny13A, 502 B on the ATtiny85, 504 B on the
ATmega328P. The device speaks primitives; every composite — verify, erase,
(attributes allowed), built for every chip libavr targets, **fitting each
chip's smallest boot sector**: 512 bytes everywhere — 488 B on the
ATtiny13A, 502 B on the ATtiny85, 466504 B across the megas — except the
ATmega1284P, 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 that chip
simply does 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`).
The image is **position-independent**: control flow is PC-relative, the
read/write paths take wire addresses, the write guard protects the 512-byte
slot the code is *running* in (from the runtime return address), the info
block is addressed from that same anchor, and the application jump is an
indirect call to an absolute entry. The identical binary therefore runs from
any 512-byte 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. On the tinies the
budget is 510, not 512: a slot's last word belongs to the host-managed
trampoline (below).
read/write paths take wire addresses, the write guard protects the slot the
code is *running* in (from the runtime return address), the info block is
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
belongs to the host-managed trampoline (below).
## Link
| Chip | Serial | Baud | Clock assumed |
|---|---|---|---|
| ATmega328P | USART0, RXD/TXD = PD0/PD1 | 115200 8N1 | 16 MHz crystal |
| every ATmega (8/8A, 16, 32/32A, 168A, 328P, 1284P) | the hardware USART (USART0), RXD/TXD per pinout | 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 |
@@ -55,6 +61,10 @@ 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.
On chips whose flash exceeds 64 KiB (the 1284P — info-block flag bit 1) the
`R`/`W` flash addresses are **word** addresses; everywhere else they are byte
addresses. EEPROM addresses are always bytes, counts always bytes.
| Cmd | Arguments | Reply |
|---|---|---|
| `b` | — | the 12-byte info block |
@@ -88,18 +98,29 @@ The info block (`b`):
|---|---|
| 02 | `'P'`, `'B'`, protocol version (1) |
| 35 | device signature |
| 6 | SPM page size in bytes |
| 78 | loader base — application flash ends here |
| 6 | SPM page size in bytes (0 means 256) |
| 78 | loader base — application flash ends here (a word address when bit 1 is set) |
| 910 | EEPROM size |
| 11 | bit 0 set: host must patch the reset vector (no hardware boot section) |
| 11 | bit 0: host must patch the reset vector (no hardware boot section); bit 1: flash wire addresses are word addresses |
Composites are the host's job: verify = read back and compare, erase =
write `0xff` (per page for flash, per byte for EEPROM).
## Deployment
**ATmega328P**: program the loader at 0x7e00 with an external programmer.
Two fuse profiles, same binary:
**Megas**: program the loader at `flash 512` with an external programmer.
Every mega's smallest-but-one BOOTSZ puts the boot-section start exactly at
the loader base (512 B — the m8/16/168A reach it at their second-smallest
step, the m32/328P at their smallest), so the ATmega328P profiles below
apply to all of them with their own addresses; the per-chip BOOTSZ ladders
live in the host tool (`BOOT_FUSE`). The **ATmega1284P** is the exception:
its smallest boot section is 1 KB, so the standalone profile does not exist
— BOOTSZ = 512 words always, and with BOOTRST programmed reset lands at
0x1f800, one erased slot below the loader (the loader-first walk behavior
below, built in). Its staging slot sits inside that same 1 KB section, so
self-update needs no fuse change.
ATmega328P profiles (addresses for its 32 KiB):
| BOOTSZ | BOOTRST | Behavior |
|---|---|---|

View File

@@ -53,14 +53,40 @@ consteval avr::hertz_t clock()
using dev = avr::device<{.clock = clock()}>;
// Geometry: the resident loader owns the top 512 bytes of flash; the word
// below it is the trampoline (the application's relocated reset vector) on
// chips without a hardware boot section. The RWWSRE bit marks a separate
// boot section — on classic AVR the two capabilities coincide.
constexpr std::uint16_t boot_bytes = 512;
constexpr std::uint16_t base = static_cast<std::uint16_t>(spm::flash_bytes - boot_bytes);
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
consteval std::int16_t wdrf_field()
{
auto reg = std::string_view{avr::hw::db.regs[static_cast<std::size_t>(avr::power::detail::reset_reg())].name};
return avr::hw::db.field_index(reg, "WDRF");
}
// Geometry: the resident loader owns the top slot of flash — 512 bytes,
// except on the >64 KiB chips whose own smallest boot sector is 1 KiB (the
// 1284P): there the slot is 1 KiB, matching the hardware boundary the
// 512-byte figure comes from everywhere else. The word below the slot is
// the trampoline (the application's relocated reset vector) on chips
// without a hardware boot section. The RWWSRE bit marks a separate boot
// section — on classic AVR the two capabilities coincide (the m8/m32 packs
// spell its register SPMCR).
constexpr std::uint16_t slot_bytes = spm::flash_bytes > 65536 ? 1024 : 512;
constexpr std::uint32_t base = spm::flash_bytes - slot_bytes;
constexpr std::uint16_t page = spm::page_bytes;
constexpr bool boot_section = avr::hw::db.field_index("SPMCSR", "RWWSRE") >= 0;
constexpr bool boot_section = [] {
for (auto reg : {"SPMCSR", "SPMCR"})
if (avr::hw::db.field_index(reg, "RWWSRE") >= 0)
return true;
return false;
}();
// Past 64 KiB a byte address no longer fits the wire's 16 bits, so on the
// large chips every flash address on the wire — and all slot arithmetic —
// is a word address instead ('J' always was one). A slot spans the same
// wire-high-byte pair in either unit (512 B = 2 x 256 bytes, 1 KiB =
// 2 x 256 words), so the slot index is the high byte with its low bit
// dropped everywhere.
constexpr bool word_flash = spm::flash_bytes > 65536;
constexpr std::uint16_t wire_base = word_flash ? static_cast<std::uint16_t>(base / 2) : static_cast<std::uint16_t>(base);
constexpr std::uint16_t wire_page_mask = word_flash ? (page / 2 - 1) : (page - 1);
// The activation window, in seconds, is a compile-time constant (the build
// may override it): the whole EEPROM belongs to the application, and
@@ -71,8 +97,10 @@ constexpr bool boot_section = avr::hw::db.field_index("SPMCSR", "RWWSRE") >= 0;
constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
// The 12-byte info block the host reads with the 'b' command; flash-resident
// (there is no crt to copy a .data image).
inline constexpr std::array<std::uint8_t, 12> info_data = {
// (there is no crt to copy a .data image), word-aligned so its wire (word)
// address is exact on the large chips. The page byte is the wire count
// convention: 0 means 256.
[[gnu::progmem]] alignas(2) inline constexpr std::array<std::uint8_t, 12> info_data = {
'P',
'B',
1, // magic, protocol version
@@ -80,13 +108,14 @@ inline constexpr std::array<std::uint8_t, 12> info_data = {
avr::hw::db.signature[1],
avr::hw::db.signature[2],
static_cast<std::uint8_t>(page),
base & 0xff,
base >> 8, // app flash ends here; resident loader base
wire_base & 0xff,
wire_base >> 8, // app flash ends here; resident loader base (a word address on large chips)
avr::hw::db.mem.eeprom_size & 0xff,
avr::hw::db.mem.eeprom_size >> 8,
boot_section ? 0 : 1, // bit 0: host must patch the reset vector (no hardware boot section)
// bit 0: host must patch the reset vector (no hardware boot section);
// bit 1: flash wire addresses are word addresses
static_cast<std::uint8_t>((boot_section ? 0 : 1) | (word_flash ? 2 : 0)),
};
using info = avr::flash_table<info_data>;
// The serial link: the hardware USART where the chip has one, the polled
// software UART (no vector — the table belongs to the application) on PB0/PB1
@@ -98,19 +127,19 @@ using info = avr::flash_table<info_data>;
template <avr::hertz_t C>
consteval std::int16_t rxc_field()
{
return avr::hw::db.field_index("UCSR0A", "RXC0");
return avr::uart::detail::ufield<'0', "UCSR#A", "RXC#">();
}
template <avr::hertz_t C>
consteval std::int16_t txc_field()
{
return avr::hw::db.field_index("UCSR0A", "TXC0");
return avr::uart::detail::ufield<'0', "UCSR#A", "TXC#">();
}
template <avr::hertz_t C>
consteval std::int16_t status_reg()
{
return avr::hw::db.reg_index("UCSR0A");
return avr::uart::detail::ureg<'0', "UCSR#A">();
}
template <avr::hertz_t C>
@@ -190,7 +219,8 @@ struct software_link {
}
};
using link = std::conditional_t<avr::hw::db.has_reg("UDR0"), hardware_link<dev::clock>, software_link<dev::clock>>;
using link = std::conditional_t<avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART"),
hardware_link<dev::clock>, software_link<dev::clock>>;
// The application's entry, an absolute address the linker pins (--defsym in
// CMakeLists.txt): 0x0000 on the mega (word 0 stays the application's own
@@ -245,19 +275,30 @@ std::uint16_t rx16()
return static_cast<std::uint16_t>(low | (link::rx() << 8));
}
const std::uint8_t *flash_ptr(std::uint16_t address)
{
return reinterpret_cast<const std::uint8_t *>(address);
}
// The streamers take the count in the wire's 8-bit form: 0 means 256.
// send_flash stays out of line: its two callers ('b' and 'R') otherwise each
// inline a private copy of the loop.
// inline a private copy of the loop. On the large chips the address is a
// word address and the read goes through ELPM (flash_load_far).
[[gnu::noinline]] void send_flash(std::uint16_t address, std::uint8_t count)
{
do
link::tx(avr::flash_load(flash_ptr(address++)));
while (--count);
if constexpr (word_flash) {
// The 24-bit cursor as the machine holds it: the RAMPZ byte and a
// 16-bit Z, carried explicitly (the reassembled 32-bit address
// folds away inside the inlined far load). A single read never
// crosses a 64 KiB boundary — the protocol forbids it and the host
// splits its chunks there — so RAMPZ holds for the whole run.
std::uint8_t rampz = static_cast<std::uint8_t>(address >> 15);
std::uint16_t z = static_cast<std::uint16_t>(address << 1);
do {
link::tx(avr::flash_load_far<std::uint8_t>((static_cast<std::uint32_t>(rampz) << 16) | z));
if (++z == 0)
++rampz; // robustness for a host that reads across 64 KiB
} while (--count);
} else {
do
link::tx(avr::flash_load(reinterpret_cast<const std::uint8_t *>(address++)));
while (--count);
}
}
void send_eeprom(std::uint16_t address, std::uint8_t count)
@@ -287,7 +328,7 @@ void store_eeprom(std::uint16_t address, std::uint8_t count)
// copy flashed one slot lower may rewrite the slot above it — how pureboot
// updates itself. On the mega the RWW section is re-enabled so reads work
// immediately.
void program_flash(std::uint16_t address, std::uint8_t slot_high)
void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
{
// A buffer word cannot be loaded twice without an erase (§26.2.1), so a
// refused page's drained data must not linger for the next write:
@@ -297,19 +338,46 @@ void program_flash(std::uint16_t address, std::uint8_t slot_high)
spm::rww_enable<off>();
else
spm::clear_buffer<off>();
// The address is the loop's only state: pages are aligned, so the walk
// ends when the offset bits wrap back to zero.
do {
std::uint8_t low = link::rx();
std::uint8_t high = link::rx();
spm::fill<off>(address, static_cast<std::uint16_t>(low | (high << 8)));
address += 2;
} while (static_cast<std::uint8_t>(address) & (page - 1));
address -= 2; // back inside the page — erase and write ignore the word bits
const std::uint8_t page_high = static_cast<std::uint8_t>(address >> 8) & 0xfe;
// One induction either way. On the byte-addressed chips the wire address
// itself walks the page (aligned, so the offset bits wrap to zero); on
// the word-addressed large chips the wire word address becomes a 32-bit
// byte cursor once, and their 256-byte page makes its low byte the whole
// in-page offset. The slot index is one high byte of the wire address —
// two values on byte-addressed chips (the & ~1), bits 16:9 re-packed on
// the large ones.
spm::flash_address_t address;
std::uint8_t page_high;
if constexpr (word_flash) {
// Pages are aligned, so one page never crosses a 64 KiB boundary:
// RAMPZ is a per-page constant and the fill cursor is a 16-bit Z
// whose low byte is the whole in-page offset (256-byte pages). The
// slot index is simply the wire word address's high byte.
const std::uint8_t rampz = static_cast<std::uint8_t>(wire_address >> 15);
const std::uint16_t z0 = static_cast<std::uint16_t>(wire_address << 1);
std::uint16_t z = z0;
do {
std::uint8_t low = link::rx();
std::uint8_t high = link::rx();
spm::fill<off>((static_cast<spm::flash_address_t>(rampz) << 16) | z,
static_cast<std::uint16_t>(low | (high << 8)));
z += 2;
} while (static_cast<std::uint8_t>(z));
address = (static_cast<spm::flash_address_t>(rampz) << 16) | z0;
page_high = static_cast<std::uint8_t>(wire_address >> 8) & 0xfe;
} else {
address = static_cast<spm::flash_address_t>(wire_address);
do {
std::uint8_t low = link::rx();
std::uint8_t high = link::rx();
spm::fill<off>(address, static_cast<std::uint16_t>(low | (high << 8)));
address += 2;
} while (static_cast<std::uint8_t>(address) & (page - 1));
address -= 2; // back inside the page — erase and write ignore the word bits
page_high = static_cast<std::uint8_t>(address >> 8) & 0xfe;
}
if (page_high != slot_high) {
// The tinies halt the CPU through the erase and the write, so only
// the mega — running on while its RWW section programs — waits.
// the megas — running on while their RWW section programs — wait.
spm::erase_page<off>(address);
if constexpr (boot_section)
spm::wait();
@@ -336,18 +404,20 @@ void send_fuses()
{
// A watchdog reset belongs to the application (whose watchdog stays
// forced on until it clears WDRF) — no activation window in its way.
if (avr::hw::mcusr::wdrf.test())
// The flag register is MCUSR, or the classic megas' MCUCSR.
if (avr::hw::field_impl<wdrf_field()>::test())
run_app();
link::init();
// The high byte of the 512-byte-aligned base this copy runs at: the word
// return address's high byte is the byte address >> 9 (the slot index),
// doubled back into address terms. program_flash refuses this one slot
// and the info block is addressed from it, so both follow wherever the
// code was flashed.
const std::uint8_t slot_high =
static_cast<std::uint8_t>((reinterpret_cast<std::uint16_t>(__builtin_return_address(0)) >> 8) << 1);
// The high byte of the 512-byte-aligned base this copy runs at: the
// return address is a word address, whose high byte is the 256-word slot
// index — on byte-addressed chips doubled back into byte terms.
// program_flash refuses this one slot and the info block is addressed
// from it, so both follow wherever the code was flashed.
const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
const std::uint8_t slot_high = word_flash ? static_cast<std::uint8_t>(ra_words >> 8) & 0xfe
: static_cast<std::uint8_t>((ra_words >> 8) << 1);
// The knock: 'p' then 'b', each under a fresh window; any other byte is
// line noise and waits again. Falling out of a window runs the app.
@@ -364,11 +434,15 @@ void send_fuses()
case 'b': { // info block, read relative to the running slot
// The block sits in the image's first 256 bytes (the build lint
// asserts it), and slots are 512-aligned — so the low byte of its
// link address is its offset in any slot, and the high byte of
// its runtime address is the running slot's. Built as a byte
// pair so no absolute 16-bit address is ever materialized.
const std::uint8_t low = static_cast<std::uint8_t>(reinterpret_cast<std::uint16_t>(info::storage.data()));
send_flash(std::bit_cast<std::uint16_t>(std::array{low, slot_high}), info::size());
// link address (in wire units: bytes, or words on the large
// chips) is its offset in any slot, and the high byte of its
// runtime address is the running slot's. Built as a byte pair so
// no absolute address is ever materialized.
const auto link_low = reinterpret_cast<std::uint16_t>(info_data.data());
const std::uint8_t low =
word_flash ? static_cast<std::uint8_t>(link_low >> 1) : static_cast<std::uint8_t>(link_low);
send_flash(std::bit_cast<std::uint16_t>(std::array{low, slot_high}),
static_cast<std::uint8_t>(info_data.size()));
break;
}
case 'J': { // jump to a wire word address: hand-over and staging transfer

View File

@@ -36,7 +36,7 @@ else:
PROMPT = b"+"
PROTOCOL_VERSION = 1
SLOT = 512 # the loader slot size; also the self-update staging distance
SLOT = 512 # the loader slot on byte-addressed chips; word-addressed ones (>64 KiB) use 1 KiB — their own smallest boot sector
class Error(Exception):
@@ -270,12 +270,17 @@ class Info:
raise Error(f"protocol version {raw[2]}, tool speaks {PROTOCOL_VERSION}")
self.raw = bytes(raw)
self.signature = raw[3:6]
self.page = raw[6]
self.base = raw[7] | (raw[8] << 8)
self.eeprom_size = raw[9] | (raw[10] << 8)
self.page = raw[6] or 256 # the wire count convention: 0 means 256
self.patch_vector = bool(raw[11] & 1)
self.flash_size = self.base + SLOT
self.stage = self.base - SLOT # where a staging copy of the loader goes
# Large chips speak word addresses for flash (bit 1); the host keeps
# every address in bytes and converts at the wire.
self.word_flash = bool(raw[11] & 2)
scale = 2 if self.word_flash else 1
self.base = (raw[7] | (raw[8] << 8)) * scale
self.eeprom_size = raw[9] | (raw[10] << 8)
self.slot = 1024 if self.word_flash else SLOT
self.flash_size = self.base + self.slot
self.stage = self.base - self.slot # where a staging copy of the loader goes
# The hand-over target, as the word address 'J' takes: the trampoline
# below the loader (tinies), or word 0 (mega — the application's own
# reset vector; BOOTRST re-vectors a reset into the loader instead).
@@ -331,25 +336,41 @@ class Loader:
self._expect_prompt(timeout)
return reply
def _stream_read(self, command, address, count):
def _stream_read(self, command, address, count, address_scale=1):
data = b""
while count:
chunk = min(count, 256)
head = bytes((ord(command), address & 0xFF, address >> 8, chunk & 0xFF))
wire = address // address_scale
head = bytes((ord(command), wire & 0xFF, wire >> 8, chunk & 0xFF))
data += self._command(head, chunk, 5.0)
address += chunk
count -= chunk
return data
def read_flash(self, address, count):
return self._stream_read("R", address, count)
if not self.info.word_flash:
return self._stream_read("R", address, count)
# Word-addressed wire: widen to even bounds and never let one read
# cross a 64 KiB boundary (the device holds RAMPZ for a whole run).
start = address & ~1
span = (address + count + 1 & ~1) - start
data = b""
at = start
remaining = span
while remaining:
chunk = min(remaining, 0x10000 - (at & 0xFFFF))
data += self._stream_read("R", at, chunk, address_scale=2)
at += chunk
remaining -= chunk
return data[address - start : address - start + count]
def read_eeprom(self, address, count):
return self._stream_read("r", address, count)
def write_page(self, address, data):
assert len(data) == self.info.page and address % self.info.page == 0
head = bytes((ord("W"), address & 0xFF, address >> 8))
wire = address // (2 if self.info.word_flash else 1)
head = bytes((ord("W"), wire & 0xFF, wire >> 8))
self._command(head + data, 0, 2.0)
def write_eeprom(self, address, data):
@@ -495,14 +516,32 @@ def covered(pages, info, skip_blank):
# ----------------------------------------------------------------- fuses ---
def mega_boot(high_fuse):
"""Decode the ATmega328P high fuse's boot configuration (DS40002061B
§27.3, Table 27-13/27-16): BOOTSZ1:0 in bits 2:1 select the boot-section
words, BOOTRST in bit 0 (programmed = 0) re-vectors reset to its start.
Returns (bootrst_programmed, boot_section_start_byte)."""
bootsz = (high_fuse >> 1) & 0x03
words = {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}[bootsz]
return (high_fuse & 1) == 0, 0x8000 - words * 2
# Per-chip boot fuse geometry, keyed by the signature's family/part bytes:
# which byte of the 'F' reply (low, lock, extended, high) carries BOOTSZ/
# BOOTRST, and the BOOTSZ->words ladder. Sources: Atmel-2486/2466/2503
# (HIGH fuse), Atmel-8271 (m168A: EXTENDED; m328P: HIGH), Atmel-42719.
BOOT_FUSE = {
bytes((0x93, 0x07)): (3, {0b11: 128, 0b10: 256, 0b01: 512, 0b00: 1024}), # m8/8A
bytes((0x94, 0x03)): (3, {0b11: 128, 0b10: 256, 0b01: 512, 0b00: 1024}), # m16
bytes((0x95, 0x02)): (3, {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}), # m32/32A
bytes((0x94, 0x06)): (2, {0b11: 128, 0b10: 256, 0b01: 512, 0b00: 1024}), # m168A
bytes((0x95, 0x0F)): (3, {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}), # m328P
bytes((0x97, 0x05)): (3, {0b11: 512, 0b10: 1024, 0b01: 2048, 0b00: 4096}), # 1284P
}
def mega_boot(info, fuse_bytes):
"""Decode a mega's boot configuration from its fuses (the byte and the
BOOTSZ ladder are per chip): BOOTSZ1:0 in bits 2:1 select the
boot-section words, BOOTRST in bit 0 (programmed = 0) re-vectors reset
to its start. Returns (bootrst_programmed, boot_section_start_byte)."""
entry = BOOT_FUSE.get(bytes(info.signature[1:3]))
if entry is None:
raise Error(f"unknown mega signature {info.signature.hex()} — no boot fuse map")
which, ladder = entry
fuse = fuse_bytes[which]
words = ladder[(fuse >> 1) & 0x03]
return (fuse & 1) == 0, info.flash_size - words * 2
# ---------------------------------------------------------- loader update ---
@@ -534,12 +573,13 @@ def staging_content(image, info):
that word, which for a staging copy is the slot's own last word: an rjmp
to the resident base. The staging copy's fall-through and 'J'-free exit
both land in a loader instead of garbage."""
if len(image) > (SLOT - 2 if info.patch_vector else SLOT):
raise Error(f"loader image is {len(image)} B, the slot holds {SLOT - 2 if info.patch_vector else SLOT}")
content = bytearray(image) + bytearray([0xFF] * (SLOT - len(image)))
slot = info.slot
if len(image) > (slot - 2 if info.patch_vector else slot):
raise Error(f"loader image is {len(image)} B, the slot holds {slot - 2 if info.patch_vector else slot}")
content = bytearray(image) + bytearray([0xFF] * (slot - len(image)))
if info.patch_vector:
through = rjmp_to((info.base - 2) // 2, info.base // 2, info.flash_size // 2)
content[SLOT - 2], content[SLOT - 1] = through & 0xFF, through >> 8
content[slot - 2], content[slot - 1] = through & 0xFF, through >> 8
return bytes(content)
@@ -557,14 +597,13 @@ def update_preflight(image, info, fuse_bytes):
if not info.patch_vector:
if fuse_bytes is None:
raise Error("a loader update on this chip needs its fuses — unreadable? pass --assume-fuses")
high = fuse_bytes[3]
bootrst, bls_start = mega_boot(high)
bootrst, bls_start = mega_boot(info, fuse_bytes)
if info.stage < bls_start:
raise Error(
f"cannot self-update: the staging slot {info.stage:#06x} lies below the "
f"boot section ({bls_start:#06x}, high fuse {high:#04x}) where SPM is disabled "
f"— a boot section of at least 1 KB (BOOTSZ) is required, and only an "
f"external programmer can change fuses"
f"boot section ({bls_start:#06x}) where SPM is disabled "
f"— a boot section of at least two slots ({2 * info.slot} B, BOOTSZ) is "
f"required, and only an external programmer can change fuses"
)
if not bootrst:
warnings.append(
@@ -605,7 +644,7 @@ class UpdateState:
self.data = {
"signature": info.signature.hex(),
"base": info.base,
"staging": loader.read_flash(info.stage, SLOT).hex(),
"staging": loader.read_flash(info.stage, info.slot).hex(),
"page0": loader.read_flash(0, info.page).hex() if info.patch_vector else "",
}
with open(self.path, "w") as f:
@@ -661,7 +700,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
for warning in update_preflight(image, info, fuse_bytes):
print(f"note: {warning}")
staged = staging_content(image, info)
resident = bytes(image) + bytes([0xFF] * (SLOT - len(image)))
resident = bytes(image) + bytes([0xFF] * (info.slot - len(image)))
page = info.page
state = UpdateState(state_path)
@@ -671,7 +710,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
# address 0 (the 1 KB tiny13A), its first page carries the reset vector:
# written last, so any earlier interruption still resets into the old
# resident, and from then on resets enter the staging copy.
order = list(range(0, SLOT, page))
order = list(range(0, info.slot, page))
if info.stage == 0:
order = order[1:] + [0]
if write_differing(loader, info.stage, staged, order):
@@ -694,7 +733,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
loader.enter_copy(info.base, wait)
if redirect:
write_differing(loader, 0, state.page0)
order = list(range(0, SLOT, page))
order = list(range(0, info.slot, page))
if info.stage == 0:
order = [0] + order[1:]
write_differing(loader, info.stage, state.staging, order)
@@ -710,7 +749,7 @@ def check_walk_region(pages, info, fuse_bytes, force):
Only checkable when the fuses are known (--fuses or --assume-fuses)."""
if info.patch_vector or fuse_bytes is None:
return
bootrst, bls_start = mega_boot(fuse_bytes[3])
bootrst, bls_start = mega_boot(info, fuse_bytes)
if not bootrst or bls_start >= info.base:
return
overlap = [a for a in sorted(pages) if a >= bls_start and pages[a].count(0xFF) != len(pages[a])]

View File

@@ -37,7 +37,7 @@ def main():
sys.exit(1)
symbols = subprocess.run([nm, "-C", elf], capture_output=True, text=True, check=True).stdout
info = [line for line in symbols.splitlines() if "flash_table" in line and "::storage" in line]
info = [line for line in symbols.splitlines() if "info_data" in line]
if len(info) != 1:
print(f"FAIL: expected one info-block storage symbol, found {len(info)}")
sys.exit(1)

View File

@@ -26,7 +26,8 @@ consteval avr::hertz_t clock()
using dev = avr::device<{.clock = clock()}>;
template <avr::hertz_t C, bool Hardware = avr::hw::db.has_reg("UDR0")>
template <avr::hertz_t C,
bool Hardware = avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART")>
struct link {
using tx_t = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>;
static void tx(char c)
@@ -35,9 +36,12 @@ struct link {
}
[[noreturn]] static void idle()
{
// 'L' hands back to the loader at the top slot — 512 bytes, or the
// 1 KiB the >64 KiB chips use.
constexpr std::uint32_t slot = avr::hw::db.mem.flash_size > 65536 ? 1024 : 512;
for (;;)
if (tx_t::read_blocking() == 'L')
reinterpret_cast<void (*)()>((avr::hw::db.mem.flash_size - 512) / 2)();
reinterpret_cast<void (*)()>(static_cast<std::uint16_t>((avr::hw::db.mem.flash_size - slot) / 2))();
}
};

View File

@@ -24,7 +24,7 @@ def fail(message):
def main():
device_bin, elf, mcu, hz, base_hex, page, baud, tool, workdir = sys.argv[1:]
base, page, baud = int(base_hex, 0), int(page), int(baud)
stage = base - 512
stage = None # derived from the device's own info (slot-sized) below
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pbsim
@@ -46,6 +46,7 @@ def main():
resident_info = info.raw
# Install the staging copy exactly as the update flow would.
stage = info.stage
staged = pb.staging_content(image, info)
pb.write_differing(loader, stage, staged)
@@ -78,7 +79,7 @@ def main():
# Restore the resident image through the staged copy, then 'J' back
# into it and prove it lives.
resident = image + b"\xff" * (512 - len(image))
resident = image + b"\xff" * (info.slot - len(image))
pb.write_differing(loader, base, resident)
back_info = loader.enter_copy(base, 25)
if back_info.raw != resident_info:

View File

@@ -11,7 +11,7 @@ class Device:
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None):
cmd = [binary, elf, mcu, hz, base_hex, str(page), str(baud), dump]
if reset_hex is not None or resume is not None:
cmd.append(reset_hex if reset_hex is not None else ("0" if mcu != "atmega328p" else base_hex))
cmd.append(reset_hex if reset_hex is not None else ("0" if not mcu.startswith("atmega") else base_hex))
if resume is not None:
cmd.append(resume)
self.log = open(dump + ".log", "a")

View File

@@ -90,11 +90,17 @@ def main():
read_flash = os.path.join(workdir, "readback_flash.bin")
read_eeprom = os.path.join(workdir, "readback_eeprom.bin")
# The geometry the host will discover, for computing the expected image.
# The geometry the host will discover, for computing the expected image:
# megas carry a boot section (no vector surgery), the large ones speak
# word addresses, and the page byte is the wire's 0-means-256.
mega = mcu.startswith("atmega")
word_flash = base + 512 > 0x10000
wire_base = base // 2 if word_flash else base
flags = (0 if mega else 1) | (2 if word_flash else 0)
info = pb.Info(
bytes([ord("P"), ord("B"), 1, 0, 0, 0, page])
+ bytes([base & 0xFF, base >> 8, eeprom_size & 0xFF, eeprom_size >> 8])
+ bytes([0 if mcu == "atmega328p" else 1])
bytes([ord("P"), ord("B"), 1, 0, 0, 0, page & 0xFF])
+ bytes([wire_base & 0xFF, wire_base >> 8, eeprom_size & 0xFF, eeprom_size >> 8])
+ bytes([flags])
)
device = Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump)
@@ -150,8 +156,9 @@ def main():
fail("loader region looks erased in the ground-truth dump")
# The surgery, decoded independently: the patched vector must land on the
# loader, the trampoline on the application's own entry.
if mcu != "atmega328p":
# loader, the trampoline on the application's own entry (tinies only —
# the megas' word 0 stays the application's).
if not mega:
flash_words = (base + 512) // 2
app = open(app_bin, "rb").read()
word0 = flash_true[0] | (flash_true[1] << 8)

View File

@@ -41,10 +41,21 @@ class PowerFail(Exception):
pass
MEGA_FUSES = "ffffffdd" # high 0xdd: BOOTSZ = 1 KB, BOOTRST unprogrammed
def assumed_fuses(pb, image):
"""Synthetic 'F' bytes for --assume-fuses: the smallest boot section
covering both the resident and the staging slot (two slots — what a
self-update needs), BOOTRST unprogrammed — the per-chip BOOTSZ ladder
and fuse byte come from the tool's own table, keyed by the update
image's embedded signature."""
info = pb.image_info(image)
which, ladder = pb.BOOT_FUSE[bytes(info.signature[1:3])]
bits = min((b for b in ladder if ladder[b] * 2 >= 2 * info.slot), key=lambda b: ladder[b])
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
fuses[which] = 0xF8 | (bits << 1) | 1
return bytes(fuses)
def make_fault_loader(pb, base, kill_region, kill_hits, device):
def make_fault_loader(pb, base, slot, kill_region, kill_hits, device):
"""A Loader whose write_page kills the device (or, with device=None,
just the host) at the Nth write into a region; the sequence
stage->resident->stage distinguishes the install from the restore."""
@@ -59,7 +70,7 @@ def make_fault_loader(pb, base, kill_region, kill_hits, device):
if address >= base:
phase = "resident"
self.seen_resident = True
elif address >= base - 512:
elif address >= base - slot:
phase = "stage_restore" if self.seen_resident else "stage"
else:
phase = "app"
@@ -77,7 +88,8 @@ def make_fault_loader(pb, base, kill_region, kill_hits, device):
def main():
(device_bin, elf, update_elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:]
base, page, baud = int(base_hex, 0), int(page), int(baud)
mega = mcu == "atmega328p"
mega = mcu.startswith("atmega")
slot = 1024 if base + 1024 > 0x10000 and mega else 512 # word-addressed chips use the 1 KiB slot
reset_hex = "0" if mega else None # the mega runs BOOTRST-unprogrammed here
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
@@ -95,7 +107,7 @@ def main():
fail("the update image is byte-identical to the resident build")
dump = os.path.join(workdir, "dump.bin")
state = os.path.join(workdir, "update.pbstate")
fuses = bytes.fromhex(MEGA_FUSES) if mega else None
fuses = assumed_fuses(pb, images["v0"]) if mega else None
def connect(device):
port = pb.Port(device.pty, baud)
@@ -109,16 +121,16 @@ def main():
return port, loader
def padded(image):
return image + b"\xff" * (512 - len(image))
return image + b"\xff" * (slot - len(image))
def resident_bytes(loader):
return loader.read_flash(base, 256) + loader.read_flash(base + 256, 256)
return loader.read_flash(base, slot)
def assert_state(loader, image, app_pages):
if resident_bytes(loader) != padded(image):
fail("resident loader does not match the update image")
stage = base - 512
got = loader.read_flash(stage, 256) + loader.read_flash(stage + 256, 256)
stage = base - slot
got = loader.read_flash(stage, slot)
for address, data in app_pages.items():
if stage <= address < base:
if got[address - stage : address - stage + page] != data:
@@ -136,7 +148,7 @@ def main():
# A clean CLI update, resident -> v9.
args = ["--update-loader", os.path.join(workdir, "v9.bin"), "--state", state, "--stay"]
if mega:
args += ["--assume-fuses", MEGA_FUSES]
args += ["--assume-fuses", fuses.hex()]
out = pbsim.run_tool(tool, device.pty, baud, *args)
if "loader updated" not in out:
fail("update did not report success")
@@ -167,7 +179,7 @@ def main():
port, loader = connect(device)
target = "v9" if resident_bytes(loader) == padded(images["v0"]) else "v0"
image_path = os.path.join(workdir, target + ".bin")
injected = make_fault_loader(pb, base, kill_region, kill_hits, device if kill_device else None)(port)
injected = make_fault_loader(pb, base, slot, kill_region, kill_hits, device if kill_device else None)(port)
injected.info = loader.info
try:
pb.op_update_loader(injected, 25, image_path, state, fuses)
@@ -193,10 +205,10 @@ def main():
# Ground truth: the simulator's own flash against the final state, and
# on the tinies an independent decode of the reset routing.
flash = open(dump, "rb").read()
if flash[base : base + 512] != padded(images[final]):
if flash[base : base + slot] != padded(images[final]):
fail("ground-truth resident region does not match the final image")
if not mega:
flash_words = (base + 512) // 2
flash_words = (base + slot) // 2
word0 = flash[0] | (flash[1] << 8)
if rjmp_decode(word0, 0, flash_words) != base // 2:
fail("ground-truth reset vector does not land on the loader")

View File

@@ -3,7 +3,7 @@
// the patched vector are not what is under test), and exposes the loader's
// serial link as a pty for the real host tool:
//
// - ATmega328P: the hardware USART0 through simavr's uart_pty.
// - Megas: the hardware USART through simavr's uart_pty.
// - Tinies: an 8N1 bridge between a pty and the GPIO software UART
// (drives PB0, the loader's RX; decodes PB1, its TX), timed against the
// simulated cycle counter.
@@ -279,7 +279,7 @@ int main(int argc, char *argv[])
unsigned page = (unsigned)atoi(argv[5]);
unsigned baud = (unsigned)atoi(argv[6]);
dump_path = argv[7];
use_uart_pty = strcmp(mcu_name, "atmega328p") == 0;
use_uart_pty = strncmp(mcu_name, "atmega", 6) == 0; // every mega links over its hardware USART
avr = avr_make_mcu_by_name(mcu_name);
if (!avr) {

View File

@@ -27,9 +27,12 @@ def expect_error(what, fn, *needles):
fail(f"{what}: no error raised")
def info_of(pb, base, page, patch, flash):
raw = bytes((0x50, 0x42, 1, 0x1E, 0x93, 0x0B, page, base & 0xFF, base >> 8,
0, 2, 1 if patch else 0))
def info_of(pb, base, page, patch, flash, signature=(0x1E, 0x93, 0x0B), word_flash=False):
scale = 2 if word_flash else 1
wire_base = base // scale
flags = (1 if patch else 0) | (2 if word_flash else 0)
raw = bytes((0x50, 0x42, 1, *signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8,
0, 2, flags))
info = pb.Info(raw)
assert info.flash_size == flash
return info
@@ -50,16 +53,41 @@ def main():
import pureboot as pb
tiny = info_of(pb, 0x1E00, 64, True, 0x2000)
mega = info_of(pb, 0x7E00, 128, False, 0x8000)
mega = info_of(pb, 0x7E00, 128, False, 0x8000, signature=(0x1E, 0x95, 0x0F))
# mega_boot: BOOTSZ words and the BOOTRST sense, DS40002061B §27.
for bits, start in ((0b11, 0x7E00), (0b10, 0x7C00), (0b01, 0x7800), (0b00, 0x7000)):
prog, at = pb.mega_boot((0xF8 | (bits << 1)) & ~1)
if not prog or at != start:
fail(f"mega_boot BOOTSZ={bits:02b} programmed: {prog} {at:#06x}")
prog, at = pb.mega_boot(0xF8 | (bits << 1) | 1)
if prog or at != start:
fail(f"mega_boot BOOTSZ={bits:02b} unprogrammed: {prog} {at:#06x}")
# mega_boot: BOOTSZ words and the BOOTRST sense per chip — the fuse byte
# index (HIGH everywhere but the m168A's EXTENDED) and the per-family
# ladders (Atmel-2486/2466/2503/8271/42719). Synthetic 'F' replies: only
# the boot byte carries meaning.
cases = (
((0x1E, 0x93, 0x07), 0x2000, 3, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m8
((0x1E, 0x94, 0x03), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m16
((0x1E, 0x95, 0x02), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m32
((0x1E, 0x94, 0x06), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168A
((0x1E, 0x95, 0x0F), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m328P
((0x1E, 0x97, 0x05), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284P
)
for signature, flash, which, ladder in cases:
# Word-addressed chips carry the 1 KiB slot (their smallest boot sector).
slot = 1024 if flash > 0x10000 else 512
chip = info_of(pb, flash - slot, 128 if flash < 0x20000 else 0, False, flash,
signature=signature, word_flash=flash > 0x10000)
for bits, start in ladder.items():
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
fuses[which] = (0xF8 | (bits << 1)) & ~1
prog, at = pb.mega_boot(chip, bytes(fuses))
if not prog or at != start:
fail(f"mega_boot {signature[1]:02x}{signature[2]:02x} BOOTSZ={bits:02b} programmed: {prog} {at:#07x}")
fuses[which] |= 1
prog, at = pb.mega_boot(chip, bytes(fuses))
if prog or at != start:
fail(f"mega_boot {signature[1]:02x}{signature[2]:02b} unprogrammed: {prog} {at:#07x}")
# Word-addressed info decode: the 1284P's base/page ride the wire scaled,
# and its slot is 1 KiB.
big = info_of(pb, 0x1FC00, 0, False, 0x20000, signature=(0x1E, 0x97, 0x05), word_flash=True)
if big.page != 256 or big.base != 0x1FC00 or big.stage != 0x1F800 or big.slot != 1024:
fail(f"word-addressed info decode: page {big.page}, base {big.base:#x}, stage {big.stage:#x}")
# Surgery: word 0 lands on the loader, the trampoline on the original
# entry — checked with an independent decoder.