Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bad8b6b43e | |||
| 5d1b4497d4 | |||
| a743ea64a3 | |||
| aa66cfccff | |||
| 459d463283 | |||
| 8e7cc86fb3 | |||
| c8ac61779e | |||
| 4362886c39 | |||
| 0513d07e87 | |||
| d4ab28aa17 | |||
| b60f182105 | |||
| 5bc35a9733 | |||
| 8f6319c068 |
@@ -202,6 +202,21 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
${CMAKE_BINARY_DIR}/pbtest-work)
|
${CMAKE_BINARY_DIR}/pbtest-work)
|
||||||
set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180)
|
set_tests_properties(pureboot.protocol PROPERTIES TIMEOUT 180)
|
||||||
|
|
||||||
|
# The activation window as a measured duration: application installed,
|
||||||
|
# line idle, the first transmit is the application's banner — its
|
||||||
|
# cycle is the window the source declares, held to ±2 % (one
|
||||||
|
# mis-counted cycle per poll is a 10 % shift).
|
||||||
|
add_test(NAME pureboot.window
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbwindow.py
|
||||||
|
--device ${PB_DEVICE} --loader $<TARGET_FILE:pureboot>
|
||||||
|
--mcu ${PUREBOOT_SIM_MCU} --hz ${_pb_stock_hz}
|
||||||
|
--base ${PUREBOOT_BASE_HEX} --page ${PUREBOOT_PAGE}
|
||||||
|
--baud ${_pb_stock_baud} --app $<TARGET_FILE:pbapp>.bin
|
||||||
|
--seconds ${PUREBOOT_TIMEOUT}
|
||||||
|
--tool ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
--workdir ${CMAKE_BINARY_DIR}/pbwindow-work)
|
||||||
|
set_tests_properties(pureboot.window PROPERTIES TIMEOUT 300)
|
||||||
|
|
||||||
# The position-independence acceptance test: the identical image,
|
# The position-independence acceptance test: the identical image,
|
||||||
# installed one slot lower, must serve the full command set.
|
# installed one slot lower, must serve the full command set.
|
||||||
add_test(NAME pureboot.reloc
|
add_test(NAME pureboot.reloc
|
||||||
@@ -283,12 +298,14 @@ 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
|
# The measured unit's home is wire contract, not layout accident: the
|
||||||
# start of SRAM — where the host reads the bit period from (--info's
|
# host reads the bit period from it (--info's measured clock). In the
|
||||||
# measured clock), so the address is wire contract, not layout accident.
|
# GPIOR home the image must carry no RAM copy at all; in the RAM home it
|
||||||
|
# is the loader's only RAM object, at the very start of SRAM.
|
||||||
add_test(NAME pureboot_autobaud.unit
|
add_test(NAME pureboot_autobaud.unit
|
||||||
COMMAND ${CMAKE_COMMAND} -DOBJDUMP=${CMAKE_OBJDUMP} -DELF=$<TARGET_FILE:pureboot_autobaud>
|
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)
|
-DRAM_START=${PUREBOOT_RAM_START} -DGPIOR=${PUREBOOT_UNIT_GPIOR}
|
||||||
|
-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
|
||||||
@@ -541,5 +558,19 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
${CMAKE_BINARY_DIR}/pbautobaud-work)
|
${CMAKE_BINARY_DIR}/pbautobaud-work)
|
||||||
set_tests_properties(pureboot.autobaud PROPERTIES TIMEOUT 240)
|
set_tests_properties(pureboot.autobaud PROPERTIES TIMEOUT 240)
|
||||||
|
|
||||||
|
# The autobaud window: the calibration poll budget, at the measured
|
||||||
|
# 10 cycles a poll (pbwindow.py pins the constant the README's
|
||||||
|
# seconds arithmetic uses; the budget itself is the clock-free knob).
|
||||||
|
add_test(NAME pureboot.window.autobaud
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbwindow.py
|
||||||
|
--device ${PB_DEVICE} --loader $<TARGET_FILE:pureboot_autobaud>
|
||||||
|
--mcu ${PUREBOOT_SIM_MCU} --hz 1000000
|
||||||
|
--base ${PUREBOOT_BASE_HEX} --page ${PUREBOOT_PAGE}
|
||||||
|
--baud 9600 --app $<TARGET_FILE:pbapp_autobaud>.bin
|
||||||
|
--autobaud-polls 4000000 --link sw
|
||||||
|
--tool ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
--workdir ${CMAKE_BINARY_DIR}/pbwindow-autobaud-work)
|
||||||
|
set_tests_properties(pureboot.window.autobaud PROPERTIES TIMEOUT 300)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
`master` carries `bootloader.atsln`, so this branch does too: `ide/bootloader.atsln`
|
`master` carries `bootloader.atsln`, so this branch does too: `ide/bootloader.atsln`
|
||||||
builds the loaders from the same sources Ninja does, to a **byte-identical
|
builds the loaders from the same sources Ninja does, to a **byte-identical
|
||||||
`.text`** — 400 B for the 328P pureboot loader, 510 B for the `tsb_asm` tier in
|
`.text`** — 390 B for the 328P pureboot loader, 510 B for the `tsb_asm` tier in
|
||||||
its 512-byte section. CMake remains the build system; the solution is here so the
|
its 512-byte section. CMake remains the build system; the solution is here so the
|
||||||
port opens in Studio as its predecessor did.
|
port opens in Studio as its predecessor did.
|
||||||
|
|
||||||
|
|||||||
2
libavr
2
libavr
Submodule libavr updated: 911a87538f...a9fe6bed50
@@ -138,15 +138,24 @@ endif()
|
|||||||
|
|
||||||
# Where SRAM begins: the classic megas keep it right after the plain I/O
|
# 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
|
# 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
|
# the tinies match the classics. An autobaud loader keeps its measured unit
|
||||||
# exactly this address (the host reads it there — pureboot.py), and the
|
# in GPIOR2:GPIOR1 wherever the chip has the pair (data 0x32 on the
|
||||||
# unit-position test holds the layout to it.
|
# t25/45/85, 0x4A from the x8 generation on) and as the first RAM object at
|
||||||
|
# SRAM start where it does not (the t13s and classic megas). The host reads
|
||||||
|
# whichever home applies (pureboot.py's geometry), and the unit-position
|
||||||
|
# test holds the image to the same split.
|
||||||
if(LIBAVR_MCU MATCHES "^atmega(8|16|32)a?$")
|
if(LIBAVR_MCU MATCHES "^atmega(8|16|32)a?$")
|
||||||
set(_pb_ram 0x60)
|
set(_pb_ram 0x60)
|
||||||
|
set(_pb_unit_gpior "")
|
||||||
elseif(LIBAVR_MCU MATCHES "^atmega")
|
elseif(LIBAVR_MCU MATCHES "^atmega")
|
||||||
set(_pb_ram 0x100)
|
set(_pb_ram 0x100)
|
||||||
|
set(_pb_unit_gpior 0x4A)
|
||||||
|
elseif(LIBAVR_MCU MATCHES "^attiny13")
|
||||||
|
set(_pb_ram 0x60)
|
||||||
|
set(_pb_unit_gpior "")
|
||||||
else()
|
else()
|
||||||
set(_pb_ram 0x60)
|
set(_pb_ram 0x60)
|
||||||
|
set(_pb_unit_gpior 0x32)
|
||||||
endif()
|
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
|
||||||
@@ -169,6 +178,7 @@ 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_RAM_START ${_pb_ram} PARENT_SCOPE)
|
||||||
|
set(PUREBOOT_UNIT_GPIOR "${_pb_unit_gpior}" 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)
|
||||||
@@ -366,9 +376,18 @@ function(pureboot_add_loader name)
|
|||||||
# load-immediate it saves. The set is fitted to the loader's body and has to
|
# load-immediate it saves. The set is fitted to the loader's body and has to
|
||||||
# be re-measured when that body changes: -fno-move-loop-invariants belonged
|
# be re-measured when that body changes: -fno-move-loop-invariants belonged
|
||||||
# here while the command loop carried four transfer bodies and costs bytes
|
# here while the command loop carried four transfer bodies and costs bytes
|
||||||
# now that it carries one.
|
# now that it carries one, and -fno-ivopts is fitted per backend — an
|
||||||
target_compile_options(${name} PRIVATE
|
# autobaud body needs ivopts to keep the calibration countdown a single
|
||||||
-fno-ivopts -fira-algorithm=priority -fno-tree-ter -fno-split-wide-types)
|
# induction variable (without it the counter is duplicated and the
|
||||||
|
# measurement loop runs 9 cycles instead of its contracted 7), while the
|
||||||
|
# fixed-baud bodies still measure smaller with it off.
|
||||||
|
if(PB_SERIAL STREQUAL "autobaud")
|
||||||
|
target_compile_options(${name} PRIVATE
|
||||||
|
-fira-algorithm=priority -fno-tree-ter -fno-split-wide-types)
|
||||||
|
else()
|
||||||
|
target_compile_options(${name} PRIVATE
|
||||||
|
-fno-ivopts -fira-algorithm=priority -fno-tree-ter -fno-split-wide-types)
|
||||||
|
endif()
|
||||||
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${_base_hex}
|
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${_base_hex}
|
||||||
-Wl,--defsym=pureboot_app=${_app} ${_wrap})
|
-Wl,--defsym=pureboot_app=${_app} ${_wrap})
|
||||||
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
|
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by
|
A serial bootloader on [libavr](https://git.blackmark.me/avr/libavr), pure by
|
||||||
constraint: one C++ source, no inline assembly, no global register variables
|
constraint: one C++ source, no inline assembly, no global register variables
|
||||||
(attributes and compiler flags allowed), **512 bytes on every chip libavr
|
(attributes and compiler flags allowed), **a 512-byte slot on every chip
|
||||||
targets — all 37**. The device speaks primitives; every composite — verify,
|
libavr targets — all 37**. The device speaks primitives; every composite — verify,
|
||||||
erase, reset-vector surgery, updating the loader itself — lives in the host
|
erase, reset-vector surgery, updating the loader itself — lives in the host
|
||||||
tool (`pureboot.py`).
|
tool (`pureboot.py`).
|
||||||
|
|
||||||
@@ -19,42 +19,42 @@ come out byte-identical linked at a different base.
|
|||||||
|
|
||||||
## Chips
|
## Chips
|
||||||
|
|
||||||
Sizes are the default configuration: the hardware USART0 at 115200 8N1 on a
|
The Stock column is the default configuration: the hardware USART0 at 115200
|
||||||
16 MHz crystal, or the software UART on RX = PB0 / TX = PB1 at 57600 8N1 on
|
8N1 on a 16 MHz crystal, or the software UART on RX = PB0 / TX = PB1 at
|
||||||
the tinies' RC oscillator (9.6 MHz on the t13s, 8 MHz above). Every axis moves
|
57600 8N1 on the tinies' RC oscillator (9.6 MHz on the t13s, 8 MHz above).
|
||||||
per build — see *Configuration*. The autobaud column is the clock-free build,
|
Every axis moves per build — see *Configuration*. The Autobaud column is the
|
||||||
which is the largest the space produces and the tightest fit in the matrix;
|
worst configuration the space produces for the chip: the clock-free build —
|
||||||
it carries the calibration machinery and no clock at all.
|
it alone carries the calibration machinery — with the `OSCCAL` trim baked
|
||||||
|
and, where the chip has a USART, the link deployed on that USART's own pins,
|
||||||
|
which the loader then has to release (*Pin ownership*). On default pins
|
||||||
|
without the trim the same loaders run 4–10 B smaller.
|
||||||
|
|
||||||
| Chip | Flash | Loader at | Link | Stock | Autobaud |
|
| Chip | Flash | Loader at | Link | Stock | Autobaud |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 390 B | 460 B |
|
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 384 B | 474 B |
|
||||||
| ATtiny25 † | 2 KiB | 0x0600 | software | 394 B | 464 B |
|
| ATtiny25 † | 2 KiB | 0x0600 | software | 388 B | 466 B |
|
||||||
| ATtiny45 † | 4 KiB | 0x0e00 | software | 398 B | 468 B |
|
| ATtiny45 † | 4 KiB | 0x0e00 | software | 388 B | 466 B |
|
||||||
| ATtiny85 † | 8 KiB | 0x1e00 | software | 398 B | 468 B |
|
| ATtiny85 † | 8 KiB | 0x1e00 | software | 388 B | 466 B |
|
||||||
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 360 B | 474 B |
|
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 362 B | 494 B |
|
||||||
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 362 B | 480 B |
|
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 364 B | 496 B |
|
||||||
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 362 B | 480 B |
|
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 364 B | 496 B |
|
||||||
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 388 B | 464 B |
|
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 378 B | 468 B |
|
||||||
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 398 B | 474 B |
|
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 388 B | 478 B |
|
||||||
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 400 B | 480 B |
|
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 390 B | 480 B |
|
||||||
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 400 B | 480 B |
|
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 390 B | 480 B |
|
||||||
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 400 B | 480 B |
|
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 390 B | 480 B |
|
||||||
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 400 B | 480 B |
|
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 390 B | 480 B |
|
||||||
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 394 B | 474 B |
|
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 384 B | 474 B |
|
||||||
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 420 B | 500 B |
|
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 410 B | 502 B |
|
||||||
|
|
||||||
† No hardware boot section: the host patches the reset vector, and the budget
|
† No hardware boot section: the host patches the reset vector, and the budget
|
||||||
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 therefore the 1284s' 502 of their
|
||||||
USART's own pins with the `OSCCAL` trim baked, 510 of its 512 — they alone
|
512: they alone carry the far-flash machinery (ELPM reads, RAMPZ page
|
||||||
carry the far-flash machinery (ELPM reads, RAMPZ page commands), autobaud
|
commands) on top of everything the column already stacks. The flash bank
|
||||||
alone carries the calibration loop, a bit-banged link on a USART's pins alone
|
riding in a transfer's selector byte keeps even those chips' addressing the
|
||||||
has to release it (below), and the trim adds its one register write. Without
|
same 16-bit form every other chip uses, which is why they are no longer the
|
||||||
the trim that build is 504; on the default pins, 500. The flash bank riding
|
|
||||||
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.
|
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
|
||||||
@@ -88,7 +88,8 @@ the usual one where a board's USB bridge is wired to RXD/TXD: the link's `init`
|
|||||||
clears that USART's `UCSRnB` first, because while its `TXEN` is set the USART —
|
clears that USART's `UCSRnB` first, because while its `TXEN` is set the USART —
|
||||||
not the port register — owns the TX pin, and a loader entered from an
|
not the port register — owns the TX pin, and a loader entered from an
|
||||||
application that left it enabled would receive and obey while answering nothing
|
application that left it enabled would receive and obey while answering nothing
|
||||||
(§20.2). It costs four bytes, and only on those pins.
|
(§20.6.3). It costs one store — four bytes on the extended-I/O chips, two on
|
||||||
|
the classic megas — and only on those pins.
|
||||||
|
|
||||||
`SERIAL autobaud` takes neither: the loader **measures** the host's bit timing
|
`SERIAL autobaud` takes neither: the loader **measures** the host's bit timing
|
||||||
at run time, so `CLOCK` and `BAUD` are not build parameters there and one
|
at run time, so `CLOCK` and `BAUD` are not build parameters there and one
|
||||||
@@ -99,7 +100,10 @@ where a fixed-baud software build has to be rebuilt per clock and still drifts
|
|||||||
out of tolerance. The cost is that it is software-serial only (a hardware USART
|
out of tolerance. The cost is that it is software-serial only (a hardware USART
|
||||||
needs its divisor programmed) and that activation counts poll iterations rather
|
needs its divisor programmed) and that activation counts poll iterations rather
|
||||||
than seconds, since there is no clock to convert them against
|
than seconds, since there is no clock to convert them against
|
||||||
(`PUREBOOT_AUTOBAUD_POLLS`, default 4,000,000).
|
(`PUREBOOT_AUTOBAUD_POLLS`, default 4,000,000). The wait spends nine cycles a
|
||||||
|
poll (measured, and held by the `pureboot.window.autobaud` gate), so the
|
||||||
|
default window is 36 M cycles: 4.5 s at 8 MHz, 3.75 s at 9.6 MHz, 36 s at
|
||||||
|
1 MHz.
|
||||||
|
|
||||||
**Pick the rate by cycles a bit, and leave the oscillator room.** What the
|
**Pick the rate by cycles a bit, and leave the oscillator room.** What the
|
||||||
calibration can measure is bounded by how many clock cycles one bit lasts, so a
|
calibration can measure is bounded by how many clock cycles one bit lasts, so a
|
||||||
@@ -187,8 +191,10 @@ reply, repeat.
|
|||||||
|
|
||||||
Addresses are **byte addresses within a 64 KiB bank**, and the bank rides in
|
Addresses are **byte addresses within a 64 KiB bank**, and the bank rides in
|
||||||
the command's selector byte, so no command has to speak word addresses. `J` is
|
the command's selector byte, so no command has to speak word addresses. `J` is
|
||||||
the exception: it takes a word address, because that is what the hardware's own
|
the exception: its address is a word address, because that is what the
|
||||||
jump takes. EEPROM and data-space addresses and all counts are bytes.
|
hardware's own jump takes — it still carries a selector byte (reserved,
|
||||||
|
ignored) so its decode is the same three reads as every other command's.
|
||||||
|
EEPROM and data-space addresses and all counts are bytes.
|
||||||
|
|
||||||
The loader trusts the host to keep addresses in range: it does not bound them
|
The loader trusts the host to keep addresses in range: it does not bound them
|
||||||
against the chip. **Gotcha:** a write (or read) that runs past `E2END` wraps —
|
against the chip. **Gotcha:** a write (or read) that runs past `E2END` wraps —
|
||||||
@@ -203,7 +209,7 @@ better spent on features than on re-checking a bound the host already holds.
|
|||||||
| `G` | sel8, addr16, n8 | n bytes from the selected space (n = 0 means 256) |
|
| `G` | sel8, addr16, n8 | n bytes from the selected space (n = 0 means 256) |
|
||||||
| `g` | sel8, addr16, n8, then n data bytes | `+` per byte, sent once its write has begun |
|
| `g` | sel8, addr16, n8, then n data bytes | `+` per byte, sent once its write has begun |
|
||||||
| `W` | sel8, addr16, then one page of data | — (completion = next prompt) |
|
| `W` | sel8, addr16, then one page of data | — (completion = next prompt) |
|
||||||
| `J` | word address (16-bit) | `+`, then execution continues there |
|
| `J` | sel8 (reserved), word address (16-bit) | `+`, then execution continues there |
|
||||||
| other | — | ignored; the loop re-prompts (send a junk byte, await `+`, to resync) |
|
| other | — | ignored; the loop re-prompts (send a junk byte, await `+`, to resync) |
|
||||||
|
|
||||||
`G` and `g` are one letter in two cases, which is the whole command set for
|
`G` and `g` are one letter in two cases, which is the whole command set for
|
||||||
@@ -291,7 +297,12 @@ from `b`, and a command per memory (`R`/`W` flash, `r`/`w` EEPROM, `F` fuses).
|
|||||||
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. **6** changes
|
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
|
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.
|
(Configuration), so a tool driving an update knows such images exist. **7**
|
||||||
|
moves `J` onto the unified decode — it gains the selector byte the table
|
||||||
|
shows, which older loaders do not read, so the tool sends each form to the
|
||||||
|
version that speaks it — and re-homes the autobaud unit into the GPIOR pair
|
||||||
|
on the chips that have one (Session: what must not be written), which is
|
||||||
|
where `--info`'s measured clock now reads it on those parts.
|
||||||
|
|
||||||
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
|
||||||
@@ -476,11 +487,14 @@ Reads are safe anywhere; **two small regions cannot be written without ending th
|
|||||||
session,** because they are what the loader is standing on:
|
session,** because they are what the loader is standing on:
|
||||||
|
|
||||||
- the **top of SRAM**, where its stack lives — a handful of bytes below RAMEND;
|
- the **top of SRAM**, where its stack lives — a handful of bytes below RAMEND;
|
||||||
- on an **autobaud** build, the **two bytes at RAMSTART**: the measured bit
|
- on an **autobaud** build, the **measured bit period**: two bytes in
|
||||||
period, in `.noinit`, which is the whole of that loader's static RAM. Overwrite
|
GPIOR2:GPIOR1 where the chip has the pair (data `0x32..0x33` on the
|
||||||
it and its next reply is timed against garbage. On an ATtiny13A that is
|
t25/45/85, `0x4A..0x4B` from the x8 generation on — such a loader has *no*
|
||||||
`0x60..0x61`, and the symptom is a mangled prompt byte rather than any error —
|
static RAM at all), and the two bytes at RAMSTART on the chips without one
|
||||||
the loader is fine, it simply is no longer speaking the agreed rate.
|
(the t13s and classic megas), where they are the whole of the loader's
|
||||||
|
static RAM. Overwrite either home and the next reply is timed against
|
||||||
|
garbage — the symptom is a mangled prompt byte rather than any error; the
|
||||||
|
loader is fine, it simply is no longer speaking the agreed rate.
|
||||||
|
|
||||||
Both are self-inflicted rather than defects, and a reset clears them. Note also
|
Both are self-inflicted rather than defects, and a reset clears them. Note also
|
||||||
that `--poke` can write OSCCAL, which does take effect — but a session can only
|
that `--poke` can write OSCCAL, which does take effect — but a session can only
|
||||||
@@ -518,9 +532,10 @@ Per chip preset, `ctest` runs:
|
|||||||
below sweeps. `pureboot*osccal*.size` add the `OSCCAL` trim on the stock
|
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
|
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;
|
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
|
- `pureboot_autobaud.unit` — the measured bit period sits where `--info`
|
||||||
object and sits exactly at ram_start, where `--info` reads it: wire
|
reads it (wire contract, not layout accident): in the GPIOR pair, with no
|
||||||
contract, not layout accident;
|
RAM object at all, on the chips that have one; as the loader's only RAM
|
||||||
|
object at exactly ram_start elsewhere;
|
||||||
- `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) ×
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// pureboot — a serial bootloader on libavr: one C++ source, no inline
|
// pureboot — a serial bootloader on libavr: one C++ source, no inline
|
||||||
// assembly, no global register variables, 512 bytes on every chip libavr
|
// assembly, no global register variables, a 512-byte slot on every chip
|
||||||
// targets. The device speaks primitives; every composite (verify, erase,
|
// libavr targets. The device speaks primitives; every composite (verify, erase,
|
||||||
// reset-vector surgery, self-update) lives in the host tool. Protocol,
|
// reset-vector surgery, self-update) lives in the host tool. Protocol,
|
||||||
// deployment and configuration: README.md next to this file.
|
// deployment and configuration: README.md next to this file.
|
||||||
//
|
//
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
// is what makes a copy one slot below able to rewrite the resident one, and
|
// is what makes a copy one slot below able to rewrite the resident one, and
|
||||||
// every change here has to keep it (test/check_pi.py).
|
// every change here has to keep it (test/check_pi.py).
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
#include <libavr/libavr.hpp>
|
#include <libavr/libavr.hpp>
|
||||||
|
|
||||||
using namespace avr::literals;
|
using namespace avr::literals;
|
||||||
@@ -75,7 +77,7 @@ static_assert(PUREBOOT_OSCCAL >= 0 && PUREBOOT_OSCCAL <= 0xff, "PUREBOOT_OSCCAL
|
|||||||
|
|
||||||
// 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 = 6;
|
constexpr std::uint8_t version = 7;
|
||||||
|
|
||||||
// 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
|
||||||
@@ -162,18 +164,30 @@ constexpr std::uint8_t bank_shift = 16 - slot_shift;
|
|||||||
#define PUREBOOT_TX pb1
|
#define PUREBOOT_TX pb1
|
||||||
#endif
|
#endif
|
||||||
#if defined(PUREBOOT_USART)
|
#if defined(PUREBOOT_USART)
|
||||||
constexpr char usart_digit = '0' + PUREBOOT_USART;
|
constexpr int usart_unit = PUREBOOT_USART;
|
||||||
#else
|
#else
|
||||||
constexpr char usart_digit = '0';
|
constexpr int usart_unit = 0;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
template <avr::hertz_t C, avr::baud_t B>
|
template <avr::hertz_t C, avr::baud_t B>
|
||||||
struct hardware_link {
|
struct hardware_link {
|
||||||
using uart = avr::uart::usart<usart_digit, C, {.baud = B, .max_baud_error = 2.5_pct}>;
|
using uart = avr::uart::usart<usart_unit, C, {.baud = B, .max_baud_error = 2.5_pct}>;
|
||||||
|
|
||||||
// The compiled idle poll: lds UCSR0A (2), sbrc skipping the exit (2),
|
// The compiled idle poll around the window's narrow (uint24_t) countdown:
|
||||||
// sbiw + sbci + sbci + brne (6).
|
// the RXC test, then sbiw + sbci + brne (5). The test's cost follows the
|
||||||
static constexpr std::uint8_t poll_cycles = 10;
|
// status register's home — a 2-cycle bit-skip where UCSRnA sits in
|
||||||
|
// bit-addressable I/O (the classic megas), lds + skip (4) in extended
|
||||||
|
// I/O. A uint32_t countdown pays one more sbci — window_polls() adds it
|
||||||
|
// where the count forces the wide type. Held by the pureboot.window gate.
|
||||||
|
// The lookup rides the baud parameter so it stays dependent: the trait is
|
||||||
|
// an incomplete type on the USART-less chips, which parse this template
|
||||||
|
// without ever instantiating it.
|
||||||
|
template <avr::baud_t Baud, typename U = avr::hw::usart_of<usart_unit>>
|
||||||
|
static consteval std::uint8_t poll_cost()
|
||||||
|
{
|
||||||
|
return U::ucsra::addr < 0x40 ? 7 : 9;
|
||||||
|
}
|
||||||
|
static constexpr std::uint8_t poll_cycles = poll_cost<B>();
|
||||||
|
|
||||||
static void init()
|
static void init()
|
||||||
{
|
{
|
||||||
@@ -197,7 +211,10 @@ struct hardware_link {
|
|||||||
|
|
||||||
static void drain()
|
static void drain()
|
||||||
{
|
{
|
||||||
uart::drain();
|
// A drain here always follows this link's own write — the frame is
|
||||||
|
// in flight by construction, so the completion the wait needs is
|
||||||
|
// guaranteed and the bounded default's countdown would be dead bytes.
|
||||||
|
uart::drain_unbounded();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -206,9 +223,11 @@ struct software_link {
|
|||||||
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, B>;
|
using rx_t = avr::uart::software_rx_polled<C, avr::PUREBOOT_RX, B>;
|
||||||
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, B>;
|
using tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, B>;
|
||||||
|
|
||||||
// The compiled idle poll: sbis skipping the exit (2), sbiw + sbci +
|
// The compiled idle poll around the window's narrow (uint24_t) countdown:
|
||||||
// sbci + brne (6).
|
// sbis skipping the exit (2), sbiw + sbci + brne (5). A uint32_t
|
||||||
static constexpr std::uint8_t poll_cycles = 8;
|
// countdown pays one more sbci — window_polls() adds it where the count
|
||||||
|
// forces the wide type. Held by the pureboot.window gate.
|
||||||
|
static constexpr std::uint8_t poll_cycles = 7;
|
||||||
|
|
||||||
static void init()
|
static void init()
|
||||||
{
|
{
|
||||||
@@ -241,7 +260,10 @@ struct software_link {
|
|||||||
// every rate. Activation differs in kind from the other two — there is no
|
// every rate. Activation differs in kind from the other two — there is no
|
||||||
// clock to time a window against — so this backend brings its own, below.
|
// clock to time a window against — so this backend brings its own, below.
|
||||||
struct autobaud_link {
|
struct autobaud_link {
|
||||||
using uart = avr::uart::software_autobaud<avr::PUREBOOT_RX, avr::PUREBOOT_TX>;
|
// The unit in GPIOR2:GPIOR1 where the chip has them: the loader owns the
|
||||||
|
// whole chip while it runs, and the pair costs one word per access where
|
||||||
|
// the RAM word costs two — six words across the image.
|
||||||
|
using uart = avr::uart::software_autobaud<avr::PUREBOOT_RX, avr::PUREBOOT_TX, avr::uart::unit_home::gpior>;
|
||||||
|
|
||||||
static void init()
|
static void init()
|
||||||
{
|
{
|
||||||
@@ -260,19 +282,22 @@ struct autobaud_link {
|
|||||||
|
|
||||||
static void drain()
|
static void drain()
|
||||||
{
|
{
|
||||||
uart::drain();
|
// A drain here always follows this link's own write — the frame is
|
||||||
|
// in flight by construction, so the completion the wait needs is
|
||||||
|
// guaranteed and the bounded default's countdown would be dead bytes.
|
||||||
|
uart::drain_unbounded();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#if defined(PUREBOOT_AUTOBAUD)
|
#if defined(PUREBOOT_AUTOBAUD)
|
||||||
using link = autobaud_link;
|
using link = autobaud_link;
|
||||||
#elif defined(PUREBOOT_USART)
|
#elif defined(PUREBOOT_USART)
|
||||||
static_assert(avr::uart::has_usart<usart_digit>(), "PUREBOOT_USART selects a hardware USART this chip does not have");
|
static_assert(avr::uart::has_usart<usart_unit>(), "PUREBOOT_USART selects a hardware USART this chip does not have");
|
||||||
using link = hardware_link<dev::clock, wire_baud>;
|
using link = hardware_link<dev::clock, wire_baud>;
|
||||||
#elif defined(PUREBOOT_SOFT_SERIAL)
|
#elif defined(PUREBOOT_SOFT_SERIAL)
|
||||||
using link = software_link<dev::clock, wire_baud>;
|
using link = software_link<dev::clock, wire_baud>;
|
||||||
#else
|
#else
|
||||||
using link = std::conditional_t<avr::uart::has_usart<usart_digit>(), hardware_link<dev::clock, wire_baud>,
|
using link = std::conditional_t<avr::uart::has_usart<usart_unit>(), hardware_link<dev::clock, wire_baud>,
|
||||||
software_link<dev::clock, wire_baud>>;
|
software_link<dev::clock, wire_baud>>;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -288,7 +313,7 @@ extern "C" [[noreturn]] void pureboot_app();
|
|||||||
__builtin_unreachable();
|
__builtin_unreachable();
|
||||||
}
|
}
|
||||||
|
|
||||||
[[gnu::noinline, noreturn]] void run_app()
|
[[noreturn]] void run_app()
|
||||||
{
|
{
|
||||||
jump(pureboot_app);
|
jump(pureboot_app);
|
||||||
}
|
}
|
||||||
@@ -314,17 +339,35 @@ void await_host()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// The window as one 32-bit countdown, divided by the backend's counted
|
// The window as one countdown, divided by the backend's counted poll-loop
|
||||||
// poll-loop cycles. Whole seconds is all it promises.
|
// cycles. Whole seconds is all it promises. The per-poll cost depends on the
|
||||||
|
// countdown's own width (a uint32_t decrement chain is one sbci longer), and
|
||||||
|
// the width depends on the poll count — solved narrow-first: a count that
|
||||||
|
// fits 24 bits at the narrow cost keeps the narrow loop, anything else takes
|
||||||
|
// the wide loop at its own cost. A count fitting 24 bits only at the wide
|
||||||
|
// cost stays wide, so the choice cannot oscillate on the boundary.
|
||||||
|
consteval std::uint32_t polls_at(std::uint32_t per_poll)
|
||||||
|
{
|
||||||
|
// Whole-window cycles first, then the per-poll division: one truncation
|
||||||
|
// instead of one per second. Same instructions either way — only the
|
||||||
|
// countdown's immediate moves.
|
||||||
|
return static_cast<std::uint32_t>(dev::cycles_for<std::chrono::seconds{timeout_seconds}>() / per_poll);
|
||||||
|
}
|
||||||
|
|
||||||
|
consteval bool narrow_window()
|
||||||
|
{
|
||||||
|
return polls_at(link::poll_cycles) <= 0xffffff;
|
||||||
|
}
|
||||||
|
|
||||||
consteval std::uint32_t window_polls()
|
consteval std::uint32_t window_polls()
|
||||||
{
|
{
|
||||||
return timeout_seconds * static_cast<std::uint32_t>(dev::clock.hz / link::poll_cycles);
|
return polls_at(narrow_window() ? link::poll_cycles : link::poll_cycles + 1u);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The countdown in the narrowest type that holds it: a fourth byte would
|
// The countdown in the narrowest type that holds it: a fourth byte would
|
||||||
// cost a wider decrement chain at every poll for range most windows never
|
// cost a wider decrement chain at every poll for range most windows never
|
||||||
// use (the autobaud budget makes the same choice).
|
// use (the autobaud budget makes the same choice).
|
||||||
using window_t = std::conditional_t<window_polls() <= 0xffffff, avr::uint24_t, std::uint32_t>;
|
using window_t = std::conditional_t<narrow_window(), avr::uint24_t, std::uint32_t>;
|
||||||
|
|
||||||
bool pending_before_deadline()
|
bool pending_before_deadline()
|
||||||
{
|
{
|
||||||
@@ -488,12 +531,6 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
|
|||||||
tx_ack();
|
tx_ack();
|
||||||
const std::uint8_t command = link::rx();
|
const std::uint8_t command = link::rx();
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'J': { // jump to a wire word address: hand-over and staging transfer
|
|
||||||
auto target = reinterpret_cast<void (*)()>(rx16());
|
|
||||||
tx_ack();
|
|
||||||
link::drain();
|
|
||||||
jump(target);
|
|
||||||
}
|
|
||||||
case 'b': // identity: the version, then the three signature bytes
|
case 'b': // identity: the version, then the three signature bytes
|
||||||
// Straight out of the stamp, so the wire and the image can never
|
// Straight out of the stamp, so the wire and the image can never
|
||||||
// disagree about what this loader is. The indices are constant and
|
// disagree about what this loader is. The indices are constant and
|
||||||
@@ -502,18 +539,26 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
|
|||||||
for (std::uint8_t at = stamp_identity; at != sizeof identity_stamp; ++at)
|
for (std::uint8_t at = stamp_identity; at != sizeof identity_stamp; ++at)
|
||||||
link::tx(identity_stamp[at]);
|
link::tx(identity_stamp[at]);
|
||||||
break;
|
break;
|
||||||
|
case 'J': // jump: sel8 (reserved), addr16 as a wire word address
|
||||||
case 'W': // fill one flash page buffer: sel8, addr16, then page bytes
|
case 'W': // fill one flash page buffer: sel8, addr16, then page bytes
|
||||||
case 'G': // read: sel8, addr16, n8 (0 = 256)
|
case 'G': // read: sel8, addr16, n8 (0 = 256)
|
||||||
case 'g': { // write: sel8, addr16, n8, then n bytes, each acked
|
case 'g': { // write: sel8, addr16, n8, then n bytes, each acked
|
||||||
// One decode, one cursor and one loop for every space and both
|
// One decode, one cursor and one loop for every space, both
|
||||||
// directions: a command per memory would carry a copy of all three
|
// directions and the jump: a command per memory would carry a copy
|
||||||
// each. 'W' joins the same decode rather than keeping an address
|
// of all three each. 'J' — the hand-over and staging transfer —
|
||||||
// form of its own, so flash addressing is uniform across every
|
// carries a selector it ignores so its address rides the same two
|
||||||
// command that names it.
|
// reads as everything else; 'W' joins the same decode rather than
|
||||||
|
// keeping an address form of its own, so flash addressing is
|
||||||
|
// uniform across every command that names it.
|
||||||
const std::uint8_t selector = link::rx();
|
const std::uint8_t selector = link::rx();
|
||||||
const std::uint8_t space = space_of(selector);
|
const std::uint8_t space = space_of(selector);
|
||||||
const std::uint8_t bank = bank_of(selector);
|
const std::uint8_t bank = bank_of(selector);
|
||||||
std::uint16_t at = rx16();
|
std::uint16_t at = rx16();
|
||||||
|
if (command == 'J') {
|
||||||
|
tx_ack();
|
||||||
|
link::drain();
|
||||||
|
jump(reinterpret_cast<void (*)()>(at));
|
||||||
|
}
|
||||||
if (command == 'W') {
|
if (command == 'W') {
|
||||||
fill_page(bank, at);
|
fill_page(bank, at);
|
||||||
break;
|
break;
|
||||||
@@ -540,4 +585,7 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
|
|||||||
} // namespace
|
} // namespace
|
||||||
} // namespace pureboot
|
} // namespace pureboot
|
||||||
|
|
||||||
template struct avr::startup::entry<pureboot::run>;
|
// stack::hardware: activation is reset-only, so the reset logic's own
|
||||||
|
// SP = RAMEND stands wherever the datasheet guarantees it (the classic
|
||||||
|
// megas still get the write); a 'J' entry runs on the caller's live stack.
|
||||||
|
template struct avr::startup::entry<pureboot::run, avr::startup::stack::hardware>;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ else:
|
|||||||
import termios
|
import termios
|
||||||
|
|
||||||
PROMPT = b"+"
|
PROMPT = b"+"
|
||||||
VERSION = 6 # this tool's own version — free to drift from a loader's
|
VERSION = 8 # this tool's own version — free to drift from a loader's
|
||||||
# The loader versions this tool can drive. 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: the tool keeps a decoder for every generation in it (1–4 speak
|
# map lives: the tool keeps a decoder for every generation in it (1–4 speak
|
||||||
@@ -34,7 +34,7 @@ VERSION = 6 # this tool's own version — free to drift from a loader's
|
|||||||
# builds and changes nothing on the wire), and a version it has no decoder
|
# builds and changes nothing on the wire), and a version it has no decoder
|
||||||
# for moves the floor.
|
# for moves the floor.
|
||||||
OLDEST_LOADER = 1
|
OLDEST_LOADER = 1
|
||||||
NEWEST_LOADER = 6
|
NEWEST_LOADER = 7
|
||||||
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
|
||||||
|
|
||||||
@@ -42,14 +42,20 @@ RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops
|
|||||||
# 'g' writes, each taking a selector byte, a 16-bit address and a count, over
|
# 'g' writes, each taking a selector byte, a 16-bit address and a count, over
|
||||||
# the spaces below. The loader carries one transfer loop instead of four bodies
|
# the spaces below. The loader carries one transfer loop instead of four bodies
|
||||||
# — which is what buys the data space and the host-issued SPM operations.
|
# — which is what buys the data space and the host-issued SPM operations.
|
||||||
|
# 6 marks the builds that may carry a baked OSCCAL trim, nothing on the wire;
|
||||||
|
# 7 gives 'J' a selector byte (older loaders take the bare address — jump()
|
||||||
|
# sends each form to the version that speaks it) and re-homes the autobaud
|
||||||
|
# unit into the GPIOR pair where the chip has one.
|
||||||
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
|
# An autobaud loader keeps its measured bit period readable, encoded as
|
||||||
# as delay-loop counts: (bit cycles − UNIT_DISCOUNT) / UNIT_LOOP_CYCLES,
|
# delay-loop counts: (bit cycles − UNIT_DISCOUNT) / UNIT_LOOP_CYCLES,
|
||||||
# floored — the spin granule and per-bit overhead of libavr's software UART.
|
# 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
|
# v5/v6 keep it at ram_start; v7 moves it into GPIOR2:GPIOR1 on the chips
|
||||||
# within one granule below it.
|
# that have the pair (their data addresses are in the geometry) and keeps
|
||||||
|
# ram_start only where they do not exist. --info undoes the encoding to
|
||||||
|
# report the true clock, which therefore sits within one granule below it.
|
||||||
UNIT_LOOP_CYCLES, UNIT_DISCOUNT = 4, 8
|
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
|
||||||
@@ -77,36 +83,40 @@ 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, ram_start)
|
# signature : (flash, page, eeprom, patch_vector, ram_start, gpior1)
|
||||||
# ram_start is where SRAM begins in data space: the classic megas and the
|
# ram_start is where SRAM begins in data space: the classic megas and the
|
||||||
# tinies keep it right after the plain I/O registers (0x60), the x8/x4
|
# tinies keep it right after the plain I/O registers (0x60), the x8/x4
|
||||||
# generations past their extended I/O file (0x100). An autobaud loader's
|
# generations past their extended I/O file (0x100). gpior1 is GPIOR1's
|
||||||
# measured bit period lives at exactly ram_start (its only RAM object;
|
# data address — 0x32 on the t25/45/85, 0x4A from the x8 generation on,
|
||||||
# the loader's own build pins the layout), which is what --info reads.
|
# None where the chip has no pair (t13, classic megas). A v7 autobaud
|
||||||
(0x1E, 0x90, 0x07): (1024, 32, 64, True, 0x60), # ATtiny13/13A
|
# loader's measured bit period lives in GPIOR2:GPIOR1 where they exist
|
||||||
(0x1E, 0x91, 0x08): (2048, 32, 128, True, 0x60), # ATtiny25
|
# and at exactly ram_start elsewhere (its only RAM object; the loader's
|
||||||
(0x1E, 0x92, 0x06): (4096, 64, 256, True, 0x60), # ATtiny45
|
# own build pins the layout); v5/v6 always used ram_start. --info reads
|
||||||
(0x1E, 0x93, 0x0B): (8192, 64, 512, True, 0x60), # ATtiny85
|
# whichever home the answering version implies.
|
||||||
(0x1E, 0x92, 0x05): (4096, 64, 256, True, 0x100), # ATmega48/48A
|
(0x1E, 0x90, 0x07): (1024, 32, 64, True, 0x60, None), # ATtiny13/13A
|
||||||
(0x1E, 0x92, 0x0A): (4096, 64, 256, True, 0x100), # ATmega48P/48PA
|
(0x1E, 0x91, 0x08): (2048, 32, 128, True, 0x60, 0x32), # ATtiny25
|
||||||
(0x1E, 0x93, 0x07): (8192, 64, 512, False, 0x60), # ATmega8/8A
|
(0x1E, 0x92, 0x06): (4096, 64, 256, True, 0x60, 0x32), # ATtiny45
|
||||||
(0x1E, 0x93, 0x0A): (8192, 64, 512, False, 0x100), # ATmega88/88A
|
(0x1E, 0x93, 0x0B): (8192, 64, 512, True, 0x60, 0x32), # ATtiny85
|
||||||
(0x1E, 0x93, 0x0F): (8192, 64, 512, False, 0x100), # ATmega88P/88PA
|
(0x1E, 0x92, 0x05): (4096, 64, 256, True, 0x100, 0x4A), # ATmega48/48A
|
||||||
(0x1E, 0x94, 0x03): (16384, 128, 512, False, 0x60), # ATmega16/16A
|
(0x1E, 0x92, 0x0A): (4096, 64, 256, True, 0x100, 0x4A), # ATmega48P/48PA
|
||||||
(0x1E, 0x94, 0x06): (16384, 128, 512, False, 0x100), # ATmega168/168A
|
(0x1E, 0x93, 0x07): (8192, 64, 512, False, 0x60, None), # ATmega8/8A
|
||||||
(0x1E, 0x94, 0x0B): (16384, 128, 512, False, 0x100), # ATmega168P/168PA
|
(0x1E, 0x93, 0x0A): (8192, 64, 512, False, 0x100, 0x4A), # ATmega88/88A
|
||||||
(0x1E, 0x94, 0x0A): (16384, 128, 512, False, 0x100), # ATmega164P/164PA
|
(0x1E, 0x93, 0x0F): (8192, 64, 512, False, 0x100, 0x4A), # ATmega88P/88PA
|
||||||
(0x1E, 0x94, 0x0F): (16384, 128, 512, False, 0x100), # ATmega164A
|
(0x1E, 0x94, 0x03): (16384, 128, 512, False, 0x60, None), # ATmega16/16A
|
||||||
(0x1E, 0x95, 0x02): (32768, 128, 1024, False, 0x60), # ATmega32/32A
|
(0x1E, 0x94, 0x06): (16384, 128, 512, False, 0x100, 0x4A), # ATmega168/168A
|
||||||
(0x1E, 0x95, 0x0F): (32768, 128, 1024, False, 0x100), # ATmega328P
|
(0x1E, 0x94, 0x0B): (16384, 128, 512, False, 0x100, 0x4A), # ATmega168P/168PA
|
||||||
(0x1E, 0x95, 0x14): (32768, 128, 1024, False, 0x100), # ATmega328
|
(0x1E, 0x94, 0x0A): (16384, 128, 512, False, 0x100, 0x4A), # ATmega164P/164PA
|
||||||
(0x1E, 0x95, 0x08): (32768, 128, 1024, False, 0x100), # ATmega324P
|
(0x1E, 0x94, 0x0F): (16384, 128, 512, False, 0x100, 0x4A), # ATmega164A
|
||||||
(0x1E, 0x95, 0x11): (32768, 128, 1024, False, 0x100), # ATmega324PA
|
(0x1E, 0x95, 0x02): (32768, 128, 1024, False, 0x60, None), # ATmega32/32A
|
||||||
(0x1E, 0x95, 0x15): (32768, 128, 1024, False, 0x100), # ATmega324A
|
(0x1E, 0x95, 0x0F): (32768, 128, 1024, False, 0x100, 0x4A), # ATmega328P
|
||||||
(0x1E, 0x96, 0x09): (65536, 256, 2048, False, 0x100), # ATmega644/644A
|
(0x1E, 0x95, 0x14): (32768, 128, 1024, False, 0x100, 0x4A), # ATmega328
|
||||||
(0x1E, 0x96, 0x0A): (65536, 256, 2048, False, 0x100), # ATmega644P/644PA
|
(0x1E, 0x95, 0x08): (32768, 128, 1024, False, 0x100, 0x4A), # ATmega324P
|
||||||
(0x1E, 0x97, 0x05): (131072, 256, 4096, False, 0x100),# ATmega1284P
|
(0x1E, 0x95, 0x11): (32768, 128, 1024, False, 0x100, 0x4A), # ATmega324PA
|
||||||
(0x1E, 0x97, 0x06): (131072, 256, 4096, False, 0x100),# ATmega1284
|
(0x1E, 0x95, 0x15): (32768, 128, 1024, False, 0x100, 0x4A), # ATmega324A
|
||||||
|
(0x1E, 0x96, 0x09): (65536, 256, 2048, False, 0x100, 0x4A), # ATmega644/644A
|
||||||
|
(0x1E, 0x96, 0x0A): (65536, 256, 2048, False, 0x100, 0x4A), # ATmega644P/644PA
|
||||||
|
(0x1E, 0x97, 0x05): (131072, 256, 4096, False, 0x100, 0x4A),# ATmega1284P
|
||||||
|
(0x1E, 0x97, 0x06): (131072, 256, 4096, False, 0x100, 0x4A),# ATmega1284
|
||||||
}
|
}
|
||||||
|
|
||||||
VERBOSE = False
|
VERBOSE = False
|
||||||
@@ -450,7 +460,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
|
||||||
@@ -491,6 +501,11 @@ class Info:
|
|||||||
# permits where from_identity refuses.
|
# permits where from_identity refuses.
|
||||||
geometry = CHIP_GEOMETRY.get(tuple(self.signature))
|
geometry = CHIP_GEOMETRY.get(tuple(self.signature))
|
||||||
self.ram = geometry[4] if geometry else None
|
self.ram = geometry[4] if geometry else None
|
||||||
|
# Where this loader keeps the measured bit period (None when a fixed
|
||||||
|
# signature row is missing): the GPIOR pair from v7 where the chip
|
||||||
|
# has one, ram_start before that and everywhere without the pair.
|
||||||
|
gpior1 = geometry[5] if geometry else None
|
||||||
|
self.unit_home = gpior1 if self.version >= 7 and gpior1 is not None else self.ram
|
||||||
|
|
||||||
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)
|
||||||
@@ -531,6 +546,11 @@ class Loader:
|
|||||||
# Set once a session is established over an autobaud link, so a
|
# Set once a session is established over an autobaud link, so a
|
||||||
# re-entry after 'J' repeats the handshake that worked.
|
# re-entry after 'J' repeats the handshake that worked.
|
||||||
self.autobaud = False
|
self.autobaud = False
|
||||||
|
# The pre-knock drain runs once per port: the bytes it exists for are
|
||||||
|
# leftovers from before this process opened the port. Re-knocks later
|
||||||
|
# in the same session must not pay it — a fresh activation window is
|
||||||
|
# already burning while they wait.
|
||||||
|
self._line_drained = False
|
||||||
# The link this session is speaking. It moves when the host follows a
|
# The link this session is speaking. It moves when the host follows a
|
||||||
# staging copy built for another one (enter_copy).
|
# staging copy built for another one (enter_copy).
|
||||||
self.baud = getattr(port, "baud", None)
|
self.baud = getattr(port, "baud", None)
|
||||||
@@ -540,10 +560,16 @@ class Loader:
|
|||||||
"""The 'b' reply, in either of the two layouts a loader may send.
|
"""The 'b' reply, in either of the two layouts a loader may send.
|
||||||
pureboot 5 answers with its version and the signature; older loaders
|
pureboot 5 answers with its version and the signature; older loaders
|
||||||
answer with a 12-byte block. The version byte cannot be mistaken for
|
answer with a 12-byte block. The version byte cannot be mistaken for
|
||||||
the older block's 'P', so four bytes are enough to tell them apart."""
|
the older block's 'P', so four bytes are enough to tell them apart.
|
||||||
head = self.port.read_exact(4, 2.0)
|
|
||||||
|
The timeout is short on purpose: a real answer follows the prompt
|
||||||
|
within a frame time or two, so half a second is dozens of times the
|
||||||
|
worst case — while a *false* prompt match (a stale byte, reset
|
||||||
|
garbage) makes this read collect noise, and every second spent on it
|
||||||
|
comes out of the activation window the retry needs."""
|
||||||
|
head = self.port.read_exact(4, 0.5)
|
||||||
if head[0:2] == b"PB":
|
if head[0:2] == b"PB":
|
||||||
return Info(head + self.port.read_exact(8, 2.0))
|
return Info(head + self.port.read_exact(8, 0.5))
|
||||||
return Info.from_identity(head)
|
return Info.from_identity(head)
|
||||||
|
|
||||||
def _handshake(self, wait, knock, what):
|
def _handshake(self, wait, knock, what):
|
||||||
@@ -554,8 +580,23 @@ class Loader:
|
|||||||
into a fresh window, where a command without its knock is discarded.
|
into a fresh window, where a command without its knock is discarded.
|
||||||
Each attempt is therefore the whole handshake. This also converges into
|
Each attempt is therefore the whole handshake. This also converges into
|
||||||
an already-live session: the knock bytes are ignored there and the
|
an already-live session: the knock bytes are ignored there and the
|
||||||
drain absorbs whatever they produced."""
|
drain absorbs whatever they produced.
|
||||||
|
|
||||||
|
Before the port's first knock ever, the line is drained until quiet: a
|
||||||
|
prompt from a previous session (`--stay`) can still be in the USB
|
||||||
|
pipeline when the port opens, where a flush cannot clear what has not
|
||||||
|
arrived yet — and on a board that resets when its port opens, trusting
|
||||||
|
that stale byte would spend the fresh activation window reading noise
|
||||||
|
from a device that never heard the knock. Once only, and bounded:
|
||||||
|
later re-knocks in this session face no foreign leftovers, and their
|
||||||
|
own window is already burning."""
|
||||||
deadline = time.monotonic() + wait
|
deadline = time.monotonic() + wait
|
||||||
|
if not self._line_drained:
|
||||||
|
self._line_drained = True
|
||||||
|
drain = time.monotonic() + 0.25
|
||||||
|
while self.port.read_available(0.05):
|
||||||
|
if time.monotonic() > drain:
|
||||||
|
break
|
||||||
knocks = 0
|
knocks = 0
|
||||||
refusal = None
|
refusal = None
|
||||||
while True:
|
while True:
|
||||||
@@ -745,8 +786,13 @@ class Loader:
|
|||||||
return self._command(b"F", 4, 2.0)
|
return self._command(b"F", 4, 2.0)
|
||||||
|
|
||||||
def jump(self, word_address):
|
def jump(self, word_address):
|
||||||
"""The device acks, then execution continues at the word address."""
|
"""The device acks, then execution continues at the word address.
|
||||||
self.port.write(bytes((ord("J"), word_address & 0xFF, word_address >> 8)))
|
From v7 'J' rides the unified decode, so it carries a selector byte
|
||||||
|
the loader ignores; older loaders take the bare address."""
|
||||||
|
if self.info.version >= 7:
|
||||||
|
self.port.write(bytes((ord("J"), 0, word_address & 0xFF, word_address >> 8)))
|
||||||
|
else:
|
||||||
|
self.port.write(bytes((ord("J"), word_address & 0xFF, word_address >> 8)))
|
||||||
self._expect_prompt()
|
self._expect_prompt()
|
||||||
|
|
||||||
def enter_copy(self, byte_address, wait, link=None):
|
def enter_copy(self, byte_address, wait, link=None):
|
||||||
@@ -1541,14 +1587,14 @@ 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:
|
if args.autobaud and info.unit_home is not None:
|
||||||
# The whole of the loader's RAM is the measured bit period at
|
# The measured bit period, from wherever this version keeps it
|
||||||
# ram_start; decoded and times the rate this session drives,
|
# (unit_home); decoded and times the rate this session drives,
|
||||||
# that is the true clock — the number to hold an OSCCAL bake
|
# that is the true clock — the number to hold an OSCCAL bake
|
||||||
# or a fixed-baud build against (README.md: deployment). The
|
# or a fixed-baud build against (README.md: deployment). The
|
||||||
# autobaud identity path refuses unknown signatures, so ram is
|
# autobaud identity path refuses unknown signatures, so the
|
||||||
# always known here; the guard states that dependency.
|
# home is always known here; the guard states that dependency.
|
||||||
unit = int.from_bytes(loader.read_ram(info.ram, 2), "little")
|
unit = int.from_bytes(loader.read_ram(info.unit_home, 2), "little")
|
||||||
cycles = unit * UNIT_LOOP_CYCLES + UNIT_DISCOUNT
|
cycles = unit * UNIT_LOOP_CYCLES + UNIT_DISCOUNT
|
||||||
clock = cycles * args.baud
|
clock = cycles * args.baud
|
||||||
offset = f", {(clock / args.clock - 1) * 100:+.1f} % of {args.clock}" if args.clock else ""
|
offset = f", {(clock / args.clock - 1) * 100:+.1f} % of {args.clock}" if args.clock else ""
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
# Asserts the autobaud loader's measured unit is the first RAM object: the
|
# Asserts the autobaud loader's measured unit sits where the host will read
|
||||||
# host tool reads the bit period from ram_start (--info's measured clock), so
|
# it (--info's measured clock — the address is wire contract). Two homes: on
|
||||||
# the unit's address is wire contract. Run as
|
# a chip with the GPIOR pair the unit lives there and the image must carry no
|
||||||
# cmake -DOBJDUMP=... -DELF=... -DRAM_START=<data address> -P check_unit.cmake
|
# RAM word for it at all; elsewhere it is the first RAM object at SRAM start.
|
||||||
|
# Run as
|
||||||
|
# cmake -DOBJDUMP=... -DELF=... -DRAM_START=<data address> [-DGPIOR=<data address>]
|
||||||
|
# -P check_unit.cmake
|
||||||
|
|
||||||
execute_process(COMMAND ${OBJDUMP} -t ${ELF} OUTPUT_VARIABLE _syms RESULT_VARIABLE _res)
|
execute_process(COMMAND ${OBJDUMP} -t ${ELF} OUTPUT_VARIABLE _syms RESULT_VARIABLE _res)
|
||||||
if(NOT _res EQUAL 0)
|
if(NOT _res EQUAL 0)
|
||||||
@@ -9,7 +12,17 @@ if(NOT _res EQUAL 0)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
# The symbol line: "00800100 l O .noinit 00000002 <mangled>unit_E".
|
# 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}")
|
string(REGEX MATCH "\n0*([0-9a-f]+)[^\n]+[ \t][^ \t\n]*unit_E\n" _line "${_syms}")
|
||||||
|
|
||||||
|
if(GPIOR)
|
||||||
|
if(_line)
|
||||||
|
message(FATAL_ERROR "unit_ RAM symbol present although the unit's home is GPIOR ${GPIOR} — "
|
||||||
|
"the host peeks the pair, and a RAM copy would be dead weight")
|
||||||
|
endif()
|
||||||
|
message(STATUS "no unit_ RAM object — the unit lives in the GPIOR pair at ${GPIOR}")
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
if(NOT _line)
|
if(NOT _line)
|
||||||
message(FATAL_ERROR "no unit_ symbol in ${ELF} — is this the autobaud loader?")
|
message(FATAL_ERROR "no unit_ symbol in ${ELF} — is this the autobaud loader?")
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ consteval bool use_hardware()
|
|||||||
#if defined(PUREBOOT_SOFT_SERIAL)
|
#if defined(PUREBOOT_SOFT_SERIAL)
|
||||||
return false;
|
return false;
|
||||||
#else
|
#else
|
||||||
return avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART");
|
return avr::uart::has_usart<0>();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ struct link {
|
|||||||
#else
|
#else
|
||||||
static constexpr avr::baud_t baud{115200};
|
static constexpr avr::baud_t baud{115200};
|
||||||
#endif
|
#endif
|
||||||
using tx_t = avr::uart::usart<'0' + PUREBOOT_USART, C, {.baud = baud, .max_baud_error = 2.5_pct}>;
|
using tx_t = avr::uart::usart<PUREBOOT_USART, C, {.baud = baud, .max_baud_error = 2.5_pct}>;
|
||||||
static void tx(char c)
|
static void tx(char c)
|
||||||
{
|
{
|
||||||
tx_t::write(static_cast<std::uint8_t>(c));
|
tx_t::write(static_cast<std::uint8_t>(c));
|
||||||
|
|||||||
@@ -64,8 +64,9 @@ def main():
|
|||||||
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
|
# The measured clock, decoded from the unit at whichever home this
|
||||||
# runner's clock is exact, so the figure must land inside the
|
# version keeps it in. The runner's clock is exact, so the figure
|
||||||
|
# must land inside the
|
||||||
# encoding's own envelope: the loader floors the bit period to
|
# encoding's own envelope: the loader floors the bit period to
|
||||||
# 4-cycle spin granules after an 8-cycle discount, and the edge
|
# 4-cycle spin granules after an 8-cycle discount, and the edge
|
||||||
# poll can shave a few cycles more — one granule of slack below
|
# poll can shave a few cycles more — one granule of slack below
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ import subprocess
|
|||||||
|
|
||||||
|
|
||||||
class Device:
|
class Device:
|
||||||
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None, link=None):
|
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None, link=None,
|
||||||
|
window=False):
|
||||||
cmd = [binary]
|
cmd = [binary]
|
||||||
if link:
|
if link:
|
||||||
cmd += ["-l", link]
|
cmd += ["-l", link]
|
||||||
|
if window:
|
||||||
|
cmd.append("-w") # report the first-transmit cycle, free-run idle
|
||||||
cmd += [elf, mcu, hz, base_hex, str(page), str(baud), dump]
|
cmd += [elf, mcu, hz, base_hex, str(page), str(baud), dump]
|
||||||
if reset_hex is not None or resume is not None:
|
if reset_hex is not None or resume is not None:
|
||||||
# Chips without a hardware boot section — the tinies and the
|
# Chips without a hardware boot section — the tinies and the
|
||||||
|
|||||||
137
test/pbwindow.py
Normal file
137
test/pbwindow.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""The activation window as a behavioral duration gate.
|
||||||
|
|
||||||
|
The loader's window is a counted poll loop whose per-poll cost is hand-counted
|
||||||
|
in the source (`link::poll_cycles`) — but the loop compiles in consumer
|
||||||
|
context, so only the running image can prove the count. This test installs a
|
||||||
|
real application beside the loader (the host tool's own `plan_flash` supplies
|
||||||
|
the reset-vector surgery), starts the simulator with the line idle, and reads
|
||||||
|
the cycle of the first transmit activity: nothing talks until the window
|
||||||
|
closes and the application banners, so that cycle *is* the window, give or
|
||||||
|
take a banner lead measured in microseconds. Asserted at ±2 % — one
|
||||||
|
mis-counted cycle per poll shifts a window by 10 % and more.
|
||||||
|
|
||||||
|
Fixed-baud loaders declare their window in seconds (--seconds, the build's
|
||||||
|
TIMEOUT). The autobaud loader's window is its calibration poll budget
|
||||||
|
(--autobaud-polls); the seconds it amounts to are budget × 10 / f_cpu, the
|
||||||
|
measured cost of the calibrate() wait loop this gate pins.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import importlib.util
|
||||||
|
import pathlib
|
||||||
|
import select
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
|
||||||
|
from pbsim import Device
|
||||||
|
|
||||||
|
# The calibrate() budget loop's cycles per poll in the built image — what the
|
||||||
|
# README's window arithmetic rests on, verified here. A measured fact, not a
|
||||||
|
# design constant: the wait's exit branches land where the compiler's block
|
||||||
|
# layout puts them, and the bounded-calibration rework moved the loop from
|
||||||
|
# ten cycles to nine.
|
||||||
|
AUTOBAUD_POLL_CYCLES = 9
|
||||||
|
|
||||||
|
|
||||||
|
def load_tool(path):
|
||||||
|
spec = importlib.util.spec_from_file_location("pureboot", path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def compose_flash(pb, loader_bytes, app_bytes, mcu, base, page):
|
||||||
|
"""The flash image a completed programming session leaves: application
|
||||||
|
(with the tinies' vector surgery), loader at base — built through the
|
||||||
|
host tool's own planner so the surgery is the shipped one, not a copy."""
|
||||||
|
flash_size = base + pb.SLOT
|
||||||
|
patch = not mcu.startswith("atmega") or mcu.startswith("atmega48")
|
||||||
|
word_flash = flash_size > 0x10000
|
||||||
|
wire_base = base // 2 if word_flash else base
|
||||||
|
flags = (1 if patch else 0) | (2 if word_flash else 0)
|
||||||
|
raw = bytes((ord("P"), ord("B"), 5, 0, 0, 0, page & 0xFF,
|
||||||
|
wire_base & 0xFF, wire_base >> 8, 0, 0, flags))
|
||||||
|
info = pb.Info(raw)
|
||||||
|
|
||||||
|
flash = bytearray(b"\xff" * flash_size)
|
||||||
|
for address, content in pb.plan_flash(app_bytes, info).items():
|
||||||
|
flash[address:address + len(content)] = content
|
||||||
|
flash[base:base + len(loader_bytes)] = loader_bytes
|
||||||
|
return bytes(flash)
|
||||||
|
|
||||||
|
|
||||||
|
def first_tx_cycle(device, deadline):
|
||||||
|
"""The PB_WINDOW_TX report, or None. The runner prints it once."""
|
||||||
|
stream = device.proc.stdout
|
||||||
|
while True:
|
||||||
|
remaining = deadline - time.monotonic()
|
||||||
|
if remaining <= 0:
|
||||||
|
return None
|
||||||
|
ready, _, _ = select.select([stream], [], [], remaining)
|
||||||
|
if not ready:
|
||||||
|
return None
|
||||||
|
line = stream.readline()
|
||||||
|
if not line:
|
||||||
|
return None
|
||||||
|
if line.startswith("PB_WINDOW_TX"):
|
||||||
|
return int(line.split()[1])
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--device", required=True)
|
||||||
|
parser.add_argument("--loader", required=True)
|
||||||
|
parser.add_argument("--mcu", required=True)
|
||||||
|
parser.add_argument("--hz", type=int, required=True)
|
||||||
|
parser.add_argument("--base", required=True)
|
||||||
|
parser.add_argument("--page", type=int, required=True)
|
||||||
|
parser.add_argument("--baud", type=int, required=True)
|
||||||
|
parser.add_argument("--app", required=True)
|
||||||
|
parser.add_argument("--tool", required=True)
|
||||||
|
parser.add_argument("--workdir", required=True)
|
||||||
|
parser.add_argument("--link", default=None)
|
||||||
|
parser.add_argument("--seconds", type=float, default=None)
|
||||||
|
parser.add_argument("--autobaud-polls", type=int, default=None)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if (args.seconds is None) == (args.autobaud_polls is None):
|
||||||
|
parser.error("exactly one of --seconds / --autobaud-polls")
|
||||||
|
|
||||||
|
pb = load_tool(args.tool)
|
||||||
|
base = int(args.base, 0)
|
||||||
|
expected = (args.seconds if args.seconds is not None
|
||||||
|
else args.autobaud_polls * AUTOBAUD_POLL_CYCLES / args.hz)
|
||||||
|
|
||||||
|
work = pathlib.Path(args.workdir)
|
||||||
|
work.mkdir(parents=True, exist_ok=True)
|
||||||
|
# Every loader target objcopies its slot content beside the ELF (.bin).
|
||||||
|
loader_bytes = pathlib.Path(args.loader + ".bin").read_bytes()
|
||||||
|
app_bytes = pathlib.Path(args.app).read_bytes()
|
||||||
|
flash_file = work / "window-flash.bin"
|
||||||
|
flash_file.write_bytes(compose_flash(pb, loader_bytes, app_bytes, args.mcu, base, args.page))
|
||||||
|
|
||||||
|
device = Device(args.device, args.loader, args.mcu, str(args.hz), args.base, args.page,
|
||||||
|
args.baud, str(work / "window-dump.bin"), resume=str(flash_file),
|
||||||
|
link=args.link, window=True)
|
||||||
|
try:
|
||||||
|
# Simulation speed is machine-dependent; a few hundred thousand
|
||||||
|
# cycles per wall second is the pessimistic floor.
|
||||||
|
budget = max(60.0, expected * args.hz / 300000)
|
||||||
|
cycle = first_tx_cycle(device, time.monotonic() + budget)
|
||||||
|
finally:
|
||||||
|
device.stop()
|
||||||
|
|
||||||
|
if cycle is None:
|
||||||
|
print(f" [FAIL] no transmit activity within {budget:.0f} s wall "
|
||||||
|
f"(expected a {expected:.2f} s window)")
|
||||||
|
return 1
|
||||||
|
measured = cycle / args.hz
|
||||||
|
error = (measured - expected) / expected
|
||||||
|
ok = abs(error) <= 0.02
|
||||||
|
print(f" [{'PASS' if ok else 'FAIL'}] window {measured:.3f} s vs declared "
|
||||||
|
f"{expected:.3f} s ({error:+.1%}, gate ±2%)")
|
||||||
|
return 0 if ok else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -62,6 +62,29 @@ const char *dump_path;
|
|||||||
std::uint32_t reset_pc;
|
std::uint32_t reset_pc;
|
||||||
volatile std::sig_atomic_t reset_requested;
|
volatile std::sig_atomic_t reset_requested;
|
||||||
|
|
||||||
|
// -w: report the cycle of the first transmit activity, once. What the
|
||||||
|
// activation-window gate reads — with an idle line and an application
|
||||||
|
// installed, the first thing that ever talks is the application's banner,
|
||||||
|
// so this cycle *is* the loader's window plus a banner lead measured in
|
||||||
|
// microseconds. Idle pacing is skipped in this mode: there is no real-time
|
||||||
|
// host in the loop, and a paced multi-second window would take hours.
|
||||||
|
bool window_report;
|
||||||
|
bool window_tx_seen;
|
||||||
|
|
||||||
|
void window_first_tx()
|
||||||
|
{
|
||||||
|
if (!window_report || window_tx_seen)
|
||||||
|
return;
|
||||||
|
window_tx_seen = true;
|
||||||
|
std::println("PB_WINDOW_TX {}", avr->cycle);
|
||||||
|
std::fflush(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
void window_uart_hook(avr_irq_t *, std::uint32_t, void *)
|
||||||
|
{
|
||||||
|
window_first_tx();
|
||||||
|
}
|
||||||
|
|
||||||
int parse_link(std::string_view spec)
|
int parse_link(std::string_view spec)
|
||||||
{
|
{
|
||||||
if (spec == "usart0" || spec == "usart1") {
|
if (spec == "usart0" || spec == "usart1") {
|
||||||
@@ -195,6 +218,20 @@ std::uint8_t tx_shift;
|
|||||||
|
|
||||||
avr_cycle_count_t tx_sample(avr_t *, avr_cycle_count_t when, void *)
|
avr_cycle_count_t tx_sample(avr_t *, avr_cycle_count_t when, void *)
|
||||||
{
|
{
|
||||||
|
if (tx_bit < 0) {
|
||||||
|
// Half a bit into the start bit: a real receiver re-samples here and
|
||||||
|
// abandons a false start. The device's own init produces one — DDR
|
||||||
|
// drives the pin low for the instructions until the idle level is
|
||||||
|
// written — and without this check that glitch decodes as a stray
|
||||||
|
// byte (and would read as first transmit activity under -w).
|
||||||
|
if (tx_level) {
|
||||||
|
tx_active = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
window_first_tx();
|
||||||
|
tx_bit = 0;
|
||||||
|
return when + bit_cycles;
|
||||||
|
}
|
||||||
if (tx_bit < 8) {
|
if (tx_bit < 8) {
|
||||||
tx_shift = static_cast<std::uint8_t>((tx_shift >> 1) | (tx_level ? 0x80 : 0));
|
tx_shift = static_cast<std::uint8_t>((tx_shift >> 1) | (tx_level ? 0x80 : 0));
|
||||||
if (++tx_bit < 8)
|
if (++tx_bit < 8)
|
||||||
@@ -256,10 +293,10 @@ void tx_hook(avr_irq_t *, std::uint32_t value, void *)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int level = value & 1;
|
int level = value & 1;
|
||||||
if (!tx_active && tx_level == 1 && level == 0) { // start edge
|
if (!tx_active && tx_level == 1 && level == 0) { // start edge, confirmed mid-bit
|
||||||
tx_active = 1;
|
tx_active = 1;
|
||||||
tx_bit = 0;
|
tx_bit = -1;
|
||||||
avr_cycle_timer_register(avr, bit_cycles + bit_cycles / 2, tx_sample, nullptr);
|
avr_cycle_timer_register(avr, bit_cycles / 2, tx_sample, nullptr);
|
||||||
}
|
}
|
||||||
tx_level = level;
|
tx_level = level;
|
||||||
}
|
}
|
||||||
@@ -315,7 +352,16 @@ void bridge_reset()
|
|||||||
rx_active = 0;
|
rx_active = 0;
|
||||||
tx_active = 0;
|
tx_active = 0;
|
||||||
tx_level = 1;
|
tx_level = 1;
|
||||||
avr_raise_irq(rx_pin, 1); // idle line
|
// Re-drive the idle line through a forced transition: ioport pin irqs are
|
||||||
|
// IRQ_FLAG_FILTERED, and avr_reset zeroes the port latch while the irq
|
||||||
|
// keeps its pre-reset cached value — so a plain raise(1) against a cached
|
||||||
|
// 1 is dropped and the device reads the line stuck low. A loader entering
|
||||||
|
// calibration on that line measures reset-to-first-edge as one giant
|
||||||
|
// pulse and mis-locks or boots the application on the first real knock.
|
||||||
|
// No cycles run between the two raises, so the device only ever sees the
|
||||||
|
// final idle-high.
|
||||||
|
avr_raise_irq(rx_pin, 0);
|
||||||
|
avr_raise_irq(rx_pin, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void poll_pty()
|
void poll_pty()
|
||||||
@@ -364,7 +410,11 @@ void poll_pty()
|
|||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
bool link_given = false;
|
bool link_given = false;
|
||||||
for (int opt; (opt = getopt(argc, argv, "l:")) != -1;) {
|
for (int opt; (opt = getopt(argc, argv, "l:w")) != -1;) {
|
||||||
|
if (opt == 'w') {
|
||||||
|
window_report = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (opt != 'l' || parse_link(optarg) != 0) {
|
if (opt != 'l' || parse_link(optarg) != 0) {
|
||||||
std::println(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)");
|
std::println(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)");
|
||||||
return 2;
|
return 2;
|
||||||
@@ -374,10 +424,12 @@ int main(int argc, char *argv[])
|
|||||||
int args = argc - optind;
|
int args = argc - optind;
|
||||||
if (args < 7 || args > 9) {
|
if (args < 7 || args > 9) {
|
||||||
std::print(stderr,
|
std::print(stderr,
|
||||||
"usage: {} [-l link] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
|
"usage: {} [-l link] [-w] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
|
||||||
" [reset_hex] [resume_flash]\n"
|
" [reset_hex] [resume_flash]\n"
|
||||||
" -l link: usart0 | usart1 | sw[:B0,B1[@0]] (RX,TX, then the USART owning\n"
|
" -l link: usart0 | usart1 | sw[:B0,B1[@0]] (RX,TX, then the USART owning\n"
|
||||||
" them); default: the chip's own\n"
|
" them); default: the chip's own\n"
|
||||||
|
" -w: print PB_WINDOW_TX <cycle> at the first transmit activity and\n"
|
||||||
|
" free-run idle time (window measurement mode)\n"
|
||||||
" reset_hex: reset vector (default: base with a boot section, else 0)\n"
|
" reset_hex: reset vector (default: base with a boot section, else 0)\n"
|
||||||
" resume_flash: raw full-flash image loaded instead of the ELF — a prior\n"
|
" resume_flash: raw full-flash image loaded instead of the ELF — a prior\n"
|
||||||
" run's dump, for power-fail resume tests\n",
|
" run's dump, for power-fail resume tests\n",
|
||||||
@@ -468,6 +520,9 @@ int main(int argc, char *argv[])
|
|||||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
|
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
|
||||||
uart_pty_init(avr, &uart_pty);
|
uart_pty_init(avr, &uart_pty);
|
||||||
uart_pty_connect(&uart_pty, uart_digit);
|
uart_pty_connect(&uart_pty, uart_digit);
|
||||||
|
if (window_report)
|
||||||
|
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_UART_GETIRQ(uart_digit), UART_IRQ_OUTPUT),
|
||||||
|
window_uart_hook, nullptr);
|
||||||
std::println("PB_PTY {}", uart_pty.pty.slavename);
|
std::println("PB_PTY {}", uart_pty.pty.slavename);
|
||||||
} else {
|
} else {
|
||||||
bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly
|
bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly
|
||||||
@@ -524,7 +579,7 @@ int main(int argc, char *argv[])
|
|||||||
// entirely. Pace the simulation only while the bridge is fully
|
// entirely. Pace the simulation only while the bridge is fully
|
||||||
// quiet (nothing decoding, nothing queued); transfers keep full
|
// quiet (nothing decoding, nothing queued); transfers keep full
|
||||||
// speed, and a quiet window stretches toward real time.
|
// speed, and a quiet window stretches toward real time.
|
||||||
if (!rx_active && !tx_active && rx_head == rx_tail)
|
if (!window_report && !rx_active && !tx_active && rx_head == rx_tail)
|
||||||
usleep(200);
|
usleep(200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Host-tool activation handshake: it must not hang on a flooding target.
|
"""Host-tool activation handshake: bounded against a line that misbehaves.
|
||||||
|
|
||||||
`_handshake` drains the line after it sees a prompt, to absorb a real loader's
|
`_handshake` drains the line after it sees a prompt, to absorb a real loader's
|
||||||
trailing bytes before it asks for the identity. That drain must be bounded: a
|
trailing bytes before it asks for the identity. That drain must be bounded: a
|
||||||
@@ -8,6 +8,13 @@ this, ~60 reboots/s of UART-reset garbage in which a stray 0x2b reads as a
|
|||||||
prompt — otherwise spins the tool forever. Regression for that hang, plus a
|
prompt — otherwise spins the tool forever. Regression for that hang, plus a
|
||||||
control that a well-behaved loader still connects.
|
control that a well-behaved loader still connects.
|
||||||
|
|
||||||
|
The handshake must also survive its own leftovers: after `--stay` the loader's
|
||||||
|
final prompt can still be in the USB pipeline when the next invocation opens
|
||||||
|
the port, and on a board wired to reset on open, that opening starts a fresh
|
||||||
|
activation window the stale prompt then betrays — the tool commits to an
|
||||||
|
identity read against a device that never heard its knock, and what it finally
|
||||||
|
collects is the application's banner. StaleDTRPort is that moment as a port.
|
||||||
|
|
||||||
Stdlib only, no device: host-tool logic, so it runs on every chip's preset
|
Stdlib only, no device: host-tool logic, so it runs on every chip's preset
|
||||||
beside pureboot.planner.
|
beside pureboot.planner.
|
||||||
"""
|
"""
|
||||||
@@ -49,27 +56,117 @@ class FloodPort:
|
|||||||
|
|
||||||
|
|
||||||
class LoaderPort:
|
class LoaderPort:
|
||||||
"""A well-behaved pureboot 5: one prompt to the knock, then quiet, then the
|
"""A well-behaved pureboot 5: a prompt to the knock, then quiet, then the
|
||||||
slim identity (version 5 + m328p signature) and a closing prompt."""
|
slim identity (version 5 + m328p signature) and a closing prompt."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.reads = self.exacts = 0
|
self.pending = b""
|
||||||
|
self.exacts = 0
|
||||||
|
|
||||||
def flush_input(self):
|
def flush_input(self):
|
||||||
pass
|
self.pending = b""
|
||||||
|
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
pass
|
if b"p" in data:
|
||||||
|
self.pending = b"+" # the prompt answers the knock, nothing else
|
||||||
|
|
||||||
def read_available(self, wait):
|
def read_available(self, wait):
|
||||||
self.reads += 1
|
data, self.pending = self.pending, b""
|
||||||
return b"+" if self.reads == 1 else b"" # prompt once, then settle quiet
|
return data
|
||||||
|
|
||||||
def read_exact(self, count, timeout):
|
def read_exact(self, count, timeout):
|
||||||
self.exacts += 1
|
self.exacts += 1
|
||||||
return b"\x05\x1e\x95\x0f" if self.exacts == 1 else b"+" # identity, then prompt
|
return b"\x05\x1e\x95\x0f" if self.exacts == 1 else b"+" # identity, then prompt
|
||||||
|
|
||||||
|
|
||||||
|
class StaleDTRPort:
|
||||||
|
"""`--stay`, then a fresh invocation on a board that resets when its port
|
||||||
|
opens. Three facts of that moment, all timed from the open: the previous
|
||||||
|
session's final prompt is still in transit and lands only after the
|
||||||
|
opening flush has already run; the reset holds the device off the line
|
||||||
|
at first, eating anything written before it completes; and the fresh
|
||||||
|
window is finite — once it expires the application boots and prints a
|
||||||
|
banner whose bytes are what a pending identity read collects. A
|
||||||
|
handshake that trusts the stale prompt spends the whole window waiting
|
||||||
|
on a device that never heard its knock; one that drains the line first
|
||||||
|
knocks into the real window and connects."""
|
||||||
|
|
||||||
|
STALE_AT = 0.02 # the leftover prompt becomes visible (post-flush)
|
||||||
|
READY_AT = 0.05 # reset complete, activation window opens
|
||||||
|
WINDOW = 1.0 # window length; expiry boots the application
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.t0 = time.monotonic()
|
||||||
|
# (visible-from, bytes): the line as a timed queue.
|
||||||
|
self.queue = [(self.t0 + self.STALE_AT, b"+")]
|
||||||
|
self.armed = False # a 'p' heard inside the window arms 'b'
|
||||||
|
self.booted = False
|
||||||
|
|
||||||
|
def _boot_check(self):
|
||||||
|
if not self.booted and time.monotonic() > self.t0 + self.READY_AT + self.WINDOW:
|
||||||
|
self.booted = True
|
||||||
|
self.queue.append((self.t0 + self.READY_AT + self.WINDOW,
|
||||||
|
b"W r libavr tempmon\r\n"))
|
||||||
|
|
||||||
|
def _visible(self):
|
||||||
|
self._boot_check()
|
||||||
|
now = time.monotonic()
|
||||||
|
return b"".join(d for t, d in self.queue if t <= now)
|
||||||
|
|
||||||
|
def _consume(self, n):
|
||||||
|
now = time.monotonic()
|
||||||
|
left = []
|
||||||
|
for t, d in self.queue:
|
||||||
|
if t <= now and n:
|
||||||
|
take = min(n, len(d))
|
||||||
|
d = d[take:]
|
||||||
|
n -= take
|
||||||
|
if d:
|
||||||
|
left.append((t, d))
|
||||||
|
self.queue = left
|
||||||
|
|
||||||
|
def flush_input(self):
|
||||||
|
self._consume(len(self._visible()))
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
self._boot_check()
|
||||||
|
now = time.monotonic()
|
||||||
|
if now < self.t0 + self.READY_AT or self.booted:
|
||||||
|
return # still in reset, or the application owns the line
|
||||||
|
if b"p" in data:
|
||||||
|
self.armed = True
|
||||||
|
self.queue.append((now + 0.01, b"+"))
|
||||||
|
if b"b" in data and self.armed:
|
||||||
|
# The slim identity (version 5 + m328p signature) and a prompt.
|
||||||
|
self.queue.append((now + 0.01, b"\x05\x1e\x95\x0f+"))
|
||||||
|
|
||||||
|
def read_available(self, wait):
|
||||||
|
deadline = time.monotonic() + wait
|
||||||
|
while True:
|
||||||
|
data = self._visible()
|
||||||
|
if data:
|
||||||
|
self._consume(len(data))
|
||||||
|
return data
|
||||||
|
if time.monotonic() >= deadline:
|
||||||
|
return b""
|
||||||
|
time.sleep(0.005)
|
||||||
|
|
||||||
|
def read_exact(self, count, timeout):
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
data = b""
|
||||||
|
while len(data) < count:
|
||||||
|
visible = self._visible()
|
||||||
|
if visible:
|
||||||
|
take = visible[:count - len(data)]
|
||||||
|
self._consume(len(take))
|
||||||
|
data += take
|
||||||
|
elif time.monotonic() >= deadline:
|
||||||
|
raise pb.Error(f"timeout: got {len(data)} of {count} bytes")
|
||||||
|
else:
|
||||||
|
time.sleep(0.005)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def terminates(port, wait, budget):
|
def terminates(port, wait, budget):
|
||||||
"""Run connect_autobaud in a thread; True if it returns/raises within
|
"""Run connect_autobaud in a thread; True if it returns/raises within
|
||||||
`budget` seconds rather than hanging."""
|
`budget` seconds rather than hanging."""
|
||||||
@@ -97,6 +194,16 @@ def main():
|
|||||||
info = pb.Loader(LoaderPort()).connect_autobaud(2.0)
|
info = pb.Loader(LoaderPort()).connect_autobaud(2.0)
|
||||||
check("well-behaved loader still connects (version 5)", info.version == 5)
|
check("well-behaved loader still connects (version 5)", info.version == 5)
|
||||||
|
|
||||||
|
# the stale prompt: a --stay leftover plus reset-on-open must not burn the
|
||||||
|
# fresh window — the pre-knock drain absorbs it and the first real knock
|
||||||
|
# lands inside the window.
|
||||||
|
try:
|
||||||
|
stale_ok = pb.Loader(StaleDTRPort()).connect(2.5).version == 5
|
||||||
|
except pb.Error as failed:
|
||||||
|
print(f" ({failed})")
|
||||||
|
stale_ok = False
|
||||||
|
check("stale --stay prompt + reset-on-open: connects in the fresh window", stale_ok)
|
||||||
|
|
||||||
print(f"\n {P} passed, {F} failed")
|
print(f"\n {P} passed, {F} failed")
|
||||||
return 1 if F else 0
|
return 1 if F else 0
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,20 @@ def collect() -> dict[str, list[tuple[str, int, int]]]:
|
|||||||
for match in SIZE_TEST.finditer((tree / "CTestTestfile.cmake").read_text()):
|
for match in SIZE_TEST.finditer((tree / "CTestTestfile.cmake").read_text()):
|
||||||
found.setdefault(chip, []).append((match["name"], match["elf"], int(match["limit"])))
|
found.setdefault(chip, []).append((match["name"], match["elf"], int(match["limit"])))
|
||||||
sizes = measure([elf for rows in found.values() for _, elf, _ in rows], tool)
|
sizes = measure([elf for rows in found.values() for _, elf, _ in rows], tool)
|
||||||
|
# A chip's generated and reflect trees must answer with the same bytes
|
||||||
|
# (the identity invariant), so the same target measuring two sizes means
|
||||||
|
# a stale tree — or an identity breach. Either is a finding; picking one
|
||||||
|
# silently is how a gate reports another build's numbers as today's.
|
||||||
|
for chip, rows in found.items():
|
||||||
|
seen: dict[str, tuple[int, str]] = {}
|
||||||
|
for name, elf, _ in rows:
|
||||||
|
if elf not in sizes:
|
||||||
|
continue
|
||||||
|
if name in seen and seen[name][0] != sizes[elf]:
|
||||||
|
sys.exit(f"{chip} {name}: {seen[name][0]} B in {seen[name][1]} but "
|
||||||
|
f"{sizes[elf]} B in {elf} — a stale tree (rebuild or remove it) "
|
||||||
|
f"or a cross-mode identity breach")
|
||||||
|
seen.setdefault(name, (sizes[elf], elf))
|
||||||
measured = {
|
measured = {
|
||||||
chip: sorted(((name, sizes[elf], limit) for name, elf, limit in rows if elf in sizes),
|
chip: sorted(((name, sizes[elf], limit) for name, elf, limit in rows if elf in sizes),
|
||||||
key=lambda row: -row[1])
|
key=lambda row: -row[1])
|
||||||
@@ -120,7 +134,9 @@ def cmd_max(args) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def cmd_check_readme(args) -> int:
|
def cmd_check_readme(args) -> int:
|
||||||
"""The README's per-chip table, against the stock and autobaud builds."""
|
"""The README's per-chip table, against the stock build and the worst
|
||||||
|
autobaud configuration (OSCCAL baked, plus the USART-pin release where
|
||||||
|
the chip has a USART) — the config the Autobaud column documents."""
|
||||||
readme = (ROOT / "pureboot" / "README.md").read_text()
|
readme = (ROOT / "pureboot" / "README.md").read_text()
|
||||||
measured = collect()
|
measured = collect()
|
||||||
rows = re.findall(r"^\|\s*(AT\w+[^|]*?)\s*\|[^|]*\|[^|]*\|[^|]*\|\s*(\d+) B\s*\|\s*(\d+) B\s*\|$",
|
rows = re.findall(r"^\|\s*(AT\w+[^|]*?)\s*\|[^|]*\|[^|]*\|[^|]*\|\s*(\d+) B\s*\|\s*(\d+) B\s*\|$",
|
||||||
@@ -132,7 +148,9 @@ def cmd_check_readme(args) -> int:
|
|||||||
# "ATmega48, 48A, 48P, 48PA †" — the first name is the family's base.
|
# "ATmega48, 48A, 48P, 48PA †" — the first name is the family's base.
|
||||||
chip = re.sub(r"[^a-z0-9]", "", chips.split(",")[0].strip().lower())
|
chip = re.sub(r"[^a-z0-9]", "", chips.split(",")[0].strip().lower())
|
||||||
built = {name: text for name, text, _ in measured.get(chip, [])}
|
built = {name: text for name, text, _ in measured.get(chip, [])}
|
||||||
for target, documented in (("pureboot", stock_doc), ("pureboot_autobaud", auto_doc)):
|
worst = ("pureboot_autobaud_osccal_on_usart0"
|
||||||
|
if "pureboot_autobaud_osccal_on_usart0" in built else "pureboot_autobaud_osccal")
|
||||||
|
for target, documented in (("pureboot", stock_doc), (worst, auto_doc)):
|
||||||
if target not in built:
|
if target not in built:
|
||||||
skipped += 1
|
skipped += 1
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use — only the divisor low
|
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use — only the divisor low
|
||||||
// byte and U2X0 need a store. The library still does the datasheet work.
|
// byte and U2X0 need a store. The library still does the datasheet work.
|
||||||
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
||||||
hw::reg<"UBRR0">::write(static_cast<std::uint8_t>(baud.ubrr));
|
hw::ubrr0::write(static_cast<std::uint8_t>(baud.ubrr));
|
||||||
hw::ucsr0a::write(hw::ucsr0a::u2x0(1));
|
hw::ucsr0a::write(hw::ucsr0a::u2x0(1));
|
||||||
// General-purpose registers are undefined at power-on (no crt zeroes them);
|
// General-purpose registers are undefined at power-on (no crt zeroes them);
|
||||||
// the direction latch must start "not receiving" so the first rx() enables
|
// the direction latch must start "not receiving" so the first rx() enables
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
{
|
{
|
||||||
constexpr auto sol = avr::uart::solve_baud(dev::clock, 115200_Bd);
|
constexpr auto sol = avr::uart::solve_baud(dev::clock, 115200_Bd);
|
||||||
static_assert(sol.u2x && sol.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
static_assert(sol.u2x && sol.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
||||||
avr::hw::reg<"UBRR0">::write(static_cast<std::uint8_t>(sol.ubrr));
|
avr::hw::ubrr0::write(static_cast<std::uint8_t>(sol.ubrr));
|
||||||
avr::hw::ucsr0a::write(avr::hw::ucsr0a::u2x0(1));
|
avr::hw::ucsr0a::write(avr::hw::ucsr0a::u2x0(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
|
|||||||
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use — only the divisor low
|
// 0, and rx()/tx() raise RXEN0/TXEN0 on first use — only the divisor low
|
||||||
// byte and U2X0 need a store. The library still does the datasheet work.
|
// byte and U2X0 need a store. The library still does the datasheet work.
|
||||||
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
static_assert(baud.u2x && baud.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
|
||||||
hw::reg<"UBRR0">::write(static_cast<std::uint8_t>(baud.ubrr));
|
hw::ubrr0::write(static_cast<std::uint8_t>(baud.ubrr));
|
||||||
hw::ucsr0a::write(hw::ucsr0a::u2x0(1));
|
hw::ucsr0a::write(hw::ucsr0a::u2x0(1));
|
||||||
// General-purpose registers are undefined at power-on (no crt zeroes them);
|
// General-purpose registers are undefined at power-on (no crt zeroes them);
|
||||||
// the direction latch must start "not receiving" so the first rx() enables
|
// the direction latch must start "not receiving" so the first rx() enables
|
||||||
|
|||||||
Reference in New Issue
Block a user