Compare commits
3 Commits
v5
...
1b18f10f4e
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b18f10f4e | |||
| 52c4cdab32 | |||
| 69f089e53a |
@@ -167,6 +167,9 @@ 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)
|
||||||
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 +273,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 +399,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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -72,6 +72,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
|
||||||
@@ -285,7 +286,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 +353,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 +389,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 +450,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
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -24,13 +24,15 @@ else:
|
|||||||
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 +43,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 +75,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
|
||||||
@@ -410,7 +424,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 +460,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)
|
||||||
@@ -1351,6 +1370,58 @@ 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)
|
||||||
|
port = Port(port_path, rate)
|
||||||
|
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 +1437,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 +1489,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 +1504,16 @@ 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:
|
||||||
|
# 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).
|
||||||
|
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)
|
||||||
|
|||||||
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()
|
||||||
53
test/test_scan.py
Normal file
53
test/test_scan.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/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}")
|
||||||
|
|
||||||
|
print("OK")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user