Compare commits
8 Commits
0513d07e87
...
v7
| Author | SHA1 | Date | |
|---|---|---|---|
| bad8b6b43e | |||
| 5d1b4497d4 | |||
| a743ea64a3 | |||
| aa66cfccff | |||
| 459d463283 | |||
| 8e7cc86fb3 | |||
| c8ac61779e | |||
| 4362886c39 |
@@ -202,6 +202,21 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
${CMAKE_BINARY_DIR}/pbtest-work)
|
||||
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,
|
||||
# installed one slot lower, must serve the full command set.
|
||||
add_test(NAME pureboot.reloc
|
||||
@@ -543,5 +558,19 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
1000000 9600 ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||
${CMAKE_BINARY_DIR}/pbautobaud-work)
|
||||
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()
|
||||
|
||||
2
libavr
2
libavr
Submodule libavr updated: f5d71225a2...a9fe6bed50
@@ -376,9 +376,18 @@ function(pureboot_add_loader name)
|
||||
# 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
|
||||
# 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
|
||||
# autobaud body needs ivopts to keep the calibration countdown a single
|
||||
# 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}
|
||||
-Wl,--defsym=pureboot_app=${_app} ${_wrap})
|
||||
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
|
||||
|
||||
@@ -19,42 +19,42 @@ come out byte-identical linked at a different base.
|
||||
|
||||
## Chips
|
||||
|
||||
Sizes are the default configuration: the hardware USART0 at 115200 8N1 on a
|
||||
16 MHz crystal, or the software UART on RX = PB0 / TX = PB1 at 57600 8N1 on
|
||||
the tinies' RC oscillator (9.6 MHz on the t13s, 8 MHz above). Every axis moves
|
||||
per build — see *Configuration*. The autobaud column is the clock-free build,
|
||||
which is the largest the space produces and the tightest fit in the matrix;
|
||||
it carries the calibration machinery and no clock at all.
|
||||
The Stock column is the default configuration: the hardware USART0 at 115200
|
||||
8N1 on a 16 MHz crystal, or the software UART on RX = PB0 / TX = PB1 at
|
||||
57600 8N1 on the tinies' RC oscillator (9.6 MHz on the t13s, 8 MHz above).
|
||||
Every axis moves per build — see *Configuration*. The Autobaud column is the
|
||||
worst configuration the space produces for the chip: the clock-free build —
|
||||
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 |
|
||||
|---|---|---|---|---|---|
|
||||
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 384 B | 452 B |
|
||||
| ATtiny25 † | 2 KiB | 0x0600 | software | 388 B | 442 B |
|
||||
| ATtiny45 † | 4 KiB | 0x0e00 | software | 388 B | 442 B |
|
||||
| ATtiny85 † | 8 KiB | 0x1e00 | software | 388 B | 442 B |
|
||||
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 358 B | 470 B |
|
||||
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 360 B | 474 B |
|
||||
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 360 B | 474 B |
|
||||
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 378 B | 438 B |
|
||||
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 388 B | 448 B |
|
||||
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 390 B | 454 B |
|
||||
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 390 B | 454 B |
|
||||
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 390 B | 454 B |
|
||||
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 390 B | 454 B |
|
||||
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 384 B | 448 B |
|
||||
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 410 B | 474 B |
|
||||
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 384 B | 474 B |
|
||||
| ATtiny25 † | 2 KiB | 0x0600 | software | 388 B | 466 B |
|
||||
| ATtiny45 † | 4 KiB | 0x0e00 | software | 388 B | 466 B |
|
||||
| ATtiny85 † | 8 KiB | 0x1e00 | software | 388 B | 466 B |
|
||||
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 362 B | 494 B |
|
||||
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 364 B | 496 B |
|
||||
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 364 B | 496 B |
|
||||
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 378 B | 468 B |
|
||||
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 388 B | 478 B |
|
||||
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 390 B | 480 B |
|
||||
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 390 B | 480 B |
|
||||
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 390 B | 480 B |
|
||||
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 390 B | 480 B |
|
||||
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 384 B | 474 B |
|
||||
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 410 B | 502 B |
|
||||
|
||||
† 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.
|
||||
|
||||
The tightest fit in the whole space is the 1284s' autobaud build deployed on a
|
||||
USART's own pins with the `OSCCAL` trim baked, 484 of its 512 — they alone
|
||||
carry the far-flash machinery (ELPM reads, RAMPZ page commands), autobaud
|
||||
alone carries the calibration loop, a bit-banged link on a USART's pins alone
|
||||
has to release it (below), and the trim adds its one register write. Without
|
||||
the trim that build is 478; on the default pins, 474. 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
|
||||
The tightest fit in the whole space is therefore the 1284s' 502 of their
|
||||
512: they alone carry the far-flash machinery (ELPM reads, RAMPZ page
|
||||
commands) on top of everything the column already stacks. 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.
|
||||
|
||||
The software UART enables the RX pull-up; TX idles high. All multi-byte wire
|
||||
@@ -100,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
|
||||
needs its divisor programmed) and that activation counts poll iterations rather
|
||||
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
|
||||
calibration can measure is bounded by how many clock cycles one bit lasts, so a
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
// 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).
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include <libavr/libavr.hpp>
|
||||
|
||||
using namespace avr::literals;
|
||||
@@ -162,18 +164,30 @@ constexpr std::uint8_t bank_shift = 16 - slot_shift;
|
||||
#define PUREBOOT_TX pb1
|
||||
#endif
|
||||
#if defined(PUREBOOT_USART)
|
||||
constexpr char usart_digit = '0' + PUREBOOT_USART;
|
||||
constexpr int usart_unit = PUREBOOT_USART;
|
||||
#else
|
||||
constexpr char usart_digit = '0';
|
||||
constexpr int usart_unit = 0;
|
||||
#endif
|
||||
|
||||
template <avr::hertz_t C, avr::baud_t B>
|
||||
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),
|
||||
// sbiw + sbci + sbci + brne (6).
|
||||
static constexpr std::uint8_t poll_cycles = 10;
|
||||
// The compiled idle poll around the window's narrow (uint24_t) countdown:
|
||||
// the RXC test, then sbiw + sbci + brne (5). The test's cost follows the
|
||||
// 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()
|
||||
{
|
||||
@@ -197,7 +211,10 @@ struct hardware_link {
|
||||
|
||||
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 tx_t = avr::uart::software_tx<C, avr::PUREBOOT_TX, B>;
|
||||
|
||||
// The compiled idle poll: sbis skipping the exit (2), sbiw + sbci +
|
||||
// sbci + brne (6).
|
||||
static constexpr std::uint8_t poll_cycles = 8;
|
||||
// The compiled idle poll around the window's narrow (uint24_t) countdown:
|
||||
// sbis skipping the exit (2), sbiw + sbci + brne (5). 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.
|
||||
static constexpr std::uint8_t poll_cycles = 7;
|
||||
|
||||
static void init()
|
||||
{
|
||||
@@ -263,19 +282,22 @@ struct autobaud_link {
|
||||
|
||||
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)
|
||||
using link = autobaud_link;
|
||||
#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>;
|
||||
#elif defined(PUREBOOT_SOFT_SERIAL)
|
||||
using link = software_link<dev::clock, wire_baud>;
|
||||
#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>>;
|
||||
#endif
|
||||
|
||||
@@ -317,17 +339,35 @@ void await_host()
|
||||
}
|
||||
}
|
||||
#else
|
||||
// The window as one 32-bit countdown, divided by the backend's counted
|
||||
// poll-loop cycles. Whole seconds is all it promises.
|
||||
// The window as one countdown, divided by the backend's counted poll-loop
|
||||
// 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()
|
||||
{
|
||||
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
|
||||
// cost a wider decrement chain at every poll for range most windows never
|
||||
// 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()
|
||||
{
|
||||
|
||||
@@ -26,7 +26,7 @@ else:
|
||||
import termios
|
||||
|
||||
PROMPT = b"+"
|
||||
VERSION = 7 # 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
|
||||
# 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
|
||||
@@ -546,6 +546,11 @@ class Loader:
|
||||
# Set once a session is established over an autobaud link, so a
|
||||
# re-entry after 'J' repeats the handshake that worked.
|
||||
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
|
||||
# staging copy built for another one (enter_copy).
|
||||
self.baud = getattr(port, "baud", None)
|
||||
@@ -555,10 +560,16 @@ class Loader:
|
||||
"""The 'b' reply, in either of the two layouts a loader may send.
|
||||
pureboot 5 answers with its version and the signature; older loaders
|
||||
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."""
|
||||
head = self.port.read_exact(4, 2.0)
|
||||
the older block's 'P', so four bytes are enough to tell them apart.
|
||||
|
||||
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":
|
||||
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)
|
||||
|
||||
def _handshake(self, wait, knock, what):
|
||||
@@ -569,8 +580,23 @@ class Loader:
|
||||
into a fresh window, where a command without its knock is discarded.
|
||||
Each attempt is therefore the whole handshake. This also converges into
|
||||
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
|
||||
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
|
||||
refusal = None
|
||||
while True:
|
||||
|
||||
@@ -50,7 +50,7 @@ consteval bool use_hardware()
|
||||
#if defined(PUREBOOT_SOFT_SERIAL)
|
||||
return false;
|
||||
#else
|
||||
return avr::hw::db.has_instance("USART0") || avr::hw::db.has_instance("USART");
|
||||
return avr::uart::has_usart<0>();
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ struct link {
|
||||
#else
|
||||
static constexpr avr::baud_t baud{115200};
|
||||
#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)
|
||||
{
|
||||
tx_t::write(static_cast<std::uint8_t>(c));
|
||||
|
||||
@@ -8,10 +8,13 @@ import subprocess
|
||||
|
||||
|
||||
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]
|
||||
if 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]
|
||||
if reset_hex is not None or resume is not None:
|
||||
# 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;
|
||||
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)
|
||||
{
|
||||
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 *)
|
||||
{
|
||||
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) {
|
||||
tx_shift = static_cast<std::uint8_t>((tx_shift >> 1) | (tx_level ? 0x80 : 0));
|
||||
if (++tx_bit < 8)
|
||||
@@ -256,10 +293,10 @@ void tx_hook(avr_irq_t *, std::uint32_t value, void *)
|
||||
return;
|
||||
}
|
||||
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_bit = 0;
|
||||
avr_cycle_timer_register(avr, bit_cycles + bit_cycles / 2, tx_sample, nullptr);
|
||||
tx_bit = -1;
|
||||
avr_cycle_timer_register(avr, bit_cycles / 2, tx_sample, nullptr);
|
||||
}
|
||||
tx_level = level;
|
||||
}
|
||||
@@ -315,7 +352,16 @@ void bridge_reset()
|
||||
rx_active = 0;
|
||||
tx_active = 0;
|
||||
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()
|
||||
@@ -364,7 +410,11 @@ void poll_pty()
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
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) {
|
||||
std::println(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)");
|
||||
return 2;
|
||||
@@ -374,10 +424,12 @@ int main(int argc, char *argv[])
|
||||
int args = argc - optind;
|
||||
if (args < 7 || args > 9) {
|
||||
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"
|
||||
" -l link: usart0 | usart1 | sw[:B0,B1[@0]] (RX,TX, then the USART owning\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"
|
||||
" resume_flash: raw full-flash image loaded instead of the ELF — a prior\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);
|
||||
uart_pty_init(avr, &uart_pty);
|
||||
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);
|
||||
} else {
|
||||
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
|
||||
// quiet (nothing decoding, nothing queued); transfers keep full
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/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
|
||||
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
|
||||
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
|
||||
beside pureboot.planner.
|
||||
"""
|
||||
@@ -49,27 +56,117 @@ class FloodPort:
|
||||
|
||||
|
||||
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."""
|
||||
|
||||
def __init__(self):
|
||||
self.reads = self.exacts = 0
|
||||
self.pending = b""
|
||||
self.exacts = 0
|
||||
|
||||
def flush_input(self):
|
||||
pass
|
||||
self.pending = b""
|
||||
|
||||
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):
|
||||
self.reads += 1
|
||||
return b"+" if self.reads == 1 else b"" # prompt once, then settle quiet
|
||||
data, self.pending = self.pending, b""
|
||||
return data
|
||||
|
||||
def read_exact(self, count, timeout):
|
||||
self.exacts += 1
|
||||
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):
|
||||
"""Run connect_autobaud in a thread; True if it returns/raises within
|
||||
`budget` seconds rather than hanging."""
|
||||
@@ -97,6 +194,16 @@ def main():
|
||||
info = pb.Loader(LoaderPort()).connect_autobaud(2.0)
|
||||
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")
|
||||
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()):
|
||||
found.setdefault(chip, []).append((match["name"], match["elf"], int(match["limit"])))
|
||||
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 = {
|
||||
chip: sorted(((name, sizes[elf], limit) for name, elf, limit in rows if elf in sizes),
|
||||
key=lambda row: -row[1])
|
||||
@@ -120,7 +134,9 @@ def cmd_max(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()
|
||||
measured = collect()
|
||||
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.
|
||||
chip = re.sub(r"[^a-z0-9]", "", chips.split(",")[0].strip().lower())
|
||||
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:
|
||||
skipped += 1
|
||||
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
|
||||
// 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");
|
||||
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));
|
||||
// General-purpose registers are undefined at power-on (no crt zeroes them);
|
||||
// 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);
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
// 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");
|
||||
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));
|
||||
// General-purpose registers are undefined at power-on (no crt zeroes them);
|
||||
// the direction latch must start "not receiving" so the first rx() enables
|
||||
|
||||
Reference in New Issue
Block a user