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

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

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -2,13 +2,14 @@
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by 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 allowed), built for every chip libavr targets, **fitting each (attributes allowed), built for **every chip libavr targets — all 37 —
chip's smallest boot sector**: 512 bytes everywhere — 488 B on the fitting each chip's smallest boot sector**: 512 bytes everywhere — 488 B on
ATtiny13A, 502 B on the ATtiny85, 466504 B across the megas — except the the tiny13s, 498502 B on the tiny25/45/85, 466504 B across the megas
ATmega1284P, whose smallest boot sector is 1 KiB and whose far-flash (474 B on the boot-section-less m48s, 498 B on the 644s) — except the
ATmega1284/1284P, whose smallest boot sector is 1 KiB and whose far-flash
machinery (ELPM reads, RAMPZ page commands, word-addressed wire) lands at machinery (ELPM reads, RAMPZ page commands, word-addressed wire) lands at
558 B in a 1 KiB slot: the 512-byte figure is a hardware boundary that chip 558 B in a 1 KiB slot: the 512-byte figure is a hardware boundary those
simply does not have, and no implementation of this feature set fits it chips simply do not have, and no implementation of this feature set fits it
there. The device speaks primitives; every composite — verify, erase, there. The device speaks primitives; every composite — verify, erase,
reset-vector surgery, updating the loader itself — lives in the host tool reset-vector surgery, updating the loader itself — lives in the host tool
(`pureboot.py`). (`pureboot.py`).
@@ -29,19 +30,20 @@ belongs to the host-managed trampoline (below).
| Chip | Serial | Baud | Clock assumed | | Chip | Serial | Baud | Clock assumed |
|---|---|---|---| |---|---|---|---|
| every ATmega (8/8A, 16, 32/32A, 168A, 328P, 1284P) | the hardware USART (USART0), RXD/TXD per pinout | 115200 8N1 | 16 MHz crystal | | every ATmega | the hardware USART (USART0), RXD/TXD per pinout | 115200 8N1 | 16 MHz crystal |
| ATtiny85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC | | ATtiny25/45/85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC |
| ATtiny13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC | | ATtiny13/13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC |
The tiny RX pin has its pull-up enabled; TX idles high. All multi-byte The tiny RX pin has its pull-up enabled; TX idles high. All multi-byte
quantities on the wire are little-endian. quantities on the wire are little-endian.
## Activation ## Activation
Reset enters the loader (BOOTRST on the mega, the patched reset vector on the Reset enters the loader (BOOTRST on the boot-sectioned megas; the patched
tinies) — except a watchdog reset, which hands straight to the application reset vector on the tinies and the boot-section-less m48s) — except a
(the application owns its watchdog; it must clear WDRF itself, which also watchdog reset, which hands straight to the application (the application
releases the WDRF-forced WDE). owns its watchdog; it must clear WDRF itself, which also releases the
WDRF-forced WDE).
The host then has one activation window per awaited byte to knock: `p` then The host then has one activation window per awaited byte to knock: `p` then
`b`. Each awaited byte gets a fresh window; any other byte is discarded and `b`. Each awaited byte gets a fresh window; any other byte is discarded and
@@ -61,9 +63,10 @@ write to finish and sends the prompt `+` (0x2b) — the prompt is therefore
also the completion ack of the previous command. A session is: await `+`, also the completion ack of the previous command. A session is: await `+`,
send a command, read its reply, repeat. send a command, read its reply, repeat.
On chips whose flash exceeds 64 KiB (the 1284P — info-block flag bit 1) the On chips whose flash exceeds 64 KiB (the 1284s — info-block flag bit 1) the
`R`/`W` flash addresses are **word** addresses; everywhere else they are byte `R`/`W` flash addresses are **word** addresses; everywhere else they are byte
addresses. EEPROM addresses are always bytes, counts always bytes. addresses (the 644s' 64 KiB is exactly the 16-bit byte space and stays
byte-addressed). EEPROM addresses are always bytes, counts always bytes.
| Cmd | Arguments | Reply | | Cmd | Arguments | Reply |
|---|---|---| |---|---|---|
@@ -108,16 +111,23 @@ write `0xff` (per page for flash, per byte for EEPROM).
## Deployment ## Deployment
**Megas**: program the loader at `flash slot` with an external programmer. **Boot-sectioned megas**: program the loader at `flash slot` with an
Every mega has a BOOTSZ step whose boot section is exactly the loader slot — external programmer. Every such mega has a BOOTSZ step whose boot section
512 B, the m8/16/168A's second-smallest step, the m32/328P's smallest; on is exactly the loader slot — 512 B, the second-smallest step on the 8 KiB
the m1284P that step is its smallest, 512 words, which is why its slot is and 16 KiB chips (m8, m88, m16, m168, m164), the smallest on the 32 KiB
1 KiB — so the ATmega328P profiles below apply to every mega with its own ones (m32, m328, m324); on the 1284s that step is the smallest, 512 words,
addresses and slot size; the per-chip BOOTSZ ladders live in the host tool which is why their slot is 1 KiB — so the ATmega328P profiles below apply
(`BOOT_FUSE`). The 1284P's numbers: standalone = BOOTSZ 512 words (reset at to every one of them with its own addresses and slot size; the per-chip
the loader base 0x1fc00); self-update = 1024 words, covering both 1 KiB BOOTSZ ladders live in the host tool (`BOOT_FUSE`). The 1284s' numbers:
slots, the loader-first reset landing at 0x1f800 — the staging slot, walked standalone = BOOTSZ 512 words (reset at the loader base 0x1fc00);
across when erased. self-update = 1024 words, covering both 1 KiB slots, the loader-first
reset landing at 0x1f800 — the staging slot, walked across when erased.
The **644s** are the geometry's sweet spot: their smallest boot section
(512 words = 1 KiB) is exactly *two* 512-byte slots, so the resident and
its staging slot both live inside the minimum section — self-update needs
no fuse step up, and the standalone profile does not exist (reset lands at
0xfc00, one erased slot below the loader: the loader-first walk built in).
ATmega328P profiles (addresses for its 32 KiB): ATmega328P profiles (addresses for its 32 KiB):
@@ -130,15 +140,18 @@ ATmega328P profiles (addresses for its 32 KiB):
Applications are flashed unmodified — word 0 stays the application's own Applications are flashed unmodified — word 0 stays the application's own
reset vector, and the hand-over jumps to 0. reset vector, and the hand-over jumps to 0.
**Tinies** (no boot section): program the loader at `flash 512`; erased **Patched-vector chips — the tinies and the m48s** (no boot section; the
flash below it walks up into the loader, so a virgin chip activates. When m48s' SPM runs from the entire flash, Atmel-8271 §26): program the loader
flashing an application the host performs reset-vector surgery: word 0 is at `flash 512`; erased flash below it walks up into the loader, so a
rewritten to `rjmp` to the loader base, and the application's own entry is virgin chip activates. When flashing an application the host performs
re-encoded as a trampoline `rjmp` in the word just below the loader reset-vector surgery: word 0 is rewritten to `rjmp` to the loader base, and
(`base 2`, where the hand-over jumps). Every other vector stays the the application's own entry is re-encoded as a trampoline `rjmp` in the
application's. The patched page 0 and the trampoline page are written word just below the loader (`base 2`, where the hand-over jumps). Every
*first*, so from the first write on an interrupted flash still resets into other vector stays the application's. The patched page 0 and the trampoline
the loader; an erase runs top-down for the same reason. page are written *first*, so from the first write on an interrupted flash
still resets into the loader; an erase runs top-down for the same reason.
The m48s speak this profile over their hardware USART — no fuse preflight,
BOOTRST does not exist there.
## Updating the loader ## Updating the loader
@@ -148,16 +161,18 @@ loader itself as its own staging loader. The image is the loader's own 512
bytes as a raw binary, or the Intel HEX the build emits beside it, which bytes as a raw binary, or the Intel HEX the build emits beside it, which
links the loader at its base inside an otherwise blank flash image: links the loader at its base inside an otherwise blank flash image:
1. The staging slot `[base512, base)` is saved to a host-side state file 1. The staging slot `[baseslot, base)` is saved to a host-side state file
(on the 1 KB tiny13A that is the whole application, vectors included). (on the 1 KB tiny13s that is the whole application, vectors included).
2. The resident installs the identical update image there. On the tinies the 2. The resident installs the identical update image there. On the
host composes the slot's last word — the same address as the resident's patched-vector chips the host composes the slot's last word — the same
trampoline — as a jump to the resident base, so even an abandoned staging address as the resident's trampoline — as a jump to the resident base,
copy times out into a loader, never into garbage. so even an abandoned staging copy times out into a loader, never into
garbage.
3. `J` enters the staging copy, which rewrites the resident slot. On the 3. `J` enters the staging copy, which rewrites the resident slot. On the
t85 the host first re-aims word 0 at the staging copy, so a power loss patched-vector chips whose staging slot sits away from page 0 the host
mid-rewrite still resets into a loader; on the t13a the staging slot first re-aims word 0 at the staging copy, so a power loss mid-rewrite
carries the reset vector itself. still resets into a loader; on the tiny13s the staging slot carries the
reset vector itself.
4. `J` enters the new resident, which restores the staging slot's saved 4. `J` enters the new resident, which restores the staging slot's saved
content (word 0 and the trampoline with it) and the state file is content (word 0 and the trampoline with it) and the state file is
discarded. discarded.
@@ -166,9 +181,10 @@ Every phase is idempotent and keyed off the actual flash state: re-running
the same command after any interruption resumes and completes. The state the same command after any interruption resumes and completes. The state
file carries the only bytes not recoverable from the device; if it is lost file carries the only bytes not recoverable from the device; if it is lost
mid-update the update still completes, and the staging region is restored by mid-update the update still completes, and the staging region is restored by
reflashing the application. The mega needs its fuses for the preflight reflashing the application. A boot-sectioned mega needs its fuses for the
(BOOTSZ gate, profile notes) — read from the device, or supplied with preflight (BOOTSZ gate, profile notes) — read from the device, or supplied
`--assume-fuses` where reading is impossible (simulators). with `--assume-fuses` where reading is impossible (simulators); the
patched-vector chips need none.
## Host tool ## Host tool

View File

@@ -16,9 +16,10 @@
// resident — how pureboot updates itself, host-driven, with no other // resident — how pureboot updates itself, host-driven, with no other
// firmware involved. // firmware involved.
// //
// Entry: reset lands in avr::startup::entry below (BOOTRST on the mega; the // Entry: reset lands in avr::startup::entry below (BOOTRST on the
// patched reset vector — or erased flash walking up into the loader — on the // boot-sectioned megas; the patched reset vector — or erased flash walking
// tinies). A watchdog reset hands straight to the application. Otherwise the // up into the loader — on the tinies and the boot-section-less m48s). A
// watchdog reset hands straight to the application. Otherwise the
// host has one activation window per awaited knock byte ("pb"); an idle line // host has one activation window per awaited knock byte ("pb"); an idle line
// boots the application. A session then stays in the command loop until 'J' // boots the application. A session then stays in the command loop until 'J'
// jumps away or the chip resets. // jumps away or the chip resets.
@@ -44,9 +45,10 @@ constexpr std::uint8_t ack = '+';
// from code. // from code.
consteval avr::hertz_t clock() consteval avr::hertz_t clock()
{ {
if (avr::hw::db.name == "ATtiny13A") auto name = std::string_view{avr::hw::db.name};
if (name.starts_with("ATtiny13"))
return 9.6_MHz; return 9.6_MHz;
if (avr::hw::db.name == "ATtiny85") if (name.starts_with("ATtiny"))
return 8_MHz; return 8_MHz;
return 16_MHz; return 16_MHz;
} }
@@ -62,21 +64,19 @@ consteval std::int16_t wdrf_field()
// Geometry: the resident loader owns the top slot of flash — 512 bytes, // Geometry: the resident loader owns the top slot of flash — 512 bytes,
// except on the >64 KiB chips whose own smallest boot sector is 1 KiB (the // except on the >64 KiB chips whose own smallest boot sector is 1 KiB (the
// 1284P): there the slot is 1 KiB, matching the hardware boundary the // 1284s): there the slot is 1 KiB, matching the hardware boundary the
// 512-byte figure comes from everywhere else. The word below the slot is // 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 RWWSRE bit marks a separate boot // without a hardware boot section — the tinies and the m48s, whose SPM
// section — on classic AVR the two capabilities coincide (the m8/m32 packs // runs from anywhere (Atmel-8271 §26). A boot section also means the CPU
// spell its register SPMCR). // runs on while the RWW section programs; everywhere else it halts through
// the operation. The m48s still carry RWWSRE as their temporary-buffer
// discard (§26.2), so the discard picks by that bit, not by the section.
constexpr std::uint16_t slot_bytes = spm::flash_bytes > 65536 ? 1024 : 512; constexpr std::uint16_t slot_bytes = spm::flash_bytes > 65536 ? 1024 : 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 = [] { constexpr bool boot_section = avr::hw::curated::has_boot_section();
for (auto reg : {"SPMCSR", "SPMCR"}) constexpr bool rww_discard = spm::detail::has_rww();
if (avr::hw::db.field_index(reg, "RWWSRE") >= 0)
return true;
return false;
}();
// Past 64 KiB a byte address no longer fits the wire's 16 bits, so on the // 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 —
@@ -85,7 +85,8 @@ constexpr bool boot_section = [] {
// 2 x 256 words), so the slot index is the high byte with its low bit // 2 x 256 words), so the slot index is the high byte with its low bit
// dropped everywhere. // dropped everywhere.
constexpr bool word_flash = spm::flash_bytes > 65536; constexpr bool word_flash = spm::flash_bytes > 65536;
constexpr std::uint16_t wire_base = word_flash ? static_cast<std::uint16_t>(base / 2) : static_cast<std::uint16_t>(base); constexpr std::uint16_t wire_base =
word_flash ? static_cast<std::uint16_t>(base / 2) : static_cast<std::uint16_t>(base);
constexpr std::uint16_t wire_page_mask = word_flash ? (page / 2 - 1) : (page - 1); 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
@@ -332,9 +333,10 @@ void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
{ {
// A buffer word cannot be loaded twice without an erase (§26.2.1), so a // A buffer word cannot be loaded twice without an erase (§26.2.1), so a
// refused page's drained data must not linger for the next write: // refused page's drained data must not linger for the next write:
// discard the buffer up front — CTPB on the tinies; on the mega writing // discard the buffer up front — CTPB on the tinies; on the megas
// RWWSRE aborts a pending load (§26.2.2). // writing RWWSRE aborts a pending load (§26.2.2 — on the m48s that
if constexpr (boot_section) // flush is the bit's whole documented job).
if constexpr (rww_discard)
spm::rww_enable<off>(); spm::rww_enable<off>();
else else
spm::clear_buffer<off>(); spm::clear_buffer<off>();
@@ -376,8 +378,9 @@ void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
page_high = static_cast<std::uint8_t>(address >> 8) & 0xfe; page_high = static_cast<std::uint8_t>(address >> 8) & 0xfe;
} }
if (page_high != slot_high) { if (page_high != slot_high) {
// The tinies halt the CPU through the erase and the write, so only // The tinies and the m48s halt the CPU through the erase and the
// the megas — running on while their RWW section programs — wait. // write, so only the boot-sectioned megas — running on while their
// RWW section programs — wait.
spm::erase_page<off>(address); spm::erase_page<off>(address);
if constexpr (boot_section) if constexpr (boot_section)
spm::wait(); spm::wait();
@@ -416,8 +419,8 @@ void send_fuses()
// program_flash refuses this one slot and the info block is addressed // program_flash refuses this one slot and the info block is addressed
// from it, so both follow wherever the code was flashed. // from it, so both follow wherever the code was flashed.
const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0)); const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
const std::uint8_t slot_high = word_flash ? static_cast<std::uint8_t>(ra_words >> 8) & 0xfe const std::uint8_t slot_high =
: static_cast<std::uint8_t>((ra_words >> 8) << 1); word_flash ? static_cast<std::uint8_t>(ra_words >> 8) & 0xfe : static_cast<std::uint8_t>((ra_words >> 8) << 1);
// The knock: 'p' then 'b', each under a fresh window; any other byte is // 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.

View File

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

View File

@@ -17,17 +17,17 @@ namespace {
consteval avr::hertz_t clock() consteval avr::hertz_t clock()
{ {
if (avr::hw::db.name == "ATtiny13A") auto name = std::string_view{avr::hw::db.name};
if (name.starts_with("ATtiny13"))
return 9.6_MHz; return 9.6_MHz;
if (avr::hw::db.name == "ATtiny85") if (name.starts_with("ATtiny"))
return 8_MHz; return 8_MHz;
return 16_MHz; return 16_MHz;
} }
using dev = avr::device<{.clock = clock()}>; using dev = avr::device<{.clock = clock()}>;
template <avr::hertz_t C, template <avr::hertz_t C, bool Hardware = avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART")>
bool Hardware = avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART")>
struct link { struct link {
using tx_t = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>; using tx_t = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>;
static void tx(char c) static void tx(char c)

View File

@@ -11,7 +11,11 @@ class Device:
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None): def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None):
cmd = [binary, elf, mcu, hz, base_hex, str(page), str(baud), dump] cmd = [binary, elf, mcu, hz, base_hex, str(page), str(baud), dump]
if reset_hex is not None or resume is not None: if reset_hex is not None or resume is not None:
cmd.append(reset_hex if reset_hex is not None else ("0" if not mcu.startswith("atmega") else base_hex)) # Chips without a hardware boot section — the tinies and the
# m48s — reset to address 0 like silicon; the boot-sectioned
# megas re-vector to the loader base (BOOTRST).
patch = not mcu.startswith("atmega") or mcu.startswith("atmega48")
cmd.append(reset_hex if reset_hex is not None else ("0" if patch else base_hex))
if resume is not None: if resume is not None:
cmd.append(resume) cmd.append(resume)
self.log = open(dump + ".log", "a") self.log = open(dump + ".log", "a")

View File

@@ -91,12 +91,14 @@ def main():
read_eeprom = os.path.join(workdir, "readback_eeprom.bin") read_eeprom = os.path.join(workdir, "readback_eeprom.bin")
# The geometry the host will discover, for computing the expected image: # The geometry the host will discover, for computing the expected image:
# megas carry a boot section (no vector surgery), the large ones speak # the boot-sectioned megas need no vector surgery (the tinies and the
# word addresses, and the page byte is the wire's 0-means-256. # boot-section-less m48s do), the large chips speak word addresses, and
# the page byte is the wire's 0-means-256.
mega = mcu.startswith("atmega") mega = mcu.startswith("atmega")
patch = not mega or mcu.startswith("atmega48")
word_flash = base + 512 > 0x10000 word_flash = base + 512 > 0x10000
wire_base = base // 2 if word_flash else base wire_base = base // 2 if word_flash else base
flags = (0 if mega else 1) | (2 if word_flash else 0) flags = (1 if patch else 0) | (2 if word_flash else 0)
info = pb.Info( info = pb.Info(
bytes([ord("P"), ord("B"), 1, 0, 0, 0, page & 0xFF]) bytes([ord("P"), ord("B"), 1, 0, 0, 0, page & 0xFF])
+ bytes([wire_base & 0xFF, wire_base >> 8, eeprom_size & 0xFF, eeprom_size >> 8]) + bytes([wire_base & 0xFF, wire_base >> 8, eeprom_size & 0xFF, eeprom_size >> 8])
@@ -156,9 +158,9 @@ def main():
fail("loader region looks erased in the ground-truth dump") fail("loader region looks erased in the ground-truth dump")
# The surgery, decoded independently: the patched vector must land on the # The surgery, decoded independently: the patched vector must land on the
# loader, the trampoline on the application's own entry (tinies only — # loader, the trampoline on the application's own entry (patched-vector
# the megas' word 0 stays the application's). # chips only — a boot-sectioned mega's word 0 stays the application's).
if not mega: if patch:
flash_words = (base + 512) // 2 flash_words = (base + 512) // 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)

View File

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

View File

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

View File

@@ -56,15 +56,26 @@ def main():
mega = info_of(pb, 0x7E00, 128, False, 0x8000, signature=(0x1E, 0x95, 0x0F)) mega = info_of(pb, 0x7E00, 128, False, 0x8000, signature=(0x1E, 0x95, 0x0F))
# mega_boot: BOOTSZ words and the BOOTRST sense per chip — the fuse byte # mega_boot: BOOTSZ words and the BOOTRST sense per chip — the fuse byte
# index (HIGH everywhere but the m168A's EXTENDED) and the per-family # index (EXTENDED on the x8 line except the m328s' HIGH, HIGH elsewhere)
# ladders (Atmel-2486/2466/2503/8271/42719). Synthetic 'F' replies: only # and the per-family ladders (Atmel-2486/2466/2503/2545/8271/DS40002065/
# the boot byte carries meaning. # 8272/8011/2593/42719). Synthetic 'F' replies: only the boot byte
# carries meaning.
cases = ( cases = (
((0x1E, 0x93, 0x07), 0x2000, 3, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m8 ((0x1E, 0x93, 0x07), 0x2000, 3, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m8
((0x1E, 0x94, 0x03), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m16 ((0x1E, 0x94, 0x03), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m16
((0x1E, 0x95, 0x02), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m32 ((0x1E, 0x95, 0x02), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m32
((0x1E, 0x94, 0x06), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168A ((0x1E, 0x93, 0x0A), 0x2000, 2, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m88
((0x1E, 0x93, 0x0F), 0x2000, 2, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m88P
((0x1E, 0x94, 0x06), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168/168A
((0x1E, 0x94, 0x0B), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168P
((0x1E, 0x95, 0x14), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m328
((0x1E, 0x95, 0x0F), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m328P ((0x1E, 0x95, 0x0F), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m328P
((0x1E, 0x94, 0x0F), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m164A
((0x1E, 0x94, 0x0A), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m164P
((0x1E, 0x95, 0x15), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m324A
((0x1E, 0x96, 0x09), 0x10000, 3, {0b11: 0xFC00, 0b10: 0xF800, 0b01: 0xF000, 0b00: 0xE000}), # m644
((0x1E, 0x96, 0x0A), 0x10000, 3, {0b11: 0xFC00, 0b10: 0xF800, 0b01: 0xF000, 0b00: 0xE000}), # m644P
((0x1E, 0x97, 0x06), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284
((0x1E, 0x97, 0x05), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284P ((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: