pureboot: every libavr chip — 37 loaders, the m48 class, the 644 geometry

The chip table becomes family blocks covering all 37 targets. The m48s
are a new deployment class: no boot section, so the tiny profile spoken
over the hardware USART — host-patched reset vector, trampoline
hand-over, a 510-byte budget (474 B built), no fuse preflight — while
their RWWSRE store stays the buffer discard (Atmel-8271 §26.2); the
device keys the patch flag and the CPU-halt waits on the curated
boot-section capability and the discard on the RWWSRE bit itself. The
644s' 64 KiB is exactly the 16-bit byte space: plain LPM, byte wire
addresses, 498 B in a 512-byte slot — and their 1 KiB minimum boot
section holds the resident and staging slots together, so self-update
needs no fuse step (the update test's slot pick now keys word-flash on
base >= 64 KiB; base + slot merely touching the boundary stays
byte-addressed). The 1284 joins the 1284P's word-addressed 1 KiB slot at
558 B. BOOT_FUSE gains every boot-sectioned family's ladder and fuse
byte; the planner exercises them all. The sim scaffolding keys
patch-vector-ness instead of the atmega name prefix, the fixture app
picks its clock by family (the tiny25/45/13 builds surfaced the 16 MHz
fallthrough as garbled banners), and the runner's wrapped flash ioctl
performs the m48 discard simavr's no-RWW cores turn into a stray buffer
fill. Sizes across the fleet: 466-504 B megas, 474 B m48s, 498 B 644s,
488-502 B tinies, 558 B 1284s — every chip passing
size/pi/planner/protocol/reloc/update.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 18:53:04 +02:00
parent b7d082ff11
commit 9cbe2d682d
11 changed files with 2059 additions and 661 deletions

View File

@@ -142,18 +142,34 @@ 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")
# Per-family geometry. The boot-sectioned megas run the loader from the
# hardware boot section and boot the application at word 0; the tinies and
# the boot-section-less m48s 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. --pmem-wrap-around models AVR's modulo-flash PC where
# the flash is big enough to need it (an rjmp reaches all of 4 KiB by
# itself).
if(LIBAVR_MCU MATCHES "^attiny13a?$")
set(_pb_flash 1024)
set(_pb_wrap "")
set(_pb_page 32)
set(_pb_hz 9600000)
set(_pb_baud 57600)
set(_pb_eeprom 64)
set(_pb_limit 510)
elseif(LIBAVR_MCU STREQUAL "attiny25")
set(_pb_flash 2048)
set(_pb_wrap "")
set(_pb_page 32)
set(_pb_hz 8000000)
set(_pb_baud 57600)
set(_pb_eeprom 128)
elseif(LIBAVR_MCU STREQUAL "attiny45")
set(_pb_flash 4096)
set(_pb_wrap "")
set(_pb_page 64)
set(_pb_hz 8000000)
set(_pb_baud 57600)
set(_pb_eeprom 256)
elseif(LIBAVR_MCU STREQUAL "attiny85")
set(_pb_flash 8192)
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
@@ -161,40 +177,47 @@ elseif(LIBAVR_MCU STREQUAL "attiny85")
set(_pb_hz 8000000)
set(_pb_baud 57600)
set(_pb_eeprom 512)
set(_pb_limit 510)
elseif(LIBAVR_MCU MATCHES "^atmega8a?$")
elseif(LIBAVR_MCU MATCHES "^atmega48(a|p|pa)?$")
set(_pb_flash 4096)
set(_pb_wrap "")
set(_pb_page 64)
set(_pb_hz 16000000)
set(_pb_baud 115200)
set(_pb_eeprom 256)
elseif(LIBAVR_MCU MATCHES "^atmega8a?$" OR LIBAVR_MCU MATCHES "^atmega88(a|p|pa)?$")
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")
elseif(LIBAVR_MCU MATCHES "^atmega16a?$" OR LIBAVR_MCU MATCHES "^atmega168(a|p|pa)?$" OR
LIBAVR_MCU MATCHES "^atmega164(a|p|pa)$")
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?$")
elseif(LIBAVR_MCU MATCHES "^atmega32a?$" OR LIBAVR_MCU MATCHES "^atmega328p?$" OR
LIBAVR_MCU MATCHES "^atmega324(a|p|pa)$")
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)
elseif(LIBAVR_MCU MATCHES "^atmega644(a|p|pa)?$")
# 64 KiB is exactly the 16-bit byte space: plain LPM reaches everything,
# and the smallest boot section (1 KiB) holds the loader and its staging
# slot together (see pureboot/README.md).
set(_pb_flash 65536)
set(_pb_wrap -Wl,--pmem-wrap-around=64k)
set(_pb_page 256)
set(_pb_hz 16000000)
set(_pb_baud 115200)
set(_pb_eeprom 512)
set(_pb_limit 512)
elseif(LIBAVR_MCU STREQUAL "atmega1284p")
set(_pb_eeprom 2048)
elseif(LIBAVR_MCU MATCHES "^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
@@ -205,36 +228,35 @@ elseif(LIBAVR_MCU STREQUAL "atmega1284p")
set(_pb_hz 16000000)
set(_pb_baud 115200)
set(_pb_eeprom 4096)
set(_pb_limit 1024)
set(_pb_slot 1024)
set(_pb_limit 1024)
else()
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)
message(FATAL_ERROR "pureboot: no geometry for ${LIBAVR_MCU}")
endif()
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 MATCHES "^atmega")
# Patched-vector chips hand over through the trampoline word below the slot,
# which is also the slot's own last word — their budget is slot 2.
if(LIBAVR_MCU MATCHES "^atmega" AND NOT LIBAVR_MCU MATCHES "^atmega48")
set(_pb_app 0)
if(NOT DEFINED _pb_limit)
set(_pb_limit ${_pb_slot})
endif()
else()
math(EXPR _pb_app "${_pb_base} - 2")
math(EXPR _pb_limit "${_pb_slot} - 2")
endif()
# simavr names its cores after the base dies; the A revisions run on them.
# simavr names its cores after the base dies; the A revisions run on them
# (the 644PA on the 644P core).
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)
if(LIBAVR_MCU MATCHES "^atmega(8|16|32|48|88|164|168|644)a$")
string(REGEX REPLACE "a$" "" _pb_sim_mcu ${LIBAVR_MCU})
elseif(LIBAVR_MCU STREQUAL "atmega644pa")
set(_pb_sim_mcu atmega644p)
endif()
add_executable(pureboot pureboot/pureboot.cpp)

File diff suppressed because it is too large Load Diff

View File

@@ -2,13 +2,14 @@
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, **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
(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, 498502 B on the tiny25/45/85, 466504 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 that chip
simply does not have, and no implementation of this feature set fits it
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`).
@@ -29,19 +30,20 @@ belongs to the host-managed trampoline (below).
| Chip | Serial | Baud | Clock assumed |
|---|---|---|---|
| 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 |
| 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.
## 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).
Reset enters the loader (BOOTRST on the boot-sectioned megas; the patched
reset vector on the tinies and the boot-section-less m48s) — 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
@@ -61,9 +63,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
On chips whose flash exceeds 64 KiB (the 1284s — 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.
addresses (the 644s' 64 KiB is exactly the 16-bit byte space and stays
byte-addressed). EEPROM addresses are always bytes, counts always bytes.
| Cmd | Arguments | Reply |
|---|---|---|
@@ -108,16 +111,23 @@ write `0xff` (per page for flash, per byte for EEPROM).
## Deployment
**Megas**: program the loader at `flash slot` with an external programmer.
Every mega has a BOOTSZ step whose boot section is exactly the loader slot —
512 B, the m8/16/168A's second-smallest step, the m32/328P's smallest; on
the m1284P that step is its smallest, 512 words, which is why its slot is
1 KiB — so the ATmega328P profiles below apply to every mega with its own
addresses and slot size; the per-chip BOOTSZ ladders live in the host tool
(`BOOT_FUSE`). The 1284P's 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.
**Boot-sectioned megas**: program the loader at `flash slot` 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.
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).
ATmega328P profiles (addresses for its 32 KiB):
@@ -130,15 +140,18 @@ ATmega328P profiles (addresses for its 32 KiB):
Applications are flashed unmodified — word 0 stays the application's own
reset vector, and the hand-over 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: word 0 is
rewritten to `rjmp` to the loader base, and the application's own entry is
re-encoded as a trampoline `rjmp` in the word just below the loader
(`base 2`, where the hand-over jumps). Every other vector stays the
application's. The patched page 0 and the trampoline page are written
*first*, so from the first write on an interrupted flash still resets into
the loader; an erase runs top-down for the same reason.
**Patched-vector chips — the tinies and the m48s** (no boot section; the
m48s' SPM runs from the entire flash, Atmel-8271 §26): 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: word 0 is rewritten to `rjmp` to the loader base, and
the application's own entry is re-encoded as a trampoline `rjmp` in the
word just below the loader (`base 2`, where the hand-over jumps). Every
other vector stays the application's. The patched page 0 and the trampoline
page are written *first*, so from the first write on an interrupted flash
still resets into the loader; an erase runs top-down for the same reason.
The m48s speak this profile over their hardware USART — no fuse preflight,
BOOTRST does not exist there.
## Updating the loader
@@ -148,16 +161,18 @@ loader itself as its own staging loader. The image is the loader's own 512
bytes as a raw binary, or the Intel HEX the build emits beside it, which
links the loader at its base inside an otherwise blank flash image:
1. The staging slot `[base512, base)` is saved to a host-side state file
(on the 1 KB tiny13A that is the whole application, vectors included).
2. The resident installs the identical update image there. On the tinies the
host composes the slot's last word — the same address as the resident's
trampoline — as a jump to the resident base, so even an abandoned staging
copy times out into a loader, never into garbage.
1. The staging slot `[baseslot, base)` is saved to a host-side state file
(on the 1 KB tiny13s that is the whole application, vectors included).
2. The resident installs the identical update image there. On the
patched-vector chips the host composes the slot's last word — the same
address as the resident's trampoline — as a jump to the resident base,
so even an abandoned staging copy times out into a loader, never into
garbage.
3. `J` enters the staging copy, which rewrites the resident slot. On the
t85 the host first re-aims word 0 at the staging copy, so a power loss
mid-rewrite still resets into a loader; on the t13a the staging slot
carries the reset vector itself.
patched-vector chips whose staging slot sits away from page 0 the host
first re-aims word 0 at the staging copy, so a power loss mid-rewrite
still resets into a loader; on the tiny13s the staging slot carries the
reset vector itself.
4. `J` enters the new resident, which restores the staging slot's saved
content (word 0 and the trampoline with it) and the state file is
discarded.
@@ -166,9 +181,10 @@ Every phase is idempotent and keyed off the actual flash state: re-running
the same command after any interruption resumes and completes. The state
file carries the only bytes not recoverable from the device; if it is lost
mid-update the update still completes, and the staging region is restored by
reflashing the application. The mega needs its fuses for the preflight
(BOOTSZ gate, profile notes) — read from the device, or supplied with
`--assume-fuses` where reading is impossible (simulators).
reflashing the application. A boot-sectioned mega needs its fuses for the
preflight (BOOTSZ gate, profile notes) — read from the device, or supplied
with `--assume-fuses` where reading is impossible (simulators); the
patched-vector chips need none.
## Host tool

View File

@@ -16,9 +16,10 @@
// resident — how pureboot updates itself, host-driven, with no other
// firmware involved.
//
// Entry: reset lands in avr::startup::entry below (BOOTRST on the mega; the
// patched reset vector — or erased flash walking up into the loader — on the
// tinies). A watchdog reset hands straight to the application. Otherwise the
// Entry: reset lands in avr::startup::entry below (BOOTRST on the
// boot-sectioned megas; the patched reset vector — or erased flash walking
// up into the loader — on the tinies and the boot-section-less m48s). A
// watchdog reset hands straight to the application. Otherwise the
// host has one activation window per awaited knock byte ("pb"); an idle line
// boots the application. A session then stays in the command loop until 'J'
// jumps away or the chip resets.
@@ -44,9 +45,10 @@ constexpr std::uint8_t ack = '+';
// from code.
consteval avr::hertz_t clock()
{
if (avr::hw::db.name == "ATtiny13A")
auto name = std::string_view{avr::hw::db.name};
if (name.starts_with("ATtiny13"))
return 9.6_MHz;
if (avr::hw::db.name == "ATtiny85")
if (name.starts_with("ATtiny"))
return 8_MHz;
return 16_MHz;
}
@@ -62,21 +64,19 @@ consteval std::int16_t wdrf_field()
// 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
// 1284s): 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).
// without a hardware boot section — the tinies and the m48s, whose SPM
// runs from anywhere (Atmel-8271 §26). A boot section also means the CPU
// runs on while the RWW section programs; everywhere else it halts through
// the operation. The m48s still carry RWWSRE as their temporary-buffer
// discard (§26.2), so the discard picks by that bit, not by the section.
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 = [] {
for (auto reg : {"SPMCSR", "SPMCR"})
if (avr::hw::db.field_index(reg, "RWWSRE") >= 0)
return true;
return false;
}();
constexpr bool boot_section = avr::hw::curated::has_boot_section();
constexpr bool rww_discard = spm::detail::has_rww();
// 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 —
@@ -85,7 +85,8 @@ constexpr bool boot_section = [] {
// 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_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
@@ -332,9 +333,10 @@ 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:
// discard the buffer up front — CTPB on the tinies; on the mega writing
// RWWSRE aborts a pending load (§26.2.2).
if constexpr (boot_section)
// discard the buffer up front — CTPB on the tinies; on the megas
// writing RWWSRE aborts a pending load (§26.2.2 — on the m48s that
// flush is the bit's whole documented job).
if constexpr (rww_discard)
spm::rww_enable<off>();
else
spm::clear_buffer<off>();
@@ -376,8 +378,9 @@ void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
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 megas — running on while their RWW section programs — wait.
// The tinies and the m48s halt the CPU through the erase and the
// write, so only the boot-sectioned megas — running on while their
// RWW section programs — wait.
spm::erase_page<off>(address);
if constexpr (boot_section)
spm::wait();
@@ -416,8 +419,8 @@ void send_fuses()
// 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);
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.

View File

@@ -518,15 +518,34 @@ def covered(pages, info, skip_blank):
# 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.
# BOOTRST, and the BOOTSZ->words ladder. A die revision shares its base
# signature, so one row covers it. The m48s have no boot section and no
# row — their info block says patch-vector and this table is never
# consulted. Sources: Atmel-2486/2466/2503 (HIGH fuse), Atmel-2545/8271/
# DS40002065 (x8: EXTENDED, except the m328s' HIGH), Atmel-8272/8011/2593/
# 42719 (x4: HIGH).
_LADDER_128 = {0b11: 128, 0b10: 256, 0b01: 512, 0b00: 1024}
_LADDER_256 = {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}
_LADDER_512 = {0b11: 512, 0b10: 1024, 0b01: 2048, 0b00: 4096}
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
bytes((0x93, 0x07)): (3, _LADDER_128), # m8/8A
bytes((0x94, 0x03)): (3, _LADDER_128), # m16/16A
bytes((0x95, 0x02)): (3, _LADDER_256), # m32/32A
bytes((0x93, 0x0A)): (2, _LADDER_128), # m88/88A
bytes((0x93, 0x0F)): (2, _LADDER_128), # m88P/88PA
bytes((0x94, 0x06)): (2, _LADDER_128), # m168/168A
bytes((0x94, 0x0B)): (2, _LADDER_128), # m168P/168PA
bytes((0x95, 0x14)): (3, _LADDER_256), # m328
bytes((0x95, 0x0F)): (3, _LADDER_256), # m328P
bytes((0x94, 0x0F)): (3, _LADDER_128), # m164A
bytes((0x94, 0x0A)): (3, _LADDER_128), # m164P/164PA
bytes((0x95, 0x15)): (3, _LADDER_256), # m324A
bytes((0x95, 0x08)): (3, _LADDER_256), # m324P
bytes((0x95, 0x11)): (3, _LADDER_256), # m324PA
bytes((0x96, 0x09)): (3, _LADDER_512), # m644/644A
bytes((0x96, 0x0A)): (3, _LADDER_512), # m644P/644PA
bytes((0x97, 0x06)): (3, _LADDER_512), # m1284
bytes((0x97, 0x05)): (3, _LADDER_512), # m1284P
}

View File

@@ -17,17 +17,17 @@ namespace {
consteval avr::hertz_t clock()
{
if (avr::hw::db.name == "ATtiny13A")
auto name = std::string_view{avr::hw::db.name};
if (name.starts_with("ATtiny13"))
return 9.6_MHz;
if (avr::hw::db.name == "ATtiny85")
if (name.starts_with("ATtiny"))
return 8_MHz;
return 16_MHz;
}
using dev = avr::device<{.clock = clock()}>;
template <avr::hertz_t C,
bool Hardware = avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART")>
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)

View File

@@ -11,7 +11,11 @@ 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 not mcu.startswith("atmega") else base_hex))
# Chips without a hardware boot section — the tinies and the
# m48s — reset to address 0 like silicon; the boot-sectioned
# megas re-vector to the loader base (BOOTRST).
patch = not mcu.startswith("atmega") or mcu.startswith("atmega48")
cmd.append(reset_hex if reset_hex is not None else ("0" if patch else base_hex))
if resume is not None:
cmd.append(resume)
self.log = open(dump + ".log", "a")

View File

@@ -91,12 +91,14 @@ def main():
read_eeprom = os.path.join(workdir, "readback_eeprom.bin")
# 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.
# the boot-sectioned megas need no vector surgery (the tinies and the
# boot-section-less m48s do), the large chips speak word addresses, and
# the page byte is the wire's 0-means-256.
mega = mcu.startswith("atmega")
patch = not mega or mcu.startswith("atmega48")
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)
flags = (1 if patch else 0) | (2 if word_flash else 0)
info = pb.Info(
bytes([ord("P"), ord("B"), 1, 0, 0, 0, page & 0xFF])
+ bytes([wire_base & 0xFF, wire_base >> 8, eeprom_size & 0xFF, eeprom_size >> 8])
@@ -156,9 +158,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 (tinies only —
# the megas' word 0 stays the application's).
if not mega:
# loader, the trampoline on the application's own entry (patched-vector
# chips only — a boot-sectioned mega's word 0 stays the application's).
if patch:
flash_words = (base + 512) // 2
app = open(app_bin, "rb").read()
word0 = flash_true[0] | (flash_true[1] << 8)

View File

@@ -5,12 +5,13 @@ replaces itself with a re-timed build through the host tool's
killing the simulated device mid-write, restarting it from its flash dump,
and letting a re-run complete the update.
The mega runs the BOOTRST-unprogrammed profile (reset boots the application;
the fixture application's 'L' jump is the application-owned loader entry),
with --assume-fuses standing in for the fuse read simavr cannot model. The
tinies reset into a loader at every phase by construction — the t13a because
its staging slot carries the reset vector itself, the t85 through the word-0
redirect the tool plants around the resident rewrite.
The boot-sectioned megas run the BOOTRST-unprogrammed profile (reset boots
the application; the fixture application's 'L' jump is the application-owned
loader entry), with --assume-fuses standing in for the fuse read simavr
cannot model. The patched-vector chips — the tinies and the m48s — reset
into a loader at every phase by construction: the t13a because its staging
slot carries the reset vector itself, the others through the word-0 redirect
the tool plants around the resident rewrite.
Usage: pbupdate.py <device_bin> <pureboot_elf> <update_elf> <mcu> <hz>
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
@@ -89,8 +90,14 @@ 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.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
# The m48s are megas without a boot section: patched vector, no fuse
# preflight, and the same reset-to-0 the tinies get.
patch = not mega or mcu.startswith("atmega48")
# Word-addressed (>64 KiB) chips use the 1 KiB slot; their loader base
# itself sits beyond the 16-bit byte space — the 644's base + slot only
# touches the 64 KiB boundary and stays byte-addressed.
slot = 1024 if base >= 0x10000 and mega else 512
reset_hex = "0" if mega else None # the boot-sectioned 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__)))
import pbsim
@@ -107,7 +114,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 = assumed_fuses(pb, images["v0"]) if mega else None
fuses = assumed_fuses(pb, images["v0"]) if mega and not patch else None
def connect(device):
port = pb.Port(device.pty, baud)
@@ -147,7 +154,7 @@ def main():
# A clean CLI update, resident -> v9.
args = ["--update-loader", os.path.join(workdir, "v9.bin"), "--state", state, "--stay"]
if mega:
if fuses:
args += ["--assume-fuses", fuses.hex()]
out = pbsim.run_tool(tool, device.pty, baud, *args)
if "loader updated" not in out:
@@ -172,7 +179,7 @@ def main():
# cost of that profile (README).
for kill_region, kill_hits, kill_device in (
("stage", 2, True),
("resident", 1, not mega),
("resident", 1, patch),
("stage_restore", 2, True),
):
device.reset() # the previous round left the application running
@@ -203,11 +210,11 @@ def main():
device.stop()
# Ground truth: the simulator's own flash against the final state, and
# on the tinies an independent decode of the reset routing.
# on the patched-vector chips an independent decode of the reset routing.
flash = open(dump, "rb").read()
if flash[base : base + slot] != padded(images[final]):
fail("ground-truth resident region does not match the final image")
if not mega:
if patch:
flash_words = (base + slot) // 2
word0 = flash[0] | (flash[1] << 8)
if rjmp_decode(word0, 0, flash_words) != base // 2:

View File

@@ -49,6 +49,13 @@ static volatile sig_atomic_t reset_requested;
// anywhere inside the page wipes half the neighbouring page in simulation
// only. Wrap the mega's registered flash ioctl and re-dispatch page erases
// with Z forced to the page boundary; everything else passes through.
//
// A second gap on the boot-section-less m48s: their RWWSRE bit is the
// temporary-buffer discard (Atmel-8271 §26.2/§26.3.1), but the stock model
// gates its RWWSRE branch on AVR_SELFPROG_HAVE_RWW — absent on the m48
// core — so the discard store falls through into the buffer-fill branch and
// plants whatever Z/R1:R0 happen to hold. Perform the silicon's discard
// here instead.
static avr_flash_t *mega_flash;
static int (*mega_flash_ioctl)(avr_io_t *io, uint32_t ctl, void *param);
@@ -64,6 +71,15 @@ static int fixed_flash_ioctl(avr_io_t *io, uint32_t ctl, void *param)
io->avr->data[31] = (uint8_t)(z >> 8);
return result;
}
if (ctl == AVR_IOCTL_FLASH_SPM && !(mega_flash->flags & AVR_SELFPROG_HAVE_RWW) &&
(io->avr->data[mega_flash->r_spm] & 0x11) == 0x11) { // RWWSRE|SELFPRGEN: the m48 buffer discard
for (int i = 0; i < mega_flash->spm_pagesize / 2; i++) {
mega_flash->tmppage[i] = 0xffff;
mega_flash->tmppage_used[i] = 0;
}
avr_regbit_clear(io->avr, mega_flash->selfprgen);
return 0;
}
return mega_flash_ioctl(io, ctl, param);
}
@@ -306,11 +322,13 @@ int main(int argc, char *argv[])
}
memcpy(avr->flash + base, fw.flash, fw.flashsize);
}
// The mega enters the loader in hardware (BOOTRST, not modeled — the
// argument picks the modeled fuse's target); the tinies reset to word 0
// like silicon — erased flash walks up into the loader, and after the
// host's surgery the patched vector routes there.
reset_pc = argc > 8 ? (uint32_t)strtoul(argv[8], NULL, 0) : (use_uart_pty ? base : 0);
// The boot-sectioned megas enter the loader in hardware (BOOTRST, not
// modeled — the argument picks the modeled fuse's target); the tinies
// and the boot-section-less m48s reset to word 0 like silicon — erased
// flash walks up into the loader, and after the host's surgery the
// patched vector routes there.
int boot_section = use_uart_pty && strncmp(mcu_name, "atmega48", 8) != 0;
reset_pc = argc > 8 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? base : 0);
avr->pc = reset_pc;
avr->codeend = avr->flashend;

View File

@@ -56,15 +56,26 @@ def main():
mega = info_of(pb, 0x7E00, 128, False, 0x8000, signature=(0x1E, 0x95, 0x0F))
# 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.
# index (EXTENDED on the x8 line except the m328s' HIGH, HIGH elsewhere)
# and the per-family ladders (Atmel-2486/2466/2503/2545/8271/DS40002065/
# 8272/8011/2593/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, 0x93, 0x0A), 0x2000, 2, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m88
((0x1E, 0x93, 0x0F), 0x2000, 2, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m88P
((0x1E, 0x94, 0x06), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168/168A
((0x1E, 0x94, 0x0B), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168P
((0x1E, 0x95, 0x14), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m328
((0x1E, 0x95, 0x0F), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m328P
((0x1E, 0x94, 0x0F), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m164A
((0x1E, 0x94, 0x0A), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m164P
((0x1E, 0x95, 0x15), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m324A
((0x1E, 0x96, 0x09), 0x10000, 3, {0b11: 0xFC00, 0b10: 0xF800, 0b01: 0xF000, 0b00: 0xE000}), # m644
((0x1E, 0x96, 0x0A), 0x10000, 3, {0b11: 0xFC00, 0b10: 0xF800, 0b01: 0xF000, 0b00: 0xE000}), # m644P
((0x1E, 0x97, 0x06), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284
((0x1E, 0x97, 0x05), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284P
)
for signature, flash, which, ladder in cases: