Compare commits
6 Commits
v5
...
531ae6c8dc
| Author | SHA1 | Date | |
|---|---|---|---|
| 531ae6c8dc | |||
| b3f41caf6e | |||
| 7716e1e291 | |||
| 1b18f10f4e | |||
| 52c4cdab32 | |||
| 69f089e53a |
@@ -66,16 +66,18 @@ function(add_image_outputs name)
|
|||||||
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
|
$<TARGET_FILE:${name}> $<TARGET_FILE:${name}>.bin)
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
# The TinySafeBoot protocol reimplemented on libavr in three variants that trade
|
# The TinySafeBoot protocol reimplemented on libavr in variants that trade
|
||||||
# clarity for size. Each links into the ATmega328P boot section (BOOTSZ selects
|
# clarity for size. Each links into the ATmega328P boot section (BOOTSZ selects
|
||||||
# its size; BOOTRST vectors a reset to its base) with -nostartfiles — a polled
|
# its size; BOOTRST vectors a reset to its base) with -nostartfiles — a polled
|
||||||
# loader has no use for the crt or the vector table. The naked entry sits in
|
# loader has no use for the crt or the vector table. The entry sits in
|
||||||
# .vectors, laid first, and runs. The boot base is FLASHEND+1 minus the section
|
# .vectors, laid first, and runs — avr::startup::entry on the policy tier,
|
||||||
# size; the linker section-start and the source's boot_bytes agree. tsb_app is
|
# the experiment tiers' own naked stubs elsewhere, each documented in its
|
||||||
|
# source. The boot base is FLASHEND+1 minus the section size; the linker
|
||||||
|
# section-start and the source's boot_bytes agree. tsb_app is
|
||||||
# the application's reset vector, pinned to 0 here so the loaders jump to a
|
# the application's reset vector, pinned to 0 here so the loaders jump to a
|
||||||
# named function; --pmem-wrap-around lets relaxation turn that absolute jump
|
# named function; --pmem-wrap-around lets relaxation turn that absolute jump
|
||||||
# into the wrapped rjmp AVR's modulo-flash PC actually executes.
|
# into the wrapped rjmp AVR's modulo-flash PC actually executes.
|
||||||
# All three implement the full oracle feature set (see oracle/README.md):
|
# All four implement the full oracle feature set (see oracle/README.md):
|
||||||
# watchdog bail, one-wire half-duplex, config-page activation timeout, password
|
# watchdog bail, one-wire half-duplex, config-page activation timeout, password
|
||||||
# gate, emergency erase, config/flash/EEPROM read-write. They differ only in how,
|
# gate, emergency erase, config/flash/EEPROM read-write. They differ only in how,
|
||||||
# and the size gradient is the cost of that "how" — see dev/lessons.md.
|
# and the size gradient is the cost of that "how" — see dev/lessons.md.
|
||||||
@@ -167,6 +169,14 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
add_test(NAME pureboot.planner
|
add_test(NAME pureboot.planner
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_planner.py
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_planner.py
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
|
||||||
|
add_test(NAME pureboot.scan
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_scan.py
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py)
|
||||||
|
# CMakePresets.json is generated; hand edits drift the moment the
|
||||||
|
# generator reruns, so the gate holds the pair together.
|
||||||
|
add_test(NAME presets.generated
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/make_presets.py
|
||||||
|
--check)
|
||||||
add_test(NAME pureboot.handshake
|
add_test(NAME pureboot.handshake
|
||||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_handshake.py)
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_handshake.py)
|
||||||
add_test(NAME pureboot.updatelink
|
add_test(NAME pureboot.updatelink
|
||||||
@@ -270,6 +280,12 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
add_test(NAME pureboot_autobaud.size
|
add_test(NAME pureboot_autobaud.size
|
||||||
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot_autobaud>
|
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:pureboot_autobaud>
|
||||||
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
||||||
|
# The measured unit is the loader's only RAM object and sits at the very
|
||||||
|
# start of SRAM — where the host reads the bit period from (--info's
|
||||||
|
# measured clock), so the address is wire contract, not layout accident.
|
||||||
|
add_test(NAME pureboot_autobaud.unit
|
||||||
|
COMMAND ${CMAKE_COMMAND} -DOBJDUMP=${CMAKE_OBJDUMP} -DELF=$<TARGET_FILE:pureboot_autobaud>
|
||||||
|
-DRAM_START=${PUREBOOT_RAM_START} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_unit.cmake)
|
||||||
|
|
||||||
# One point of the exhaustive matrix, named from its resolved parameters
|
# One point of the exhaustive matrix, named from its resolved parameters
|
||||||
# so the enumeration cannot collide with itself. `pins` is empty for the
|
# so the enumeration cannot collide with itself. `pins` is empty for the
|
||||||
@@ -390,6 +406,38 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
RX ${PUREBOOT_USART1_RX} TX ${PUREBOOT_USART1_TX})
|
RX ${PUREBOOT_USART1_RX} TX ${PUREBOOT_USART1_TX})
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# The OSCCAL axis at its fixed points: the stock shape, and the tightest
|
||||||
|
# image in the space with the trim on top — the axis adds one register
|
||||||
|
# write, and these points hold both of its addressing encodings to every
|
||||||
|
# chip's budget.
|
||||||
|
pureboot_size_variant(pureboot_osccal OSCCAL 0x9c)
|
||||||
|
pureboot_size_variant(pureboot_autobaud_osccal SERIAL autobaud OSCCAL 0x9c)
|
||||||
|
if(PUREBOOT_HAS_USART)
|
||||||
|
pureboot_size_variant(pureboot_autobaud_osccal_on_usart0 SERIAL autobaud OSCCAL 0x9c
|
||||||
|
RX ${PUREBOOT_USART0_RX} TX ${PUREBOOT_USART0_TX})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# The trim byte, observed through the wire from the first prompt — one
|
||||||
|
# chip per OSCCAL addressing class: extended I/O on the 328P (data 0x66,
|
||||||
|
# an sts — DS40002061B §36), plain I/O on the 85 (data 0x51, an out —
|
||||||
|
# Atmel-2586 §21).
|
||||||
|
if(LIBAVR_MCU MATCHES "^(atmega328p|attiny85)$" AND DEFINED PB_DEVICE)
|
||||||
|
if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||||
|
set(_osccal_addr 0x66)
|
||||||
|
else()
|
||||||
|
set(_osccal_addr 0x51)
|
||||||
|
endif()
|
||||||
|
get_target_property(_osccal_hz pureboot_osccal PUREBOOT_HZ)
|
||||||
|
get_target_property(_osccal_baud pureboot_osccal PUREBOOT_BAUD)
|
||||||
|
add_test(NAME pureboot.osccal
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbosccal.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot_osccal> ${PUREBOOT_SIM_MCU}
|
||||||
|
${_osccal_hz} ${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_osccal_baud}
|
||||||
|
${_osccal_addr} 0x9c ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbosccal-work)
|
||||||
|
set_tests_properties(pureboot.osccal PROPERTIES TIMEOUT 120)
|
||||||
|
endif()
|
||||||
|
|
||||||
# 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
|
||||||
@@ -468,9 +516,9 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
set_tests_properties(pureboot.usart1 PROPERTIES TIMEOUT 180)
|
set_tests_properties(pureboot.usart1 PROPERTIES TIMEOUT 180)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# The autobaud variants driven end to end over the software-UART bridge (both
|
# The autobaud loader driven end to end over the software-UART bridge:
|
||||||
# under review — pureboot/autobaud.md): the host sends the 0xC0 calibration
|
# the host sends the 0xC0 calibration pulse, the loader times it, locks,
|
||||||
# pulse, the loader times it, locks, and programs. Run on the near-flash 328P
|
# and programs. Run on the near-flash 328P
|
||||||
# and the word-addressed 1284P — the two flash-addressing classes — and each
|
# and the word-addressed 1284P — the two flash-addressing classes — and each
|
||||||
# at two clocks with the one binary, which is the clock-agnostic property
|
# at two clocks with the one binary, which is the clock-agnostic property
|
||||||
# autobaud exists for (test/pbautobaud.py). The fixture application banners
|
# autobaud exists for (test/pbautobaud.py). The fixture application banners
|
||||||
|
|||||||
2
libavr
2
libavr
Submodule libavr updated: 43b1f34ed1...26b80e262d
@@ -136,6 +136,19 @@ elseif(LIBAVR_MCU STREQUAL "atmega644pa")
|
|||||||
set(_pb_sim_mcu atmega644p)
|
set(_pb_sim_mcu atmega644p)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Where SRAM begins: the classic megas keep it right after the plain I/O
|
||||||
|
# registers, the x8/x4 generations push it past their extended I/O file, and
|
||||||
|
# the tinies match the classics. An autobaud loader's measured unit lives at
|
||||||
|
# exactly this address (the host reads it there — pureboot.py), and the
|
||||||
|
# unit-position test holds the layout to it.
|
||||||
|
if(LIBAVR_MCU MATCHES "^atmega(8|16|32)a?$")
|
||||||
|
set(_pb_ram 0x60)
|
||||||
|
elseif(LIBAVR_MCU MATCHES "^atmega")
|
||||||
|
set(_pb_ram 0x100)
|
||||||
|
else()
|
||||||
|
set(_pb_ram 0x60)
|
||||||
|
endif()
|
||||||
|
|
||||||
# The function runs in its caller's scope, so everything it needs crosses
|
# The function runs in its caller's scope, so everything it needs crosses
|
||||||
# scopes as global properties.
|
# scopes as global properties.
|
||||||
set_property(GLOBAL PROPERTY PUREBOOT_BASE_HEX ${_pb_base_hex})
|
set_property(GLOBAL PROPERTY PUREBOOT_BASE_HEX ${_pb_base_hex})
|
||||||
@@ -155,6 +168,7 @@ set(PUREBOOT_SLOT ${_pb_slot} PARENT_SCOPE)
|
|||||||
set(PUREBOOT_LIMIT ${_pb_limit} PARENT_SCOPE)
|
set(PUREBOOT_LIMIT ${_pb_limit} PARENT_SCOPE)
|
||||||
set(PUREBOOT_EEPROM ${_pb_eeprom} PARENT_SCOPE)
|
set(PUREBOOT_EEPROM ${_pb_eeprom} PARENT_SCOPE)
|
||||||
set(PUREBOOT_DEFAULT_HZ ${_pb_hz} PARENT_SCOPE)
|
set(PUREBOOT_DEFAULT_HZ ${_pb_hz} PARENT_SCOPE)
|
||||||
|
set(PUREBOOT_RAM_START ${_pb_ram} PARENT_SCOPE)
|
||||||
set(PUREBOOT_HAS_USART ${_pb_has_usart} PARENT_SCOPE)
|
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)
|
||||||
@@ -212,7 +226,7 @@ endfunction()
|
|||||||
|
|
||||||
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
# pureboot_add_loader(<name> [CLOCK <hz>] [BAUD <bd>]
|
||||||
# [SERIAL auto|hardware|software|autobaud] [USART <n>]
|
# [SERIAL auto|hardware|software|autobaud] [USART <n>]
|
||||||
# [RX <pin>] [TX <pin>] [TIMEOUT <s>])
|
# [RX <pin>] [TX <pin>] [TIMEOUT <s>] [OSCCAL <byte>])
|
||||||
#
|
#
|
||||||
# The loader target plus its flashable images (<name>.hex for a programmer,
|
# The loader target plus its flashable images (<name>.hex for a programmer,
|
||||||
# <name>.bin for --update-loader). The resolved deployment is stamped on the
|
# <name>.bin for --update-loader). The resolved deployment is stamped on the
|
||||||
@@ -225,8 +239,15 @@ endfunction()
|
|||||||
# and one binary per chip serves every F_CPU and every rate. The stamped
|
# and one binary per chip serves every F_CPU and every rate. The stamped
|
||||||
# PUREBOOT_HZ/PUREBOOT_BAUD then record what a harness should *drive* it at,
|
# PUREBOOT_HZ/PUREBOOT_BAUD then record what a harness should *drive* it at,
|
||||||
# not what it was built for.
|
# not what it was built for.
|
||||||
|
#
|
||||||
|
# OSCCAL bakes a measured oscillator trim into the loader (README.md: the
|
||||||
|
# RC-oscillator deployment answer): the byte is written at the top of run(),
|
||||||
|
# so every reset path — the watchdog hand-over included — runs on the
|
||||||
|
# corrected clock. Orthogonal to the backend: an autobaud build may carry it
|
||||||
|
# purely for the application's benefit, its own link being clock-free. No
|
||||||
|
# value, no code.
|
||||||
function(pureboot_add_loader name)
|
function(pureboot_add_loader name)
|
||||||
cmake_parse_arguments(PB "" "CLOCK;BAUD;SERIAL;USART;RX;TX;TIMEOUT" "" ${ARGN})
|
cmake_parse_arguments(PB "" "CLOCK;BAUD;SERIAL;USART;RX;TX;TIMEOUT;OSCCAL" "" ${ARGN})
|
||||||
if(PB_UNPARSED_ARGUMENTS)
|
if(PB_UNPARSED_ARGUMENTS)
|
||||||
message(FATAL_ERROR "pureboot_add_loader(${name}): unknown arguments ${PB_UNPARSED_ARGUMENTS}")
|
message(FATAL_ERROR "pureboot_add_loader(${name}): unknown arguments ${PB_UNPARSED_ARGUMENTS}")
|
||||||
endif()
|
endif()
|
||||||
@@ -325,6 +346,13 @@ function(pureboot_add_loader name)
|
|||||||
set(_defines PUREBOOT_CLOCK_HZ=${PB_CLOCK} PUREBOOT_BAUD=${PB_BAUD} PUREBOOT_TIMEOUT=${PB_TIMEOUT}
|
set(_defines PUREBOOT_CLOCK_HZ=${PB_CLOCK} PUREBOOT_BAUD=${PB_BAUD} PUREBOOT_TIMEOUT=${PB_TIMEOUT}
|
||||||
${_serial_defines})
|
${_serial_defines})
|
||||||
endif()
|
endif()
|
||||||
|
if(DEFINED PB_OSCCAL)
|
||||||
|
math(EXPR _osccal "${PB_OSCCAL}" OUTPUT_FORMAT DECIMAL)
|
||||||
|
if(_osccal LESS 0 OR _osccal GREATER 255)
|
||||||
|
message(FATAL_ERROR "pureboot_add_loader(${name}): OSCCAL ${PB_OSCCAL} is not one byte")
|
||||||
|
endif()
|
||||||
|
list(APPEND _defines PUREBOOT_OSCCAL=${_osccal})
|
||||||
|
endif()
|
||||||
|
|
||||||
add_executable(${name} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/pureboot.cpp)
|
add_executable(${name} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/pureboot.cpp)
|
||||||
target_link_libraries(${name} PRIVATE libavr)
|
target_link_libraries(${name} PRIVATE libavr)
|
||||||
|
|||||||
@@ -48,12 +48,14 @@ it carries the calibration machinery and no clock at all.
|
|||||||
is 510 bytes, since the slot's last word is the trampoline.
|
is 510 bytes, since the slot's last word is the trampoline.
|
||||||
|
|
||||||
The tightest fit in the whole space is the 1284s' autobaud build deployed on a
|
The tightest fit in the whole space is the 1284s' autobaud build deployed on a
|
||||||
USART's own pins, 506 of its 512 — they alone carry the far-flash machinery
|
USART's own pins with the `OSCCAL` trim baked, 510 of its 512 — they alone
|
||||||
(ELPM reads, RAMPZ page commands), autobaud alone carries the calibration loop,
|
carry the far-flash machinery (ELPM reads, RAMPZ page commands), autobaud
|
||||||
and a bit-banged link on a USART's pins alone has to release it (below). The
|
alone carries the calibration loop, a bit-banged link on a USART's pins alone
|
||||||
same build on the default pins is 502. The flash bank riding in a transfer's
|
has to release it (below), and the trim adds its one register write. Without
|
||||||
selector byte keeps even those chips' addressing the same 16-bit form every
|
the trim that build is 504; on the default pins, 502. The flash bank riding
|
||||||
other chip uses, which is why they are no longer the outlier they were.
|
in a transfer's selector byte keeps even those chips' addressing the same
|
||||||
|
16-bit form every other chip uses, which is why they are no longer the
|
||||||
|
outlier they were.
|
||||||
|
|
||||||
The software UART enables the RX pull-up; TX idles high. All multi-byte wire
|
The software UART enables the RX pull-up; TX idles high. All multi-byte wire
|
||||||
quantities are little-endian.
|
quantities are little-endian.
|
||||||
@@ -72,6 +74,7 @@ repo's build and by a downstream project alike:
|
|||||||
| `USART <n>` | the USART instance (x4 megas carry two) | 0 |
|
| `USART <n>` | the USART instance (x4 megas carry two) | 0 |
|
||||||
| `RX <pin>`, `TX <pin>` | software-UART pins | `pb0`, `pb1` |
|
| `RX <pin>`, `TX <pin>` | software-UART pins | `pb0`, `pb1` |
|
||||||
| `TIMEOUT <s>` | the activation window | 8 |
|
| `TIMEOUT <s>` | the activation window | 8 |
|
||||||
|
| `OSCCAL <byte>` | a measured oscillator trim, applied before anything runs | none — no value, no code |
|
||||||
|
|
||||||
The default baud is the fastest of 115200/57600/38400/19200/9600 the clock
|
The default baud is the fastest of 115200/57600/38400/19200/9600 the clock
|
||||||
reaches within 2.5 % — the same U2X-included divisor search libavr's baud
|
reaches within 2.5 % — the same U2X-included divisor search libavr's baud
|
||||||
@@ -122,12 +125,13 @@ window per reset. Measure with an application in place.
|
|||||||
A downstream project brings its usual libavr setup (the `libavr` target, the
|
A downstream project brings its usual libavr setup (the `libavr` target, the
|
||||||
chip via the `LIBAVR_MCU` toolchain preset), consumes this directory, and
|
chip via the `LIBAVR_MCU` toolchain preset), consumes this directory, and
|
||||||
states its deployment — an ATmega328P on its shipped 1 MHz fuses with the
|
states its deployment — an ATmega328P on its shipped 1 MHz fuses with the
|
||||||
software UART on hand-picked pins, say:
|
software UART on hand-picked pins, say. A submodule pins the loader version
|
||||||
|
(the tags name them; this repo pins its own libavr the same way), where
|
||||||
|
FetchContent tracks whatever `main` is:
|
||||||
|
|
||||||
```cmake
|
```cmake
|
||||||
FetchContent_Declare(bootloader GIT_REPOSITORY git@git.blackmark.me:avr/bootloader.git GIT_TAG main)
|
# git submodule add <forge>/avr/bootloader.git bootloader — or FetchContent
|
||||||
FetchContent_MakeAvailable(bootloader)
|
add_subdirectory(bootloader/pureboot pureboot)
|
||||||
add_subdirectory(${bootloader_SOURCE_DIR}/pureboot pureboot)
|
|
||||||
|
|
||||||
pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
|
pureboot_add_loader(myboot CLOCK 1000000 SERIAL software TX pb1 RX pb5)
|
||||||
```
|
```
|
||||||
@@ -285,7 +289,9 @@ Two generations exist. **1 through 4** speak one session — a 12-byte info bloc
|
|||||||
from `b`, and a command per memory (`R`/`W` flash, `r`/`w` EEPROM, `F` fuses).
|
from `b`, and a command per memory (`R`/`W` flash, `r`/`w` EEPROM, `F` fuses).
|
||||||
**5** replaced those with the single `G`/`g` pair over selector-named spaces
|
**5** replaced those with the single `G`/`g` pair over selector-named spaces
|
||||||
above; the shipped tool speaks both, choosing on the version it reads, so a
|
above; the shipped tool speaks both, choosing on the version it reads, so a
|
||||||
deployed pureboot 4 stays drivable and self-updatable to 5.
|
deployed pureboot 4 stays drivable and self-updatable to 5. **6** changes
|
||||||
|
nothing on the wire: it marks the builds that may carry a baked `OSCCAL` trim
|
||||||
|
(Configuration), so a tool driving an update knows such images exist.
|
||||||
|
|
||||||
Every closed generation is tagged in this repo at its era's last commit — the
|
Every closed generation is tagged in this repo at its era's last commit — the
|
||||||
commit just before the next version bump, so a tag holds everything its
|
commit just before the next version bump, so a tag holds everything its
|
||||||
@@ -350,6 +356,18 @@ mega (SPM only executes from the boot section — reflash the .hex), but *runs*
|
|||||||
on a patched-vector chip, and the ordinary `--update-loader` flow re-homes it
|
on a patched-vector chip, and the ordinary `--update-loader` flow re-homes it
|
||||||
into the top slot from there (`pureboot.rehome`).
|
into the top slot from there (`pureboot.rehome`).
|
||||||
|
|
||||||
|
**Fixed-baud on an internal RC oscillator is a deployment risk the build
|
||||||
|
cannot see.** The factory trim is ±10 % where an 8N1 frame survives about
|
||||||
|
±4: a part at the edge answers nothing at the built rate, and the symptom —
|
||||||
|
silence — reads as a wiring fault (a real ATtiny13A measured −5.5 %, outside
|
||||||
|
every standard rate at its own documented default). The **autobaud build is
|
||||||
|
the deployment-proof backend**: it has no rate to miss. Where fixed-baud on
|
||||||
|
RC is wanted anyway, measure first and bake the trim: an autobaud session's
|
||||||
|
`--info` prints the part's true clock from the loader's own measured bit
|
||||||
|
period, OSCCAL moves the oscillator about 1 % per step, and `OSCCAL <byte>`
|
||||||
|
builds the correction in — one build–measure iteration converges. A loader
|
||||||
|
already deployed and silent is diagnosed with `--scan` (Host tool).
|
||||||
|
|
||||||
## Updating the loader
|
## Updating the loader
|
||||||
|
|
||||||
`pureboot.py --update-loader new_pureboot.bin` replaces the resident loader
|
`pureboot.py --update-loader new_pureboot.bin` replaces the resident loader
|
||||||
@@ -374,6 +392,11 @@ The host retunes on the open port, so no DTR pulse resets the copy it is talking
|
|||||||
to. Omit them against a changed link and the update stops after installing the
|
to. Omit them against a changed link and the update stops after installing the
|
||||||
staging copy, saying so and naming this as the cause.
|
staging copy, saying so and naming this as the cause.
|
||||||
|
|
||||||
|
An `OSCCAL`-baked image is a link change in effect even at an unchanged rate
|
||||||
|
on paper: the staging copy shifts the physical clock the moment its `run()`
|
||||||
|
starts, and from then on speaks exactly what it was built for. Declare it
|
||||||
|
like any other link change — `--staged-baud` with the new build's rate.
|
||||||
|
|
||||||
The preflight refuses an image built for another chip: the stamp every pureboot
|
The preflight refuses an image built for another chip: the stamp every pureboot
|
||||||
binary carries must resolve to the device's own geometry, and the error names
|
binary carries must resolve to the device's own geometry, and the error names
|
||||||
both. Die revisions share their base signature and geometry, so their images
|
both. Die revisions share their base signature and geometry, so their images
|
||||||
@@ -430,7 +453,19 @@ application data into a mega's reset walk region.
|
|||||||
|
|
||||||
`--autobaud` opens with the calibration pulse instead of the plain knock, for a
|
`--autobaud` opens with the calibration pulse instead of the plain knock, for a
|
||||||
loader built `SERIAL autobaud`; the rest of the session is identical, at
|
loader built `SERIAL autobaud`; the rest of the session is identical, at
|
||||||
whatever `--baud` the host chose.
|
whatever `--baud` the host chose. Its `--info` adds the **measured clock** —
|
||||||
|
the loader's bit-period unit, decoded and multiplied by the session rate —
|
||||||
|
which is the number an `OSCCAL` bake or a fixed-baud build for the part is
|
||||||
|
held against; `--clock <hz>` states the drift against a nominal.
|
||||||
|
|
||||||
|
`--scan` is the diagnosis once a fixed-baud loader has gone silent: it walks
|
||||||
|
±10 % around `--baud` in 2 % steps, nearest first, one probe per activation
|
||||||
|
window — reset the target as each probe announces itself (a board with DTR
|
||||||
|
wired to reset is pulsed by the probe's own port-open). A loader
|
||||||
|
off-frequency answers at its oscillator's ratio, and the report gives the
|
||||||
|
found rate as the session workaround, the offset, the OSCCAL correction's
|
||||||
|
direction at ~1 % per step, and the autobaud way out. Standalone — no other
|
||||||
|
operation combines with it.
|
||||||
|
|
||||||
`--peek ADDR[:N]` and `--poke ADDR:HEX` reach the data space (pureboot 5) —
|
`--peek ADDR[:N]` and `--poke ADDR:HEX` reach the data space (pureboot 5) —
|
||||||
SRAM, and through the same address space the register file and every I/O
|
SRAM, and through the same address space the register file and every I/O
|
||||||
@@ -480,7 +515,12 @@ Per chip preset, `ctest` runs:
|
|||||||
too. The timeout is a constant and is no axis;
|
too. The timeout is a constant and is no axis;
|
||||||
- `pureboot_autobaud.size` — the clock-free build, which has no clock or baud
|
- `pureboot_autobaud.size` — the clock-free build, which has no clock or baud
|
||||||
axis of its own: one binary per chip has to serve every point the matrix
|
axis of its own: one binary per chip has to serve every point the matrix
|
||||||
below sweeps;
|
below sweeps. `pureboot*osccal*.size` add the `OSCCAL` trim on the stock
|
||||||
|
shape and on the tightest image in the space (autobaud on a USART's own
|
||||||
|
pins), holding both of the trim write's addressing encodings to the budget;
|
||||||
|
- `pureboot_autobaud.unit` — the measured bit period is the loader's only RAM
|
||||||
|
object and sits exactly at ram_start, where `--info` reads it: wire
|
||||||
|
contract, not layout accident;
|
||||||
- `pbm_*.size` — with `PUREBOOT_FULL_MATRIX=1`, the exhaustive cross product
|
- `pbm_*.size` — with `PUREBOOT_FULL_MATRIX=1`, the exhaustive cross product
|
||||||
replacing that compact matrix, on **every** chip: every plausible oscillator
|
replacing that compact matrix, on **every** chip: every plausible oscillator
|
||||||
(the internal ones, the CKDIV8 floor, the plain and the UART crystals) ×
|
(the internal ones, the CKDIV8 floor, the plain and the UART crystals) ×
|
||||||
@@ -503,6 +543,13 @@ Per chip preset, `ctest` runs:
|
|||||||
recovery properties, the surgery, the staging composition, the boot-fuse
|
recovery properties, the surgery, the staging composition, the boot-fuse
|
||||||
decode, the update preflight over synthetic fuse bytes, and the repairing
|
decode, the update preflight over synthetic fuse bytes, and the repairing
|
||||||
verify against a fake device;
|
verify against a fake device;
|
||||||
|
- `pureboot.scan` — `--scan`'s walk and report logic: the probe order, the
|
||||||
|
rate arithmetic, and the trim advice's direction. A pty carries bytes at
|
||||||
|
any termios rate, so the rate physics itself belongs to the hardware
|
||||||
|
harness, and what the wire would arbitrate is pinned as logic;
|
||||||
|
- `presets.generated` — CMakePresets.json matches its generator
|
||||||
|
(`tools/make_presets.py --check`), so a hand edit or a generator change
|
||||||
|
cannot drift the pair apart;
|
||||||
- `pureboot.protocol` — end to end against a simavr device
|
- `pureboot.protocol` — end to end against a simavr device
|
||||||
(`test/pureboot_device.c`: a hardware USART as a pty, or a cycle-timed
|
(`test/pureboot_device.c`: a hardware USART as a pty, or a cycle-timed
|
||||||
GPIO⇄pty bridge for a software-UART build, plus the SPM/NVM module simavr's
|
GPIO⇄pty bridge for a software-UART build, plus the SPM/NVM module simavr's
|
||||||
@@ -535,15 +582,21 @@ Per chip preset, `ctest` runs:
|
|||||||
- `pureboot.update` — the full `--update-loader` flow, then every power-fail
|
- `pureboot.update` — the full `--update-loader` flow, then every power-fail
|
||||||
phase: the device is killed mid-write, restarted from its flash dump, and a
|
phase: the device is killed mid-write, restarted from its flash dump, and a
|
||||||
re-run must complete the update with the application intact;
|
re-run must complete the update with the application intact;
|
||||||
|
- `pureboot.osccal` (328P, t85) — a loader built with the `OSCCAL` axis holds
|
||||||
|
the trim register at the built byte from its first prompt, observed through
|
||||||
|
the wire on one chip per addressing encoding (`sts` and low-I/O `out`);
|
||||||
- `pureboot.autobaud` (328P, 1284P) — the clock-free build over the GPIO⇄pty
|
- `pureboot.autobaud` (328P, 1284P) — the clock-free build over the GPIO⇄pty
|
||||||
bridge: the calibration handshake, a flash + EEPROM + fuse round trip against
|
bridge: the calibration handshake, a flash + EEPROM + fuse round trip against
|
||||||
the simulator's own memory, a data-space round trip, the hand-over — then the
|
the simulator's own memory, a data-space round trip, the hand-over — then the
|
||||||
same binary again at double the clock, which is the property the backend
|
same binary again at double the clock, which is the property the backend
|
||||||
exists for. A lone calibration pulse with no knock behind it must still let
|
exists for. The measured clock `--info` prints is asserted against the
|
||||||
|
simulator's exact clock, inside the unit encoding's own envelope, at both
|
||||||
|
points. A lone calibration pulse with no knock behind it must still let
|
||||||
the application boot, so no wait in activation can be unbounded.
|
the application boot, so no wait in activation can be unbounded.
|
||||||
|
|
||||||
`size`, `pi`, `planner` and `handshake` are host logic and run anywhere; the
|
`size`, `unit`, `pi`, `planner`, `scan` and `handshake` are host logic and run
|
||||||
simulator-driven targets need simavr and a pty, so they are POSIX-only.
|
anywhere; the simulator-driven targets need simavr and a pty, so they are
|
||||||
|
POSIX-only.
|
||||||
|
|
||||||
## Hardware
|
## Hardware
|
||||||
|
|
||||||
|
|||||||
@@ -72,9 +72,17 @@ constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT;
|
|||||||
#endif
|
#endif
|
||||||
constexpr avr::uint24_t autobaud_budget = PUREBOOT_AUTOBAUD_POLLS;
|
constexpr avr::uint24_t autobaud_budget = PUREBOOT_AUTOBAUD_POLLS;
|
||||||
|
|
||||||
|
// A build may bake a measured oscillator trim (README.md: the RC-oscillator
|
||||||
|
// deployment answer); the byte is applied at the top of run(). Orthogonal to
|
||||||
|
// the serial backend — an autobaud build may carry it for the application's
|
||||||
|
// benefit alone.
|
||||||
|
#if defined(PUREBOOT_OSCCAL)
|
||||||
|
static_assert(PUREBOOT_OSCCAL >= 0 && PUREBOOT_OSCCAL <= 0xff, "PUREBOOT_OSCCAL is one OSCCAL byte");
|
||||||
|
#endif
|
||||||
|
|
||||||
// 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 = 5;
|
constexpr std::uint8_t version = 6;
|
||||||
|
|
||||||
// The image's identity stamp, for the host tool rather than for the wire: an
|
// The image's identity stamp, for the host tool rather than for the wire: an
|
||||||
// update image is a bare 512-byte slot, and without this nothing in it says
|
// update image is a bare 512-byte slot, and without this nothing in it says
|
||||||
@@ -477,6 +485,12 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
|
|||||||
|
|
||||||
[[noreturn]] void run()
|
[[noreturn]] void run()
|
||||||
{
|
{
|
||||||
|
#if defined(PUREBOOT_OSCCAL)
|
||||||
|
// The build's oscillator trim, ahead of everything — the WDRF bail
|
||||||
|
// included — so every path out of reset, the watchdog hand-over to the
|
||||||
|
// application first among them, runs on the corrected clock.
|
||||||
|
avr::clock::calibrate(PUREBOOT_OSCCAL);
|
||||||
|
#endif
|
||||||
// A watchdog reset belongs to the application, whose watchdog stays forced
|
// A watchdog reset belongs to the application, whose watchdog stays forced
|
||||||
// on until it clears WDRF — no activation window in its way.
|
// on until it clears WDRF — no activation window in its way.
|
||||||
if (avr::hw::field_impl<wdrf_field()>::test())
|
if (avr::hw::field_impl<wdrf_field()>::test())
|
||||||
|
|||||||
@@ -20,17 +20,21 @@ if os.name == "nt":
|
|||||||
import ctypes
|
import ctypes
|
||||||
from ctypes import wintypes
|
from ctypes import wintypes
|
||||||
else:
|
else:
|
||||||
|
import array
|
||||||
|
import fcntl
|
||||||
import select
|
import select
|
||||||
import termios
|
import termios
|
||||||
|
|
||||||
PROMPT = b"+"
|
PROMPT = b"+"
|
||||||
VERSION = 5 # this tool's own version — free to drift from a loader's
|
VERSION = 6 # 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 can drive. 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: the tool keeps a decoder for every generation in it (1–4 speak
|
||||||
# changes it becomes the new floor here.
|
# the per-memory commands, 5 the unified pair; 6 marks the OSCCAL-carrying
|
||||||
|
# builds and changes nothing on the wire), and a version it has no decoder
|
||||||
|
# for moves the floor.
|
||||||
OLDEST_LOADER = 1
|
OLDEST_LOADER = 1
|
||||||
NEWEST_LOADER = 5
|
NEWEST_LOADER = 6
|
||||||
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
|
||||||
|
|
||||||
@@ -41,6 +45,13 @@ RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
|||||||
UNIFIED_LOADER = 5
|
UNIFIED_LOADER = 5
|
||||||
SP_FLASH, SP_EEPROM, SP_RAM, SP_FUSE, SP_SPM = 0, 1, 2, 3, 4
|
SP_FLASH, SP_EEPROM, SP_RAM, SP_FUSE, SP_SPM = 0, 1, 2, 3, 4
|
||||||
|
|
||||||
|
# A v5+ autobaud loader keeps its measured bit period at ram_start, encoded
|
||||||
|
# as delay-loop counts: (bit cycles − UNIT_DISCOUNT) / UNIT_LOOP_CYCLES,
|
||||||
|
# floored — the spin granule and per-bit overhead of libavr's software UART.
|
||||||
|
# --info undoes the encoding to report the true clock, which therefore sits
|
||||||
|
# within one granule below it.
|
||||||
|
UNIT_LOOP_CYCLES, UNIT_DISCOUNT = 4, 8
|
||||||
|
|
||||||
# A selector's high nibble is the flash bank — the address bits above the 16-bit
|
# A selector's high nibble is the flash bank — the address bits above the 16-bit
|
||||||
# wire address — so a transfer names a byte address within one 64 KiB bank and
|
# wire address — so a transfer names a byte address within one 64 KiB bank and
|
||||||
# no command has to speak word addresses. No single transfer may cross a bank
|
# no command has to speak word addresses. No single transfer may cross a bank
|
||||||
@@ -66,31 +77,36 @@ CALIBRATE = 0xC0
|
|||||||
# from its chip database at build time). Die revisions that share a signature
|
# from its chip database at build time). Die revisions that share a signature
|
||||||
# share this row, as they share the silicon.
|
# share this row, as they share the silicon.
|
||||||
CHIP_GEOMETRY = {
|
CHIP_GEOMETRY = {
|
||||||
# signature : (flash, page, eeprom, patch_vector)
|
# signature : (flash, page, eeprom, patch_vector, ram_start)
|
||||||
(0x1E, 0x90, 0x07): (1024, 32, 64, True), # ATtiny13/13A
|
# ram_start is where SRAM begins in data space: the classic megas and the
|
||||||
(0x1E, 0x91, 0x08): (2048, 32, 128, True), # ATtiny25
|
# tinies keep it right after the plain I/O registers (0x60), the x8/x4
|
||||||
(0x1E, 0x92, 0x06): (4096, 64, 256, True), # ATtiny45
|
# generations past their extended I/O file (0x100). An autobaud loader's
|
||||||
(0x1E, 0x93, 0x0B): (8192, 64, 512, True), # ATtiny85
|
# measured bit period lives at exactly ram_start (its only RAM object;
|
||||||
(0x1E, 0x92, 0x05): (4096, 64, 256, True), # ATmega48/48A
|
# the loader's own build pins the layout), which is what --info reads.
|
||||||
(0x1E, 0x92, 0x0A): (4096, 64, 256, True), # ATmega48P/48PA
|
(0x1E, 0x90, 0x07): (1024, 32, 64, True, 0x60), # ATtiny13/13A
|
||||||
(0x1E, 0x93, 0x07): (8192, 64, 512, False), # ATmega8/8A
|
(0x1E, 0x91, 0x08): (2048, 32, 128, True, 0x60), # ATtiny25
|
||||||
(0x1E, 0x93, 0x0A): (8192, 64, 512, False), # ATmega88/88A
|
(0x1E, 0x92, 0x06): (4096, 64, 256, True, 0x60), # ATtiny45
|
||||||
(0x1E, 0x93, 0x0F): (8192, 64, 512, False), # ATmega88P/88PA
|
(0x1E, 0x93, 0x0B): (8192, 64, 512, True, 0x60), # ATtiny85
|
||||||
(0x1E, 0x94, 0x03): (16384, 128, 512, False), # ATmega16/16A
|
(0x1E, 0x92, 0x05): (4096, 64, 256, True, 0x100), # ATmega48/48A
|
||||||
(0x1E, 0x94, 0x06): (16384, 128, 512, False), # ATmega168/168A
|
(0x1E, 0x92, 0x0A): (4096, 64, 256, True, 0x100), # ATmega48P/48PA
|
||||||
(0x1E, 0x94, 0x0B): (16384, 128, 512, False), # ATmega168P/168PA
|
(0x1E, 0x93, 0x07): (8192, 64, 512, False, 0x60), # ATmega8/8A
|
||||||
(0x1E, 0x94, 0x0A): (16384, 128, 512, False), # ATmega164P/164PA
|
(0x1E, 0x93, 0x0A): (8192, 64, 512, False, 0x100), # ATmega88/88A
|
||||||
(0x1E, 0x94, 0x0F): (16384, 128, 512, False), # ATmega164A
|
(0x1E, 0x93, 0x0F): (8192, 64, 512, False, 0x100), # ATmega88P/88PA
|
||||||
(0x1E, 0x95, 0x02): (32768, 128, 1024, False), # ATmega32/32A
|
(0x1E, 0x94, 0x03): (16384, 128, 512, False, 0x60), # ATmega16/16A
|
||||||
(0x1E, 0x95, 0x0F): (32768, 128, 1024, False), # ATmega328P
|
(0x1E, 0x94, 0x06): (16384, 128, 512, False, 0x100), # ATmega168/168A
|
||||||
(0x1E, 0x95, 0x14): (32768, 128, 1024, False), # ATmega328
|
(0x1E, 0x94, 0x0B): (16384, 128, 512, False, 0x100), # ATmega168P/168PA
|
||||||
(0x1E, 0x95, 0x08): (32768, 128, 1024, False), # ATmega324P
|
(0x1E, 0x94, 0x0A): (16384, 128, 512, False, 0x100), # ATmega164P/164PA
|
||||||
(0x1E, 0x95, 0x11): (32768, 128, 1024, False), # ATmega324PA
|
(0x1E, 0x94, 0x0F): (16384, 128, 512, False, 0x100), # ATmega164A
|
||||||
(0x1E, 0x95, 0x15): (32768, 128, 1024, False), # ATmega324A
|
(0x1E, 0x95, 0x02): (32768, 128, 1024, False, 0x60), # ATmega32/32A
|
||||||
(0x1E, 0x96, 0x09): (65536, 256, 2048, False), # ATmega644/644A
|
(0x1E, 0x95, 0x0F): (32768, 128, 1024, False, 0x100), # ATmega328P
|
||||||
(0x1E, 0x96, 0x0A): (65536, 256, 2048, False), # ATmega644P/644PA
|
(0x1E, 0x95, 0x14): (32768, 128, 1024, False, 0x100), # ATmega328
|
||||||
(0x1E, 0x97, 0x05): (131072, 256, 4096, False),# ATmega1284P
|
(0x1E, 0x95, 0x08): (32768, 128, 1024, False, 0x100), # ATmega324P
|
||||||
(0x1E, 0x97, 0x06): (131072, 256, 4096, False),# ATmega1284
|
(0x1E, 0x95, 0x11): (32768, 128, 1024, False, 0x100), # ATmega324PA
|
||||||
|
(0x1E, 0x95, 0x15): (32768, 128, 1024, False, 0x100), # ATmega324A
|
||||||
|
(0x1E, 0x96, 0x09): (65536, 256, 2048, False, 0x100), # ATmega644/644A
|
||||||
|
(0x1E, 0x96, 0x0A): (65536, 256, 2048, False, 0x100), # ATmega644P/644PA
|
||||||
|
(0x1E, 0x97, 0x05): (131072, 256, 4096, False, 0x100),# ATmega1284P
|
||||||
|
(0x1E, 0x97, 0x06): (131072, 256, 4096, False, 0x100),# ATmega1284
|
||||||
}
|
}
|
||||||
|
|
||||||
VERBOSE = False
|
VERBOSE = False
|
||||||
@@ -144,36 +160,60 @@ class Progress:
|
|||||||
|
|
||||||
|
|
||||||
class PosixPort:
|
class PosixPort:
|
||||||
"""A raw serial port with deadline-based reads, over termios."""
|
"""A raw serial port with deadline-based reads, over termios. A rate with
|
||||||
|
no B-constant — the off-nominal probes `--scan` walks — goes through
|
||||||
|
Linux's termios2 BOTHER; a platform without that ioctl refuses the rate
|
||||||
|
by name."""
|
||||||
|
|
||||||
|
# The termios2 ioctl pair and cflag bits, and the struct's ispeed/ospeed
|
||||||
|
# word offsets: four flag words, then a line-discipline byte and 19
|
||||||
|
# control chars padded to word 9 (include/uapi/asm-generic/termbits.h).
|
||||||
|
_TCGETS2, _TCSETS2 = 0x802C542A, 0x402C542B
|
||||||
|
_BOTHER, _CBAUD = 0o010000, 0o010017
|
||||||
|
_ISPEED, _OSPEED = 9, 10
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _speed(baud):
|
def _speed(baud):
|
||||||
|
return getattr(termios, f"B{baud}", None)
|
||||||
|
|
||||||
|
def _set_arbitrary(self, baud):
|
||||||
|
buf = array.array("i", [0] * (self._OSPEED + 1))
|
||||||
try:
|
try:
|
||||||
return getattr(termios, f"B{baud}")
|
fcntl.ioctl(self.fd, self._TCGETS2, buf, True)
|
||||||
except AttributeError:
|
buf[2] = (buf[2] & ~self._CBAUD) | self._BOTHER
|
||||||
raise Error(f"unsupported baud rate {baud}") from None
|
buf[self._ISPEED] = buf[self._OSPEED] = baud
|
||||||
|
fcntl.ioctl(self.fd, self._TCSETS2, buf)
|
||||||
|
except OSError:
|
||||||
|
raise Error(f"this platform cannot set {baud} Bd (no termios2)") from None
|
||||||
|
|
||||||
|
def _apply_baud(self, attrs, baud):
|
||||||
|
speed = self._speed(baud)
|
||||||
|
attrs[4] = attrs[5] = speed if speed is not None else termios.B38400
|
||||||
|
termios.tcsetattr(self.fd, termios.TCSANOW, attrs)
|
||||||
|
if speed is None:
|
||||||
|
self._set_arbitrary(baud)
|
||||||
|
self.baud = baud
|
||||||
|
|
||||||
def __init__(self, path, baud):
|
def __init__(self, path, baud):
|
||||||
self.fd = os.open(path, os.O_RDWR | os.O_NOCTTY)
|
self.fd = os.open(path, os.O_RDWR | os.O_NOCTTY)
|
||||||
|
try:
|
||||||
attrs = termios.tcgetattr(self.fd)
|
attrs = termios.tcgetattr(self.fd)
|
||||||
attrs[0] = 0 # iflag
|
attrs[0] = 0 # iflag
|
||||||
attrs[1] = 0 # oflag
|
attrs[1] = 0 # oflag
|
||||||
attrs[2] = termios.CREAD | termios.CLOCAL | termios.CS8 # cflag
|
attrs[2] = termios.CREAD | termios.CLOCAL | termios.CS8 # cflag
|
||||||
attrs[3] = 0 # lflag
|
attrs[3] = 0 # lflag
|
||||||
attrs[4] = attrs[5] = self._speed(baud)
|
|
||||||
attrs[6][termios.VMIN] = 0
|
attrs[6][termios.VMIN] = 0
|
||||||
attrs[6][termios.VTIME] = 0
|
attrs[6][termios.VTIME] = 0
|
||||||
termios.tcsetattr(self.fd, termios.TCSANOW, attrs)
|
self._apply_baud(attrs, baud)
|
||||||
self.baud = baud
|
except BaseException:
|
||||||
|
os.close(self.fd)
|
||||||
|
raise
|
||||||
|
|
||||||
def set_baud(self, baud):
|
def set_baud(self, baud):
|
||||||
"""Retune the port without closing it — the fd stays open, so no DTR
|
"""Retune the port without closing it — the fd stays open, so no DTR
|
||||||
pulse and no reset. That matters: the only caller is mid-session with a
|
pulse and no reset. That matters: the only caller is mid-session with a
|
||||||
loader copy that a reset would throw away."""
|
loader copy that a reset would throw away."""
|
||||||
attrs = termios.tcgetattr(self.fd)
|
self._apply_baud(termios.tcgetattr(self.fd), baud)
|
||||||
attrs[4] = attrs[5] = self._speed(baud)
|
|
||||||
termios.tcsetattr(self.fd, termios.TCSANOW, attrs)
|
|
||||||
self.baud = baud
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
os.close(self.fd)
|
os.close(self.fd)
|
||||||
@@ -410,7 +450,7 @@ class Info:
|
|||||||
if geometry is None:
|
if geometry is None:
|
||||||
sig = " ".join(f"{b:02x}" for b in signature)
|
sig = " ".join(f"{b:02x}" for b in signature)
|
||||||
raise Error(f"unknown signature {sig} — this tool has no geometry for it")
|
raise Error(f"unknown signature {sig} — this tool has no geometry for it")
|
||||||
flash, page, eeprom, patch = geometry
|
flash, page, eeprom, patch, _ = geometry
|
||||||
base = flash - SLOT
|
base = flash - SLOT
|
||||||
word_flash = flash > 0x10000
|
word_flash = flash > 0x10000
|
||||||
wire_base = base // 2 if word_flash else base
|
wire_base = base // 2 if word_flash else base
|
||||||
@@ -446,6 +486,11 @@ class Info:
|
|||||||
# The hand-over target as 'J' takes it: the trampoline below the
|
# The hand-over target as 'J' takes it: the trampoline below the
|
||||||
# loader, or word 0 where BOOTRST re-vectors reset in hardware.
|
# loader, or word 0 where BOOTRST re-vectors reset in hardware.
|
||||||
self.app_entry_word = (self.base - 2) // 2 if self.patch_vector else 0
|
self.app_entry_word = (self.base - 2) // 2 if self.patch_vector else 0
|
||||||
|
# Where SRAM begins, from the signature — None only for a chip this
|
||||||
|
# tool has no geometry row for, which the wire-block path (v1–4)
|
||||||
|
# permits where from_identity refuses.
|
||||||
|
geometry = CHIP_GEOMETRY.get(tuple(self.signature))
|
||||||
|
self.ram = geometry[4] if geometry else None
|
||||||
|
|
||||||
def describe(self):
|
def describe(self):
|
||||||
sig = " ".join(f"{b:02x}" for b in self.signature)
|
sig = " ".join(f"{b:02x}" for b in self.signature)
|
||||||
@@ -512,6 +557,7 @@ class Loader:
|
|||||||
drain absorbs whatever they produced."""
|
drain absorbs whatever they produced."""
|
||||||
deadline = time.monotonic() + wait
|
deadline = time.monotonic() + wait
|
||||||
knocks = 0
|
knocks = 0
|
||||||
|
refusal = None
|
||||||
while True:
|
while True:
|
||||||
self.port.flush_input()
|
self.port.flush_input()
|
||||||
self.port.write(knock)
|
self.port.write(knock)
|
||||||
@@ -533,12 +579,18 @@ class Loader:
|
|||||||
except Error as failed:
|
except Error as failed:
|
||||||
if "pureboot" in str(failed):
|
if "pureboot" in str(failed):
|
||||||
raise
|
raise
|
||||||
|
# A malformed or unknown identity is retried as noise, but
|
||||||
|
# it was an answer: if nothing better ever arrives, naming
|
||||||
|
# it beats reporting silence.
|
||||||
|
refusal = failed
|
||||||
self.info = None
|
self.info = None
|
||||||
if self.info is not None:
|
if self.info is not None:
|
||||||
self._expect_prompt()
|
self._expect_prompt()
|
||||||
verbose(f"loader answered {what} {knocks}; identity read")
|
verbose(f"loader answered {what} {knocks}; identity read")
|
||||||
return self.info
|
return self.info
|
||||||
if time.monotonic() > deadline:
|
if time.monotonic() > deadline:
|
||||||
|
if refusal is not None:
|
||||||
|
raise Error(f"no usable answer — the last identity reply failed: {refusal}")
|
||||||
raise Error("no answer — reset the device within its activation window")
|
raise Error("no answer — reset the device within its activation window")
|
||||||
|
|
||||||
def connect(self, wait):
|
def connect(self, wait):
|
||||||
@@ -1351,6 +1403,62 @@ def op_fuses(loader):
|
|||||||
return fuse_bytes
|
return fuse_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def scan_ratios():
|
||||||
|
"""The probe walk, in percent of the built rate: the built rate itself
|
||||||
|
first, then ±10 % in 2 % steps nearest-first — a drifted oscillator near
|
||||||
|
its trim is the common case, and each probe costs a reset."""
|
||||||
|
return [0] + [sign * step for step in (2, 4, 6, 8, 10) for sign in (-1, 1)]
|
||||||
|
|
||||||
|
|
||||||
|
def scan_rate(baud, pct):
|
||||||
|
return round(baud * (100 + pct) / 100)
|
||||||
|
|
||||||
|
|
||||||
|
def scan_report(baud, pct, version, clock=None):
|
||||||
|
"""The findings, one per line: the found rate is the session workaround,
|
||||||
|
its ratio to the built rate is the oscillator's offset, and the fixes are
|
||||||
|
the OSCCAL bake (≈1 %/step, opposing the drift) or the autobaud build."""
|
||||||
|
rate = scan_rate(baud, pct)
|
||||||
|
lines = [f"scan: answered at {rate} Bd ({pct:+d} % of the built rate) — pureboot {version}",
|
||||||
|
f" session --baud {rate}"]
|
||||||
|
if clock:
|
||||||
|
lines.append(f" clock ~{clock * (100 + pct) // 100} Hz (built for {clock})")
|
||||||
|
if pct:
|
||||||
|
direction = "lower" if pct > 0 else "higher"
|
||||||
|
lines.append(f" fix rebuild with OSCCAL ~{abs(pct)} steps {direction} (~1 %/step), "
|
||||||
|
"or the autobaud build")
|
||||||
|
else:
|
||||||
|
lines.append(" fix none — the built rate answers; check the earlier wiring instead")
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def op_scan(port_path, baud, wait, clock=None):
|
||||||
|
"""A fixed-baud loader whose oscillator drifted still answers — at the
|
||||||
|
drifted ratio, since its rate scales with its clock. One probe per
|
||||||
|
activation window, and with an application resident the window opens
|
||||||
|
exactly once per reset, so each probe announces itself and expects a
|
||||||
|
fresh reset before knocking."""
|
||||||
|
for pct in scan_ratios():
|
||||||
|
rate = scan_rate(baud, pct)
|
||||||
|
print(f"scan: {rate} Bd ({pct:+d} %) — reset the target", flush=True)
|
||||||
|
try:
|
||||||
|
port = Port(port_path, rate)
|
||||||
|
except Error as unmakeable:
|
||||||
|
print(f"scan: {rate} Bd skipped — {unmakeable}")
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
info = Loader(port).connect(wait)
|
||||||
|
except Error:
|
||||||
|
continue
|
||||||
|
finally:
|
||||||
|
port.close()
|
||||||
|
for line in scan_report(baud, pct, info.version, clock):
|
||||||
|
print(line)
|
||||||
|
return
|
||||||
|
raise Error("no answer within ±10 % of the built rate — check the wiring, or deploy the "
|
||||||
|
"autobaud build, which has no rate to miss (README.md)")
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------- cli ---
|
# -------------------------------------------------------------------- cli ---
|
||||||
|
|
||||||
|
|
||||||
@@ -1366,6 +1474,12 @@ def main():
|
|||||||
parser.add_argument("--autobaud", action="store_true",
|
parser.add_argument("--autobaud", action="store_true",
|
||||||
help="drive an autobaud loader: send the 0xC0 calibration pulse and a single "
|
help="drive an autobaud loader: send the 0xC0 calibration pulse and a single "
|
||||||
"knock, and take geometry from the signature (no clock/baud baked in)")
|
"knock, and take geometry from the signature (no clock/baud baked in)")
|
||||||
|
parser.add_argument("--scan", action="store_true",
|
||||||
|
help="walk ±10%% around --baud for a fixed-baud loader gone silent — one "
|
||||||
|
"reset per probe, standalone (README.md: deployment)")
|
||||||
|
parser.add_argument("--clock", type=int, metavar="HZ",
|
||||||
|
help="the clock the loader was built for — lets --scan and an autobaud "
|
||||||
|
"--info state drift in absolute terms")
|
||||||
parser.add_argument("--info", action="store_true", help="print the device info block")
|
parser.add_argument("--info", action="store_true", help="print the device info block")
|
||||||
parser.add_argument("--fuses", action="store_true", help="read the fuse and lock bytes")
|
parser.add_argument("--fuses", action="store_true", help="read the fuse and lock bytes")
|
||||||
parser.add_argument("--update-loader", metavar="FILE", help="replace the loader with this pureboot binary")
|
parser.add_argument("--update-loader", metavar="FILE", help="replace the loader with this pureboot binary")
|
||||||
@@ -1412,6 +1526,12 @@ def main():
|
|||||||
except (ValueError, AssertionError):
|
except (ValueError, AssertionError):
|
||||||
parser.error("--assume-fuses takes 8 hex digits: low,lock,extended,high")
|
parser.error("--assume-fuses takes 8 hex digits: low,lock,extended,high")
|
||||||
|
|
||||||
|
if args.scan:
|
||||||
|
if args.autobaud:
|
||||||
|
parser.error("--scan probes fixed rates; an autobaud loader has none to miss")
|
||||||
|
op_scan(args.port, args.baud, args.wait, args.clock)
|
||||||
|
return
|
||||||
|
|
||||||
port = Port(args.port, args.baud)
|
port = Port(args.port, args.baud)
|
||||||
verbose(f"{args.port}: {args.baud} Bd 8N1, DTR/RTS asserted")
|
verbose(f"{args.port}: {args.baud} Bd 8N1, DTR/RTS asserted")
|
||||||
try:
|
try:
|
||||||
@@ -1421,6 +1541,18 @@ def main():
|
|||||||
print("device:")
|
print("device:")
|
||||||
for line in info.lines():
|
for line in info.lines():
|
||||||
print(f" {line}")
|
print(f" {line}")
|
||||||
|
if args.autobaud and info.ram is not None:
|
||||||
|
# The whole of the loader's RAM is the measured bit period at
|
||||||
|
# ram_start; decoded and times the rate this session drives,
|
||||||
|
# that is the true clock — the number to hold an OSCCAL bake
|
||||||
|
# or a fixed-baud build against (README.md: deployment). The
|
||||||
|
# autobaud identity path refuses unknown signatures, so ram is
|
||||||
|
# always known here; the guard states that dependency.
|
||||||
|
unit = int.from_bytes(loader.read_ram(info.ram, 2), "little")
|
||||||
|
cycles = unit * UNIT_LOOP_CYCLES + UNIT_DISCOUNT
|
||||||
|
clock = cycles * args.baud
|
||||||
|
offset = f", {(clock / args.clock - 1) * 100:+.1f} % of {args.clock}" if args.clock else ""
|
||||||
|
print(f" measured {clock} Hz ({cycles} cycles/bit × {args.baud} Bd{offset})")
|
||||||
fuse_bytes = fuse_override
|
fuse_bytes = fuse_override
|
||||||
if args.fuses or (args.update_loader and not info.patch_vector and fuse_bytes is None):
|
if args.fuses or (args.update_loader and not info.patch_vector and fuse_bytes is None):
|
||||||
read = op_fuses(loader)
|
read = op_fuses(loader)
|
||||||
@@ -1468,7 +1600,7 @@ def main():
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
main()
|
main()
|
||||||
except Error as error:
|
except (Error, OSError) as error:
|
||||||
print(f"error: {error}", file=sys.stderr)
|
print(f"error: {error}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
23
test/check_unit.cmake
Normal file
23
test/check_unit.cmake
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Asserts the autobaud loader's measured unit is the first RAM object: the
|
||||||
|
# host tool reads the bit period from ram_start (--info's measured clock), so
|
||||||
|
# the unit's address is wire contract. Run as
|
||||||
|
# cmake -DOBJDUMP=... -DELF=... -DRAM_START=<data address> -P check_unit.cmake
|
||||||
|
|
||||||
|
execute_process(COMMAND ${OBJDUMP} -t ${ELF} OUTPUT_VARIABLE _syms RESULT_VARIABLE _res)
|
||||||
|
if(NOT _res EQUAL 0)
|
||||||
|
message(FATAL_ERROR "${OBJDUMP} -t ${ELF} failed")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# The symbol line: "00800100 l O .noinit 00000002 <mangled>unit_E".
|
||||||
|
string(REGEX MATCH "\n0*([0-9a-f]+)[^\n]+[ \t][^ \t\n]*unit_[^ \t\n]*\n" _line "${_syms}")
|
||||||
|
if(NOT _line)
|
||||||
|
message(FATAL_ERROR "no unit_ symbol in ${ELF} — is this the autobaud loader?")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# AVR data-space symbols carry the 0x800000 VMA offset.
|
||||||
|
math(EXPR _want "0x800000 + ${RAM_START}" OUTPUT_FORMAT HEXADECIMAL)
|
||||||
|
math(EXPR _have "0x${CMAKE_MATCH_1}" OUTPUT_FORMAT HEXADECIMAL)
|
||||||
|
if(NOT _have STREQUAL _want)
|
||||||
|
message(FATAL_ERROR "unit_ sits at ${_have}, ram_start is ${_want} — the host peeks ram_start")
|
||||||
|
endif()
|
||||||
|
message(STATUS "unit_ at ${_have} == ram_start")
|
||||||
@@ -16,6 +16,7 @@ baked in.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -58,11 +59,23 @@ def main():
|
|||||||
try:
|
try:
|
||||||
# The host tool, in autobaud mode, sends the 0xC0 calibration pulse
|
# The host tool, in autobaud mode, sends the 0xC0 calibration pulse
|
||||||
# and a single knock at `baud`; the loader locks to it.
|
# and a single knock at `baud`; the loader locks to it.
|
||||||
out = pbsim.run_tool(tool, device.pty, baud, "--autobaud", "--info", "--fuses",
|
out = pbsim.run_tool(tool, device.pty, baud, "--autobaud", "--info", "--clock", str(hz),
|
||||||
"--flash", app_bin, "--eeprom", ee_path, "--stay")
|
"--fuses", "--flash", app_bin, "--eeprom", ee_path, "--stay")
|
||||||
for needed in ("version", "signature", "fuses", "verify:", "stays"):
|
for needed in ("version", "signature", "fuses", "verify:", "stays"):
|
||||||
if needed not in out:
|
if needed not in out:
|
||||||
fail(f"{label}: session output lacks {needed!r}\n{out}")
|
fail(f"{label}: session output lacks {needed!r}\n{out}")
|
||||||
|
# The measured clock, decoded from the unit at ram_start. The
|
||||||
|
# runner's clock is exact, so the figure must land inside the
|
||||||
|
# encoding's own envelope: the loader floors the bit period to
|
||||||
|
# 4-cycle spin granules after an 8-cycle discount, and the edge
|
||||||
|
# poll can shave a few cycles more — one granule of slack below
|
||||||
|
# the true clock, none above (in cycles per bit, times the rate).
|
||||||
|
measured = re.search(r"measured\s+(\d+) Hz", out)
|
||||||
|
if not measured:
|
||||||
|
fail(f"{label}: --info lacks the measured clock\n{out}")
|
||||||
|
measured = int(measured.group(1))
|
||||||
|
if not hz - 19 * baud <= measured <= hz + 4 * baud:
|
||||||
|
fail(f"{label}: measured clock {measured} Hz is {measured - hz:+d} off the true {hz}")
|
||||||
# Read both memories back over the locked link and check them.
|
# Read both memories back over the locked link and check them.
|
||||||
read_flash = os.path.join(workdir, f"rf_{label}.bin")
|
read_flash = os.path.join(workdir, f"rf_{label}.bin")
|
||||||
read_eeprom = os.path.join(workdir, f"re_{label}.bin")
|
read_eeprom = os.path.join(workdir, f"re_{label}.bin")
|
||||||
|
|||||||
46
test/pbosccal.py
Normal file
46
test/pbosccal.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""The build-time OSCCAL trim, observed through the wire: a loader built with
|
||||||
|
the OSCCAL axis holds the trim register at the built byte from its first
|
||||||
|
prompt on — the write sits at the top of run(), ahead of the WDRF bail, so
|
||||||
|
every path out of reset runs on the corrected clock. simavr's clock does not
|
||||||
|
follow OSCCAL, which is what makes the value assertable at all: the register
|
||||||
|
is plain state there, and the peek must return exactly what the build
|
||||||
|
declared rather than whatever the oscillator needed.
|
||||||
|
|
||||||
|
Usage: pbosccal.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||||
|
<baud> <osccal_addr> <osccal_value> <tool_py> <workdir>
|
||||||
|
[link]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
print(f"FAIL: {message}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = sys.argv[1:]
|
||||||
|
link = args.pop() if len(args) == 12 else None
|
||||||
|
(device_bin, elf, mcu, hz, base_hex, page, baud, addr, value, tool, workdir) = args
|
||||||
|
addr, value, baud = int(addr, 0), int(value, 0), int(baud)
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import pbsim
|
||||||
|
|
||||||
|
os.makedirs(workdir, exist_ok=True)
|
||||||
|
dump = os.path.join(workdir, "flash_dump.bin")
|
||||||
|
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, link=link)
|
||||||
|
try:
|
||||||
|
out = pbsim.run_tool(tool, device.pty, baud, "--peek", f"{addr:#x}:1")
|
||||||
|
want = f"{addr:#06x} {value:02x}"
|
||||||
|
if want not in out:
|
||||||
|
fail(f"OSCCAL at {addr:#x} did not read back {value:#04x}:\n{out}")
|
||||||
|
finally:
|
||||||
|
device.stop()
|
||||||
|
print("OK")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -193,10 +193,11 @@ static avr_cycle_count_t tx_sample(avr_t *mcu, avr_cycle_count_t when, void *par
|
|||||||
tx_shift = (uint8_t)((tx_shift >> 1) | (tx_level ? 0x80 : 0));
|
tx_shift = (uint8_t)((tx_shift >> 1) | (tx_level ? 0x80 : 0));
|
||||||
if (++tx_bit < 8)
|
if (++tx_bit < 8)
|
||||||
return when + bit_cycles;
|
return when + bit_cycles;
|
||||||
/* The byte is not delivered until its stop bit has passed. A real
|
/* The byte is delivered at the stop bit's sampling point (9.5 bit
|
||||||
* receiver cannot answer sooner, and a host that did would put its
|
* times), where a hardware receiver raises its RXC — not sooner: a
|
||||||
* start bit on the wire while the device is still driving the stop
|
* host answering before the stop bit would put its start bit on the
|
||||||
* bit — which the device, transmitting, is not watching for. */
|
* wire while the device is still driving, which the device,
|
||||||
|
* transmitting, is not watching for. */
|
||||||
return when + bit_cycles;
|
return when + bit_cycles;
|
||||||
}
|
}
|
||||||
if (write(pty_master, &tx_shift, 1) != 1)
|
if (write(pty_master, &tx_shift, 1) != 1)
|
||||||
@@ -412,6 +413,14 @@ int main(int argc, char *argv[])
|
|||||||
fprintf(stderr, "device: cannot read %s\n", argv[1]);
|
fprintf(stderr, "device: cannot read %s\n", argv[1]);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
// An image past flash end would smash the simulator's heap and turn
|
||||||
|
// into phantom peripheral behavior (lessons: believe the size gate
|
||||||
|
// first) — refuse it loudly instead.
|
||||||
|
if (base + fw.flashsize > avr->flashend + 1) {
|
||||||
|
fprintf(stderr, "device: %u B at 0x%x runs past flash end 0x%x — image does not fit its slot\n",
|
||||||
|
(unsigned)fw.flashsize, base, avr->flashend);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
memcpy(avr->flash + base, fw.flash, fw.flashsize);
|
memcpy(avr->flash + base, fw.flash, fw.flashsize);
|
||||||
}
|
}
|
||||||
// The boot-sectioned megas enter the loader in hardware (BOOTRST, not
|
// The boot-sectioned megas enter the loader in hardware (BOOTRST, not
|
||||||
|
|||||||
71
test/test_scan.py
Normal file
71
test/test_scan.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""--scan's walk and report logic, no simulator: the probe order, the rate
|
||||||
|
arithmetic, and the advice's direction. The rate physics itself is not
|
||||||
|
sim-testable — a pty carries bytes at any termios rate — so what the wire
|
||||||
|
would arbitrate is pinned here as logic instead.
|
||||||
|
|
||||||
|
Usage: test_scan.py <tool_py>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
print(f"FAIL: {message}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(sys.argv[1])))
|
||||||
|
import pureboot as pb
|
||||||
|
|
||||||
|
walk = pb.scan_ratios()
|
||||||
|
if walk != [0, -2, 2, -4, 4, -6, 6, -8, 8, -10, 10]:
|
||||||
|
fail(f"probe walk is not built-rate-first, nearest-out: {walk}")
|
||||||
|
|
||||||
|
if pb.scan_rate(9600, 4) != 9984 or pb.scan_rate(9600, -4) != 9216:
|
||||||
|
fail("probe rate arithmetic")
|
||||||
|
if pb.scan_rate(115200, 0) != 115200:
|
||||||
|
fail("the built rate must probe unchanged")
|
||||||
|
|
||||||
|
# A loader answering fast means a fast oscillator: the trim goes down.
|
||||||
|
report = "\n".join(pb.scan_report(9600, 4, 6))
|
||||||
|
for needle in ("9984", "+4 %", "--baud 9984", "4 steps lower", "pureboot 6"):
|
||||||
|
if needle not in report:
|
||||||
|
fail(f"+4 % report lacks {needle!r}:\n{report}")
|
||||||
|
report = "\n".join(pb.scan_report(9600, -6, 6))
|
||||||
|
if "6 steps higher" not in report:
|
||||||
|
fail(f"-6 % report advises the wrong direction:\n{report}")
|
||||||
|
|
||||||
|
report = "\n".join(pb.scan_report(9600, 0, 6))
|
||||||
|
if "none" not in report or "steps" in report:
|
||||||
|
fail(f"an on-rate answer must advise no trim:\n{report}")
|
||||||
|
|
||||||
|
report = "\n".join(pb.scan_report(9600, 4, 6, clock=9600000))
|
||||||
|
if "9984000" not in report:
|
||||||
|
fail(f"the absolute clock must scale with the found ratio:\n{report}")
|
||||||
|
|
||||||
|
# The walk's rates mostly have no termios B-constant, so the POSIX port
|
||||||
|
# must set them through termios2 — probed on a pty, which accepts the
|
||||||
|
# ioctl without caring about the speed. Without this every off-nominal
|
||||||
|
# probe would abort the walk on the platform --scan matters most on.
|
||||||
|
if os.name == "posix":
|
||||||
|
import pty
|
||||||
|
|
||||||
|
master, slave = pty.openpty()
|
||||||
|
try:
|
||||||
|
port = pb.Port(os.ttyname(slave), pb.scan_rate(9600, 4))
|
||||||
|
port.set_baud(pb.scan_rate(9600, -4))
|
||||||
|
port.close()
|
||||||
|
except pb.Error as error:
|
||||||
|
fail(f"PosixPort refused an off-nominal probe rate: {error}")
|
||||||
|
finally:
|
||||||
|
os.close(master)
|
||||||
|
os.close(slave)
|
||||||
|
|
||||||
|
print("OK")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -7,11 +7,14 @@ port's TUs compile identically; the sims prove nothing new there) exist for
|
|||||||
libavr's reflect spot set only, mirroring its rule: the full reflect matrix
|
libavr's reflect spot set only, mirroring its rule: the full reflect matrix
|
||||||
is never built, one chip per hardware class and pack vintage is.
|
is never built, one chip per hardware class and pack vintage is.
|
||||||
|
|
||||||
Run from the repo root: tools/make_presets.py
|
Run from the repo root: tools/make_presets.py — or with --check, which
|
||||||
|
verifies the committed file matches this generator and edits nothing (the
|
||||||
|
ctest entry `presets.generated` runs that, so drift reds the gate).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
CHIPS = [
|
CHIPS = [
|
||||||
"attiny13", "attiny13a", "attiny25", "attiny45", "attiny85",
|
"attiny13", "attiny13a", "attiny25", "attiny45", "attiny85",
|
||||||
@@ -41,7 +44,7 @@ def main():
|
|||||||
"hidden": True,
|
"hidden": True,
|
||||||
"generator": "Ninja",
|
"generator": "Ninja",
|
||||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||||
"toolchainFile": "$env{LIBAVR_ROOT}/cmake/avr-toolchain.cmake",
|
"toolchainFile": "${sourceDir}/libavr/cmake/avr-toolchain.cmake",
|
||||||
"cacheVariables": {
|
"cacheVariables": {
|
||||||
"CMAKE_BUILD_TYPE": "Release",
|
"CMAKE_BUILD_TYPE": "Release",
|
||||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
||||||
@@ -72,6 +75,9 @@ def main():
|
|||||||
for chip in REFLECT_SPOT:
|
for chip in REFLECT_SPOT:
|
||||||
add(chip, "reflect")
|
add(chip, "reflect")
|
||||||
|
|
||||||
|
# CMake rejects unknown fields in the presets root, $comment included, so
|
||||||
|
# the file cannot carry a generated-file marker; the --check ctest is the
|
||||||
|
# whole of rule 10's guard here.
|
||||||
presets = {
|
presets = {
|
||||||
"version": 8,
|
"version": 8,
|
||||||
"configurePresets": configure,
|
"configurePresets": configure,
|
||||||
@@ -79,12 +85,19 @@ def main():
|
|||||||
"testPresets": test,
|
"testPresets": test,
|
||||||
"workflowPresets": workflows,
|
"workflowPresets": workflows,
|
||||||
}
|
}
|
||||||
|
rendered = json.dumps(presets, indent=1) + "\n"
|
||||||
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "CMakePresets.json")
|
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "CMakePresets.json")
|
||||||
|
if "--check" in sys.argv[1:]:
|
||||||
|
current = open(path).read() if os.path.exists(path) else ""
|
||||||
|
if current != rendered:
|
||||||
|
print("CMakePresets.json does not match its generator — run tools/make_presets.py")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
with open(path, "w") as f:
|
with open(path, "w") as f:
|
||||||
json.dump(presets, f, indent=1)
|
f.write(rendered)
|
||||||
f.write("\n")
|
|
||||||
print(f"{len(CHIPS)} chips, {len(REFLECT_SPOT)} reflect: {os.path.normpath(path)}")
|
print(f"{len(CHIPS)} chips, {len(REFLECT_SPOT)} reflect: {os.path.normpath(path)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
sys.exit(main())
|
||||||
|
|||||||
@@ -72,6 +72,39 @@ class Suite:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def scan(self) -> None:
|
||||||
|
"""The --scan walk against real termios and a real oscillator: every
|
||||||
|
probe rate must open a port (the off-nominal rates exist only through
|
||||||
|
termios2), and one probe must answer — the nominal on a healthy board,
|
||||||
|
a neighbor on a drifted one. The rig injects the one reset per probe
|
||||||
|
the operator supplies in the field; this is the rate physics the
|
||||||
|
simulator cannot arbitrate (a pty carries bytes at any rate), pinned
|
||||||
|
on silicon."""
|
||||||
|
module = pbrig.load_pureboot(self.rig.d.pureboot)
|
||||||
|
found = None
|
||||||
|
try:
|
||||||
|
for pct in module.scan_ratios():
|
||||||
|
rate = module.scan_rate(self.rig.d.baud, pct)
|
||||||
|
self.rig.reset()
|
||||||
|
try:
|
||||||
|
port = module.Port(self.rig.d.port, rate)
|
||||||
|
except module.Error as error:
|
||||||
|
self.check("scan opens every probe rate", False, f"{rate} Bd: {error}")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
module.Loader(port).connect(min(self.rig.d.wait, 6.0))
|
||||||
|
found = pct
|
||||||
|
break
|
||||||
|
except module.Error:
|
||||||
|
continue
|
||||||
|
finally:
|
||||||
|
port.close()
|
||||||
|
except Exception as error: # noqa: BLE001 — a rig hiccup is a result
|
||||||
|
self.check("scan walks the probe ladder", False, str(error)[:70])
|
||||||
|
return
|
||||||
|
self.check("scan finds the board's rate", found is not None,
|
||||||
|
"no probe answered" if found is None else f"{found:+d} % of {self.rig.d.baud} Bd")
|
||||||
|
|
||||||
def eeprom(self, info) -> None:
|
def eeprom(self, info) -> None:
|
||||||
size = info.eeprom_size
|
size = info.eeprom_size
|
||||||
if not size:
|
if not size:
|
||||||
@@ -161,6 +194,10 @@ class Suite:
|
|||||||
print("\nthe loader never answered; nothing below can be trusted")
|
print("\nthe loader never answered; nothing below can be trusted")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
if not self.rig.d.autobaud:
|
||||||
|
print("\nscan")
|
||||||
|
self.scan()
|
||||||
|
|
||||||
print("\nEEPROM")
|
print("\nEEPROM")
|
||||||
self.eeprom(info)
|
self.eeprom(info)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user