pureboot 7: the same features in fewer words on every chip

Four cuts, none touching what the loader can do. The entry stub stops
re-doing the reset logic's own SP write where the datasheet guarantees
RAMEND (stack::hardware — the classic megas keep theirs). The autobaud
unit moves into GPIOR2:GPIOR1 wherever the chip has the pair: one-word
accesses, no RAM object, and the host's measured-clock peek follows it
by version and geometry. 'J' rides the unified decode, carrying a
selector it ignores so its address is the same two reads as every other
command — the tool sends the bare form to older residents. run_app stops
insisting on a body of its own. The fleet lands at 358–410 B stock and
438–474 B autobaud; the tightest image in the space — the 1284s'
autobaud on a USART's own pins with the OSCCAL trim — drops from 510 to
484 of its 512. Every chip's suite is green on the wire that changed,
and the README's table is machine-checked against the built images.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 17:02:38 +02:00
parent a3ea099105
commit 8f6319c068
9 changed files with 164 additions and 103 deletions

View File

@@ -283,12 +283,14 @@ if(PROJECT_IS_TOP_LEVEL)
add_test(NAME pureboot_autobaud.size add_test(NAME pureboot_autobaud.size
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot_autobaud> COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot_autobaud>
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake) -DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
# The measured unit is the loader's only RAM object and sits at the very # The measured unit's home is wire contract, not layout accident: the
# start of SRAM — where the host reads the bit period from (--info's # host reads the bit period from it (--info's measured clock). In the
# measured clock), so the address is wire contract, not layout accident. # GPIOR home the image must carry no RAM copy at all; in the RAM home it
# is the loader's only RAM object, at the very start of SRAM.
add_test(NAME pureboot_autobaud.unit add_test(NAME pureboot_autobaud.unit
COMMAND ${CMAKE_COMMAND} -DOBJDUMP=${CMAKE_OBJDUMP} -DELF=$<TARGET_FILE:pureboot_autobaud> COMMAND ${CMAKE_COMMAND} -DOBJDUMP=${CMAKE_OBJDUMP} -DELF=$<TARGET_FILE:pureboot_autobaud>
-DRAM_START=${PUREBOOT_RAM_START} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_unit.cmake) -DRAM_START=${PUREBOOT_RAM_START} -DGPIOR=${PUREBOOT_UNIT_GPIOR}
-P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_unit.cmake)
# One point of the exhaustive matrix, named from its resolved parameters # One point of the exhaustive matrix, named from its resolved parameters
# so the enumeration cannot collide with itself. `pins` is empty for the # so the enumeration cannot collide with itself. `pins` is empty for the

View File

@@ -2,7 +2,7 @@
`master` carries `bootloader.atsln`, so this branch does too: `ide/bootloader.atsln` `master` carries `bootloader.atsln`, so this branch does too: `ide/bootloader.atsln`
builds the loaders from the same sources Ninja does, to a **byte-identical builds the loaders from the same sources Ninja does, to a **byte-identical
`.text`** — 400 B for the 328P pureboot loader, 510 B for the `tsb_asm` tier in `.text`** — 390 B for the 328P pureboot loader, 510 B for the `tsb_asm` tier in
its 512-byte section. CMake remains the build system; the solution is here so the its 512-byte section. CMake remains the build system; the solution is here so the
port opens in Studio as its predecessor did. port opens in Studio as its predecessor did.

2
libavr

Submodule libavr updated: 911a87538f...082309a4cb

View File

@@ -138,15 +138,24 @@ endif()
# Where SRAM begins: the classic megas keep it right after the plain I/O # Where SRAM begins: the classic megas keep it right after the plain I/O
# registers, the x8/x4 generations push it past their extended I/O file, and # registers, the x8/x4 generations push it past their extended I/O file, and
# the tinies match the classics. An autobaud loader's measured unit lives at # the tinies match the classics. An autobaud loader keeps its measured unit
# exactly this address (the host reads it there — pureboot.py), and the # in GPIOR2:GPIOR1 wherever the chip has the pair (data 0x32 on the
# unit-position test holds the layout to it. # t25/45/85, 0x4A from the x8 generation on) and as the first RAM object at
# SRAM start where it does not (the t13s and classic megas). The host reads
# whichever home applies (pureboot.py's geometry), and the unit-position
# test holds the image to the same split.
if(LIBAVR_MCU MATCHES "^atmega(8|16|32)a?$") if(LIBAVR_MCU MATCHES "^atmega(8|16|32)a?$")
set(_pb_ram 0x60) set(_pb_ram 0x60)
set(_pb_unit_gpior "")
elseif(LIBAVR_MCU MATCHES "^atmega") elseif(LIBAVR_MCU MATCHES "^atmega")
set(_pb_ram 0x100) set(_pb_ram 0x100)
set(_pb_unit_gpior 0x4A)
elseif(LIBAVR_MCU MATCHES "^attiny13")
set(_pb_ram 0x60)
set(_pb_unit_gpior "")
else() else()
set(_pb_ram 0x60) set(_pb_ram 0x60)
set(_pb_unit_gpior 0x32)
endif() endif()
# The function runs in its caller's scope, so everything it needs crosses # The function runs in its caller's scope, so everything it needs crosses
@@ -169,6 +178,7 @@ set(PUREBOOT_LIMIT ${_pb_limit} PARENT_SCOPE)
set(PUREBOOT_EEPROM ${_pb_eeprom} PARENT_SCOPE) set(PUREBOOT_EEPROM ${_pb_eeprom} PARENT_SCOPE)
set(PUREBOOT_DEFAULT_HZ ${_pb_hz} PARENT_SCOPE) set(PUREBOOT_DEFAULT_HZ ${_pb_hz} PARENT_SCOPE)
set(PUREBOOT_RAM_START ${_pb_ram} PARENT_SCOPE) set(PUREBOOT_RAM_START ${_pb_ram} PARENT_SCOPE)
set(PUREBOOT_UNIT_GPIOR "${_pb_unit_gpior}" PARENT_SCOPE)
set(PUREBOOT_HAS_USART ${_pb_has_usart} PARENT_SCOPE) set(PUREBOOT_HAS_USART ${_pb_has_usart} PARENT_SCOPE)
set(PUREBOOT_HAS_USART1 ${_pb_has_usart1} PARENT_SCOPE) set(PUREBOOT_HAS_USART1 ${_pb_has_usart1} PARENT_SCOPE)
set(PUREBOOT_SIM_MCU ${_pb_sim_mcu} PARENT_SCOPE) set(PUREBOOT_SIM_MCU ${_pb_sim_mcu} PARENT_SCOPE)

View File

@@ -28,31 +28,31 @@ it carries the calibration machinery and no clock at all.
| Chip | Flash | Loader at | Link | Stock | Autobaud | | Chip | Flash | Loader at | Link | Stock | Autobaud |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 390 B | 460 B | | ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 384 B | 452 B |
| ATtiny25 † | 2 KiB | 0x0600 | software | 394 B | 464 B | | ATtiny25 † | 2 KiB | 0x0600 | software | 388 B | 442 B |
| ATtiny45 † | 4 KiB | 0x0e00 | software | 398 B | 468 B | | ATtiny45 † | 4 KiB | 0x0e00 | software | 388 B | 442 B |
| ATtiny85 † | 8 KiB | 0x1e00 | software | 398 B | 468 B | | ATtiny85 † | 8 KiB | 0x1e00 | software | 388 B | 442 B |
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 360 B | 474 B | | ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 358 B | 470 B |
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 362 B | 480 B | | ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 360 B | 474 B |
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 362 B | 480 B | | ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 360 B | 474 B |
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 388 B | 464 B | | ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 378 B | 438 B |
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 398 B | 474 B | | ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 388 B | 448 B |
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 400 B | 480 B | | ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 390 B | 454 B |
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 400 B | 480 B | | ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 390 B | 454 B |
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 400 B | 480 B | | ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 390 B | 454 B |
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 400 B | 480 B | | ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 390 B | 454 B |
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 394 B | 474 B | | ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 384 B | 448 B |
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 420 B | 500 B | | ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 410 B | 474 B |
† No hardware boot section: the host patches the reset vector, and the budget † No hardware boot section: the host patches the reset vector, and the budget
is 510 bytes, since the slot's last word is the trampoline. is 510 bytes, since the slot's last word is the trampoline.
The tightest fit in the whole space is the 1284s' autobaud build deployed on a The tightest fit in the whole space is the 1284s' autobaud build deployed on a
USART's own pins with the `OSCCAL` trim baked, 510 of its 512 — they alone USART's own pins with the `OSCCAL` trim baked, 484 of its 512 — they alone
carry the far-flash machinery (ELPM reads, RAMPZ page commands), autobaud carry the far-flash machinery (ELPM reads, RAMPZ page commands), autobaud
alone carries the calibration loop, a bit-banged link on a USART's pins alone alone carries the calibration loop, a bit-banged link on a USART's pins alone
has to release it (below), and the trim adds its one register write. Without has to release it (below), and the trim adds its one register write. Without
the trim that build is 504; on the default pins, 500. The flash bank riding the trim that build is 478; on the default pins, 474. The flash bank riding
in a transfer's selector byte keeps even those chips' addressing the same in a transfer's selector byte keeps even those chips' addressing the same
16-bit form every other chip uses, which is why they are no longer the 16-bit form every other chip uses, which is why they are no longer the
outlier they were. outlier they were.
@@ -187,8 +187,10 @@ reply, repeat.
Addresses are **byte addresses within a 64 KiB bank**, and the bank rides in Addresses are **byte addresses within a 64 KiB bank**, and the bank rides in
the command's selector byte, so no command has to speak word addresses. `J` is the command's selector byte, so no command has to speak word addresses. `J` is
the exception: it takes a word address, because that is what the hardware's own the exception: its address is a word address, because that is what the
jump takes. EEPROM and data-space addresses and all counts are bytes. hardware's own jump takes — it still carries a selector byte (reserved,
ignored) so its decode is the same three reads as every other command's.
EEPROM and data-space addresses and all counts are bytes.
The loader trusts the host to keep addresses in range: it does not bound them The loader trusts the host to keep addresses in range: it does not bound them
against the chip. **Gotcha:** a write (or read) that runs past `E2END` wraps — against the chip. **Gotcha:** a write (or read) that runs past `E2END` wraps —
@@ -203,7 +205,7 @@ better spent on features than on re-checking a bound the host already holds.
| `G` | sel8, addr16, n8 | n bytes from the selected space (n = 0 means 256) | | `G` | sel8, addr16, n8 | n bytes from the selected space (n = 0 means 256) |
| `g` | sel8, addr16, n8, then n data bytes | `+` per byte, sent once its write has begun | | `g` | sel8, addr16, n8, then n data bytes | `+` per byte, sent once its write has begun |
| `W` | sel8, addr16, then one page of data | — (completion = next prompt) | | `W` | sel8, addr16, then one page of data | — (completion = next prompt) |
| `J` | word address (16-bit) | `+`, then execution continues there | | `J` | sel8 (reserved), word address (16-bit) | `+`, then execution continues there |
| other | — | ignored; the loop re-prompts (send a junk byte, await `+`, to resync) | | other | — | ignored; the loop re-prompts (send a junk byte, await `+`, to resync) |
`G` and `g` are one letter in two cases, which is the whole command set for `G` and `g` are one letter in two cases, which is the whole command set for
@@ -291,7 +293,12 @@ from `b`, and a command per memory (`R`/`W` flash, `r`/`w` EEPROM, `F` fuses).
above; the shipped tool speaks both, choosing on the version it reads, so a above; the shipped tool speaks both, choosing on the version it reads, so a
deployed pureboot 4 stays drivable and self-updatable to 5. **6** changes deployed pureboot 4 stays drivable and self-updatable to 5. **6** changes
nothing on the wire: it marks the builds that may carry a baked `OSCCAL` trim nothing on the wire: it marks the builds that may carry a baked `OSCCAL` trim
(Configuration), so a tool driving an update knows such images exist. (Configuration), so a tool driving an update knows such images exist. **7**
moves `J` onto the unified decode — it gains the selector byte the table
shows, which older loaders do not read, so the tool sends each form to the
version that speaks it — and re-homes the autobaud unit into the GPIOR pair
on the chips that have one (Session: what must not be written), which is
where `--info`'s measured clock now reads it on those parts.
Every closed generation is tagged in this repo at its era's last commit — the Every closed generation is tagged in this repo at its era's last commit — the
commit just before the next version bump, so a tag holds everything its commit just before the next version bump, so a tag holds everything its
@@ -476,11 +483,14 @@ Reads are safe anywhere; **two small regions cannot be written without ending th
session,** because they are what the loader is standing on: session,** because they are what the loader is standing on:
- the **top of SRAM**, where its stack lives — a handful of bytes below RAMEND; - the **top of SRAM**, where its stack lives — a handful of bytes below RAMEND;
- on an **autobaud** build, the **two bytes at RAMSTART**: the measured bit - on an **autobaud** build, the **measured bit period**: two bytes in
period, in `.noinit`, which is the whole of that loader's static RAM. Overwrite GPIOR2:GPIOR1 where the chip has the pair (data `0x32..0x33` on the
it and its next reply is timed against garbage. On an ATtiny13A that is t25/45/85, `0x4A..0x4B` from the x8 generation on — such a loader has *no*
`0x60..0x61`, and the symptom is a mangled prompt byte rather than any error — static RAM at all), and the two bytes at RAMSTART on the chips without one
the loader is fine, it simply is no longer speaking the agreed rate. (the t13s and classic megas), where they are the whole of the loader's
static RAM. Overwrite either home and the next reply is timed against
garbage — the symptom is a mangled prompt byte rather than any error; the
loader is fine, it simply is no longer speaking the agreed rate.
Both are self-inflicted rather than defects, and a reset clears them. Note also Both are self-inflicted rather than defects, and a reset clears them. Note also
that `--poke` can write OSCCAL, which does take effect — but a session can only that `--poke` can write OSCCAL, which does take effect — but a session can only
@@ -518,9 +528,10 @@ Per chip preset, `ctest` runs:
below sweeps. `pureboot*osccal*.size` add the `OSCCAL` trim on the stock below sweeps. `pureboot*osccal*.size` add the `OSCCAL` trim on the stock
shape and on the tightest image in the space (autobaud on a USART's own shape and on the tightest image in the space (autobaud on a USART's own
pins), holding both of the trim write's addressing encodings to the budget; pins), holding both of the trim write's addressing encodings to the budget;
- `pureboot_autobaud.unit` — the measured bit period is the loader's only RAM - `pureboot_autobaud.unit` — the measured bit period sits where `--info`
object and sits exactly at ram_start, where `--info` reads it: wire reads it (wire contract, not layout accident): in the GPIOR pair, with no
contract, not layout accident; RAM object at all, on the chips that have one; as the loader's only RAM
object at exactly ram_start elsewhere;
- `pbm_*.size` — with `PUREBOOT_FULL_MATRIX=1`, the exhaustive cross product - `pbm_*.size` — with `PUREBOOT_FULL_MATRIX=1`, the exhaustive cross product
replacing that compact matrix, on **every** chip: every plausible oscillator replacing that compact matrix, on **every** chip: every plausible oscillator
(the internal ones, the CKDIV8 floor, the plain and the UART crystals) × (the internal ones, the CKDIV8 floor, the plain and the UART crystals) ×

View File

@@ -75,7 +75,7 @@ static_assert(PUREBOOT_OSCCAL >= 0 && PUREBOOT_OSCCAL <= 0xff, "PUREBOOT_OSCCAL
// The loader's one identity number. The protocol carries none of its own — // The loader's one identity number. The protocol carries none of its own —
// a version implies it, and the host tool holds that map (README.md). // a version implies it, and the host tool holds that map (README.md).
constexpr std::uint8_t version = 6; constexpr std::uint8_t version = 7;
// The image's identity stamp, for the host tool rather than for the wire: an // The image's identity stamp, for the host tool rather than for the wire: an
// update image is a bare 512-byte slot, and without this nothing in it says // update image is a bare 512-byte slot, and without this nothing in it says
@@ -241,7 +241,10 @@ struct software_link {
// every rate. Activation differs in kind from the other two — there is no // every rate. Activation differs in kind from the other two — there is no
// clock to time a window against — so this backend brings its own, below. // clock to time a window against — so this backend brings its own, below.
struct autobaud_link { struct autobaud_link {
using uart = avr::uart::software_autobaud<avr::PUREBOOT_RX, avr::PUREBOOT_TX>; // The unit in GPIOR2:GPIOR1 where the chip has them: the loader owns the
// whole chip while it runs, and the pair is four instructions cheaper per
// session than a RAM word.
using uart = avr::uart::software_autobaud<avr::PUREBOOT_RX, avr::PUREBOOT_TX, avr::uart::unit_home::gpior>;
static void init() static void init()
{ {
@@ -288,7 +291,7 @@ extern "C" [[noreturn]] void pureboot_app();
__builtin_unreachable(); __builtin_unreachable();
} }
[[gnu::noinline, noreturn]] void run_app() [[noreturn]] void run_app()
{ {
jump(pureboot_app); jump(pureboot_app);
} }
@@ -488,12 +491,6 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
tx_ack(); tx_ack();
const std::uint8_t command = link::rx(); const std::uint8_t command = link::rx();
switch (command) { switch (command) {
case 'J': { // jump to a wire word address: hand-over and staging transfer
auto target = reinterpret_cast<void (*)()>(rx16());
tx_ack();
link::drain();
jump(target);
}
case 'b': // identity: the version, then the three signature bytes case 'b': // identity: the version, then the three signature bytes
// Straight out of the stamp, so the wire and the image can never // Straight out of the stamp, so the wire and the image can never
// disagree about what this loader is. The indices are constant and // disagree about what this loader is. The indices are constant and
@@ -502,18 +499,26 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
for (std::uint8_t at = stamp_identity; at != sizeof identity_stamp; ++at) for (std::uint8_t at = stamp_identity; at != sizeof identity_stamp; ++at)
link::tx(identity_stamp[at]); link::tx(identity_stamp[at]);
break; break;
case 'J': // jump: sel8 (reserved), addr16 as a wire word address
case 'W': // fill one flash page buffer: sel8, addr16, then page bytes case 'W': // fill one flash page buffer: sel8, addr16, then page bytes
case 'G': // read: sel8, addr16, n8 (0 = 256) case 'G': // read: sel8, addr16, n8 (0 = 256)
case 'g': { // write: sel8, addr16, n8, then n bytes, each acked case 'g': { // write: sel8, addr16, n8, then n bytes, each acked
// One decode, one cursor and one loop for every space and both // One decode, one cursor and one loop for every space, both
// directions: a command per memory would carry a copy of all three // directions and the jump: a command per memory would carry a copy
// each. 'W' joins the same decode rather than keeping an address // of all three each. 'J' — the hand-over and staging transfer —
// form of its own, so flash addressing is uniform across every // carries a selector it ignores so its address rides the same two
// command that names it. // reads as everything else; 'W' joins the same decode rather than
// keeping an address form of its own, so flash addressing is
// uniform across every command that names it.
const std::uint8_t selector = link::rx(); const std::uint8_t selector = link::rx();
const std::uint8_t space = space_of(selector); const std::uint8_t space = space_of(selector);
const std::uint8_t bank = bank_of(selector); const std::uint8_t bank = bank_of(selector);
std::uint16_t at = rx16(); std::uint16_t at = rx16();
if (command == 'J') {
tx_ack();
link::drain();
jump(reinterpret_cast<void (*)()>(at));
}
if (command == 'W') { if (command == 'W') {
fill_page(bank, at); fill_page(bank, at);
break; break;
@@ -540,4 +545,7 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
} // namespace } // namespace
} // namespace pureboot } // namespace pureboot
template struct avr::startup::entry<pureboot::run>; // stack::hardware: activation is reset-only, so the reset logic's own
// SP = RAMEND stands wherever the datasheet guarantees it (the classic
// megas still get the write); a 'J' entry runs on the caller's live stack.
template struct avr::startup::entry<pureboot::run, avr::startup::stack::hardware>;

View File

@@ -26,7 +26,7 @@ else:
import termios import termios
PROMPT = b"+" PROMPT = b"+"
VERSION = 6 # this tool's own version — free to drift from a loader's VERSION = 7 # this tool's own version — free to drift from a loader's
# The loader versions this tool can drive. A pureboot version implies its wire # The loader versions this tool can drive. A pureboot version implies its wire
# protocol, which carries no number of its own, so this window is where that # protocol, which carries no number of its own, so this window is where that
# map lives: the tool keeps a decoder for every generation in it (14 speak # map lives: the tool keeps a decoder for every generation in it (14 speak
@@ -34,7 +34,7 @@ VERSION = 6 # this tool's own version — free to drift from a loader's
# builds and changes nothing on the wire), and a version it has no decoder # builds and changes nothing on the wire), and a version it has no decoder
# for moves the floor. # for moves the floor.
OLDEST_LOADER = 1 OLDEST_LOADER = 1
NEWEST_LOADER = 6 NEWEST_LOADER = 7
SLOT = 512 # the loader slot, on every chip SLOT = 512 # the loader slot, on every chip
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
@@ -45,11 +45,13 @@ RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
UNIFIED_LOADER = 5 UNIFIED_LOADER = 5
SP_FLASH, SP_EEPROM, SP_RAM, SP_FUSE, SP_SPM = 0, 1, 2, 3, 4 SP_FLASH, SP_EEPROM, SP_RAM, SP_FUSE, SP_SPM = 0, 1, 2, 3, 4
# A v5+ autobaud loader keeps its measured bit period at ram_start, encoded # An autobaud loader keeps its measured bit period readable, encoded as
# as delay-loop counts: (bit cycles UNIT_DISCOUNT) / UNIT_LOOP_CYCLES, # delay-loop counts: (bit cycles UNIT_DISCOUNT) / UNIT_LOOP_CYCLES,
# floored — the spin granule and per-bit overhead of libavr's software UART. # floored — the spin granule and per-bit overhead of libavr's software UART.
# --info undoes the encoding to report the true clock, which therefore sits # v5/v6 keep it at ram_start; v7 moves it into GPIOR2:GPIOR1 on the chips
# within one granule below it. # that have the pair (their data addresses are in the geometry) and keeps
# ram_start only where they do not exist. --info undoes the encoding to
# report the true clock, which therefore sits within one granule below it.
UNIT_LOOP_CYCLES, UNIT_DISCOUNT = 4, 8 UNIT_LOOP_CYCLES, UNIT_DISCOUNT = 4, 8
# A selector's high nibble is the flash bank — the address bits above the 16-bit # A selector's high nibble is the flash bank — the address bits above the 16-bit
@@ -77,36 +79,40 @@ CALIBRATE = 0xC0
# from its chip database at build time). Die revisions that share a signature # from its chip database at build time). Die revisions that share a signature
# share this row, as they share the silicon. # share this row, as they share the silicon.
CHIP_GEOMETRY = { CHIP_GEOMETRY = {
# signature : (flash, page, eeprom, patch_vector, ram_start) # signature : (flash, page, eeprom, patch_vector, ram_start, gpior1)
# ram_start is where SRAM begins in data space: the classic megas and the # ram_start is where SRAM begins in data space: the classic megas and the
# tinies keep it right after the plain I/O registers (0x60), the x8/x4 # tinies keep it right after the plain I/O registers (0x60), the x8/x4
# generations past their extended I/O file (0x100). An autobaud loader's # generations past their extended I/O file (0x100). gpior1 is GPIOR1's
# measured bit period lives at exactly ram_start (its only RAM object; # data address — 0x32 on the t25/45/85, 0x4A from the x8 generation on,
# the loader's own build pins the layout), which is what --info reads. # None where the chip has no pair (t13, classic megas). A v7 autobaud
(0x1E, 0x90, 0x07): (1024, 32, 64, True, 0x60), # ATtiny13/13A # loader's measured bit period lives in GPIOR2:GPIOR1 where they exist
(0x1E, 0x91, 0x08): (2048, 32, 128, True, 0x60), # ATtiny25 # and at exactly ram_start elsewhere (its only RAM object; the loader's
(0x1E, 0x92, 0x06): (4096, 64, 256, True, 0x60), # ATtiny45 # own build pins the layout); v5/v6 always used ram_start. --info reads
(0x1E, 0x93, 0x0B): (8192, 64, 512, True, 0x60), # ATtiny85 # whichever home the answering version implies.
(0x1E, 0x92, 0x05): (4096, 64, 256, True, 0x100), # ATmega48/48A (0x1E, 0x90, 0x07): (1024, 32, 64, True, 0x60, None), # ATtiny13/13A
(0x1E, 0x92, 0x0A): (4096, 64, 256, True, 0x100), # ATmega48P/48PA (0x1E, 0x91, 0x08): (2048, 32, 128, True, 0x60, 0x32), # ATtiny25
(0x1E, 0x93, 0x07): (8192, 64, 512, False, 0x60), # ATmega8/8A (0x1E, 0x92, 0x06): (4096, 64, 256, True, 0x60, 0x32), # ATtiny45
(0x1E, 0x93, 0x0A): (8192, 64, 512, False, 0x100), # ATmega88/88A (0x1E, 0x93, 0x0B): (8192, 64, 512, True, 0x60, 0x32), # ATtiny85
(0x1E, 0x93, 0x0F): (8192, 64, 512, False, 0x100), # ATmega88P/88PA (0x1E, 0x92, 0x05): (4096, 64, 256, True, 0x100, 0x4A), # ATmega48/48A
(0x1E, 0x94, 0x03): (16384, 128, 512, False, 0x60), # ATmega16/16A (0x1E, 0x92, 0x0A): (4096, 64, 256, True, 0x100, 0x4A), # ATmega48P/48PA
(0x1E, 0x94, 0x06): (16384, 128, 512, False, 0x100), # ATmega168/168A (0x1E, 0x93, 0x07): (8192, 64, 512, False, 0x60, None), # ATmega8/8A
(0x1E, 0x94, 0x0B): (16384, 128, 512, False, 0x100), # ATmega168P/168PA (0x1E, 0x93, 0x0A): (8192, 64, 512, False, 0x100, 0x4A), # ATmega88/88A
(0x1E, 0x94, 0x0A): (16384, 128, 512, False, 0x100), # ATmega164P/164PA (0x1E, 0x93, 0x0F): (8192, 64, 512, False, 0x100, 0x4A), # ATmega88P/88PA
(0x1E, 0x94, 0x0F): (16384, 128, 512, False, 0x100), # ATmega164A (0x1E, 0x94, 0x03): (16384, 128, 512, False, 0x60, None), # ATmega16/16A
(0x1E, 0x95, 0x02): (32768, 128, 1024, False, 0x60), # ATmega32/32A (0x1E, 0x94, 0x06): (16384, 128, 512, False, 0x100, 0x4A), # ATmega168/168A
(0x1E, 0x95, 0x0F): (32768, 128, 1024, False, 0x100), # ATmega328P (0x1E, 0x94, 0x0B): (16384, 128, 512, False, 0x100, 0x4A), # ATmega168P/168PA
(0x1E, 0x95, 0x14): (32768, 128, 1024, False, 0x100), # ATmega328 (0x1E, 0x94, 0x0A): (16384, 128, 512, False, 0x100, 0x4A), # ATmega164P/164PA
(0x1E, 0x95, 0x08): (32768, 128, 1024, False, 0x100), # ATmega324P (0x1E, 0x94, 0x0F): (16384, 128, 512, False, 0x100, 0x4A), # ATmega164A
(0x1E, 0x95, 0x11): (32768, 128, 1024, False, 0x100), # ATmega324PA (0x1E, 0x95, 0x02): (32768, 128, 1024, False, 0x60, None), # ATmega32/32A
(0x1E, 0x95, 0x15): (32768, 128, 1024, False, 0x100), # ATmega324A (0x1E, 0x95, 0x0F): (32768, 128, 1024, False, 0x100, 0x4A), # ATmega328P
(0x1E, 0x96, 0x09): (65536, 256, 2048, False, 0x100), # ATmega644/644A (0x1E, 0x95, 0x14): (32768, 128, 1024, False, 0x100, 0x4A), # ATmega328
(0x1E, 0x96, 0x0A): (65536, 256, 2048, False, 0x100), # ATmega644P/644PA (0x1E, 0x95, 0x08): (32768, 128, 1024, False, 0x100, 0x4A), # ATmega324P
(0x1E, 0x97, 0x05): (131072, 256, 4096, False, 0x100),# ATmega1284P (0x1E, 0x95, 0x11): (32768, 128, 1024, False, 0x100, 0x4A), # ATmega324PA
(0x1E, 0x97, 0x06): (131072, 256, 4096, False, 0x100),# ATmega1284 (0x1E, 0x95, 0x15): (32768, 128, 1024, False, 0x100, 0x4A), # ATmega324A
(0x1E, 0x96, 0x09): (65536, 256, 2048, False, 0x100, 0x4A), # ATmega644/644A
(0x1E, 0x96, 0x0A): (65536, 256, 2048, False, 0x100, 0x4A), # ATmega644P/644PA
(0x1E, 0x97, 0x05): (131072, 256, 4096, False, 0x100, 0x4A),# ATmega1284P
(0x1E, 0x97, 0x06): (131072, 256, 4096, False, 0x100, 0x4A),# ATmega1284
} }
VERBOSE = False VERBOSE = False
@@ -450,7 +456,7 @@ class Info:
if geometry is None: if geometry is None:
sig = " ".join(f"{b:02x}" for b in signature) sig = " ".join(f"{b:02x}" for b in signature)
raise Error(f"unknown signature {sig} — this tool has no geometry for it") raise Error(f"unknown signature {sig} — this tool has no geometry for it")
flash, page, eeprom, patch, _ = geometry flash, page, eeprom, patch, _, _ = geometry
base = flash - SLOT base = flash - SLOT
word_flash = flash > 0x10000 word_flash = flash > 0x10000
wire_base = base // 2 if word_flash else base wire_base = base // 2 if word_flash else base
@@ -491,6 +497,11 @@ class Info:
# permits where from_identity refuses. # permits where from_identity refuses.
geometry = CHIP_GEOMETRY.get(tuple(self.signature)) geometry = CHIP_GEOMETRY.get(tuple(self.signature))
self.ram = geometry[4] if geometry else None self.ram = geometry[4] if geometry else None
# Where this loader keeps the measured bit period (None when a fixed
# signature row is missing): the GPIOR pair from v7 where the chip
# has one, ram_start before that and everywhere without the pair.
gpior1 = geometry[5] if geometry else None
self.unit_home = gpior1 if self.version >= 7 and gpior1 is not None else self.ram
def describe(self): def describe(self):
sig = " ".join(f"{b:02x}" for b in self.signature) sig = " ".join(f"{b:02x}" for b in self.signature)
@@ -745,8 +756,13 @@ class Loader:
return self._command(b"F", 4, 2.0) return self._command(b"F", 4, 2.0)
def jump(self, word_address): def jump(self, word_address):
"""The device acks, then execution continues at the word address.""" """The device acks, then execution continues at the word address.
self.port.write(bytes((ord("J"), word_address & 0xFF, word_address >> 8))) From v7 'J' rides the unified decode, so it carries a selector byte
the loader ignores; older loaders take the bare address."""
if self.info.version >= 7:
self.port.write(bytes((ord("J"), 0, word_address & 0xFF, word_address >> 8)))
else:
self.port.write(bytes((ord("J"), word_address & 0xFF, word_address >> 8)))
self._expect_prompt() self._expect_prompt()
def enter_copy(self, byte_address, wait, link=None): def enter_copy(self, byte_address, wait, link=None):
@@ -1541,14 +1557,14 @@ def main():
print("device:") print("device:")
for line in info.lines(): for line in info.lines():
print(f" {line}") print(f" {line}")
if args.autobaud and info.ram is not None: if args.autobaud and info.unit_home is not None:
# The whole of the loader's RAM is the measured bit period at # The measured bit period, from wherever this version keeps it
# ram_start; decoded and times the rate this session drives, # (unit_home); decoded and times the rate this session drives,
# that is the true clock — the number to hold an OSCCAL bake # that is the true clock — the number to hold an OSCCAL bake
# or a fixed-baud build against (README.md: deployment). The # or a fixed-baud build against (README.md: deployment). The
# autobaud identity path refuses unknown signatures, so ram is # autobaud identity path refuses unknown signatures, so the
# always known here; the guard states that dependency. # home is always known here; the guard states that dependency.
unit = int.from_bytes(loader.read_ram(info.ram, 2), "little") unit = int.from_bytes(loader.read_ram(info.unit_home, 2), "little")
cycles = unit * UNIT_LOOP_CYCLES + UNIT_DISCOUNT cycles = unit * UNIT_LOOP_CYCLES + UNIT_DISCOUNT
clock = cycles * args.baud clock = cycles * args.baud
offset = f", {(clock / args.clock - 1) * 100:+.1f} % of {args.clock}" if args.clock else "" offset = f", {(clock / args.clock - 1) * 100:+.1f} % of {args.clock}" if args.clock else ""

View File

@@ -1,7 +1,10 @@
# Asserts the autobaud loader's measured unit is the first RAM object: the # Asserts the autobaud loader's measured unit sits where the host will read
# host tool reads the bit period from ram_start (--info's measured clock), so # it (--info's measured clock — the address is wire contract). Two homes: on
# the unit's address is wire contract. Run as # a chip with the GPIOR pair the unit lives there and the image must carry no
# cmake -DOBJDUMP=... -DELF=... -DRAM_START=<data address> -P check_unit.cmake # RAM word for it at all; elsewhere it is the first RAM object at SRAM start.
# Run as
# cmake -DOBJDUMP=... -DELF=... -DRAM_START=<data address> [-DGPIOR=<data address>]
# -P check_unit.cmake
execute_process(COMMAND ${OBJDUMP} -t ${ELF} OUTPUT_VARIABLE _syms RESULT_VARIABLE _res) execute_process(COMMAND ${OBJDUMP} -t ${ELF} OUTPUT_VARIABLE _syms RESULT_VARIABLE _res)
if(NOT _res EQUAL 0) if(NOT _res EQUAL 0)
@@ -9,7 +12,17 @@ if(NOT _res EQUAL 0)
endif() endif()
# The symbol line: "00800100 l O .noinit 00000002 <mangled>unit_E". # The symbol line: "00800100 l O .noinit 00000002 <mangled>unit_E".
string(REGEX MATCH "\n0*([0-9a-f]+)[^\n]+[ \t][^ \t\n]*unit_[^ \t\n]*\n" _line "${_syms}") string(REGEX MATCH "\n0*([0-9a-f]+)[^\n]+[ \t][^ \t\n]*unit_E\n" _line "${_syms}")
if(GPIOR)
if(_line)
message(FATAL_ERROR "unit_ RAM symbol present although the unit's home is GPIOR ${GPIOR} — "
"the host peeks the pair, and a RAM copy would be dead weight")
endif()
message(STATUS "no unit_ RAM object — the unit lives in the GPIOR pair at ${GPIOR}")
return()
endif()
if(NOT _line) if(NOT _line)
message(FATAL_ERROR "no unit_ symbol in ${ELF} — is this the autobaud loader?") message(FATAL_ERROR "no unit_ symbol in ${ELF} — is this the autobaud loader?")
endif() endif()

View File

@@ -64,8 +64,9 @@ def main():
for needed in ("version", "signature", "fuses", "verify:", "stays"): for needed in ("version", "signature", "fuses", "verify:", "stays"):
if needed not in out: if needed not in out:
fail(f"{label}: session output lacks {needed!r}\n{out}") fail(f"{label}: session output lacks {needed!r}\n{out}")
# The measured clock, decoded from the unit at ram_start. The # The measured clock, decoded from the unit at whichever home this
# runner's clock is exact, so the figure must land inside the # version keeps it in. The runner's clock is exact, so the figure
# must land inside the
# encoding's own envelope: the loader floors the bit period to # encoding's own envelope: the loader floors the bit period to
# 4-cycle spin granules after an 8-cycle discount, and the edge # 4-cycle spin granules after an 8-cycle discount, and the edge
# poll can shave a few cycles more — one granule of slack below # poll can shave a few cycles more — one granule of slack below