Compare commits
7 Commits
29b9e47d3d
...
ffdface1a8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffdface1a8 | ||
|
|
17ee8bd2fe | ||
|
|
c37aeeede9 | ||
|
|
dc4ec4b79e | ||
|
|
e807268dba | ||
|
|
495478771e | ||
|
|
7fb77d04c3 |
102
CMakeLists.txt
102
CMakeLists.txt
@@ -233,11 +233,11 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
# The size matrix: every configuration axis that could move the image
|
# The size matrix: every configuration axis that could move the image
|
||||||
# size — the serial backend (different code), the USART instance
|
# size — the serial backend (different code), the USART instance
|
||||||
# (different registers), the clock (different constants), and the baud
|
# (different registers), the clock (different constants), and the baud
|
||||||
# through the two shapes its bit timing takes — each combination must
|
# through the shapes its bit timing takes — each combination must still
|
||||||
# still fit the chip's slot budget. Pins are size-neutral (port and bit
|
# fit the chip's slot budget. Pins are size-neutral (port and bit are
|
||||||
# are immediate operands) and the timeout is a constant, so neither adds
|
# immediate operands) and the timeout is a constant, so neither adds an
|
||||||
# an axis. The stock build is one point of this matrix and already has
|
# axis. The stock build is one point of this matrix and already has its
|
||||||
# its test.
|
# test.
|
||||||
function(pureboot_size_variant name)
|
function(pureboot_size_variant name)
|
||||||
pureboot_add_loader(${name} ${ARGN})
|
pureboot_add_loader(${name} ${ARGN})
|
||||||
add_test(NAME ${name}.size
|
add_test(NAME ${name}.size
|
||||||
@@ -245,42 +245,86 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
||||||
endfunction()
|
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
|
# 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
|
# the crystal the stock build assumes (the tiny13's ladder is its own RC
|
||||||
# menu — it has no crystal option).
|
# menu — it has no crystal option).
|
||||||
if(LIBAVR_MCU MATCHES "^attiny13")
|
if(LIBAVR_MCU MATCHES "^attiny13")
|
||||||
set(_matrix_clocks 1200000 4800000 9600000)
|
set(_matrix_clocks 1200000 4800000 9600000)
|
||||||
|
set(_full_clocks 128000 600000 1200000 4800000 9600000)
|
||||||
else()
|
else()
|
||||||
set(_matrix_clocks 1000000 8000000 16000000)
|
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)
|
||||||
|
pureboot_size_variant(pureboot_sw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL software)
|
||||||
|
endif()
|
||||||
|
if(PUREBOOT_HAS_USART AND NOT _matrix_hz EQUAL _pb_stock_hz)
|
||||||
|
pureboot_size_variant(pureboot_hw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL hardware)
|
||||||
|
endif()
|
||||||
|
if(PUREBOOT_HAS_USART1 AND NOT _matrix_hz EQUAL _pb_stock_hz)
|
||||||
|
pureboot_size_variant(pureboot_usart1_${_matrix_khz}k CLOCK ${_matrix_hz} USART 1)
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
list(GET _matrix_clocks -1 _matrix_top_hz)
|
||||||
|
pureboot_size_variant(pureboot_sw_wide CLOCK ${_matrix_top_hz} BAUD 9600 SERIAL software)
|
||||||
endif()
|
endif()
|
||||||
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)
|
|
||||||
pureboot_size_variant(pureboot_sw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL software)
|
|
||||||
endif()
|
|
||||||
if(PUREBOOT_HAS_USART AND NOT _matrix_hz EQUAL _pb_stock_hz)
|
|
||||||
pureboot_size_variant(pureboot_hw_${_matrix_khz}k CLOCK ${_matrix_hz} SERIAL hardware)
|
|
||||||
endif()
|
|
||||||
if(PUREBOOT_HAS_USART1 AND NOT _matrix_hz EQUAL _pb_stock_hz)
|
|
||||||
pureboot_size_variant(pureboot_usart1_${_matrix_khz}k CLOCK ${_matrix_hz} USART 1)
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
if(PUREBOOT_HAS_USART1)
|
if(PUREBOOT_HAS_USART1)
|
||||||
pureboot_size_variant(pureboot_usart1 USART 1)
|
pureboot_size_variant(pureboot_usart1 USART 1)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# The baud axis, whose one size-bearing shape the ladder never picks: a
|
|
||||||
# software UART spins out each bit with _delay_loop_1 while the count
|
|
||||||
# fits a byte and with the 16-bit _delay_loop_2 beyond it, two words more
|
|
||||||
# setup at every one of its five sites — the largest image the
|
|
||||||
# configuration space produces. The ladder default takes the *fastest*
|
|
||||||
# rate a clock reaches, which always lands in the byte, so the wide form
|
|
||||||
# needs the slowest ladder rate against the fastest clock to appear. The
|
|
||||||
# hardware USART has no such shape: its baud is a divisor constant, and
|
|
||||||
# the ladder's U2X solutions are already its larger form.
|
|
||||||
list(GET _matrix_clocks -1 _matrix_top_hz)
|
|
||||||
pureboot_size_variant(pureboot_sw_wide CLOCK ${_matrix_top_hz} BAUD 9600 SERIAL software)
|
|
||||||
|
|
||||||
# One configured deployment end to end — a real board's shape rather
|
# One configured deployment end to end — a real board's shape rather
|
||||||
# than the stock assumption: the ATmega328P on its shipped 1 MHz fuses,
|
# than the stock assumption: the ATmega328P on its shipped 1 MHz fuses,
|
||||||
# the software UART on hand-picked pins (TX = PB1, RX = PB5), the ladder
|
# the software UART on hand-picked pins (TX = PB1, RX = PB5), the ladder
|
||||||
|
|||||||
@@ -146,36 +146,51 @@ set(PUREBOOT_HAS_USART ${_pb_has_usart} PARENT_SCOPE)
|
|||||||
set(PUREBOOT_HAS_USART1 ${_pb_has_usart1} PARENT_SCOPE)
|
set(PUREBOOT_HAS_USART1 ${_pb_has_usart1} PARENT_SCOPE)
|
||||||
set(PUREBOOT_SIM_MCU ${_pb_sim_mcu} PARENT_SCOPE)
|
set(PUREBOOT_SIM_MCU ${_pb_sim_mcu} PARENT_SCOPE)
|
||||||
|
|
||||||
# The fastest standard rate the clock reaches within 2.5 %, by the same
|
# The rates a default may pick, fastest first.
|
||||||
# best-of-U2X-and-plain divisor search libavr's solve_baud runs, so a default
|
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
|
# 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
|
# also needs the polled receiver's 100-cycles-a-bit floor: at low clocks the
|
||||||
# U2X divisor reaches rates the bit-banged sampler cannot.
|
# U2X divisor reaches rates the bit-banged sampler cannot.
|
||||||
function(pureboot_default_baud clock software outvar)
|
function(pureboot_baud_feasible clock baud software outvar)
|
||||||
foreach(baud 115200 57600 38400 19200 9600)
|
set(${outvar} 0 PARENT_SCOPE)
|
||||||
math(EXPR _cycles "${clock} / ${baud}")
|
math(EXPR _cycles "${clock} / ${baud}")
|
||||||
if(software AND _cycles LESS 100)
|
if(software AND _cycles LESS 100)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
foreach(divisor 8 16)
|
||||||
|
math(EXPR _step "${divisor} * ${baud}")
|
||||||
|
math(EXPR _n "(${clock} + ${_step} / 2) / ${_step}")
|
||||||
|
if(_n LESS 1 OR _n GREATER 4096)
|
||||||
continue()
|
continue()
|
||||||
endif()
|
endif()
|
||||||
foreach(divisor 8 16)
|
math(EXPR _actual "${clock} / (${divisor} * ${_n})")
|
||||||
math(EXPR _step "${divisor} * ${baud}")
|
math(EXPR _delta "${_actual} - ${baud}")
|
||||||
math(EXPR _n "(${clock} + ${_step} / 2) / ${_step}")
|
if(_delta LESS 0)
|
||||||
if(_n LESS 1 OR _n GREATER 4096)
|
math(EXPR _delta "-(${_delta})")
|
||||||
continue()
|
endif()
|
||||||
endif()
|
math(EXPR _error_bp "${_delta} * 10000 / ${baud}")
|
||||||
math(EXPR _actual "${clock} / (${divisor} * ${_n})")
|
if(_error_bp LESS_EQUAL 250)
|
||||||
math(EXPR _delta "${_actual} - ${baud}")
|
set(${outvar} 1 PARENT_SCOPE)
|
||||||
if(_delta LESS 0)
|
return()
|
||||||
math(EXPR _delta "-(${_delta})")
|
endif()
|
||||||
endif()
|
|
||||||
math(EXPR _error_bp "${_delta} * 10000 / ${baud}")
|
|
||||||
if(_error_bp LESS_EQUAL 250)
|
|
||||||
set(${outvar} ${baud} PARENT_SCOPE)
|
|
||||||
return()
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
endforeach()
|
endforeach()
|
||||||
message(FATAL_ERROR "pureboot: no standard baud rate fits a ${clock} Hz clock within 2.5 %")
|
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()
|
||||||
|
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()
|
endfunction()
|
||||||
|
|
||||||
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
# 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 enters the loader (BOOTRST on the boot-sectioned megas, the patched
|
||||||
reset vector elsewhere) — except a watchdog reset, which hands straight to the
|
reset vector elsewhere) — except a watchdog reset, which hands straight to the
|
||||||
application, since the application owns its watchdog and must clear WDRF
|
application with no activation window, since the application owns its watchdog.
|
||||||
itself.
|
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
|
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
|
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 (the 644s' 64 KiB is exactly the 16-bit byte space). EEPROM
|
||||||
addresses and all counts are bytes.
|
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 |
|
| Cmd | Arguments | Reply |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `b` | — | the 12-byte info block |
|
| `b` | — | the 12-byte info block |
|
||||||
| `R` | addr16, n8 | n flash bytes (n = 0 means 256) |
|
| `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) |
|
| `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 |
|
| `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 |
|
| `F` | — | 4 bytes: low fuse, lock, extended fuse, high fuse |
|
||||||
| `J` | word address (16-bit) | `+`, then execution continues there |
|
| `J` | word address (16-bit) | `+`, then execution continues there |
|
||||||
| other | — | ignored; the loop re-prompts (send a junk byte, await `+`, to resync) |
|
| other | — | ignored; the loop re-prompts (send a junk byte, await `+`, to resync) |
|
||||||
|
|
||||||
`W` streams exactly one page-aligned SPM page (size from the info block) into
|
`W` streams exactly one SPM page (size from the info block) into the buffer,
|
||||||
the buffer, then erases and programs — except pages inside the 512-byte slot
|
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
|
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.
|
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
|
the configuration space produces, and a shape the ladder default (always the
|
||||||
*fastest* rate a clock reaches) never picks. Pins are immediate operands and
|
*fastest* rate a clock reaches) never picks. Pins are immediate operands and
|
||||||
the timeout is a constant: neither is an axis;
|
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
|
- `pureboot.pi` — the position-independence lint: no absolute `jmp`/`call`, the
|
||||||
info block within the image's first 256 bytes;
|
info block within the image's first 256 bytes;
|
||||||
- `pureboot.planner` — the host tool's pure logic: programming orders and their
|
- `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 —
|
// The loader's one identity number. The protocol carries none of its own —
|
||||||
// a version implies it, and the host tool holds that map (README.md).
|
// a version implies it, and the host tool holds that map (README.md).
|
||||||
constexpr std::uint8_t version = 3;
|
constexpr std::uint8_t version = 4;
|
||||||
|
|
||||||
// The 'b' reply, byte for byte (layout: README.md). Flash-resident because
|
// 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
|
// 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.
|
// 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>{
|
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',
|
||||||
static_cast<std::uint8_t>(page), // 0 means 256
|
'B',
|
||||||
wire_base & 0xff, wire_base >> 8, avr::hw::db.mem.eeprom_size & 0xff, avr::hw::db.mem.eeprom_size >> 8,
|
version,
|
||||||
static_cast<std::uint8_t>((boot_section ? 0 : 1) | (word_flash ? 2 : 0)), // patch-vector, word-addressed
|
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,
|
||||||
|
static_cast<std::uint8_t>((boot_section ? 0 : 1) | (word_flash ? 2 : 0)), // patch-vector, word-addressed
|
||||||
}>
|
}>
|
||||||
info_data;
|
info_data;
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
// The serial link, per the build's PUREBOOT_USART / PUREBOOT_SOFT_SERIAL,
|
// 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
|
// 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.
|
// 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)
|
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
|
// The address names a page, so its in-page bits are dropped and the walk
|
||||||
// itself (aligned, so the offset bits wrap to zero), while a word one
|
// starts at the page base — one induction either way: a byte-addressed
|
||||||
// becomes a byte cursor once. The slot index is the wire address's high
|
// wire address walks the page itself (the offset bits wrap back to zero),
|
||||||
// byte — on byte-addressed chips the byte address's, with the low bit
|
// while a word one becomes a byte cursor once. The slot index is the wire
|
||||||
// dropped, since a slot is two of those.
|
// 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;
|
spm::flash_address_t address;
|
||||||
std::uint8_t page_high;
|
std::uint8_t page_high;
|
||||||
if constexpr (word_flash) {
|
if constexpr (word_flash) {
|
||||||
// A page is aligned, so it never crosses 64 KiB: RAMPZ is a per-page
|
// 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.
|
// 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::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;
|
std::uint16_t z = z0;
|
||||||
do {
|
do {
|
||||||
std::uint8_t low = link::rx();
|
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;
|
address = (static_cast<spm::flash_address_t>(rampz) << 16) | z0;
|
||||||
page_high = static_cast<std::uint8_t>(wire_address >> 8);
|
page_high = static_cast<std::uint8_t>(wire_address >> 8);
|
||||||
} else {
|
} else {
|
||||||
address = static_cast<spm::flash_address_t>(wire_address);
|
address = static_cast<spm::flash_address_t>(wire_address & ~static_cast<std::uint16_t>(page - 1));
|
||||||
do {
|
do {
|
||||||
std::uint8_t low = link::rx();
|
std::uint8_t low = link::rx();
|
||||||
std::uint8_t high = link::rx();
|
std::uint8_t high = link::rx();
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ else:
|
|||||||
import termios
|
import termios
|
||||||
|
|
||||||
PROMPT = b"+"
|
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
|
# 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
|
# 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
|
# map lives: every version so far speaks the same protocol, and one that
|
||||||
# changes it becomes the new floor here.
|
# changes it becomes the new floor here.
|
||||||
OLDEST_LOADER = 1
|
OLDEST_LOADER = 1
|
||||||
NEWEST_LOADER = 3
|
NEWEST_LOADER = 4
|
||||||
SLOT = 512 # the loader slot, on every chip
|
SLOT = 512 # the loader slot, on every chip
|
||||||
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
||||||
|
|
||||||
@@ -364,26 +364,37 @@ class Loader:
|
|||||||
self.info = None
|
self.info = None
|
||||||
|
|
||||||
def connect(self, wait):
|
def connect(self, wait):
|
||||||
"""Knock until the window answers, then read the info block. Also
|
"""Knock until the info block comes back. The block is what proves the
|
||||||
converges into a live session: the knock bytes are ignored there and
|
loader is listening — a prompt byte alone does not, since one left over
|
||||||
the drain absorbs whatever they produced."""
|
from a previous session can still be in the pipeline while the port
|
||||||
self.port.flush_input()
|
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
|
deadline = time.monotonic() + wait
|
||||||
knocks = 0
|
knocks = 0
|
||||||
while True:
|
while True:
|
||||||
|
self.port.flush_input()
|
||||||
self.port.write(b"pb")
|
self.port.write(b"pb")
|
||||||
knocks += 1
|
knocks += 1
|
||||||
if PROMPT in self.port.read_available(0.4):
|
if PROMPT in self.port.read_available(0.4):
|
||||||
break
|
while self.port.read_available(0.3):
|
||||||
|
pass
|
||||||
|
self.port.write(b"b")
|
||||||
|
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:
|
if time.monotonic() > deadline:
|
||||||
raise Error("no answer — reset the device within its activation window")
|
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))
|
|
||||||
self._expect_prompt()
|
|
||||||
verbose(f"loader answered knock {knocks}; info block read")
|
|
||||||
return self.info
|
|
||||||
|
|
||||||
def _expect_prompt(self, timeout=2.0):
|
def _expect_prompt(self, timeout=2.0):
|
||||||
byte = self.port.read_exact(1, timeout)
|
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.
|
# was never told about is a loader it would refuse to speak to.
|
||||||
if live.version != pb.NEWEST_LOADER:
|
if live.version != pb.NEWEST_LOADER:
|
||||||
fail(f"loader reports pureboot {live.version}, the tool's newest is {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()
|
loader.run_application()
|
||||||
banner = port.read_exact(3, 5.0)
|
banner = port.read_exact(3, 5.0)
|
||||||
if banner != b"APP":
|
if banner != b"APP":
|
||||||
|
|||||||
@@ -280,6 +280,68 @@ def main():
|
|||||||
if device.writes != pb.RETRIES + 1:
|
if device.writes != pb.RETRIES + 1:
|
||||||
fail(f"unrepairable page took {device.writes} writes, expected {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")
|
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 port's gate: every chip's generated workflow — build, size matrix, and
|
||||||
# the simulator-driven protocol suites. --full adds the reflect-spot builds
|
# the simulator-driven protocol suites. --full adds the reflect-spot builds
|
||||||
# (libavr's rule: reflect compiles are bounded to its spot set, never the
|
# (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
|
set -e
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
full=0
|
full=0
|
||||||
[[ "$1" == "--full" ]] && { full=1; shift; }
|
[[ "$1" == "--full" ]] && { full=1; shift; export PUREBOOT_FULL_MATRIX=1; }
|
||||||
|
|
||||||
CHIPS=(attiny13 attiny13a attiny25 attiny45 attiny85
|
CHIPS=(attiny13 attiny13a attiny25 attiny45 attiny85
|
||||||
atmega8 atmega8a atmega16 atmega16a atmega32 atmega32a
|
atmega8 atmega8a atmega16 atmega16a atmega32 atmega32a
|
||||||
|
|||||||
Reference in New Issue
Block a user