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:
@@ -142,18 +142,34 @@ endif()
|
||||
# compile-time constant; a different PUREBOOT_TIMEOUT builds the re-timed
|
||||
# binary a self-update then installs.
|
||||
set(PUREBOOT_TIMEOUT 8 CACHE STRING "pureboot activation window, seconds")
|
||||
# Every mega runs the loader from its hardware boot section and boots the
|
||||
# application at word 0; the tinies get the trampoline surgery. All megas
|
||||
# assume a 16 MHz crystal at 115200 Bd; the tinies their internal RC at
|
||||
# 57600 Bd over the software UART.
|
||||
if(LIBAVR_MCU STREQUAL "attiny13a")
|
||||
# Per-family geometry. The boot-sectioned megas run the loader from the
|
||||
# hardware boot section and boot the application at word 0; the tinies and
|
||||
# the boot-section-less m48s get the trampoline surgery. All megas assume a
|
||||
# 16 MHz crystal at 115200 Bd; the tinies their internal RC at 57600 Bd over
|
||||
# the software UART. --pmem-wrap-around models AVR's modulo-flash PC where
|
||||
# the flash is big enough to need it (an rjmp reaches all of 4 KiB by
|
||||
# itself).
|
||||
if(LIBAVR_MCU MATCHES "^attiny13a?$")
|
||||
set(_pb_flash 1024)
|
||||
set(_pb_wrap "")
|
||||
set(_pb_page 32)
|
||||
set(_pb_hz 9600000)
|
||||
set(_pb_baud 57600)
|
||||
set(_pb_eeprom 64)
|
||||
set(_pb_limit 510)
|
||||
elseif(LIBAVR_MCU STREQUAL "attiny25")
|
||||
set(_pb_flash 2048)
|
||||
set(_pb_wrap "")
|
||||
set(_pb_page 32)
|
||||
set(_pb_hz 8000000)
|
||||
set(_pb_baud 57600)
|
||||
set(_pb_eeprom 128)
|
||||
elseif(LIBAVR_MCU STREQUAL "attiny45")
|
||||
set(_pb_flash 4096)
|
||||
set(_pb_wrap "")
|
||||
set(_pb_page 64)
|
||||
set(_pb_hz 8000000)
|
||||
set(_pb_baud 57600)
|
||||
set(_pb_eeprom 256)
|
||||
elseif(LIBAVR_MCU STREQUAL "attiny85")
|
||||
set(_pb_flash 8192)
|
||||
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
|
||||
@@ -161,40 +177,47 @@ elseif(LIBAVR_MCU STREQUAL "attiny85")
|
||||
set(_pb_hz 8000000)
|
||||
set(_pb_baud 57600)
|
||||
set(_pb_eeprom 512)
|
||||
set(_pb_limit 510)
|
||||
elseif(LIBAVR_MCU MATCHES "^atmega8a?$")
|
||||
elseif(LIBAVR_MCU MATCHES "^atmega48(a|p|pa)?$")
|
||||
set(_pb_flash 4096)
|
||||
set(_pb_wrap "")
|
||||
set(_pb_page 64)
|
||||
set(_pb_hz 16000000)
|
||||
set(_pb_baud 115200)
|
||||
set(_pb_eeprom 256)
|
||||
elseif(LIBAVR_MCU MATCHES "^atmega8a?$" OR LIBAVR_MCU MATCHES "^atmega88(a|p|pa)?$")
|
||||
set(_pb_flash 8192)
|
||||
set(_pb_wrap -Wl,--pmem-wrap-around=8k)
|
||||
set(_pb_page 64)
|
||||
set(_pb_hz 16000000)
|
||||
set(_pb_baud 115200)
|
||||
set(_pb_eeprom 512)
|
||||
set(_pb_limit 512)
|
||||
elseif(LIBAVR_MCU STREQUAL "atmega16")
|
||||
elseif(LIBAVR_MCU MATCHES "^atmega16a?$" OR LIBAVR_MCU MATCHES "^atmega168(a|p|pa)?$" OR
|
||||
LIBAVR_MCU MATCHES "^atmega164(a|p|pa)$")
|
||||
set(_pb_flash 16384)
|
||||
set(_pb_wrap -Wl,--pmem-wrap-around=16k)
|
||||
set(_pb_page 128)
|
||||
set(_pb_hz 16000000)
|
||||
set(_pb_baud 115200)
|
||||
set(_pb_eeprom 512)
|
||||
set(_pb_limit 512)
|
||||
elseif(LIBAVR_MCU MATCHES "^atmega32a?$")
|
||||
elseif(LIBAVR_MCU MATCHES "^atmega32a?$" OR LIBAVR_MCU MATCHES "^atmega328p?$" OR
|
||||
LIBAVR_MCU MATCHES "^atmega324(a|p|pa)$")
|
||||
set(_pb_flash 32768)
|
||||
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
|
||||
set(_pb_page 128)
|
||||
set(_pb_hz 16000000)
|
||||
set(_pb_baud 115200)
|
||||
set(_pb_eeprom 1024)
|
||||
set(_pb_limit 512)
|
||||
elseif(LIBAVR_MCU STREQUAL "atmega168a")
|
||||
set(_pb_flash 16384)
|
||||
set(_pb_wrap -Wl,--pmem-wrap-around=16k)
|
||||
set(_pb_page 128)
|
||||
elseif(LIBAVR_MCU MATCHES "^atmega644(a|p|pa)?$")
|
||||
# 64 KiB is exactly the 16-bit byte space: plain LPM reaches everything,
|
||||
# and the smallest boot section (1 KiB) holds the loader and its staging
|
||||
# slot together (see pureboot/README.md).
|
||||
set(_pb_flash 65536)
|
||||
set(_pb_wrap -Wl,--pmem-wrap-around=64k)
|
||||
set(_pb_page 256)
|
||||
set(_pb_hz 16000000)
|
||||
set(_pb_baud 115200)
|
||||
set(_pb_eeprom 512)
|
||||
set(_pb_limit 512)
|
||||
elseif(LIBAVR_MCU STREQUAL "atmega1284p")
|
||||
set(_pb_eeprom 2048)
|
||||
elseif(LIBAVR_MCU MATCHES "^atmega1284p?$")
|
||||
# 128 KiB: wire flash addresses are word addresses, reads go through
|
||||
# ELPM, and the PC's modulo wrap exceeds what --pmem-wrap-around models.
|
||||
# The slot is 1 KiB — this chip's own smallest boot sector; the far
|
||||
@@ -205,36 +228,35 @@ elseif(LIBAVR_MCU STREQUAL "atmega1284p")
|
||||
set(_pb_hz 16000000)
|
||||
set(_pb_baud 115200)
|
||||
set(_pb_eeprom 4096)
|
||||
set(_pb_limit 1024)
|
||||
set(_pb_slot 1024)
|
||||
set(_pb_limit 1024)
|
||||
else()
|
||||
set(_pb_flash 32768)
|
||||
set(_pb_wrap -Wl,--pmem-wrap-around=32k)
|
||||
set(_pb_page 128)
|
||||
set(_pb_hz 16000000)
|
||||
set(_pb_baud 115200)
|
||||
set(_pb_eeprom 1024)
|
||||
set(_pb_limit 512)
|
||||
message(FATAL_ERROR "pureboot: no geometry for ${LIBAVR_MCU}")
|
||||
endif()
|
||||
if(NOT DEFINED _pb_slot)
|
||||
set(_pb_slot 512)
|
||||
endif()
|
||||
math(EXPR _pb_base "${_pb_flash} - ${_pb_slot}")
|
||||
math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL)
|
||||
if(LIBAVR_MCU MATCHES "^atmega")
|
||||
# Patched-vector chips hand over through the trampoline word below the slot,
|
||||
# which is also the slot's own last word — their budget is slot − 2.
|
||||
if(LIBAVR_MCU MATCHES "^atmega" AND NOT LIBAVR_MCU MATCHES "^atmega48")
|
||||
set(_pb_app 0)
|
||||
if(NOT DEFINED _pb_limit)
|
||||
set(_pb_limit ${_pb_slot})
|
||||
endif()
|
||||
else()
|
||||
math(EXPR _pb_app "${_pb_base} - 2")
|
||||
math(EXPR _pb_limit "${_pb_slot} - 2")
|
||||
endif()
|
||||
|
||||
# simavr names its cores after the base dies; the A revisions run on them.
|
||||
# simavr names its cores after the base dies; the A revisions run on them
|
||||
# (the 644PA on the 644P core).
|
||||
set(_pb_sim_mcu ${LIBAVR_MCU})
|
||||
if(LIBAVR_MCU STREQUAL "atmega8a")
|
||||
set(_pb_sim_mcu atmega8)
|
||||
elseif(LIBAVR_MCU STREQUAL "atmega32a")
|
||||
set(_pb_sim_mcu atmega32)
|
||||
elseif(LIBAVR_MCU STREQUAL "atmega168a")
|
||||
set(_pb_sim_mcu atmega168)
|
||||
if(LIBAVR_MCU MATCHES "^atmega(8|16|32|48|88|164|168|644)a$")
|
||||
string(REGEX REPLACE "a$" "" _pb_sim_mcu ${LIBAVR_MCU})
|
||||
elseif(LIBAVR_MCU STREQUAL "atmega644pa")
|
||||
set(_pb_sim_mcu atmega644p)
|
||||
endif()
|
||||
|
||||
add_executable(pureboot pureboot/pureboot.cpp)
|
||||
|
||||
1296
CMakePresets.json
1296
CMakePresets.json
File diff suppressed because it is too large
Load Diff
@@ -2,13 +2,14 @@
|
||||
|
||||
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by
|
||||
constraint: one C++ source, no inline assembly, no global register variables
|
||||
(attributes allowed), built for every chip libavr targets, **fitting each
|
||||
chip's smallest boot sector**: 512 bytes everywhere — 488 B on the
|
||||
ATtiny13A, 502 B on the ATtiny85, 466–504 B across the megas — except the
|
||||
ATmega1284P, whose smallest boot sector is 1 KiB and whose far-flash
|
||||
(attributes allowed), built for **every chip libavr targets — all 37 —
|
||||
fitting each chip's smallest boot sector**: 512 bytes everywhere — 488 B on
|
||||
the tiny13s, 498–502 B on the tiny25/45/85, 466–504 B across the megas
|
||||
(474 B on the boot-section-less m48s, 498 B on the 644s) — except the
|
||||
ATmega1284/1284P, whose smallest boot sector is 1 KiB and whose far-flash
|
||||
machinery (ELPM reads, RAMPZ page commands, word-addressed wire) lands at
|
||||
558 B in a 1 KiB slot: the 512-byte figure is a hardware boundary that chip
|
||||
simply does not have, and no implementation of this feature set fits it
|
||||
558 B in a 1 KiB slot: the 512-byte figure is a hardware boundary those
|
||||
chips simply do not have, and no implementation of this feature set fits it
|
||||
there. The device speaks primitives; every composite — verify, erase,
|
||||
reset-vector surgery, updating the loader itself — lives in the host tool
|
||||
(`pureboot.py`).
|
||||
@@ -29,19 +30,20 @@ belongs to the host-managed trampoline (below).
|
||||
|
||||
| Chip | Serial | Baud | Clock assumed |
|
||||
|---|---|---|---|
|
||||
| every ATmega (8/8A, 16, 32/32A, 168A, 328P, 1284P) | the hardware USART (USART0), RXD/TXD per pinout | 115200 8N1 | 16 MHz crystal |
|
||||
| ATtiny85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC |
|
||||
| ATtiny13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC |
|
||||
| every ATmega | the hardware USART (USART0), RXD/TXD per pinout | 115200 8N1 | 16 MHz crystal |
|
||||
| ATtiny25/45/85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC |
|
||||
| ATtiny13/13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC |
|
||||
|
||||
The tiny RX pin has its pull-up enabled; TX idles high. All multi-byte
|
||||
quantities on the wire are little-endian.
|
||||
|
||||
## Activation
|
||||
|
||||
Reset enters the loader (BOOTRST on the mega, the patched reset vector on the
|
||||
tinies) — except a watchdog reset, which hands straight to the application
|
||||
(the application owns its watchdog; it must clear WDRF itself, which also
|
||||
releases the WDRF-forced WDE).
|
||||
Reset enters the loader (BOOTRST on the boot-sectioned megas; the patched
|
||||
reset vector on the tinies and the boot-section-less m48s) — except a
|
||||
watchdog reset, which hands straight to the application (the application
|
||||
owns its watchdog; it must clear WDRF itself, which also releases the
|
||||
WDRF-forced WDE).
|
||||
|
||||
The host then has one activation window per awaited byte to knock: `p` then
|
||||
`b`. Each awaited byte gets a fresh window; any other byte is discarded and
|
||||
@@ -61,9 +63,10 @@ write to finish and sends the prompt `+` (0x2b) — the prompt is therefore
|
||||
also the completion ack of the previous command. A session is: await `+`,
|
||||
send a command, read its reply, repeat.
|
||||
|
||||
On chips whose flash exceeds 64 KiB (the 1284P — info-block flag bit 1) the
|
||||
On chips whose flash exceeds 64 KiB (the 1284s — info-block flag bit 1) the
|
||||
`R`/`W` flash addresses are **word** addresses; everywhere else they are byte
|
||||
addresses. EEPROM addresses are always bytes, counts always bytes.
|
||||
addresses (the 644s' 64 KiB is exactly the 16-bit byte space and stays
|
||||
byte-addressed). EEPROM addresses are always bytes, counts always bytes.
|
||||
|
||||
| Cmd | Arguments | Reply |
|
||||
|---|---|---|
|
||||
@@ -108,16 +111,23 @@ write `0xff` (per page for flash, per byte for EEPROM).
|
||||
|
||||
## Deployment
|
||||
|
||||
**Megas**: program the loader at `flash − slot` with an external programmer.
|
||||
Every mega has a BOOTSZ step whose boot section is exactly the loader slot —
|
||||
512 B, the m8/16/168A's second-smallest step, the m32/328P's smallest; on
|
||||
the m1284P that step is its smallest, 512 words, which is why its slot is
|
||||
1 KiB — so the ATmega328P profiles below apply to every mega with its own
|
||||
addresses and slot size; the per-chip BOOTSZ ladders live in the host tool
|
||||
(`BOOT_FUSE`). The 1284P's numbers: standalone = BOOTSZ 512 words (reset at
|
||||
the loader base 0x1fc00); self-update = 1024 words, covering both 1 KiB
|
||||
slots, the loader-first reset landing at 0x1f800 — the staging slot, walked
|
||||
across when erased.
|
||||
**Boot-sectioned megas**: program the loader at `flash − slot` with an
|
||||
external programmer. Every such mega has a BOOTSZ step whose boot section
|
||||
is exactly the loader slot — 512 B, the second-smallest step on the 8 KiB
|
||||
and 16 KiB chips (m8, m88, m16, m168, m164), the smallest on the 32 KiB
|
||||
ones (m32, m328, m324); on the 1284s that step is the smallest, 512 words,
|
||||
which is why their slot is 1 KiB — so the ATmega328P profiles below apply
|
||||
to every one of them with its own addresses and slot size; the per-chip
|
||||
BOOTSZ ladders live in the host tool (`BOOT_FUSE`). The 1284s' numbers:
|
||||
standalone = BOOTSZ 512 words (reset at the loader base 0x1fc00);
|
||||
self-update = 1024 words, covering both 1 KiB slots, the loader-first
|
||||
reset landing at 0x1f800 — the staging slot, walked across when erased.
|
||||
|
||||
The **644s** are the geometry's sweet spot: their smallest boot section
|
||||
(512 words = 1 KiB) is exactly *two* 512-byte slots, so the resident and
|
||||
its staging slot both live inside the minimum section — self-update needs
|
||||
no fuse step up, and the standalone profile does not exist (reset lands at
|
||||
0xfc00, one erased slot below the loader: the loader-first walk built in).
|
||||
|
||||
ATmega328P profiles (addresses for its 32 KiB):
|
||||
|
||||
@@ -130,15 +140,18 @@ ATmega328P profiles (addresses for its 32 KiB):
|
||||
Applications are flashed unmodified — word 0 stays the application's own
|
||||
reset vector, and the hand-over jumps to 0.
|
||||
|
||||
**Tinies** (no boot section): program the loader at `flash − 512`; erased
|
||||
flash below it walks up into the loader, so a virgin chip activates. When
|
||||
flashing an application the host performs reset-vector surgery: word 0 is
|
||||
rewritten to `rjmp` to the loader base, and the application's own entry is
|
||||
re-encoded as a trampoline `rjmp` in the word just below the loader
|
||||
(`base − 2`, where the hand-over jumps). Every other vector stays the
|
||||
application's. The patched page 0 and the trampoline page are written
|
||||
*first*, so from the first write on an interrupted flash still resets into
|
||||
the loader; an erase runs top-down for the same reason.
|
||||
**Patched-vector chips — the tinies and the m48s** (no boot section; the
|
||||
m48s' SPM runs from the entire flash, Atmel-8271 §26): program the loader
|
||||
at `flash − 512`; erased flash below it walks up into the loader, so a
|
||||
virgin chip activates. When flashing an application the host performs
|
||||
reset-vector surgery: word 0 is rewritten to `rjmp` to the loader base, and
|
||||
the application's own entry is re-encoded as a trampoline `rjmp` in the
|
||||
word just below the loader (`base − 2`, where the hand-over jumps). Every
|
||||
other vector stays the application's. The patched page 0 and the trampoline
|
||||
page are written *first*, so from the first write on an interrupted flash
|
||||
still resets into the loader; an erase runs top-down for the same reason.
|
||||
The m48s speak this profile over their hardware USART — no fuse preflight,
|
||||
BOOTRST does not exist there.
|
||||
|
||||
## Updating the loader
|
||||
|
||||
@@ -148,16 +161,18 @@ loader itself as its own staging loader. The image is the loader's own 512
|
||||
bytes as a raw binary, or the Intel HEX the build emits beside it, which
|
||||
links the loader at its base inside an otherwise blank flash image:
|
||||
|
||||
1. The staging slot `[base−512, base)` is saved to a host-side state file
|
||||
(on the 1 KB tiny13A that is the whole application, vectors included).
|
||||
2. The resident installs the identical update image there. On the tinies the
|
||||
host composes the slot's last word — the same address as the resident's
|
||||
trampoline — as a jump to the resident base, so even an abandoned staging
|
||||
copy times out into a loader, never into garbage.
|
||||
1. The staging slot `[base−slot, base)` is saved to a host-side state file
|
||||
(on the 1 KB tiny13s that is the whole application, vectors included).
|
||||
2. The resident installs the identical update image there. On the
|
||||
patched-vector chips the host composes the slot's last word — the same
|
||||
address as the resident's trampoline — as a jump to the resident base,
|
||||
so even an abandoned staging copy times out into a loader, never into
|
||||
garbage.
|
||||
3. `J` enters the staging copy, which rewrites the resident slot. On the
|
||||
t85 the host first re-aims word 0 at the staging copy, so a power loss
|
||||
mid-rewrite still resets into a loader; on the t13a the staging slot
|
||||
carries the reset vector itself.
|
||||
patched-vector chips whose staging slot sits away from page 0 the host
|
||||
first re-aims word 0 at the staging copy, so a power loss mid-rewrite
|
||||
still resets into a loader; on the tiny13s the staging slot carries the
|
||||
reset vector itself.
|
||||
4. `J` enters the new resident, which restores the staging slot's saved
|
||||
content (word 0 and the trampoline with it) and the state file is
|
||||
discarded.
|
||||
@@ -166,9 +181,10 @@ Every phase is idempotent and keyed off the actual flash state: re-running
|
||||
the same command after any interruption resumes and completes. The state
|
||||
file carries the only bytes not recoverable from the device; if it is lost
|
||||
mid-update the update still completes, and the staging region is restored by
|
||||
reflashing the application. The mega needs its fuses for the preflight
|
||||
(BOOTSZ gate, profile notes) — read from the device, or supplied with
|
||||
`--assume-fuses` where reading is impossible (simulators).
|
||||
reflashing the application. A boot-sectioned mega needs its fuses for the
|
||||
preflight (BOOTSZ gate, profile notes) — read from the device, or supplied
|
||||
with `--assume-fuses` where reading is impossible (simulators); the
|
||||
patched-vector chips need none.
|
||||
|
||||
## Host tool
|
||||
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
// resident — how pureboot updates itself, host-driven, with no other
|
||||
// firmware involved.
|
||||
//
|
||||
// Entry: reset lands in avr::startup::entry below (BOOTRST on the mega; the
|
||||
// patched reset vector — or erased flash walking up into the loader — on the
|
||||
// tinies). A watchdog reset hands straight to the application. Otherwise the
|
||||
// Entry: reset lands in avr::startup::entry below (BOOTRST on the
|
||||
// boot-sectioned megas; the patched reset vector — or erased flash walking
|
||||
// up into the loader — on the tinies and the boot-section-less m48s). A
|
||||
// watchdog reset hands straight to the application. Otherwise the
|
||||
// host has one activation window per awaited knock byte ("pb"); an idle line
|
||||
// boots the application. A session then stays in the command loop until 'J'
|
||||
// jumps away or the chip resets.
|
||||
@@ -44,9 +45,10 @@ constexpr std::uint8_t ack = '+';
|
||||
// from code.
|
||||
consteval avr::hertz_t clock()
|
||||
{
|
||||
if (avr::hw::db.name == "ATtiny13A")
|
||||
auto name = std::string_view{avr::hw::db.name};
|
||||
if (name.starts_with("ATtiny13"))
|
||||
return 9.6_MHz;
|
||||
if (avr::hw::db.name == "ATtiny85")
|
||||
if (name.starts_with("ATtiny"))
|
||||
return 8_MHz;
|
||||
return 16_MHz;
|
||||
}
|
||||
@@ -62,21 +64,19 @@ consteval std::int16_t wdrf_field()
|
||||
|
||||
// Geometry: the resident loader owns the top slot of flash — 512 bytes,
|
||||
// except on the >64 KiB chips whose own smallest boot sector is 1 KiB (the
|
||||
// 1284P): there the slot is 1 KiB, matching the hardware boundary the
|
||||
// 1284s): there the slot is 1 KiB, matching the hardware boundary the
|
||||
// 512-byte figure comes from everywhere else. The word below the slot is
|
||||
// the trampoline (the application's relocated reset vector) on chips
|
||||
// without a hardware boot section. The RWWSRE bit marks a separate boot
|
||||
// section — on classic AVR the two capabilities coincide (the m8/m32 packs
|
||||
// spell its register SPMCR).
|
||||
// without a hardware boot section — the tinies and the m48s, whose SPM
|
||||
// runs from anywhere (Atmel-8271 §26). A boot section also means the CPU
|
||||
// runs on while the RWW section programs; everywhere else it halts through
|
||||
// the operation. The m48s still carry RWWSRE as their temporary-buffer
|
||||
// discard (§26.2), so the discard picks by that bit, not by the section.
|
||||
constexpr std::uint16_t slot_bytes = spm::flash_bytes > 65536 ? 1024 : 512;
|
||||
constexpr std::uint32_t base = spm::flash_bytes - slot_bytes;
|
||||
constexpr std::uint16_t page = spm::page_bytes;
|
||||
constexpr bool boot_section = [] {
|
||||
for (auto reg : {"SPMCSR", "SPMCR"})
|
||||
if (avr::hw::db.field_index(reg, "RWWSRE") >= 0)
|
||||
return true;
|
||||
return false;
|
||||
}();
|
||||
constexpr bool boot_section = avr::hw::curated::has_boot_section();
|
||||
constexpr bool rww_discard = spm::detail::has_rww();
|
||||
|
||||
// Past 64 KiB a byte address no longer fits the wire's 16 bits, so on the
|
||||
// large chips every flash address on the wire — and all slot arithmetic —
|
||||
@@ -85,7 +85,8 @@ constexpr bool boot_section = [] {
|
||||
// 2 x 256 words), so the slot index is the high byte with its low bit
|
||||
// dropped everywhere.
|
||||
constexpr bool word_flash = spm::flash_bytes > 65536;
|
||||
constexpr std::uint16_t wire_base = word_flash ? static_cast<std::uint16_t>(base / 2) : static_cast<std::uint16_t>(base);
|
||||
constexpr std::uint16_t wire_base =
|
||||
word_flash ? static_cast<std::uint16_t>(base / 2) : static_cast<std::uint16_t>(base);
|
||||
constexpr std::uint16_t wire_page_mask = word_flash ? (page / 2 - 1) : (page - 1);
|
||||
|
||||
// The activation window, in seconds, is a compile-time constant (the build
|
||||
@@ -332,9 +333,10 @@ void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
|
||||
{
|
||||
// A buffer word cannot be loaded twice without an erase (§26.2.1), so a
|
||||
// refused page's drained data must not linger for the next write:
|
||||
// discard the buffer up front — CTPB on the tinies; on the mega writing
|
||||
// RWWSRE aborts a pending load (§26.2.2).
|
||||
if constexpr (boot_section)
|
||||
// discard the buffer up front — CTPB on the tinies; on the megas
|
||||
// writing RWWSRE aborts a pending load (§26.2.2 — on the m48s that
|
||||
// flush is the bit's whole documented job).
|
||||
if constexpr (rww_discard)
|
||||
spm::rww_enable<off>();
|
||||
else
|
||||
spm::clear_buffer<off>();
|
||||
@@ -376,8 +378,9 @@ void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
|
||||
page_high = static_cast<std::uint8_t>(address >> 8) & 0xfe;
|
||||
}
|
||||
if (page_high != slot_high) {
|
||||
// The tinies halt the CPU through the erase and the write, so only
|
||||
// the megas — running on while their RWW section programs — wait.
|
||||
// The tinies and the m48s halt the CPU through the erase and the
|
||||
// write, so only the boot-sectioned megas — running on while their
|
||||
// RWW section programs — wait.
|
||||
spm::erase_page<off>(address);
|
||||
if constexpr (boot_section)
|
||||
spm::wait();
|
||||
@@ -416,8 +419,8 @@ void send_fuses()
|
||||
// program_flash refuses this one slot and the info block is addressed
|
||||
// from it, so both follow wherever the code was flashed.
|
||||
const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
|
||||
const std::uint8_t slot_high = word_flash ? static_cast<std::uint8_t>(ra_words >> 8) & 0xfe
|
||||
: static_cast<std::uint8_t>((ra_words >> 8) << 1);
|
||||
const std::uint8_t slot_high =
|
||||
word_flash ? static_cast<std::uint8_t>(ra_words >> 8) & 0xfe : static_cast<std::uint8_t>((ra_words >> 8) << 1);
|
||||
|
||||
// The knock: 'p' then 'b', each under a fresh window; any other byte is
|
||||
// line noise and waits again. Falling out of a window runs the app.
|
||||
|
||||
@@ -518,15 +518,34 @@ def covered(pages, info, skip_blank):
|
||||
|
||||
# Per-chip boot fuse geometry, keyed by the signature's family/part bytes:
|
||||
# which byte of the 'F' reply (low, lock, extended, high) carries BOOTSZ/
|
||||
# BOOTRST, and the BOOTSZ->words ladder. Sources: Atmel-2486/2466/2503
|
||||
# (HIGH fuse), Atmel-8271 (m168A: EXTENDED; m328P: HIGH), Atmel-42719.
|
||||
# BOOTRST, and the BOOTSZ->words ladder. A die revision shares its base
|
||||
# signature, so one row covers it. The m48s have no boot section and no
|
||||
# row — their info block says patch-vector and this table is never
|
||||
# consulted. Sources: Atmel-2486/2466/2503 (HIGH fuse), Atmel-2545/8271/
|
||||
# DS40002065 (x8: EXTENDED, except the m328s' HIGH), Atmel-8272/8011/2593/
|
||||
# 42719 (x4: HIGH).
|
||||
_LADDER_128 = {0b11: 128, 0b10: 256, 0b01: 512, 0b00: 1024}
|
||||
_LADDER_256 = {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}
|
||||
_LADDER_512 = {0b11: 512, 0b10: 1024, 0b01: 2048, 0b00: 4096}
|
||||
BOOT_FUSE = {
|
||||
bytes((0x93, 0x07)): (3, {0b11: 128, 0b10: 256, 0b01: 512, 0b00: 1024}), # m8/8A
|
||||
bytes((0x94, 0x03)): (3, {0b11: 128, 0b10: 256, 0b01: 512, 0b00: 1024}), # m16
|
||||
bytes((0x95, 0x02)): (3, {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}), # m32/32A
|
||||
bytes((0x94, 0x06)): (2, {0b11: 128, 0b10: 256, 0b01: 512, 0b00: 1024}), # m168A
|
||||
bytes((0x95, 0x0F)): (3, {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}), # m328P
|
||||
bytes((0x97, 0x05)): (3, {0b11: 512, 0b10: 1024, 0b01: 2048, 0b00: 4096}), # 1284P
|
||||
bytes((0x93, 0x07)): (3, _LADDER_128), # m8/8A
|
||||
bytes((0x94, 0x03)): (3, _LADDER_128), # m16/16A
|
||||
bytes((0x95, 0x02)): (3, _LADDER_256), # m32/32A
|
||||
bytes((0x93, 0x0A)): (2, _LADDER_128), # m88/88A
|
||||
bytes((0x93, 0x0F)): (2, _LADDER_128), # m88P/88PA
|
||||
bytes((0x94, 0x06)): (2, _LADDER_128), # m168/168A
|
||||
bytes((0x94, 0x0B)): (2, _LADDER_128), # m168P/168PA
|
||||
bytes((0x95, 0x14)): (3, _LADDER_256), # m328
|
||||
bytes((0x95, 0x0F)): (3, _LADDER_256), # m328P
|
||||
bytes((0x94, 0x0F)): (3, _LADDER_128), # m164A
|
||||
bytes((0x94, 0x0A)): (3, _LADDER_128), # m164P/164PA
|
||||
bytes((0x95, 0x15)): (3, _LADDER_256), # m324A
|
||||
bytes((0x95, 0x08)): (3, _LADDER_256), # m324P
|
||||
bytes((0x95, 0x11)): (3, _LADDER_256), # m324PA
|
||||
bytes((0x96, 0x09)): (3, _LADDER_512), # m644/644A
|
||||
bytes((0x96, 0x0A)): (3, _LADDER_512), # m644P/644PA
|
||||
bytes((0x97, 0x06)): (3, _LADDER_512), # m1284
|
||||
bytes((0x97, 0x05)): (3, _LADDER_512), # m1284P
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -17,17 +17,17 @@ namespace {
|
||||
|
||||
consteval avr::hertz_t clock()
|
||||
{
|
||||
if (avr::hw::db.name == "ATtiny13A")
|
||||
auto name = std::string_view{avr::hw::db.name};
|
||||
if (name.starts_with("ATtiny13"))
|
||||
return 9.6_MHz;
|
||||
if (avr::hw::db.name == "ATtiny85")
|
||||
if (name.starts_with("ATtiny"))
|
||||
return 8_MHz;
|
||||
return 16_MHz;
|
||||
}
|
||||
|
||||
using dev = avr::device<{.clock = clock()}>;
|
||||
|
||||
template <avr::hertz_t C,
|
||||
bool Hardware = avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART")>
|
||||
template <avr::hertz_t C, bool Hardware = avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART")>
|
||||
struct link {
|
||||
using tx_t = avr::uart::usart0<C, {.baud = 115200_Bd, .max_baud_error = 2.5_pct}>;
|
||||
static void tx(char c)
|
||||
|
||||
@@ -11,7 +11,11 @@ class Device:
|
||||
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None):
|
||||
cmd = [binary, elf, mcu, hz, base_hex, str(page), str(baud), dump]
|
||||
if reset_hex is not None or resume is not None:
|
||||
cmd.append(reset_hex if reset_hex is not None else ("0" if not mcu.startswith("atmega") else base_hex))
|
||||
# Chips without a hardware boot section — the tinies and the
|
||||
# m48s — reset to address 0 like silicon; the boot-sectioned
|
||||
# megas re-vector to the loader base (BOOTRST).
|
||||
patch = not mcu.startswith("atmega") or mcu.startswith("atmega48")
|
||||
cmd.append(reset_hex if reset_hex is not None else ("0" if patch else base_hex))
|
||||
if resume is not None:
|
||||
cmd.append(resume)
|
||||
self.log = open(dump + ".log", "a")
|
||||
|
||||
@@ -91,12 +91,14 @@ def main():
|
||||
read_eeprom = os.path.join(workdir, "readback_eeprom.bin")
|
||||
|
||||
# The geometry the host will discover, for computing the expected image:
|
||||
# megas carry a boot section (no vector surgery), the large ones speak
|
||||
# word addresses, and the page byte is the wire's 0-means-256.
|
||||
# the boot-sectioned megas need no vector surgery (the tinies and the
|
||||
# boot-section-less m48s do), the large chips speak word addresses, and
|
||||
# the page byte is the wire's 0-means-256.
|
||||
mega = mcu.startswith("atmega")
|
||||
patch = not mega or mcu.startswith("atmega48")
|
||||
word_flash = base + 512 > 0x10000
|
||||
wire_base = base // 2 if word_flash else base
|
||||
flags = (0 if mega else 1) | (2 if word_flash else 0)
|
||||
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||
info = pb.Info(
|
||||
bytes([ord("P"), ord("B"), 1, 0, 0, 0, page & 0xFF])
|
||||
+ bytes([wire_base & 0xFF, wire_base >> 8, eeprom_size & 0xFF, eeprom_size >> 8])
|
||||
@@ -156,9 +158,9 @@ def main():
|
||||
fail("loader region looks erased in the ground-truth dump")
|
||||
|
||||
# The surgery, decoded independently: the patched vector must land on the
|
||||
# loader, the trampoline on the application's own entry (tinies only —
|
||||
# the megas' word 0 stays the application's).
|
||||
if not mega:
|
||||
# loader, the trampoline on the application's own entry (patched-vector
|
||||
# chips only — a boot-sectioned mega's word 0 stays the application's).
|
||||
if patch:
|
||||
flash_words = (base + 512) // 2
|
||||
app = open(app_bin, "rb").read()
|
||||
word0 = flash_true[0] | (flash_true[1] << 8)
|
||||
|
||||
@@ -5,12 +5,13 @@ replaces itself with a re-timed build through the host tool's
|
||||
killing the simulated device mid-write, restarting it from its flash dump,
|
||||
and letting a re-run complete the update.
|
||||
|
||||
The mega runs the BOOTRST-unprogrammed profile (reset boots the application;
|
||||
the fixture application's 'L' jump is the application-owned loader entry),
|
||||
with --assume-fuses standing in for the fuse read simavr cannot model. The
|
||||
tinies reset into a loader at every phase by construction — the t13a because
|
||||
its staging slot carries the reset vector itself, the t85 through the word-0
|
||||
redirect the tool plants around the resident rewrite.
|
||||
The boot-sectioned megas run the BOOTRST-unprogrammed profile (reset boots
|
||||
the application; the fixture application's 'L' jump is the application-owned
|
||||
loader entry), with --assume-fuses standing in for the fuse read simavr
|
||||
cannot model. The patched-vector chips — the tinies and the m48s — reset
|
||||
into a loader at every phase by construction: the t13a because its staging
|
||||
slot carries the reset vector itself, the others through the word-0 redirect
|
||||
the tool plants around the resident rewrite.
|
||||
|
||||
Usage: pbupdate.py <device_bin> <pureboot_elf> <update_elf> <mcu> <hz>
|
||||
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
|
||||
@@ -89,8 +90,14 @@ def main():
|
||||
(device_bin, elf, update_elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:]
|
||||
base, page, baud = int(base_hex, 0), int(page), int(baud)
|
||||
mega = mcu.startswith("atmega")
|
||||
slot = 1024 if base + 1024 > 0x10000 and mega else 512 # word-addressed chips use the 1 KiB slot
|
||||
reset_hex = "0" if mega else None # the mega runs BOOTRST-unprogrammed here
|
||||
# The m48s are megas without a boot section: patched vector, no fuse
|
||||
# preflight, and the same reset-to-0 the tinies get.
|
||||
patch = not mega or mcu.startswith("atmega48")
|
||||
# Word-addressed (>64 KiB) chips use the 1 KiB slot; their loader base
|
||||
# itself sits beyond the 16-bit byte space — the 644's base + slot only
|
||||
# touches the 64 KiB boundary and stays byte-addressed.
|
||||
slot = 1024 if base >= 0x10000 and mega else 512
|
||||
reset_hex = "0" if mega else None # the boot-sectioned mega runs BOOTRST-unprogrammed here
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import pbsim
|
||||
@@ -107,7 +114,7 @@ def main():
|
||||
fail("the update image is byte-identical to the resident build")
|
||||
dump = os.path.join(workdir, "dump.bin")
|
||||
state = os.path.join(workdir, "update.pbstate")
|
||||
fuses = assumed_fuses(pb, images["v0"]) if mega else None
|
||||
fuses = assumed_fuses(pb, images["v0"]) if mega and not patch else None
|
||||
|
||||
def connect(device):
|
||||
port = pb.Port(device.pty, baud)
|
||||
@@ -147,7 +154,7 @@ def main():
|
||||
|
||||
# A clean CLI update, resident -> v9.
|
||||
args = ["--update-loader", os.path.join(workdir, "v9.bin"), "--state", state, "--stay"]
|
||||
if mega:
|
||||
if fuses:
|
||||
args += ["--assume-fuses", fuses.hex()]
|
||||
out = pbsim.run_tool(tool, device.pty, baud, *args)
|
||||
if "loader updated" not in out:
|
||||
@@ -172,7 +179,7 @@ def main():
|
||||
# cost of that profile (README).
|
||||
for kill_region, kill_hits, kill_device in (
|
||||
("stage", 2, True),
|
||||
("resident", 1, not mega),
|
||||
("resident", 1, patch),
|
||||
("stage_restore", 2, True),
|
||||
):
|
||||
device.reset() # the previous round left the application running
|
||||
@@ -203,11 +210,11 @@ def main():
|
||||
device.stop()
|
||||
|
||||
# Ground truth: the simulator's own flash against the final state, and
|
||||
# on the tinies an independent decode of the reset routing.
|
||||
# on the patched-vector chips an independent decode of the reset routing.
|
||||
flash = open(dump, "rb").read()
|
||||
if flash[base : base + slot] != padded(images[final]):
|
||||
fail("ground-truth resident region does not match the final image")
|
||||
if not mega:
|
||||
if patch:
|
||||
flash_words = (base + slot) // 2
|
||||
word0 = flash[0] | (flash[1] << 8)
|
||||
if rjmp_decode(word0, 0, flash_words) != base // 2:
|
||||
|
||||
@@ -49,6 +49,13 @@ static volatile sig_atomic_t reset_requested;
|
||||
// anywhere inside the page wipes half the neighbouring page in simulation
|
||||
// only. Wrap the mega's registered flash ioctl and re-dispatch page erases
|
||||
// with Z forced to the page boundary; everything else passes through.
|
||||
//
|
||||
// A second gap on the boot-section-less m48s: their RWWSRE bit is the
|
||||
// temporary-buffer discard (Atmel-8271 §26.2/§26.3.1), but the stock model
|
||||
// gates its RWWSRE branch on AVR_SELFPROG_HAVE_RWW — absent on the m48
|
||||
// core — so the discard store falls through into the buffer-fill branch and
|
||||
// plants whatever Z/R1:R0 happen to hold. Perform the silicon's discard
|
||||
// here instead.
|
||||
static avr_flash_t *mega_flash;
|
||||
static int (*mega_flash_ioctl)(avr_io_t *io, uint32_t ctl, void *param);
|
||||
|
||||
@@ -64,6 +71,15 @@ static int fixed_flash_ioctl(avr_io_t *io, uint32_t ctl, void *param)
|
||||
io->avr->data[31] = (uint8_t)(z >> 8);
|
||||
return result;
|
||||
}
|
||||
if (ctl == AVR_IOCTL_FLASH_SPM && !(mega_flash->flags & AVR_SELFPROG_HAVE_RWW) &&
|
||||
(io->avr->data[mega_flash->r_spm] & 0x11) == 0x11) { // RWWSRE|SELFPRGEN: the m48 buffer discard
|
||||
for (int i = 0; i < mega_flash->spm_pagesize / 2; i++) {
|
||||
mega_flash->tmppage[i] = 0xffff;
|
||||
mega_flash->tmppage_used[i] = 0;
|
||||
}
|
||||
avr_regbit_clear(io->avr, mega_flash->selfprgen);
|
||||
return 0;
|
||||
}
|
||||
return mega_flash_ioctl(io, ctl, param);
|
||||
}
|
||||
|
||||
@@ -306,11 +322,13 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
memcpy(avr->flash + base, fw.flash, fw.flashsize);
|
||||
}
|
||||
// The mega enters the loader in hardware (BOOTRST, not modeled — the
|
||||
// argument picks the modeled fuse's target); the tinies reset to word 0
|
||||
// like silicon — erased flash walks up into the loader, and after the
|
||||
// host's surgery the patched vector routes there.
|
||||
reset_pc = argc > 8 ? (uint32_t)strtoul(argv[8], NULL, 0) : (use_uart_pty ? base : 0);
|
||||
// The boot-sectioned megas enter the loader in hardware (BOOTRST, not
|
||||
// modeled — the argument picks the modeled fuse's target); the tinies
|
||||
// and the boot-section-less m48s reset to word 0 like silicon — erased
|
||||
// flash walks up into the loader, and after the host's surgery the
|
||||
// patched vector routes there.
|
||||
int boot_section = use_uart_pty && strncmp(mcu_name, "atmega48", 8) != 0;
|
||||
reset_pc = argc > 8 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? base : 0);
|
||||
avr->pc = reset_pc;
|
||||
avr->codeend = avr->flashend;
|
||||
|
||||
|
||||
@@ -56,15 +56,26 @@ def main():
|
||||
mega = info_of(pb, 0x7E00, 128, False, 0x8000, signature=(0x1E, 0x95, 0x0F))
|
||||
|
||||
# mega_boot: BOOTSZ words and the BOOTRST sense per chip — the fuse byte
|
||||
# index (HIGH everywhere but the m168A's EXTENDED) and the per-family
|
||||
# ladders (Atmel-2486/2466/2503/8271/42719). Synthetic 'F' replies: only
|
||||
# the boot byte carries meaning.
|
||||
# index (EXTENDED on the x8 line except the m328s' HIGH, HIGH elsewhere)
|
||||
# and the per-family ladders (Atmel-2486/2466/2503/2545/8271/DS40002065/
|
||||
# 8272/8011/2593/42719). Synthetic 'F' replies: only the boot byte
|
||||
# carries meaning.
|
||||
cases = (
|
||||
((0x1E, 0x93, 0x07), 0x2000, 3, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m8
|
||||
((0x1E, 0x94, 0x03), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m16
|
||||
((0x1E, 0x95, 0x02), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m32
|
||||
((0x1E, 0x94, 0x06), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168A
|
||||
((0x1E, 0x93, 0x0A), 0x2000, 2, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m88
|
||||
((0x1E, 0x93, 0x0F), 0x2000, 2, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m88P
|
||||
((0x1E, 0x94, 0x06), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168/168A
|
||||
((0x1E, 0x94, 0x0B), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168P
|
||||
((0x1E, 0x95, 0x14), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m328
|
||||
((0x1E, 0x95, 0x0F), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m328P
|
||||
((0x1E, 0x94, 0x0F), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m164A
|
||||
((0x1E, 0x94, 0x0A), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m164P
|
||||
((0x1E, 0x95, 0x15), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m324A
|
||||
((0x1E, 0x96, 0x09), 0x10000, 3, {0b11: 0xFC00, 0b10: 0xF800, 0b01: 0xF000, 0b00: 0xE000}), # m644
|
||||
((0x1E, 0x96, 0x0A), 0x10000, 3, {0b11: 0xFC00, 0b10: 0xF800, 0b01: 0xF000, 0b00: 0xE000}), # m644P
|
||||
((0x1E, 0x97, 0x06), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284
|
||||
((0x1E, 0x97, 0x05), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284P
|
||||
)
|
||||
for signature, flash, which, ladder in cases:
|
||||
|
||||
Reference in New Issue
Block a user