Compare commits
5 Commits
a285347e5f
...
0f00f6bdeb
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f00f6bdeb | |||
| 3990f53180 | |||
| 3899e8ef40 | |||
| 0ddc65ca28 | |||
| 142de8775d |
@@ -166,6 +166,27 @@ constexpr char usart_digit = '0' + PUREBOOT_USART;
|
|||||||
constexpr char usart_digit = '0';
|
constexpr char usart_digit = '0';
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Release a hardware USART the application may have left enabled onto a
|
||||||
|
// bit-banged link's pins. A software transmitter drives its TX pin through the
|
||||||
|
// port register, but while that USART's TXEN is set the USART owns the pin and
|
||||||
|
// the port write does nothing — the loader would receive and obey yet never
|
||||||
|
// answer. Writing UCSRnB zero hands the pin back to the port. Guarded on the
|
||||||
|
// pin actually being a USART's TXD, so a link on non-USART pins emits nothing.
|
||||||
|
template <char Inst, avr::io::pin Tx>
|
||||||
|
[[gnu::always_inline]] inline void release_usart_on()
|
||||||
|
{
|
||||||
|
if constexpr (avr::uart::has_usart<Inst>())
|
||||||
|
if constexpr (avr::uart::detail::usart_pin<Inst>("TXD") == Tx)
|
||||||
|
avr::hw::reg_impl<avr::uart::detail::ureg<Inst, "UCSR#B">()>::write(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <avr::io::pin Tx>
|
||||||
|
[[gnu::always_inline]] inline void release_usarts_on()
|
||||||
|
{
|
||||||
|
release_usart_on<'0', Tx>();
|
||||||
|
release_usart_on<'1', Tx>();
|
||||||
|
}
|
||||||
|
|
||||||
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_digit, C, {.baud = B, .max_baud_error = 2.5_pct}>;
|
||||||
@@ -212,6 +233,7 @@ struct software_link {
|
|||||||
static void init()
|
static void init()
|
||||||
{
|
{
|
||||||
avr::init<rx_t, tx_t>();
|
avr::init<rx_t, tx_t>();
|
||||||
|
release_usarts_on<avr::PUREBOOT_TX>();
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool pending()
|
static bool pending()
|
||||||
@@ -245,6 +267,7 @@ struct autobaud_link {
|
|||||||
static void init()
|
static void init()
|
||||||
{
|
{
|
||||||
avr::init<uart>();
|
avr::init<uart>();
|
||||||
|
release_usarts_on<avr::PUREBOOT_TX>();
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::uint8_t rx()
|
static std::uint8_t rx()
|
||||||
@@ -465,8 +488,7 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
|
|||||||
// address is a word address and a slot is half as many words as bytes, so
|
// address is a word address and a slot is half as many words as bytes, so
|
||||||
// its high byte is the slot index outright. No absolute address is ever
|
// its high byte is the slot index outright. No absolute address is ever
|
||||||
// formed, so the image stays position-independent.
|
// formed, so the image stays position-independent.
|
||||||
const auto return_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
|
const auto slot_high = avr::startup::caller_page();
|
||||||
const auto slot_high = static_cast<std::uint8_t>(return_words >> 8);
|
|
||||||
|
|
||||||
await_host();
|
await_host();
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ else:
|
|||||||
import termios
|
import termios
|
||||||
|
|
||||||
PROMPT = b"+"
|
PROMPT = b"+"
|
||||||
VERSION = 4 # this tool's own version — free to drift from a loader's
|
VERSION = 5 # this tool's own version — free to drift from a loader's
|
||||||
# The loader versions this tool speaks. A pureboot version implies its wire
|
# The loader versions this tool speaks. A pureboot version implies its wire
|
||||||
# protocol, which carries no number of its own, so this window is where that
|
# protocol, which carries no number of its own, so this window is where that
|
||||||
# map lives: every version so far speaks the same protocol, and one that
|
# map lives: every version so far speaks the same protocol, and one that
|
||||||
@@ -484,8 +484,13 @@ class Loader:
|
|||||||
self.port.write(knock)
|
self.port.write(knock)
|
||||||
knocks += 1
|
knocks += 1
|
||||||
if PROMPT in self.port.read_available(0.4):
|
if PROMPT in self.port.read_available(0.4):
|
||||||
|
# Settle: absorb a real loader's trailing bytes before asking
|
||||||
|
# for the identity. Bounded by the deadline so a target that
|
||||||
|
# never falls quiet — a board stuck in a reset loop, whose
|
||||||
|
# garbage carries a stray prompt — cannot spin here forever.
|
||||||
while self.port.read_available(0.3):
|
while self.port.read_available(0.3):
|
||||||
pass
|
if time.monotonic() > deadline:
|
||||||
|
break
|
||||||
self.port.write(b"b")
|
self.port.write(b"b")
|
||||||
try:
|
try:
|
||||||
# A version the tool cannot speak is the loader's own
|
# A version the tool cannot speak is the loader's own
|
||||||
|
|||||||
104
test/test_handshake.py
Normal file
104
test/test_handshake.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Host-tool activation handshake: it must not hang on a flooding target.
|
||||||
|
|
||||||
|
`_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
|
||||||
|
target that never falls quiet — a board stuck in a reset loop presents exactly
|
||||||
|
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.
|
||||||
|
|
||||||
|
Stdlib only; run with `python test/test_handshake.py`. Not yet wired into ctest.
|
||||||
|
"""
|
||||||
|
import importlib.util
|
||||||
|
import pathlib
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
PB = pathlib.Path(__file__).resolve().parents[1] / "pureboot" / "pureboot.py"
|
||||||
|
_spec = importlib.util.spec_from_file_location("pureboot", PB)
|
||||||
|
pb = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(pb)
|
||||||
|
|
||||||
|
P = F = 0
|
||||||
|
|
||||||
|
|
||||||
|
def check(name, ok):
|
||||||
|
global P, F
|
||||||
|
P, F = P + (1 if ok else 0), F + (0 if ok else 1)
|
||||||
|
print(f" [{'PASS' if ok else 'FAIL'}] {name}")
|
||||||
|
|
||||||
|
|
||||||
|
class FloodPort:
|
||||||
|
"""A line that never falls quiet: read_available always returns bytes, and
|
||||||
|
they contain a prompt. No identity ever completes."""
|
||||||
|
|
||||||
|
def flush_input(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def read_available(self, wait):
|
||||||
|
time.sleep(0.01) # a real read waits; keep the busy loop off a core
|
||||||
|
return b"+\x00\xff"
|
||||||
|
|
||||||
|
def read_exact(self, count, timeout):
|
||||||
|
raise pb.Error("no identity")
|
||||||
|
|
||||||
|
|
||||||
|
class LoaderPort:
|
||||||
|
"""A well-behaved pureboot 5: one 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
|
||||||
|
|
||||||
|
def flush_input(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def read_available(self, wait):
|
||||||
|
self.reads += 1
|
||||||
|
return b"+" if self.reads == 1 else b"" # prompt once, then settle quiet
|
||||||
|
|
||||||
|
def read_exact(self, count, timeout):
|
||||||
|
self.exacts += 1
|
||||||
|
return b"\x05\x1e\x95\x0f" if self.exacts == 1 else b"+" # identity, then prompt
|
||||||
|
|
||||||
|
|
||||||
|
def terminates(port, wait, budget):
|
||||||
|
"""Run connect_autobaud in a thread; True if it returns/raises within
|
||||||
|
`budget` seconds rather than hanging."""
|
||||||
|
done = threading.Event()
|
||||||
|
|
||||||
|
def run():
|
||||||
|
try:
|
||||||
|
pb.Loader(port).connect_autobaud(wait)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
done.set()
|
||||||
|
|
||||||
|
threading.Thread(target=run, daemon=True).start()
|
||||||
|
return done.wait(budget)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# the hang: a flooding target must not spin the drain forever. With wait=0.5
|
||||||
|
# the whole handshake has to give up well inside a few seconds.
|
||||||
|
check("flooding target: handshake terminates, drain is bounded",
|
||||||
|
terminates(FloodPort(), wait=0.5, budget=4.0))
|
||||||
|
|
||||||
|
# the control: a real loader still connects and reads identity.
|
||||||
|
info = pb.Loader(LoaderPort()).connect_autobaud(2.0)
|
||||||
|
check("well-behaved loader still connects (version 5)", info.version == 5)
|
||||||
|
|
||||||
|
print(f"\n {P} passed, {F} failed")
|
||||||
|
return 1 if F else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user