pureboot: a 512-byte slot on every chip, the 1284s included
The word-addressed 1284s were the one family deploying in a 1 KiB slot, because the far-flash machinery (ELPM reads, RAMPZ page commands, a word-addressed wire) did not fit 512 B. It does now: 478 B stock, 494 B in the heaviest configuration the build can produce. They take the 644s' geometry, where the smallest boot section holds the resident slot and its staging slot together. Most of the saving is one restructure. The info block and a flash read are the same act, so giving all four streamed commands one address-and-count path leaves exactly one call site for the flash streamer: it inlines into the never-returning command loop and its 24-bit cursor stops being saved and restored around every transmit. Around it, the ack byte moved out of line, the wire's byte pair is bit_cast into the word it already is, the fuse loop ends on its count, the info block's in-slot offset is taken as the one-byte relocation it is, and -fno-expensive-optimizations gives way to -fno-move-loop-invariants -fno-tree-ter. Every chip shrank 14-18 B. The size matrix grew the axes it was missing: the USART1 instance across the whole clock ladder, and the shape a slow baud gives a software UART — past 255 delay iterations libavr takes the 16-bit delay loop, which the ladder default never selects and which was 4 B over the 1284's slot the first time it was built. The protocol fixture stopped deriving the loader entry from the flash size; on the 1284s it had been jumping a slot low and reaching the loader only because erased flash walked it up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -231,12 +231,13 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
# The size matrix: every configuration axis that could move the image
|
# The size matrix: every configuration axis that could move the image
|
||||||
# size — the serial backend (different code), the clock and its ladder
|
# size — the serial backend (different code), the USART instance
|
||||||
# baud (different constants and divisor shapes), the USART instance
|
# (different registers), the clock (different constants), and the baud
|
||||||
# (different register class) — each combination must still fit the
|
# through the two shapes its bit timing takes — each combination must
|
||||||
# chip's slot budget. Pins are size-neutral (port and bit are immediate
|
# still fit the chip's slot budget. Pins are size-neutral (port and bit
|
||||||
# operands) and the timeout is a constant, so neither adds an axis. The
|
# are immediate operands) and the timeout is a constant, so neither adds
|
||||||
# stock build is one point of this matrix and already has its test.
|
# an axis. The stock build is one point of this matrix and already has
|
||||||
|
# its test.
|
||||||
function(pureboot_size_variant name)
|
function(pureboot_size_variant name)
|
||||||
pureboot_add_loader(${name} ${ARGN})
|
pureboot_add_loader(${name} ${ARGN})
|
||||||
add_test(NAME ${name}.size
|
add_test(NAME ${name}.size
|
||||||
@@ -260,11 +261,26 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
if(PUREBOOT_HAS_USART AND NOT _matrix_hz EQUAL _pb_stock_hz)
|
if(PUREBOOT_HAS_USART AND NOT _matrix_hz EQUAL _pb_stock_hz)
|
||||||
pureboot_size_variant(pureboot_hw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL hardware)
|
pureboot_size_variant(pureboot_hw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL hardware)
|
||||||
endif()
|
endif()
|
||||||
|
if(PUREBOOT_HAS_USART1 AND NOT _matrix_hz EQUAL _pb_stock_hz)
|
||||||
|
pureboot_size_variant(pureboot_usart1_${_matrix_khz}k CLOCK ${_matrix_hz} USART 1)
|
||||||
|
endif()
|
||||||
endforeach()
|
endforeach()
|
||||||
if(PUREBOOT_HAS_USART1)
|
if(PUREBOOT_HAS_USART1)
|
||||||
pureboot_size_variant(pureboot_usart1 USART 1)
|
pureboot_size_variant(pureboot_usart1 USART 1)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# The baud axis, whose one size-bearing shape the ladder never picks: a
|
||||||
|
# software UART spins out each bit with _delay_loop_1 while the count
|
||||||
|
# fits a byte and with the 16-bit _delay_loop_2 beyond it, two words more
|
||||||
|
# setup at every one of its five sites — the largest image the
|
||||||
|
# configuration space produces. The ladder default takes the *fastest*
|
||||||
|
# rate a clock reaches, which always lands in the byte, so the wide form
|
||||||
|
# needs the slowest ladder rate against the fastest clock to appear. The
|
||||||
|
# hardware USART has no such shape: its baud is a divisor constant, and
|
||||||
|
# the ladder's U2X solutions are already its larger form.
|
||||||
|
list(GET _matrix_clocks -1 _matrix_top_hz)
|
||||||
|
pureboot_size_variant(pureboot_sw_wide CLOCK ${_matrix_top_hz} BAUD 9600 SERIAL software)
|
||||||
|
|
||||||
# One configured deployment end to end — a real board's shape rather
|
# One configured deployment end to end — a real board's shape rather
|
||||||
# than the stock assumption: the ATmega328P on its shipped 1 MHz fuses,
|
# than the stock assumption: the ATmega328P on its shipped 1 MHz fuses,
|
||||||
# the software UART on hand-picked pins (TX = PB1, RX = PB5), the ladder
|
# the software UART on hand-picked pins (TX = PB1, RX = PB5), the ladder
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
# software-UART cycle-floor static asserts re-check whatever is passed.
|
# software-UART cycle-floor static asserts re-check whatever is passed.
|
||||||
|
|
||||||
# Per-family geometry: flash/page/EEPROM sizes and the linker wrap the PC
|
# Per-family geometry: flash/page/EEPROM sizes and the linker wrap the PC
|
||||||
# modulo needs, the loader slot (each chip's smallest boot sector — 1 KiB on
|
# modulo needs, plus the deployment defaults (crystal assumption on the
|
||||||
# the word-addressed 1284s), and the deployment defaults (crystal assumption
|
# megas, calibrated RC on the tinies). The loader slot is 512 bytes on every
|
||||||
# on the megas, calibrated RC on the tinies). The USART flags mirror the
|
# chip. The USART flags mirror the hardware inventory the loader's own static
|
||||||
# hardware inventory the loader's own static asserts check (the plain 644 is
|
# asserts check (the plain 644 is the x4 family's one single-USART die,
|
||||||
# the x4 family's one single-USART die, Atmel-2593).
|
# Atmel-2593).
|
||||||
set(_pb_has_usart 1)
|
set(_pb_has_usart 1)
|
||||||
set(_pb_has_usart1 0)
|
set(_pb_has_usart1 0)
|
||||||
if(LIBAVR_MCU MATCHES "^attiny13a?$")
|
if(LIBAVR_MCU MATCHES "^attiny13a?$")
|
||||||
@@ -106,31 +106,25 @@ elseif(LIBAVR_MCU MATCHES "^atmega644(a|p|pa)?$")
|
|||||||
elseif(LIBAVR_MCU MATCHES "^atmega1284p?$")
|
elseif(LIBAVR_MCU MATCHES "^atmega1284p?$")
|
||||||
# 128 KiB: wire flash addresses are word addresses, reads go through
|
# 128 KiB: wire flash addresses are word addresses, reads go through
|
||||||
# ELPM, and the PC's modulo wrap exceeds what --pmem-wrap-around models.
|
# 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
|
# Its smallest boot section (512 words = 1 KiB) holds the loader and its
|
||||||
# machinery cannot fit 512 B (see README.md).
|
# staging slot together, the 644's geometry (see README.md).
|
||||||
set(_pb_flash 131072)
|
set(_pb_flash 131072)
|
||||||
set(_pb_wrap "")
|
set(_pb_wrap "")
|
||||||
set(_pb_page 256)
|
set(_pb_page 256)
|
||||||
set(_pb_hz 16000000)
|
set(_pb_hz 16000000)
|
||||||
set(_pb_eeprom 4096)
|
set(_pb_eeprom 4096)
|
||||||
set(_pb_slot 1024)
|
|
||||||
set(_pb_limit 1024)
|
|
||||||
set(_pb_has_usart1 1)
|
set(_pb_has_usart1 1)
|
||||||
else()
|
else()
|
||||||
message(FATAL_ERROR "pureboot: no geometry for ${LIBAVR_MCU}")
|
message(FATAL_ERROR "pureboot: no geometry for ${LIBAVR_MCU}")
|
||||||
endif()
|
endif()
|
||||||
if(NOT DEFINED _pb_slot)
|
set(_pb_slot 512)
|
||||||
set(_pb_slot 512)
|
|
||||||
endif()
|
|
||||||
math(EXPR _pb_base "${_pb_flash} - ${_pb_slot}")
|
math(EXPR _pb_base "${_pb_flash} - ${_pb_slot}")
|
||||||
math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL)
|
math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL)
|
||||||
# Patched-vector chips hand over through the trampoline word below the slot,
|
# 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.
|
# 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")
|
if(LIBAVR_MCU MATCHES "^atmega" AND NOT LIBAVR_MCU MATCHES "^atmega48")
|
||||||
set(_pb_app 0)
|
set(_pb_app 0)
|
||||||
if(NOT DEFINED _pb_limit)
|
|
||||||
set(_pb_limit ${_pb_slot})
|
set(_pb_limit ${_pb_slot})
|
||||||
endif()
|
|
||||||
else()
|
else()
|
||||||
math(EXPR _pb_app "${_pb_base} - 2")
|
math(EXPR _pb_app "${_pb_base} - 2")
|
||||||
math(EXPR _pb_limit "${_pb_slot} - 2")
|
math(EXPR _pb_limit "${_pb_slot} - 2")
|
||||||
@@ -294,15 +288,17 @@ function(pureboot_add_loader name)
|
|||||||
add_executable(${name} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/pureboot.cpp)
|
add_executable(${name} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/pureboot.cpp)
|
||||||
target_link_libraries(${name} PRIVATE libavr)
|
target_link_libraries(${name} PRIVATE libavr)
|
||||||
target_compile_definitions(${name} PRIVATE ${_defines})
|
target_compile_definitions(${name} PRIVATE ${_defines})
|
||||||
# Codegen shaping for the loader TU only, worth ~40 B on every chip and
|
# Codegen shaping for the loader TU only, worth 14–36 B depending on the
|
||||||
# what carries the far-flash 1284 build under 512. At -Os GCC otherwise
|
# chip. At -Os GCC otherwise rewrites the byte-stream loops' counters into
|
||||||
# rewrites the byte-stream loops' counters into end-pointer forms that
|
# end-pointer forms that cost registers (-fno-ivopts,
|
||||||
# cost registers (-fno-ivopts, -fno-split-wide-types), leaves register
|
# -fno-split-wide-types), leaves register pressure on the table with the
|
||||||
# pressure on the table with the default allocator
|
# default allocator (-fira-algorithm=priority), and keeps loop-invariant
|
||||||
# (-fira-algorithm=priority), and spends bytes on rewrites a
|
# immediates and expression temporaries in registers
|
||||||
# straight-line loader gains nothing from.
|
# (-fno-move-loop-invariants, -fno-tree-ter) — but every loop body here
|
||||||
|
# contains a call, so a register held across it costs more than the
|
||||||
|
# load-immediate it saves.
|
||||||
target_compile_options(${name} PRIVATE
|
target_compile_options(${name} PRIVATE
|
||||||
-fno-ivopts -fira-algorithm=priority -fno-expensive-optimizations -fno-split-wide-types)
|
-fno-ivopts -fira-algorithm=priority -fno-move-loop-invariants -fno-tree-ter -fno-split-wide-types)
|
||||||
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${_base_hex}
|
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${_base_hex}
|
||||||
-Wl,--defsym=pureboot_app=${_app} ${_wrap})
|
-Wl,--defsym=pureboot_app=${_app} ${_wrap})
|
||||||
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
|
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
|
||||||
|
|||||||
@@ -3,26 +3,19 @@
|
|||||||
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by
|
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by
|
||||||
constraint: one C++ source, no inline assembly, no global register variables
|
constraint: one C++ source, no inline assembly, no global register variables
|
||||||
(attributes and compiler flags allowed), built for **every chip libavr
|
(attributes and compiler flags allowed), built for **every chip libavr
|
||||||
targets — all 37 — in 512 bytes each**: 434 B on the tiny13s, 438–442 B on
|
targets — all 37 — in 512 bytes each**: 416 B on the tiny13s, 420–424 B on
|
||||||
the tiny25/45/85, 412–452 B across the megas, and 506 B on the
|
the tiny25/45/85, 396–438 B across the megas, and 478 B on the
|
||||||
ATmega1284/1284P, whose far-flash machinery (ELPM reads, RAMPZ page commands,
|
ATmega1284/1284P, whose far-flash machinery (ELPM reads, RAMPZ page commands,
|
||||||
word-addressed wire) is the heaviest. Those are the stock deployments;
|
word-addressed wire) is the heaviest. Those are the stock deployments; the
|
||||||
choosing the software UART where the chip has a USART costs 8–46 B more (a
|
rest of the configuration space costs a little more, and the dearest point
|
||||||
bit-bang against a peripheral), which every chip still absorbs inside its
|
is a software UART at a slow baud — 494 B on the 1284s, the tightest image
|
||||||
slot — on the 1284s that means their 1 KiB boot sector, where the
|
in the matrix at 18 B spare. Keeping the far-flash build inside 512 is what
|
||||||
software-serial image lands at 546 B. Bringing the 1284's default build
|
the single call site the streamed commands share (`pureboot.cpp`) and the
|
||||||
under 512 at all is what the loop-placement attributes on the byte streamers
|
codegen flags on the loader TU (`CMakeLists.txt`) are for. Clock, baud,
|
||||||
(`pureboot.cpp`) and the codegen flags on the loader TU (`CMakeLists.txt`)
|
serial backend and pins are per-build configuration (below); the size matrix
|
||||||
are for; measured against each chip's own budget the tightest is the
|
in the test suite holds every combination inside its slot. The device speaks
|
||||||
ATmega328P, 50 B spare. Clock, baud, serial backend and
|
primitives; every composite — verify, erase, reset-vector surgery, updating
|
||||||
pins are per-build configuration (below); the size matrix in the test suite
|
the loader itself — lives in the host tool (`pureboot.py`).
|
||||||
holds every combination inside its slot. The device speaks primitives; every
|
|
||||||
composite — verify, erase, reset-vector surgery, updating the loader itself —
|
|
||||||
lives in the host tool (`pureboot.py`).
|
|
||||||
|
|
||||||
The 1284s still *deploy* in a 1 KiB slot, their smallest boot sector being
|
|
||||||
512 words; at 506 B the image would also fit the 644's
|
|
||||||
two-512-byte-slots-per-boot-sector geometry.
|
|
||||||
|
|
||||||
The image is **position-independent**: control flow is PC-relative, the
|
The image is **position-independent**: control flow is PC-relative, the
|
||||||
read/write paths take wire addresses, the write guard protects the slot the
|
read/write paths take wire addresses, the write guard protects the slot the
|
||||||
@@ -31,9 +24,8 @@ addressed from that same anchor, and the application jump is an indirect
|
|||||||
call to an absolute entry. The identical binary therefore runs from any
|
call to an absolute entry. The identical binary therefore runs from any
|
||||||
slot with every command intact — which makes pureboot **its own staging
|
slot with every command intact — which makes pureboot **its own staging
|
||||||
loader**: the host installs the same binary one slot below the resident,
|
loader**: the host installs the same binary one slot below the resident,
|
||||||
jumps into it, and lets it rewrite the resident. The slot is 512 bytes
|
jumps into it, and lets it rewrite the resident. The slot is 512 bytes on
|
||||||
(1 KiB on the word-addressed large chips, matching their boot-sector
|
every chip; on the tinies the budget is 510, not 512: a slot's last word
|
||||||
minimum); on the tinies the budget is 510, not 512: a slot's last word
|
|
||||||
belongs to the host-managed trampoline (below).
|
belongs to the host-managed trampoline (below).
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@@ -215,23 +207,20 @@ a newer one). `pureboot.rehome` is the acceptance test for both
|
|||||||
positions. Flashing the application afterwards overwrites the stale copy,
|
positions. Flashing the application afterwards overwrites the stale copy,
|
||||||
vector surgery included.
|
vector surgery included.
|
||||||
|
|
||||||
**Boot-sectioned megas**: program the loader at `flash − slot` with an
|
**Boot-sectioned megas**: program the loader at `flash − 512` with an
|
||||||
external programmer. Every such mega has a BOOTSZ step whose boot section
|
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
|
is exactly the 512-byte slot — the second-smallest step on the 8 KiB and
|
||||||
and 16 KiB chips (m8, m88, m16, m168, m164), the smallest on the 32 KiB
|
16 KiB chips (m8, m88, m16, m168, m164), the smallest on the 32 KiB ones
|
||||||
ones (m32, m328, m324); on the 1284s that step is the smallest, 512 words,
|
(m32, m328, m324) — so the ATmega328P profiles below apply to every one of
|
||||||
which is why their slot is 1 KiB — so the ATmega328P profiles below apply
|
them with its own addresses; the per-chip BOOTSZ ladders live in the host
|
||||||
to every one of them with its own addresses and slot size; the per-chip
|
tool (`BOOT_FUSE`).
|
||||||
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
|
The **644s and 1284s** are the geometry's sweet spot: their smallest boot
|
||||||
(512 words = 1 KiB) is exactly *two* 512-byte slots, so the resident and
|
section (512 words = 1 KiB) is exactly *two* 512-byte slots, so the
|
||||||
its staging slot both live inside the minimum section — self-update needs
|
resident and its staging slot both live inside the minimum section —
|
||||||
no fuse step up, and the standalone profile does not exist (reset lands at
|
self-update needs no fuse step up, and the standalone profile does not
|
||||||
0xfc00, one erased slot below the loader: the loader-first walk built in).
|
exist (reset lands one erased slot below the loader — 0xfc00 on the 644s,
|
||||||
|
0x1fc00 on the 1284s: the loader-first walk built in).
|
||||||
|
|
||||||
ATmega328P profiles (addresses for its 32 KiB):
|
ATmega328P profiles (addresses for its 32 KiB):
|
||||||
|
|
||||||
@@ -344,11 +333,16 @@ the reflect-mode builds of libavr's spot set; `tools/make_presets.py`
|
|||||||
regenerates the presets). Per chip preset, `ctest` runs:
|
regenerates the presets). Per chip preset, `ctest` runs:
|
||||||
|
|
||||||
- `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget;
|
- `pureboot.size` — the 510-byte (tinies) / 512-byte (mega) budget;
|
||||||
- `pureboot_*.size` — the size matrix: the serial backends × the clock
|
- `pureboot_*.size` — the size matrix: every configuration axis that could
|
||||||
ladder (1/8/16 MHz; the t13s' own RC menu), plus the USART1 build on the
|
move the image, each variant against the same slot budget. The serial
|
||||||
x4 chips — every configuration axis that could move the image, each
|
backends × the clock ladder (1/8/16 MHz; the t13s' own RC menu), the
|
||||||
variant against the same slot budget (pins are immediate operands and the
|
USART1 instance across that same ladder on the x4 chips, and
|
||||||
timeout is a constant: size-neutral);
|
`pureboot_sw_wide` — the slowest ladder rate at the fastest clock, where
|
||||||
|
a software UART's per-bit spin outgrows its one-register delay loop and
|
||||||
|
takes the 16-bit one, the largest image the configuration space produces
|
||||||
|
and a shape the ladder default (always the *fastest* rate a clock
|
||||||
|
reaches) never picks. Pins are immediate operands and the timeout is a
|
||||||
|
constant: neither is an axis;
|
||||||
- `pureboot.custom` (328P) — the configured-deployment acceptance test: the
|
- `pureboot.custom` (328P) — the configured-deployment acceptance test: the
|
||||||
1 MHz software-serial TX=PB1/RX=PB5 build from the configuration example
|
1 MHz software-serial TX=PB1/RX=PB5 build from the configuration example
|
||||||
drives the full protocol suite through the runner's GPIO bridge, fixture
|
drives the full protocol suite through the runner's GPIO bridge, fixture
|
||||||
|
|||||||
@@ -59,30 +59,26 @@ consteval std::int16_t wdrf_field()
|
|||||||
return avr::hw::db.field_index(reg, "WDRF");
|
return avr::hw::db.field_index(reg, "WDRF");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Geometry: the resident loader owns the top slot of flash — 512 bytes,
|
// Geometry: the resident loader owns the top 512 bytes of flash, and the
|
||||||
// except on the >64 KiB chips whose own smallest boot sector is 1 KiB (the
|
// slot below it is where a staging copy goes. The word below the slot is
|
||||||
// 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
|
// the trampoline (the application's relocated reset vector) on chips
|
||||||
// without a hardware boot section — the tinies and the m48s, whose SPM
|
// 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 from anywhere (Atmel-8271 §26). A boot section also means the CPU
|
||||||
// runs on while the RWW section programs; everywhere else it halts through
|
// runs on while the RWW section programs; everywhere else it halts through
|
||||||
// the operation.
|
// the operation.
|
||||||
constexpr std::uint16_t slot_bytes = spm::flash_bytes > 65536 ? 1024 : 512;
|
constexpr std::uint16_t slot_bytes = 512;
|
||||||
constexpr std::uint32_t base = spm::flash_bytes - slot_bytes;
|
constexpr std::uint32_t base = spm::flash_bytes - slot_bytes;
|
||||||
constexpr std::uint16_t page = spm::page_bytes;
|
constexpr std::uint16_t page = spm::page_bytes;
|
||||||
constexpr bool boot_section = avr::hw::curated::has_boot_section();
|
constexpr bool boot_section = avr::hw::curated::has_boot_section();
|
||||||
|
|
||||||
// Past 64 KiB a byte address no longer fits the wire's 16 bits, so on the
|
// 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 —
|
// large chips every flash address on the wire — and all slot arithmetic —
|
||||||
// is a word address instead ('J' always was one). A slot spans the same
|
// is a word address instead ('J' always was one). In those units the slot
|
||||||
// wire-high-byte pair in either unit (512 B = 2 x 256 bytes, 1 KiB =
|
// is 256 words: one value of a wire address's high byte, where a
|
||||||
// 2 x 256 words), so the slot index is the high byte with its low bit
|
// byte-addressed chip's 512 bytes span two.
|
||||||
// dropped everywhere.
|
|
||||||
constexpr bool word_flash = spm::flash_bytes > 65536;
|
constexpr bool word_flash = spm::flash_bytes > 65536;
|
||||||
constexpr std::uint16_t wire_base =
|
constexpr std::uint16_t wire_base =
|
||||||
word_flash ? static_cast<std::uint16_t>(base / 2) : static_cast<std::uint16_t>(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
|
// The activation window, in seconds, is a compile-time constant (the build
|
||||||
// may override it): the whole EEPROM belongs to the application, and
|
// may override it): the whole EEPROM belongs to the application, and
|
||||||
@@ -279,13 +275,20 @@ std::uint8_t rx_deadline()
|
|||||||
return static_cast<std::uint16_t>(low | (link::rx() << 8));
|
return static_cast<std::uint16_t>(low | (link::rx() << 8));
|
||||||
}
|
}
|
||||||
|
|
||||||
// The streamers take the count in the wire's 8-bit form: 0 means 256.
|
// The wire's little-endian byte pair as the word it is: AVR is little-endian
|
||||||
//
|
// too, so the pair already *is* the value's storage and the cast is the
|
||||||
// Two functions, because they want opposite placement and placement is an
|
// identity that a shift-and-or spelling makes the compiler rediscover. Callers
|
||||||
// attribute: the byte-addressed loop is small enough to inline into both
|
// read the bytes into named variables first — the wire order is a sequence of
|
||||||
// callers, the word-addressed one stays out of line but flattened — a call to
|
// reads, never an argument order.
|
||||||
// the transmit inside it would strand the 24-bit cursor in callee-saved
|
[[gnu::always_inline]] inline std::uint16_t word_of(std::array<std::uint8_t, 2> pair)
|
||||||
// registers. `word_flash` picks at the call site.
|
{
|
||||||
|
return std::bit_cast<std::uint16_t>(pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The streamers take the count in the wire's 8-bit form: 0 means 256. Two
|
||||||
|
// functions, one per addressing mode, both folded into the single command
|
||||||
|
// that streams flash — where the far one's 24-bit cursor is free to sit in
|
||||||
|
// the command loop's own call-saved registers.
|
||||||
[[maybe_unused, gnu::always_inline]] inline void send_flash_near(std::uint16_t address, std::uint8_t count)
|
[[maybe_unused, gnu::always_inline]] inline void send_flash_near(std::uint16_t address, std::uint8_t count)
|
||||||
{
|
{
|
||||||
do
|
do
|
||||||
@@ -296,7 +299,7 @@ std::uint8_t rx_deadline()
|
|||||||
// The 24-bit cursor as the machine holds it: the RAMPZ byte and a 16-bit Z,
|
// The 24-bit cursor as the machine holds it: the RAMPZ byte and a 16-bit Z,
|
||||||
// carried explicitly (the reassembled 32-bit address folds away inside the
|
// carried explicitly (the reassembled 32-bit address folds away inside the
|
||||||
// inlined far load).
|
// inlined far load).
|
||||||
[[maybe_unused, gnu::flatten, gnu::noinline]] void send_flash_far(std::uint16_t address, std::uint8_t count)
|
[[maybe_unused, gnu::always_inline]] inline void send_flash_far(std::uint16_t address, std::uint8_t count)
|
||||||
{
|
{
|
||||||
std::uint8_t rampz = static_cast<std::uint8_t>(address >> 15);
|
std::uint8_t rampz = static_cast<std::uint8_t>(address >> 15);
|
||||||
std::uint16_t z = static_cast<std::uint16_t>(address << 1);
|
std::uint16_t z = static_cast<std::uint16_t>(address << 1);
|
||||||
@@ -317,6 +320,13 @@ std::uint8_t rx_deadline()
|
|||||||
send_flash_near(address, count);
|
send_flash_near(address, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The prompt, which is also every ack: out of line because three sites send
|
||||||
|
// it, and a call is shorter than each carrying its own load-immediate.
|
||||||
|
[[gnu::noinline]] void tx_ack()
|
||||||
|
{
|
||||||
|
link::tx(ack);
|
||||||
|
}
|
||||||
|
|
||||||
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
||||||
{
|
{
|
||||||
do
|
do
|
||||||
@@ -332,7 +342,7 @@ void store_eeprom(std::uint16_t address, std::uint8_t count)
|
|||||||
{
|
{
|
||||||
do {
|
do {
|
||||||
ee::write<off>(address++, link::rx());
|
ee::write<off>(address++, link::rx());
|
||||||
link::tx(ack);
|
tx_ack();
|
||||||
} while (--count);
|
} while (--count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,34 +365,32 @@ void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
|
|||||||
// itself walks the page (aligned, so the offset bits wrap to zero); on
|
// itself walks the page (aligned, so the offset bits wrap to zero); on
|
||||||
// the word-addressed large chips the wire word address becomes a 32-bit
|
// the word-addressed large chips the wire word address becomes a 32-bit
|
||||||
// byte cursor once, and their 256-byte page makes its low byte the whole
|
// byte cursor once, and their 256-byte page makes its low byte the whole
|
||||||
// in-page offset. The slot index is one high byte of the wire address —
|
// in-page offset. The slot index is the wire address's high byte — which
|
||||||
// two values on byte-addressed chips (the & ~1), bits 16:9 re-packed on
|
// on byte-addressed chips means the byte address's, with its low bit
|
||||||
// the large ones.
|
// dropped (a slot is two of those).
|
||||||
spm::flash_address_t address;
|
spm::flash_address_t address;
|
||||||
std::uint8_t page_high;
|
std::uint8_t page_high;
|
||||||
if constexpr (word_flash) {
|
if constexpr (word_flash) {
|
||||||
// Pages are aligned, so one page never crosses a 64 KiB boundary:
|
// Pages are aligned, so one page never crosses a 64 KiB boundary:
|
||||||
// RAMPZ is a per-page constant and the fill cursor is a 16-bit Z
|
// RAMPZ is a per-page constant and the fill cursor is a 16-bit Z
|
||||||
// whose low byte is the whole in-page offset (256-byte pages). The
|
// whose low byte is the whole in-page offset (256-byte pages).
|
||||||
// slot index is simply the wire word address's high byte.
|
|
||||||
const std::uint8_t rampz = static_cast<std::uint8_t>(wire_address >> 15);
|
const std::uint8_t rampz = static_cast<std::uint8_t>(wire_address >> 15);
|
||||||
const std::uint16_t z0 = static_cast<std::uint16_t>(wire_address << 1);
|
const std::uint16_t z0 = static_cast<std::uint16_t>(wire_address << 1);
|
||||||
std::uint16_t z = z0;
|
std::uint16_t z = z0;
|
||||||
do {
|
do {
|
||||||
std::uint8_t low = link::rx();
|
std::uint8_t low = link::rx();
|
||||||
std::uint8_t high = link::rx();
|
std::uint8_t high = link::rx();
|
||||||
spm::fill<off>((static_cast<spm::flash_address_t>(rampz) << 16) | z,
|
spm::fill<off>((static_cast<spm::flash_address_t>(rampz) << 16) | z, word_of({low, high}));
|
||||||
static_cast<std::uint16_t>(low | (high << 8)));
|
|
||||||
z += 2;
|
z += 2;
|
||||||
} while (static_cast<std::uint8_t>(z));
|
} while (static_cast<std::uint8_t>(z));
|
||||||
address = (static_cast<spm::flash_address_t>(rampz) << 16) | z0;
|
address = (static_cast<spm::flash_address_t>(rampz) << 16) | z0;
|
||||||
page_high = static_cast<std::uint8_t>(wire_address >> 8) & 0xfe;
|
page_high = static_cast<std::uint8_t>(wire_address >> 8);
|
||||||
} else {
|
} else {
|
||||||
address = static_cast<spm::flash_address_t>(wire_address);
|
address = static_cast<spm::flash_address_t>(wire_address);
|
||||||
do {
|
do {
|
||||||
std::uint8_t low = link::rx();
|
std::uint8_t low = link::rx();
|
||||||
std::uint8_t high = link::rx();
|
std::uint8_t high = link::rx();
|
||||||
spm::fill<off>(address, static_cast<std::uint16_t>(low | (high << 8)));
|
spm::fill<off>(address, word_of({low, high}));
|
||||||
address += 2;
|
address += 2;
|
||||||
} while (static_cast<std::uint8_t>(address) & (page - 1));
|
} while (static_cast<std::uint8_t>(address) & (page - 1));
|
||||||
address -= 2; // back inside the page — erase and write ignore the word bits
|
address -= 2; // back inside the page — erase and write ignore the word bits
|
||||||
@@ -414,7 +422,7 @@ void send_fuses()
|
|||||||
std::uint8_t which = 0;
|
std::uint8_t which = 0;
|
||||||
do
|
do
|
||||||
link::tx(spm::read_fuse<off>(static_cast<spm::fuse>(which)));
|
link::tx(spm::read_fuse<off>(static_cast<spm::fuse>(which)));
|
||||||
while (++which & 3);
|
while (++which != 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
[[noreturn]] void run()
|
[[noreturn]] void run()
|
||||||
@@ -437,7 +445,7 @@ void send_fuses()
|
|||||||
// byte pick a hand assembler writes — `>> 8` leaves the swap materialized.
|
// byte pick a hand assembler writes — `>> 8` leaves the swap materialized.
|
||||||
const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
|
const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
|
||||||
const std::uint8_t ra_high = static_cast<std::uint8_t>(std::byteswap(ra_words));
|
const std::uint8_t ra_high = static_cast<std::uint8_t>(std::byteswap(ra_words));
|
||||||
const std::uint8_t slot_high = word_flash ? ra_high & 0xfe : static_cast<std::uint8_t>(ra_high << 1);
|
const std::uint8_t slot_high = word_flash ? ra_high : static_cast<std::uint8_t>(ra_high << 1);
|
||||||
|
|
||||||
// The knock: 'p' then 'b', each under a fresh window; any other byte is
|
// 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.
|
// line noise and waits again. Falling out of a window runs the app.
|
||||||
@@ -448,40 +456,51 @@ void send_fuses()
|
|||||||
// No prompt while an EEPROM write runs: a pending write blocks SPM
|
// No prompt while an EEPROM write runs: a pending write blocks SPM
|
||||||
// and fuse reads (§26.2.1), and the ack tells the host all is done.
|
// and fuse reads (§26.2.1), and the ack tells the host all is done.
|
||||||
ee::wait();
|
ee::wait();
|
||||||
link::tx(ack);
|
tx_ack();
|
||||||
const std::uint8_t command = link::rx();
|
const std::uint8_t command = link::rx();
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'b': { // info block, read relative to the running slot
|
|
||||||
// The block sits in the image's first 256 bytes (the build lint
|
|
||||||
// asserts it), and slots are 512-aligned — so the low byte of its
|
|
||||||
// link address (in wire units: bytes, or words on the large
|
|
||||||
// chips) is its offset in any slot, and the high byte of its
|
|
||||||
// runtime address is the running slot's. Composed from the two
|
|
||||||
// bytes — the high half is runtime data, so no absolute address
|
|
||||||
// is ever materialized.
|
|
||||||
const auto link_low = reinterpret_cast<std::uint16_t>(info_data.storage.data());
|
|
||||||
const std::uint8_t low =
|
|
||||||
word_flash ? static_cast<std::uint8_t>(link_low >> 1) : static_cast<std::uint8_t>(link_low);
|
|
||||||
send_flash(static_cast<std::uint16_t>(low | (slot_high << 8)), static_cast<std::uint8_t>(info_data.size()));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'J': { // jump to a wire word address: hand-over and staging transfer
|
case 'J': { // jump to a wire word address: hand-over and staging transfer
|
||||||
auto target = reinterpret_cast<void (*)()>(rx16());
|
auto target = reinterpret_cast<void (*)()>(rx16());
|
||||||
link::tx(ack);
|
tx_ack();
|
||||||
link::drain();
|
link::drain();
|
||||||
jump(target);
|
jump(target);
|
||||||
}
|
}
|
||||||
|
case 'b': // info block, read relative to the running slot
|
||||||
case 'R': // read flash: addr16, n8 (0 = 256)
|
case 'R': // read flash: addr16, n8 (0 = 256)
|
||||||
case 'r': // read EEPROM: addr16, n8
|
case 'r': // read EEPROM: addr16, n8
|
||||||
case 'w': { // write EEPROM: addr16, n8, then n bytes each acked
|
case 'w': { // write EEPROM: addr16, n8, then n bytes each acked
|
||||||
std::uint16_t address = rx16();
|
// Every streamed command through one address-and-count path: 'b'
|
||||||
std::uint8_t count = link::rx();
|
// is a flash read whose arguments the loader already knows, so it
|
||||||
if (command == 'R')
|
// joins the wire-argument three here rather than streaming from a
|
||||||
send_flash(address, count);
|
// call site of its own. That leaves the image with exactly one
|
||||||
else if (command == 'r')
|
// flash streamer, and on the word-addressed chips it is what lets
|
||||||
|
// the streamer's 24-bit cursor live in this never-returning loop's
|
||||||
|
// own call-saved registers instead of being saved and restored
|
||||||
|
// around a call.
|
||||||
|
std::uint16_t address;
|
||||||
|
std::uint8_t count;
|
||||||
|
if (command == 'b') {
|
||||||
|
// The block sits in the image's first 256 bytes (the build
|
||||||
|
// lint asserts it), and slots are 512-aligned — so the low
|
||||||
|
// byte of its link address is its offset in any slot, halved
|
||||||
|
// where wire units are words. The high byte is the running
|
||||||
|
// slot's, runtime data, so no absolute address is ever
|
||||||
|
// materialized.
|
||||||
|
const auto link_byte =
|
||||||
|
static_cast<std::uint8_t>(reinterpret_cast<std::uint16_t>(info_data.storage.data()));
|
||||||
|
const std::uint8_t low = word_flash ? static_cast<std::uint8_t>(link_byte >> 1) : link_byte;
|
||||||
|
address = static_cast<std::uint16_t>(low | (slot_high << 8));
|
||||||
|
count = static_cast<std::uint8_t>(info_data.size());
|
||||||
|
} else {
|
||||||
|
address = rx16();
|
||||||
|
count = link::rx();
|
||||||
|
}
|
||||||
|
if (command == 'r')
|
||||||
send_eeprom(address, count);
|
send_eeprom(address, count);
|
||||||
else
|
else if (command == 'w')
|
||||||
store_eeprom(address, count);
|
store_eeprom(address, count);
|
||||||
|
else
|
||||||
|
send_flash(address, count);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'W': // program one flash page: addr16, page bytes
|
case 'W': // program one flash page: addr16, page bytes
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ VERSION = 2 # this tool's own version — free to drift from a loader's
|
|||||||
# becomes the new floor here.
|
# becomes the new floor here.
|
||||||
OLDEST_LOADER = 1
|
OLDEST_LOADER = 1
|
||||||
NEWEST_LOADER = 2
|
NEWEST_LOADER = 2
|
||||||
SLOT = 512 # the loader slot on byte-addressed chips; word-addressed ones (>64 KiB) use 1 KiB — their own smallest boot sector
|
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
|
||||||
|
|
||||||
VERBOSE = False
|
VERBOSE = False
|
||||||
@@ -336,9 +336,8 @@ class Info:
|
|||||||
scale = 2 if self.word_flash else 1
|
scale = 2 if self.word_flash else 1
|
||||||
self.base = (raw[7] | (raw[8] << 8)) * scale
|
self.base = (raw[7] | (raw[8] << 8)) * scale
|
||||||
self.eeprom_size = raw[9] | (raw[10] << 8)
|
self.eeprom_size = raw[9] | (raw[10] << 8)
|
||||||
self.slot = 1024 if self.word_flash else SLOT
|
self.flash_size = self.base + SLOT
|
||||||
self.flash_size = self.base + self.slot
|
self.stage = self.base - SLOT # where a staging copy of the loader goes
|
||||||
self.stage = self.base - self.slot # where a staging copy of the loader goes
|
|
||||||
# The hand-over target, as the word address 'J' takes: the trampoline
|
# The hand-over target, as the word address 'J' takes: the trampoline
|
||||||
# below the loader (tinies), or word 0 (mega — the application's own
|
# below the loader (tinies), or word 0 (mega — the application's own
|
||||||
# reset vector; BOOTRST re-vectors a reset into the loader instead).
|
# reset vector; BOOTRST re-vectors a reset into the loader instead).
|
||||||
@@ -365,7 +364,7 @@ class Info:
|
|||||||
f"flash {self.flash_size} B, {self.page} B pages"
|
f"flash {self.flash_size} B, {self.page} B pages"
|
||||||
+ (", word-addressed wire" if self.word_flash else ""),
|
+ (", word-addressed wire" if self.word_flash else ""),
|
||||||
f"application 0x0000..{self.base - 1:#06x} ({self.base} B)",
|
f"application 0x0000..{self.base - 1:#06x} ({self.base} B)",
|
||||||
f"loader {self.base:#06x} ({self.slot} B slot)",
|
f"loader {self.base:#06x} ({SLOT} B slot)",
|
||||||
f"staging {self.stage:#06x}",
|
f"staging {self.stage:#06x}",
|
||||||
f"EEPROM {self.eeprom_size} B",
|
f"EEPROM {self.eeprom_size} B",
|
||||||
f"hand-over {hand_over}",
|
f"hand-over {hand_over}",
|
||||||
@@ -681,13 +680,13 @@ def staging_content(image, info):
|
|||||||
that word, which for a staging copy is the slot's own last word: an rjmp
|
that word, which for a staging copy is the slot's own last word: an rjmp
|
||||||
to the resident base. The staging copy's fall-through and 'J'-free exit
|
to the resident base. The staging copy's fall-through and 'J'-free exit
|
||||||
both land in a loader instead of garbage."""
|
both land in a loader instead of garbage."""
|
||||||
slot = info.slot
|
budget = SLOT - 2 if info.patch_vector else SLOT
|
||||||
if len(image) > (slot - 2 if info.patch_vector else slot):
|
if len(image) > budget:
|
||||||
raise Error(f"loader image is {len(image)} B, the slot holds {slot - 2 if info.patch_vector else slot}")
|
raise Error(f"loader image is {len(image)} B, the slot holds {budget}")
|
||||||
content = bytearray(image) + bytearray([0xFF] * (slot - len(image)))
|
content = bytearray(image) + bytearray([0xFF] * (SLOT - len(image)))
|
||||||
if info.patch_vector:
|
if info.patch_vector:
|
||||||
through = rjmp_to((info.base - 2) // 2, info.base // 2, info.flash_size // 2)
|
through = rjmp_to((info.base - 2) // 2, info.base // 2, info.flash_size // 2)
|
||||||
content[slot - 2], content[slot - 1] = through & 0xFF, through >> 8
|
content[SLOT - 2], content[SLOT - 1] = through & 0xFF, through >> 8
|
||||||
return bytes(content)
|
return bytes(content)
|
||||||
|
|
||||||
|
|
||||||
@@ -713,7 +712,7 @@ def update_preflight(image, info, fuse_bytes):
|
|||||||
raise Error(
|
raise Error(
|
||||||
f"cannot self-update: the staging slot {info.stage:#06x} lies below the "
|
f"cannot self-update: the staging slot {info.stage:#06x} lies below the "
|
||||||
f"boot section ({bls_start:#06x}) where SPM is disabled "
|
f"boot section ({bls_start:#06x}) where SPM is disabled "
|
||||||
f"— a boot section of at least two slots ({2 * info.slot} B, BOOTSZ) is "
|
f"— a boot section of at least two slots ({2 * SLOT} B, BOOTSZ) is "
|
||||||
f"required, and only an external programmer can change fuses"
|
f"required, and only an external programmer can change fuses"
|
||||||
)
|
)
|
||||||
if not bootrst:
|
if not bootrst:
|
||||||
@@ -755,7 +754,7 @@ class UpdateState:
|
|||||||
self.data = {
|
self.data = {
|
||||||
"signature": info.signature.hex(),
|
"signature": info.signature.hex(),
|
||||||
"base": info.base,
|
"base": info.base,
|
||||||
"staging": loader.read_flash(info.stage, info.slot).hex(),
|
"staging": loader.read_flash(info.stage, SLOT).hex(),
|
||||||
"page0": loader.read_flash(0, info.page).hex() if info.patch_vector else "",
|
"page0": loader.read_flash(0, info.page).hex() if info.patch_vector else "",
|
||||||
}
|
}
|
||||||
with open(self.path, "w") as f:
|
with open(self.path, "w") as f:
|
||||||
@@ -835,7 +834,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
|||||||
update = image_info(image) # the preflight proved it is there
|
update = image_info(image) # the preflight proved it is there
|
||||||
verbose(f"installing pureboot {update.version} over pureboot {info.version}")
|
verbose(f"installing pureboot {update.version} over pureboot {info.version}")
|
||||||
staged = staging_content(image, info)
|
staged = staging_content(image, info)
|
||||||
resident = bytes(image) + bytes([0xFF] * (info.slot - len(image)))
|
resident = bytes(image) + bytes([0xFF] * (SLOT - len(image)))
|
||||||
page = info.page
|
page = info.page
|
||||||
|
|
||||||
state = UpdateState(state_path)
|
state = UpdateState(state_path)
|
||||||
@@ -858,7 +857,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
|||||||
# state file's snapshot) — a resumed, half-written install differs
|
# state file's snapshot) — a resumed, half-written install differs
|
||||||
# from its snapshot and takes the install path below, which completes
|
# from its snapshot and takes the install path below, which completes
|
||||||
# it page by page.
|
# it page by page.
|
||||||
current = loader.read_flash(info.stage, info.slot)
|
current = loader.read_flash(info.stage, SLOT)
|
||||||
staged_loader = image_info(current[:268])
|
staged_loader = image_info(current[:268])
|
||||||
if staged_loader is not None and staged_loader.raw == info.raw and current == state.staging:
|
if staged_loader is not None and staged_loader.raw == info.raw and current == state.staging:
|
||||||
print("staging slot already holds a loader — left in place")
|
print("staging slot already holds a loader — left in place")
|
||||||
@@ -867,7 +866,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
|||||||
# tiny13s), its first page carries the reset vector: written last,
|
# tiny13s), its first page carries the reset vector: written last,
|
||||||
# so any earlier interruption still resets into the old resident,
|
# so any earlier interruption still resets into the old resident,
|
||||||
# and from then on resets enter the staging copy.
|
# and from then on resets enter the staging copy.
|
||||||
order = list(range(0, info.slot, page))
|
order = list(range(0, SLOT, page))
|
||||||
if info.stage == 0:
|
if info.stage == 0:
|
||||||
order = order[1:] + [0]
|
order = order[1:] + [0]
|
||||||
if write_differing(loader, info.stage, staged, order, label="staging copy"):
|
if write_differing(loader, info.stage, staged, order, label="staging copy"):
|
||||||
@@ -894,7 +893,7 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
|
|||||||
if redirect:
|
if redirect:
|
||||||
verbose("word 0 restored")
|
verbose("word 0 restored")
|
||||||
write_differing(loader, 0, state.page0)
|
write_differing(loader, 0, state.page0)
|
||||||
order = list(range(0, info.slot, page))
|
order = list(range(0, SLOT, page))
|
||||||
if info.stage == 0:
|
if info.stage == 0:
|
||||||
order = [0] + order[1:]
|
order = [0] + order[1:]
|
||||||
write_differing(loader, info.stage, state.staging, order, label="staging restore")
|
write_differing(loader, info.stage, state.staging, order, label="staging restore")
|
||||||
|
|||||||
@@ -66,9 +66,10 @@ struct link {
|
|||||||
}
|
}
|
||||||
[[noreturn]] static void idle()
|
[[noreturn]] static void idle()
|
||||||
{
|
{
|
||||||
// 'L' hands back to the loader at the top slot — 512 bytes, or the
|
// 'L' hands back to the loader in the top slot — 512 bytes on every
|
||||||
// 1 KiB the >64 KiB chips use.
|
// chip. The jump takes a word address, which is what makes the
|
||||||
constexpr std::uint32_t slot = avr::hw::db.mem.flash_size > 65536 ? 1024 : 512;
|
// >64 KiB chips' entry reachable through a 16-bit pointer at all.
|
||||||
|
constexpr std::uint32_t slot = 512;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
auto command = tx_t::read_blocking();
|
auto command = tx_t::read_blocking();
|
||||||
if (command == 'L')
|
if (command == 'L')
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ def main():
|
|||||||
# The staging slot: erased flash with the loader sitting exactly where
|
# The staging slot: erased flash with the loader sitting exactly where
|
||||||
# a staging copy would — the tool must leave it in place and let it
|
# a staging copy would — the tool must leave it in place and let it
|
||||||
# stream the (different) update build into the resident slot.
|
# stream the (different) update build into the resident slot.
|
||||||
stage = base - 512
|
stage = base - pb.SLOT
|
||||||
rehome_from(pbsim, pb, device_bin, elf, hex(stage), hex(stage), update_bin, base, page, baud, app_bin, workdir,
|
rehome_from(pbsim, pb, device_bin, elf, hex(stage), hex(stage), update_bin, base, page, baud, app_bin, workdir,
|
||||||
mcu, hz)
|
mcu, hz)
|
||||||
print("re-home from the staging slot: converged")
|
print("re-home from the staging slot: converged")
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ def main():
|
|||||||
|
|
||||||
# Restore the resident image through the staged copy, then 'J' back
|
# Restore the resident image through the staged copy, then 'J' back
|
||||||
# into it and prove it lives.
|
# into it and prove it lives.
|
||||||
resident = image + b"\xff" * (info.slot - len(image))
|
resident = image + b"\xff" * (pb.SLOT - len(image))
|
||||||
pb.write_differing(loader, base, resident)
|
pb.write_differing(loader, base, resident)
|
||||||
back_info = loader.enter_copy(base, 25)
|
back_info = loader.enter_copy(base, 25)
|
||||||
if back_info.raw != resident_info:
|
if back_info.raw != resident_info:
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ def main():
|
|||||||
# the page byte is the wire's 0-means-256.
|
# the page byte is the wire's 0-means-256.
|
||||||
mega = mcu.startswith("atmega")
|
mega = mcu.startswith("atmega")
|
||||||
patch = not mega or mcu.startswith("atmega48")
|
patch = not mega or mcu.startswith("atmega48")
|
||||||
word_flash = base + 512 > 0x10000
|
word_flash = base + pb.SLOT > 0x10000
|
||||||
wire_base = base // 2 if word_flash else base
|
wire_base = base // 2 if word_flash else base
|
||||||
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||||
info = pb.Info(
|
info = pb.Info(
|
||||||
@@ -127,7 +127,7 @@ def main():
|
|||||||
# loader, the trampoline on the application's own entry (patched-vector
|
# loader, the trampoline on the application's own entry (patched-vector
|
||||||
# chips only — a boot-sectioned mega's word 0 stays the application's).
|
# chips only — a boot-sectioned mega's word 0 stays the application's).
|
||||||
if patch:
|
if patch:
|
||||||
flash_words = (base + 512) // 2
|
flash_words = (base + pb.SLOT) // 2
|
||||||
app = open(app_bin, "rb").read()
|
app = open(app_bin, "rb").read()
|
||||||
word0 = flash_true[0] | (flash_true[1] << 8)
|
word0 = flash_true[0] | (flash_true[1] << 8)
|
||||||
if rjmp_decode(word0, 0, flash_words) != base // 2:
|
if rjmp_decode(word0, 0, flash_words) != base // 2:
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ def assumed_fuses(pb, image):
|
|||||||
image's embedded signature."""
|
image's embedded signature."""
|
||||||
info = pb.image_info(image)
|
info = pb.image_info(image)
|
||||||
which, ladder = pb.BOOT_FUSE[bytes(info.signature[1:3])]
|
which, ladder = pb.BOOT_FUSE[bytes(info.signature[1:3])]
|
||||||
bits = min((b for b in ladder if ladder[b] * 2 >= 2 * info.slot), key=lambda b: ladder[b])
|
bits = min((b for b in ladder if ladder[b] * 2 >= 2 * pb.SLOT), key=lambda b: ladder[b])
|
||||||
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
|
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
|
||||||
fuses[which] = 0xF8 | (bits << 1) | 1
|
fuses[which] = 0xF8 | (bits << 1) | 1
|
||||||
return bytes(fuses)
|
return bytes(fuses)
|
||||||
@@ -93,16 +93,13 @@ def main():
|
|||||||
# The m48s are megas without a boot section: patched vector, no fuse
|
# The m48s are megas without a boot section: patched vector, no fuse
|
||||||
# preflight, and the same reset-to-0 the tinies get.
|
# preflight, and the same reset-to-0 the tinies get.
|
||||||
patch = not mega or mcu.startswith("atmega48")
|
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
|
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(tool)))
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
import pbsim
|
import pbsim
|
||||||
import pureboot as pb
|
import pureboot as pb
|
||||||
|
|
||||||
|
slot = pb.SLOT
|
||||||
os.makedirs(workdir, exist_ok=True)
|
os.makedirs(workdir, exist_ok=True)
|
||||||
objcopy = os.environ.get("PB_OBJCOPY", "avr-objcopy")
|
objcopy = os.environ.get("PB_OBJCOPY", "avr-objcopy")
|
||||||
images = {}
|
images = {}
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ def info_of(pb, base, page, patch, flash, signature=(0x1E, 0x93, 0x0B), word_fla
|
|||||||
raw = bytes((0x50, 0x42, pb.NEWEST_LOADER if version is None else version,
|
raw = bytes((0x50, 0x42, pb.NEWEST_LOADER if version is None else version,
|
||||||
*signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8, 0, 2, flags))
|
*signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8, 0, 2, flags))
|
||||||
info = pb.Info(raw)
|
info = pb.Info(raw)
|
||||||
assert info.flash_size == flash
|
if info.flash_size != flash:
|
||||||
|
fail(f"info_of({base:#x}) decodes to {info.flash_size:#x} of flash, not {flash:#x}")
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
@@ -94,9 +95,7 @@ def main():
|
|||||||
((0x1E, 0x97, 0x05), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284P
|
((0x1E, 0x97, 0x05), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284P
|
||||||
)
|
)
|
||||||
for signature, flash, which, ladder in cases:
|
for signature, flash, which, ladder in cases:
|
||||||
# Word-addressed chips carry the 1 KiB slot (their smallest boot sector).
|
chip = info_of(pb, flash - pb.SLOT, 128 if flash < 0x20000 else 0, False, flash,
|
||||||
slot = 1024 if flash > 0x10000 else 512
|
|
||||||
chip = info_of(pb, flash - slot, 128 if flash < 0x20000 else 0, False, flash,
|
|
||||||
signature=signature, word_flash=flash > 0x10000)
|
signature=signature, word_flash=flash > 0x10000)
|
||||||
for bits, start in ladder.items():
|
for bits, start in ladder.items():
|
||||||
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
|
fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF))
|
||||||
@@ -109,10 +108,12 @@ def main():
|
|||||||
if prog or at != start:
|
if prog or at != start:
|
||||||
fail(f"mega_boot {signature[1]:02x}{signature[2]:02b} unprogrammed: {prog} {at:#07x}")
|
fail(f"mega_boot {signature[1]:02x}{signature[2]:02b} unprogrammed: {prog} {at:#07x}")
|
||||||
|
|
||||||
# Word-addressed info decode: the 1284P's base/page ride the wire scaled,
|
# Word-addressed info decode: the 1284P's base and page ride the wire
|
||||||
# and its slot is 1 KiB.
|
# scaled — a 17-bit base halved into the block's two bytes, a 256-byte page
|
||||||
big = info_of(pb, 0x1FC00, 0, False, 0x20000, signature=(0x1E, 0x97, 0x05), word_flash=True)
|
# spelled 0 — and its slot is the same 512 bytes as everywhere else, so its
|
||||||
if big.page != 256 or big.base != 0x1FC00 or big.stage != 0x1F800 or big.slot != 1024:
|
# staging slot lands inside the 1 KiB minimum boot section.
|
||||||
|
big = info_of(pb, 0x1FE00, 0, False, 0x20000, signature=(0x1E, 0x97, 0x05), word_flash=True)
|
||||||
|
if big.page != 256 or big.base != 0x1FE00 or big.stage != 0x1FC00:
|
||||||
fail(f"word-addressed info decode: page {big.page}, base {big.base:#x}, stage {big.stage:#x}")
|
fail(f"word-addressed info decode: page {big.page}, base {big.base:#x}, stage {big.stage:#x}")
|
||||||
|
|
||||||
# Surgery: word 0 lands on the loader, the trampoline on the original
|
# Surgery: word 0 lands on the loader, the trampoline on the original
|
||||||
@@ -215,6 +216,15 @@ def main():
|
|||||||
if pb.update_preflight(bytes((0xAA,)) * 8 + tiny.raw, tiny, None) != []:
|
if pb.update_preflight(bytes((0xAA,)) * 8 + tiny.raw, tiny, None) != []:
|
||||||
fail("tiny preflight should pass without fuses")
|
fail("tiny preflight should pass without fuses")
|
||||||
|
|
||||||
|
# The 1284s' smallest boot section (512 words) is exactly the resident
|
||||||
|
# slot plus its staging slot, so self-update is possible at the minimum
|
||||||
|
# BOOTSZ — no fuse step up, the 644's geometry. That holds only while a
|
||||||
|
# slot is 512 B: at 1 KiB the staging slot would fall outside the section
|
||||||
|
# and the preflight would refuse.
|
||||||
|
notes = pb.update_preflight(bytes((0xAA,)) * 8 + big.raw, big, fuses(0xFE))
|
||||||
|
if not any("staging slot" in n for n in notes):
|
||||||
|
fail(f"1284 minimum-BOOTSZ notes: {notes}")
|
||||||
|
|
||||||
# The walk-region refusal: BOOTRST aimed below the loader plus app data
|
# The walk-region refusal: BOOTRST aimed below the loader plus app data
|
||||||
# in the walk span errors without --force; erased spans and unprogrammed
|
# in the walk span errors without --force; erased spans and unprogrammed
|
||||||
# BOOTRST pass.
|
# BOOTRST pass.
|
||||||
|
|||||||
Reference in New Issue
Block a user