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:
@@ -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
|
||||||
|
|||||||
@@ -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
2
libavr
Submodule libavr updated: 911a87538f...082309a4cb
@@ -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)
|
||||||
|
|||||||
@@ -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) ×
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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 (1–4 speak
|
# map lives: the tool keeps a decoder for every generation in it (1–4 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 ""
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user