Compare commits
7 Commits
29b9e47d3d
...
ffdface1a8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffdface1a8 | ||
|
|
17ee8bd2fe | ||
|
|
c37aeeede9 | ||
|
|
dc4ec4b79e | ||
|
|
e807268dba | ||
|
|
495478771e | ||
|
|
7fb77d04c3 |
@@ -233,11 +233,11 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
# The size matrix: every configuration axis that could move the image
|
||||
# size — the serial backend (different code), the USART instance
|
||||
# (different registers), the clock (different constants), and the baud
|
||||
# through the two shapes its bit timing takes — each combination must
|
||||
# still fit the chip's slot budget. Pins are size-neutral (port and bit
|
||||
# are immediate operands) and the timeout is a constant, so neither adds
|
||||
# an axis. The stock build is one point of this matrix and already has
|
||||
# its test.
|
||||
# through the shapes its bit timing takes — each combination must still
|
||||
# fit the chip's slot budget. Pins are size-neutral (port and bit are
|
||||
# immediate operands) and the timeout is a constant, so neither adds an
|
||||
# axis. The stock build is one point of this matrix and already has its
|
||||
# test.
|
||||
function(pureboot_size_variant name)
|
||||
pureboot_add_loader(${name} ${ARGN})
|
||||
add_test(NAME ${name}.size
|
||||
@@ -245,14 +245,67 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
||||
endfunction()
|
||||
|
||||
# One point of the exhaustive matrix, named from its resolved parameters
|
||||
# so the enumeration cannot collide with itself. Unreachable rates drop
|
||||
# out here rather than aborting the configure.
|
||||
function(pureboot_matrix_point hz baud link)
|
||||
if(link STREQUAL "software")
|
||||
pureboot_baud_feasible(${hz} ${baud} 1 _ok)
|
||||
set(_args SERIAL software)
|
||||
else()
|
||||
pureboot_baud_feasible(${hz} ${baud} 0 _ok)
|
||||
set(_args USART ${link})
|
||||
endif()
|
||||
if(_ok)
|
||||
pureboot_size_variant(pbm_${hz}_${baud}_${link} CLOCK ${hz} BAUD ${baud} ${_args})
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Clock points: the shipped-fuse floor (CKDIV8), the calibrated RC, and
|
||||
# the crystal the stock build assumes (the tiny13's ladder is its own RC
|
||||
# menu — it has no crystal option).
|
||||
if(LIBAVR_MCU MATCHES "^attiny13")
|
||||
set(_matrix_clocks 1200000 4800000 9600000)
|
||||
set(_full_clocks 128000 600000 1200000 4800000 9600000)
|
||||
else()
|
||||
set(_matrix_clocks 1000000 8000000 16000000)
|
||||
set(_full_clocks 128000 1000000 1843200 2000000 3686400 4000000 7372800 8000000
|
||||
11059200 12000000 14745600 16000000 18432000 20000000)
|
||||
endif()
|
||||
|
||||
# The exhaustive cross product: every clock a deployment plausibly runs
|
||||
# — the internal oscillators, the shipped CKDIV8 floor, the plain
|
||||
# crystals and the UART crystals — against every rate, against every
|
||||
# backend. Beyond the ladder the list carries the slow rates a
|
||||
# sub-megahertz oscillator is left with, which no ladder rate reaches
|
||||
# (16000 Bd is the only rate the 128 kHz oscillator holds exactly); at
|
||||
# the fast clocks those same rates also select the software UART's
|
||||
# 16-bit _delay_loop_2 bit spin (two words more setup at each of its five
|
||||
# sites), the largest image the space produces and a shape the ladder
|
||||
# default — always the *fastest* rate a clock reaches — never picks.
|
||||
#
|
||||
# Bounded to one chip per size-bearing class: flash addressing (the
|
||||
# word-addressed 1284), hand-over shape (the patched vector on the tinies
|
||||
# and m48s), page size, and USART inventory. Everything else in the image
|
||||
# is chip-independent code, so a further chip buys builds and no
|
||||
# coverage; every chip outside the set carries the compact matrix.
|
||||
get_property(_full_bauds GLOBAL PROPERTY PUREBOOT_BAUD_LADDER)
|
||||
list(APPEND _full_bauds 16000 4800 2400 1200)
|
||||
set(_matrix_spot attiny13a attiny85 atmega48pa atmega8a atmega168pa
|
||||
atmega328p atmega164a atmega644a atmega1284p)
|
||||
if(DEFINED ENV{PUREBOOT_FULL_MATRIX} AND LIBAVR_MCU IN_LIST _matrix_spot)
|
||||
foreach(_matrix_hz IN LISTS _full_clocks)
|
||||
foreach(_matrix_baud IN LISTS _full_bauds)
|
||||
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} software)
|
||||
if(PUREBOOT_HAS_USART)
|
||||
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 0)
|
||||
endif()
|
||||
if(PUREBOOT_HAS_USART1)
|
||||
pureboot_matrix_point(${_matrix_hz} ${_matrix_baud} 1)
|
||||
endif()
|
||||
endforeach()
|
||||
endforeach()
|
||||
else()
|
||||
foreach(_matrix_hz IN LISTS _matrix_clocks)
|
||||
math(EXPR _matrix_khz "${_matrix_hz} / 1000")
|
||||
if(PUREBOOT_HAS_USART OR NOT _matrix_hz EQUAL _pb_stock_hz)
|
||||
@@ -265,22 +318,13 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
pureboot_size_variant(pureboot_usart1_${_matrix_khz}k CLOCK ${_matrix_hz} USART 1)
|
||||
endif()
|
||||
endforeach()
|
||||
list(GET _matrix_clocks -1 _matrix_top_hz)
|
||||
pureboot_size_variant(pureboot_sw_wide CLOCK ${_matrix_top_hz} BAUD 9600 SERIAL software)
|
||||
endif()
|
||||
if(PUREBOOT_HAS_USART1)
|
||||
pureboot_size_variant(pureboot_usart1 USART 1)
|
||||
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
|
||||
# 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
|
||||
|
||||
@@ -146,16 +146,19 @@ set(PUREBOOT_HAS_USART ${_pb_has_usart} PARENT_SCOPE)
|
||||
set(PUREBOOT_HAS_USART1 ${_pb_has_usart1} PARENT_SCOPE)
|
||||
set(PUREBOOT_SIM_MCU ${_pb_sim_mcu} PARENT_SCOPE)
|
||||
|
||||
# The fastest standard rate the clock reaches within 2.5 %, by the same
|
||||
# best-of-U2X-and-plain divisor search libavr's solve_baud runs, so a default
|
||||
# The rates a default may pick, fastest first.
|
||||
set_property(GLOBAL PROPERTY PUREBOOT_BAUD_LADDER 115200 57600 38400 19200 9600)
|
||||
|
||||
# Whether <baud> is reachable from <clock> within 2.5 %, by the same
|
||||
# best-of-U2X-and-plain divisor search libavr's solve_baud runs, so a build
|
||||
# never trips the compile-time error it is checked against. A software build
|
||||
# also needs the polled receiver's 100-cycles-a-bit floor: at low clocks the
|
||||
# U2X divisor reaches rates the bit-banged sampler cannot.
|
||||
function(pureboot_default_baud clock software outvar)
|
||||
foreach(baud 115200 57600 38400 19200 9600)
|
||||
function(pureboot_baud_feasible clock baud software outvar)
|
||||
set(${outvar} 0 PARENT_SCOPE)
|
||||
math(EXPR _cycles "${clock} / ${baud}")
|
||||
if(software AND _cycles LESS 100)
|
||||
continue()
|
||||
return()
|
||||
endif()
|
||||
foreach(divisor 8 16)
|
||||
math(EXPR _step "${divisor} * ${baud}")
|
||||
@@ -170,12 +173,24 @@ function(pureboot_default_baud clock software outvar)
|
||||
endif()
|
||||
math(EXPR _error_bp "${_delta} * 10000 / ${baud}")
|
||||
if(_error_bp LESS_EQUAL 250)
|
||||
set(${outvar} 1 PARENT_SCOPE)
|
||||
return()
|
||||
endif()
|
||||
endforeach()
|
||||
endfunction()
|
||||
|
||||
# The fastest ladder rate the clock reaches.
|
||||
function(pureboot_default_baud clock software outvar)
|
||||
get_property(_ladder GLOBAL PROPERTY PUREBOOT_BAUD_LADDER)
|
||||
foreach(baud ${_ladder})
|
||||
pureboot_baud_feasible(${clock} ${baud} ${software} _ok)
|
||||
if(_ok)
|
||||
set(${outvar} ${baud} PARENT_SCOPE)
|
||||
return()
|
||||
endif()
|
||||
endforeach()
|
||||
endforeach()
|
||||
message(FATAL_ERROR "pureboot: no standard baud rate fits a ${clock} Hz clock within 2.5 %")
|
||||
message(FATAL_ERROR "pureboot: no standard baud rate fits a ${clock} Hz clock within 2.5 % "
|
||||
"— pass BAUD <rate> to deploy a non-standard one")
|
||||
endfunction()
|
||||
|
||||
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
||||
|
||||
@@ -98,8 +98,15 @@ speak to the build. This exact deployment runs the full protocol suite in CI
|
||||
|
||||
Reset enters the loader (BOOTRST on the boot-sectioned megas, the patched
|
||||
reset vector elsewhere) — except a watchdog reset, which hands straight to the
|
||||
application, since the application owns its watchdog and must clear WDRF
|
||||
itself.
|
||||
application with no activation window, since the application owns its watchdog.
|
||||
This is deliberate: it lets an application reboot itself instantly rather than
|
||||
sit through the window. The application must clear WDRF itself (libavr's
|
||||
`watchdog::disable()` does). **Gotcha:** WDRF is sticky (cleared only by
|
||||
software, not by a later reset), so an application that watchdog-resets and
|
||||
never clears it diverts *every* subsequent reset — external ones included —
|
||||
past the window too, and the loader becomes reachable only through an external
|
||||
programmer until the flag is cleared. A serial recovery path therefore assumes
|
||||
the application clears WDRF on its own reset path.
|
||||
|
||||
The host then knocks `p` then `b`, each awaited byte under a fresh activation
|
||||
window; any other byte is discarded and awaited again, so line noise can delay
|
||||
@@ -123,19 +130,26 @@ On chips whose flash exceeds 64 KiB (the 1284s — info-block flag bit 1) the
|
||||
addresses (the 644s' 64 KiB is exactly the 16-bit byte space). EEPROM
|
||||
addresses and all counts are bytes.
|
||||
|
||||
The loader trusts the host to keep addresses in range: it does not bound them
|
||||
against the info block. **Gotcha:** a `w` (or `r`) that runs past `E2END` wraps
|
||||
— EEAR is only as wide as the array, so an address past the end truncates onto
|
||||
low EEPROM and the write silently overwrites it. Keeping writes within the
|
||||
advertised sizes is the host's job (the shipped tool does); the flash budget
|
||||
is better spent on features than on re-checking a bound the host already holds.
|
||||
|
||||
| Cmd | Arguments | Reply |
|
||||
|---|---|---|
|
||||
| `b` | — | the 12-byte info block |
|
||||
| `R` | addr16, n8 | n flash bytes (n = 0 means 256) |
|
||||
| `W` | addr16, then one page of data | — (completion = next prompt) |
|
||||
| `W` | addr16 (any address in the page), then one page of data | — (completion = next prompt) |
|
||||
| `r` | addr16, n8 | n EEPROM bytes (n = 0 means 256) |
|
||||
| `w` | addr16, n8, then n data bytes | `+` per byte, sent once its write has begun |
|
||||
| `F` | — | 4 bytes: low fuse, lock, extended fuse, high fuse |
|
||||
| `J` | word address (16-bit) | `+`, then execution continues there |
|
||||
| other | — | ignored; the loop re-prompts (send a junk byte, await `+`, to resync) |
|
||||
|
||||
`W` streams exactly one page-aligned SPM page (size from the info block) into
|
||||
the buffer, then erases and programs — except pages inside the 512-byte slot
|
||||
`W` streams exactly one SPM page (size from the info block) into the buffer,
|
||||
then erases and programs — except pages inside the 512-byte slot
|
||||
the loader is *running* in, which are drained and left alone, so a broken host
|
||||
cannot brick the running copy and a staged copy may rewrite the resident.
|
||||
|
||||
@@ -309,6 +323,13 @@ Per chip preset, `ctest` runs:
|
||||
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;
|
||||
- `pbm_*.size` — under `--full`, the exhaustive cross product replacing that
|
||||
compact matrix: every plausible oscillator (the internal ones, the CKDIV8
|
||||
floor, the plain and the UART crystals) × every rate reachable from it ×
|
||||
every backend, unreachable combinations dropping out rather than aborting
|
||||
the configure. Bounded to one chip per size-bearing class — flash
|
||||
addressing, hand-over shape, page size, USART inventory — since everything
|
||||
else in the image is chip-independent code;
|
||||
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`, the
|
||||
info block within the image's first 256 bytes;
|
||||
- `pureboot.planner` — the host tool's pure logic: programming orders and their
|
||||
|
||||
@@ -67,18 +67,29 @@ constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
||||
|
||||
// The loader's one identity number. The protocol carries none of its own —
|
||||
// a version implies it, and the host tool holds that map (README.md).
|
||||
constexpr std::uint8_t version = 3;
|
||||
constexpr std::uint8_t version = 4;
|
||||
|
||||
// The 'b' reply, byte for byte (layout: README.md). Flash-resident because
|
||||
// no crt copies a .data image — and flash_table's storage carries the word
|
||||
// alignment 'b' needs to halve the address on the large chips.
|
||||
// One wire byte per line: this is the reply's layout, not a list.
|
||||
// clang-format off
|
||||
inline constexpr avr::flash_table<std::array<std::uint8_t, 12>{
|
||||
'P', 'B', version, avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2],
|
||||
'P',
|
||||
'B',
|
||||
version,
|
||||
avr::hw::db.signature[0],
|
||||
avr::hw::db.signature[1],
|
||||
avr::hw::db.signature[2],
|
||||
static_cast<std::uint8_t>(page), // 0 means 256
|
||||
wire_base & 0xff, wire_base >> 8, avr::hw::db.mem.eeprom_size & 0xff, avr::hw::db.mem.eeprom_size >> 8,
|
||||
wire_base & 0xff,
|
||||
wire_base >> 8,
|
||||
avr::hw::db.mem.eeprom_size & 0xff,
|
||||
avr::hw::db.mem.eeprom_size >> 8,
|
||||
static_cast<std::uint8_t>((boot_section ? 0 : 1) | (word_flash ? 2 : 0)), // patch-vector, word-addressed
|
||||
}>
|
||||
info_data;
|
||||
// clang-format on
|
||||
|
||||
// The serial link, per the build's PUREBOOT_USART / PUREBOOT_SOFT_SERIAL,
|
||||
// defaulting to the chip's USART0 where it has one. The software receiver is
|
||||
@@ -309,18 +320,19 @@ void store_eeprom(std::uint16_t address, std::uint8_t count)
|
||||
// that write clears the condition and the host's read-back rewrites the page.
|
||||
void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
|
||||
{
|
||||
// One induction either way: a byte-addressed wire address walks the page
|
||||
// itself (aligned, so the offset bits wrap to zero), while a word one
|
||||
// becomes a byte cursor once. The slot index is the wire address's high
|
||||
// byte — on byte-addressed chips the byte address's, with the low bit
|
||||
// dropped, since a slot is two of those.
|
||||
// The address names a page, so its in-page bits are dropped and the walk
|
||||
// starts at the page base — one induction either way: a byte-addressed
|
||||
// wire address walks the page itself (the offset bits wrap back to zero),
|
||||
// while a word one becomes a byte cursor once. The slot index is the wire
|
||||
// address's high byte — on byte-addressed chips the byte address's, with
|
||||
// the low bit dropped, since a slot is two of those.
|
||||
spm::flash_address_t address;
|
||||
std::uint8_t page_high;
|
||||
if constexpr (word_flash) {
|
||||
// A page is aligned, so it never crosses 64 KiB: RAMPZ is a per-page
|
||||
// constant and the 16-bit Z's low byte is the whole in-page offset.
|
||||
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) & ~static_cast<std::uint16_t>(page - 1);
|
||||
std::uint16_t z = z0;
|
||||
do {
|
||||
std::uint8_t low = link::rx();
|
||||
@@ -331,7 +343,7 @@ void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
|
||||
address = (static_cast<spm::flash_address_t>(rampz) << 16) | z0;
|
||||
page_high = static_cast<std::uint8_t>(wire_address >> 8);
|
||||
} else {
|
||||
address = static_cast<spm::flash_address_t>(wire_address);
|
||||
address = static_cast<spm::flash_address_t>(wire_address & ~static_cast<std::uint16_t>(page - 1));
|
||||
do {
|
||||
std::uint8_t low = link::rx();
|
||||
std::uint8_t high = link::rx();
|
||||
|
||||
@@ -24,13 +24,13 @@ else:
|
||||
import termios
|
||||
|
||||
PROMPT = b"+"
|
||||
VERSION = 2 # this tool's own version — free to drift from a loader's
|
||||
VERSION = 3 # this tool's own version — free to drift from a loader's
|
||||
# The loader versions this tool speaks. A pureboot version implies its wire
|
||||
# protocol, which carries no number of its own, so this window is where that
|
||||
# map lives: every version so far speaks the same protocol, and one that
|
||||
# changes it becomes the new floor here.
|
||||
OLDEST_LOADER = 1
|
||||
NEWEST_LOADER = 3
|
||||
NEWEST_LOADER = 4
|
||||
SLOT = 512 # the loader slot, on every chip
|
||||
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
||||
|
||||
@@ -364,26 +364,37 @@ class Loader:
|
||||
self.info = None
|
||||
|
||||
def connect(self, wait):
|
||||
"""Knock until the window answers, then read the info block. Also
|
||||
converges into a live session: the knock bytes are ignored there and
|
||||
the drain absorbs whatever they produced."""
|
||||
self.port.flush_input()
|
||||
"""Knock until the info block comes back. The block is what proves the
|
||||
loader is listening — a prompt byte alone does not, since one left over
|
||||
from a previous session can still be in the pipeline while the port
|
||||
opening resets the device into a fresh activation window, where a
|
||||
command without its knock is discarded. Each attempt is therefore the
|
||||
whole handshake, retried until it produces the block or the window
|
||||
closes. Also converges into a live session: the knock bytes are ignored
|
||||
there and the drain absorbs whatever they produced."""
|
||||
deadline = time.monotonic() + wait
|
||||
knocks = 0
|
||||
while True:
|
||||
self.port.flush_input()
|
||||
self.port.write(b"pb")
|
||||
knocks += 1
|
||||
if PROMPT in self.port.read_available(0.4):
|
||||
break
|
||||
if time.monotonic() > deadline:
|
||||
raise Error("no answer — reset the device within its activation window")
|
||||
while self.port.read_available(0.3):
|
||||
pass
|
||||
self.port.write(b"b")
|
||||
self.info = Info(self.port.read_exact(12, 2.0))
|
||||
try:
|
||||
block = self.port.read_exact(12, 2.0)
|
||||
except Error:
|
||||
block = b""
|
||||
# A version the tool cannot speak is the loader's own answer,
|
||||
# not a failed knock: Info reports it rather than retrying.
|
||||
if block[0:2] == b"PB":
|
||||
self.info = Info(block)
|
||||
self._expect_prompt()
|
||||
verbose(f"loader answered knock {knocks}; info block read")
|
||||
return self.info
|
||||
if time.monotonic() > deadline:
|
||||
raise Error("no answer — reset the device within its activation window")
|
||||
|
||||
def _expect_prompt(self, timeout=2.0):
|
||||
byte = self.port.read_exact(1, timeout)
|
||||
|
||||
@@ -105,6 +105,17 @@ def main():
|
||||
# was never told about is a loader it would refuse to speak to.
|
||||
if live.version != pb.NEWEST_LOADER:
|
||||
fail(f"loader reports pureboot {live.version}, the tool's newest is {pb.NEWEST_LOADER}")
|
||||
|
||||
# A W addressed inside a page rather than at its base must still
|
||||
# consume exactly one page and prompt. The loader's own slot is
|
||||
# the target — it is drained and never programmed — and the
|
||||
# payload is erased-state bytes, so the probe can disturb neither
|
||||
# the image nor the page buffer it leaves behind.
|
||||
wire = wire_base + 1
|
||||
port.write(bytes((ord("W"), wire & 0xFF, wire >> 8)) + b"\xff" * page)
|
||||
if port.read_exact(1, 5.0) != pb.PROMPT:
|
||||
fail("unaligned W did not return to the prompt")
|
||||
|
||||
loader.run_application()
|
||||
banner = port.read_exact(3, 5.0)
|
||||
if banner != b"APP":
|
||||
|
||||
@@ -280,6 +280,68 @@ def main():
|
||||
if device.writes != pb.RETRIES + 1:
|
||||
fail(f"unrepairable page took {device.writes} writes, expected {pb.RETRIES + 1}")
|
||||
|
||||
# The knock handshake against a device that is not listening yet — the
|
||||
# state a port open leaves behind: it resets the chip into a fresh
|
||||
# activation window while the previous session's prompt is still in
|
||||
# flight, so the first knock is lost and a prompt arrives anyway.
|
||||
class FakePort:
|
||||
"""A loader in its activation window, plus `lost` leading writes the
|
||||
reset swallows and one stale prompt still on the wire."""
|
||||
|
||||
def __init__(self, info_raw, lost=0, stale=b"", active=False):
|
||||
self.info_raw = info_raw
|
||||
self.lost = lost
|
||||
self.inflight = bytearray(stale)
|
||||
self.rx = bytearray()
|
||||
self.active = active
|
||||
self.last = None
|
||||
|
||||
def flush_input(self):
|
||||
self.rx.clear()
|
||||
|
||||
def write(self, data):
|
||||
if self.lost:
|
||||
self.lost -= 1
|
||||
return
|
||||
for byte in bytes(data):
|
||||
if not self.active:
|
||||
self.active = self.last == ord("p") and byte == ord("b")
|
||||
self.last = byte
|
||||
if self.active:
|
||||
self.rx += pb.PROMPT
|
||||
elif byte == ord("b"):
|
||||
self.rx += self.info_raw + pb.PROMPT
|
||||
else:
|
||||
self.rx += pb.PROMPT
|
||||
|
||||
def read_available(self, wait):
|
||||
self.rx = self.inflight + self.rx # the stale prompt lands late
|
||||
self.inflight.clear()
|
||||
out, self.rx = bytes(self.rx), bytearray()
|
||||
return out
|
||||
|
||||
def read_exact(self, count, timeout):
|
||||
if len(self.rx) < count:
|
||||
raise pb.Error(f"timeout: got {len(self.rx)} of {count} bytes")
|
||||
out, self.rx = bytes(self.rx[:count]), self.rx[count:]
|
||||
return out
|
||||
|
||||
raw = info_of(pb, 0x7E00, 128, False, 0x8000).raw
|
||||
for what, port in (
|
||||
("clean window", FakePort(raw)),
|
||||
("stale prompt over a lost knock", FakePort(raw, lost=1, stale=pb.PROMPT)),
|
||||
("live session", FakePort(raw, active=True)),
|
||||
):
|
||||
info = pb.Loader(port).connect(5)
|
||||
if info.raw != raw:
|
||||
fail(f"connect ({what}) returned {info.raw.hex()}")
|
||||
|
||||
# A device that never answers still says so, and a version the tool cannot
|
||||
# speak is reported as such rather than retried into a timeout.
|
||||
expect_error("dead device", lambda: pb.Loader(FakePort(raw, lost=99)).connect(0), "no answer")
|
||||
old = bytes(raw[:2]) + bytes((pb.NEWEST_LOADER + 1,)) + bytes(raw[3:])
|
||||
expect_error("unspeakable version", lambda: pb.Loader(FakePort(old)).connect(5), "needs a newer tool")
|
||||
|
||||
print("test_planner: all planner and policy checks pass")
|
||||
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
# The port's gate: every chip's generated workflow — build, size matrix, and
|
||||
# the simulator-driven protocol suites. --full adds the reflect-spot builds
|
||||
# (libavr's rule: reflect compiles are bounded to its spot set, never the
|
||||
# full matrix). LIBAVR_ROOT must point at the libavr checkout.
|
||||
# full matrix) and swaps the compact size matrix for the exhaustive
|
||||
# clock × baud × backend cross product. LIBAVR_ROOT must point at the libavr
|
||||
# checkout.
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
full=0
|
||||
[[ "$1" == "--full" ]] && { full=1; shift; }
|
||||
[[ "$1" == "--full" ]] && { full=1; shift; export PUREBOOT_FULL_MATRIX=1; }
|
||||
|
||||
CHIPS=(attiny13 attiny13a attiny25 attiny45 attiny85
|
||||
atmega8 atmega8a atmega16 atmega16a atmega32 atmega32a
|
||||
|
||||
Reference in New Issue
Block a user